JAVA OOP, Generic, Collection
OOP의 특징 4가지
캡슐화(Encapsulation)
구현을 외부에 숨기는 것을 의미합니다.
(라고 써놓고 로직을 클래스에 몰아 넣는다라고 해석합니다.)
public class CustomerOrder {
private String cancelyn;
public void cancel() {
this.cancelyn = "Y";
// 마일리지 관련 로직
// 쿠폰 관련 로직
// 재고 관련 로직
}
}
CustomerOrder customerOrder = new CustomerOrder();
customerOrder.cancel();
구현을 숨기지 않으면 어떻게 될까요?
public class CustomerOrder {
public String cancelyn;
}
CustomerOrder customerOrder = new CustomerOrder();
customerOrder.cancelyn = "Y";
// 마일리지 관련 로직
// 쿠폰 관련 로직
// 재고 관련 로직
만약 canceldate 를 추가하고자 한다면?
public class CustomerOrder {
public String cancelyn;
public LocalDateTime canceldate;
}
CustomerOrder customerOrder = new CustomerOrder();
customerOrder.cancelyn = "Y";
customerOrder.canceldate = LocalDateTime.now();
// 마일리지 관련 로직
// 쿠폰 관련 로직
// 재고 관련 로직
취소 기능이 있는 곳이 다수라면, 찾아다니면서 하나하나 코드를 추가해 주어야 합니다.
못 찾은 곳이 발생하면 버그가 됩니다.
모두 찾았다 해도, 중복코드가 산지사방에 퍼지게 됩니다.
public class CustomerOrder {
private String cancelyn;
public LocalDateTime canceldate;
public void cancel() {
this.cancelyn = "Y";
this.canceldate = LocalDateTime.now();
// 마일리지 관련 로직
// 쿠폰 관련 로직
// 재고 관련 로직
}
}
CustomerOrder customerOrder = new CustomerOrder();
customerOrder.cancel();
캡슐화했다면 메소드 하나만 수정해주면 끝납니다.
상속(Inheritance)
2개 이상의 클래스에서 중복되는 코드를 부모 클래스로 만들기도 하고,
하나의 클래스에서 기능확장을 위해 자식 클래스를 만들기도 합니다.
// 고객 주문
public class CustomerOrder { }
// 꽃배달 고객 주문
public class FlowerCustomerOrder extends CustomerOrder {
// 꽃배달 관련 로직만 추가
}
// 해외배송 고객 주문
public class ForeignCustomerOrder extends CustomerOrder {
// 해외배송 관련 로직만 추가
}
// 보너스 쿠폰
public class BonusCoupon { }
// 비율 보너스 쿠폰
public class PercentBonusCoupon extends BonusCoupon { }
// 금액 보너스 쿠폰
public class PriceBonusCoupon extends BonusCoupon { }
추상화(Abstraction)
필요한 기능은 알겠는데, 코드를 작성할 수 없을 때 사용합니다.
아래의 코드에서 getDiscountPrice() 처럼, 필요한 기능은 있지만,
실제로 코드를 작성할 수 없을때 abstract
로 추상클래스를 만들고,
자식 클래스에서 실제 코드를 작성하도록 할 수 있습니다.
public class OrderProduct {
private BigDecimal price;
private int orderCount;
// getter & setter
}
// 추상 클래스
public abstract class BonusCoupon {
// 추상 메소드
public abstract BigDecimal getDiscountPrice(List<OrderProduct> items, int index);
}
public class PercentBonusCoupon extends BonusCoupon {
@Override
public BigDecimal getDiscountPrice(List<OrderProduct> items, int index) {
// 비율쿠폰 계산 로직
return BigDecimal.ZERO;
}
}
public class PriceBonusCoupon extends BonusCoupon {
@Override
public BigDecimal getDiscountPrice(List<OrderProduct> items, int index) {
// 금액쿠폰 계산 로직
return BigDecimal.ZERO;
}
}
추상화의 극단적인 예로 interface
가 있습니다.
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
// ......
}
추상 클래스는 일부 메소드의 코드를 작성하지 않는 것이고,
인터페이스는 모든 메소드의 코드를 작성하지 않습니다.
또한, abstract
키워드를 생략할 수 있습니다.
다형성(Polymorphism)
위키피디아에 아래와 같이 설명되어 있습니다.
프로그램 언어의 다형성(多形性, polymorphism; 폴리모피즘)은 그 프로그래밍 언어의 자료형 체계의 성질을 나타내는 것으로, 프로그램 언어의 각 요소들(상수, 변수, 식, 오브젝트, 함수, 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다. 반댓말은 단형성(monomorphism)으로, 프로그램 언어의 각 요소가 한가지 형태만 가지는 성질을 가리킨다.
말이 복잡한데…
자바에서 대표적으로 사용되는 두가지 케이스를 확인해 봅니다.
객체 다형성
public class CustomerOrder {
public void applyCoupon(BonusCoupon coupon) {
// 쿠폰 적용로직
}
}
CustomerOrder customerOrder = new CustomerOrder();
PercentBonusCoupon percentBonusCoupon = new PercentBonusCoupon();
PriceBonusCoupon priceBonusCoupon = new PriceBonusCoupon();
// customerOrder.applyCoupon(percentBonusCoupon);
// or
customerOrder.applyCoupon(priceBonusCoupon);
메소드를 applyCoupon(BonusCoupon coupon)
로 지정함으로 해서,
BonusCoupon 을 상속하는 모든 클래스를 대입할 수 있는 것을 볼 수 있습니다.
추상화를 설명할 때 getDiscountPrice()
를 기능 구현도 못하는데,
왜 궂이 추가해 놓을까의 대답이 여기서 나옵니다.
추상 메소드라도 추가해 놓았기 때문에,
금액쿠폰 또는 비율쿠폰의 getDiscountPrice() 를 사용할 수 있게 됩니다.
유용한 기능으로, 매우 많이 쓰이는 속성입니다.
List<CustomerOrder> customerOrders = new ArrayList<>();
위의 경우도 List
에 List 의 하위객체인 ArrayList
를 대입하는 경우입니다.
(정확히는 인터페이스 구현체이지만)
메소드 다형성
메소드 다형성에는 메소드 오버로딩, 메소드 오버라이딩이 있습니다.
-
메소드 오버로딩
메소드명이 동일하고, 파라미터 타입(Type)은 다른 경우입니다.
String.valueOf()
처럼 메소드명이 동일하지만, 파라미터 타입은 전부 다릅니다.int i = 1; String j = new String("2"); float k = 3; System.out.println(String.valueOf(i)); System.out.println(String.valueOf(j)); System.out.println(String.valueOf(k));
-
메소드 오버라이딩
메소드명과 파라미터가 모두 동일한 경우입니다.
BigDecimal getDiscountPrice(Listitems, int index) 처럼,
메소드명, 파라미터 타입, 리턴값 모두 동일합니다.public abstract class BonusCoupon { public abstract BigDecimal getDiscountPrice(List<OrderProduct> items, int index); } public class PercentBonusCoupon extends BonusCoupon { @Override public BigDecimal getDiscountPrice(List<OrderProduct> items, int index) { // 비율쿠폰 계산 로직 return BigDecimal.ZERO; } }
Generic
클래스를 파라미터로 받을 수 있는 기능입니다.
List<CustomerOrder> customerOrders = new ArrayList<>();
Generic 이 없다면?
StringList stringList = new StringList();
IntegerList integerList = new IntegerList();
CustomerOrderList customerOrderList = new CustomerOrderList();
// ......
각각의 객체에 대해 각각의 List 클래스를 생성해야 합니다.
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
// ......
}
또한, List 에서 기본적으로 필요한 메소드들을 각각의 클래스에 구현해 주어야 하고,
코드중복이 발생할 수밖에 없습니다.
import org.mapstruct.BeanMapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
public interface GenericMapper<D, E> {
D toDto(E e);
E toEntity(D d);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateFromDto(D dto, @MappingTarget E entity);
}
위와 같이 Generic interface 를 생성해 준 경우,
아래와 같이 클래스 파라미터를 지정해 주는것만으로 코딩이 끝납니다.
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserInfoMapper extends GenericMapper<UserInfoDto, UserInfo> {
}
메소드 등에도 클래스 파라미터를 지정할 수 있습니다.
abstract
로 지정한 메소드 이외에는 코드가 자동생성되므로,
별도의 코딩이 필요없습니다.
/**
*
* CustomGenericService - 서비스의 공통 기능 추상화(CRUD)
* 재정의가 필요한 기능은 @Override해서 사용할것
*
* @author skyer9@gmail.com
*
* @param <D> DTO Type
* @param <E> Entity Type
* @param <KD> Key DTO Type
* @param <KE> Key Entity type
*/
public abstract class CustomGenericService<D, E, KD, KE> {
protected JpaRepository<E, KE> repository;
protected final String title;
public CustomGenericService(JpaRepository<E, KE> repository, String title) {
this.repository = repository;
this.title = title;
}
// 생성
@Transactional
public D create(D d) {
E e = newEntity();
updateFromDto(d, e);
return toDto(repository.save(e));
}
// 체크 및 생성
@Transactional
public D create(KD id, D d) throws DataExistsException {
KE ke = toKeyEntity(id);
if (get(ke, false) != null) {
throw new DataExistsException(title + "이(가) 이미 존재합니다.");
}
E e = newEntity(ke);
updateFromDto(d, e);
return toDto(repository.save(e));
}
// 수정
@Transactional
public void update(KD id, D d) {
E e = get(toKeyEntity(id), true);
updateFromDto(d, e);
repository.save(e);
}
// 수정 또는 생성
@Transactional
public void updateOrCreate(KD id, D d) {
E e = get(toKeyEntity(id));
updateFromDto(d, e);
repository.save(e);
}
// DTO 조회
@Transactional(readOnly = true)
public D select(KD id) {
return toDto(get(toKeyEntity(id), true));
}
// Entity 반환
@Transactional
public E get(KE id, boolean throwExceptionIfNotExists) {
E e = repository.findById(id).orElse(null);
if (throwExceptionIfNotExists && (e == null)) {
throw new DataNotFoundException(title + "이(가) 존재하지 않습니다.");
} else {
return e;
}
}
@Transactional
public E get(KE id) {
Optional<E> o = repository.findById(id);
return o.orElseGet(() -> newEntity(id));
}
public abstract SearchResponseDto search(Map<String, String> params);
protected List<D> toDto(List<E> lst) {
return lst.stream().map(this::toDto).collect(Collectors.toList());
}
protected abstract D toDto(E e);
protected abstract E toEntity(D e);
// protected abstract KD toKeyDto(KE e);
protected abstract KE toKeyEntity(KD e);
protected abstract void updateFromDto(D d, E e);
protected abstract E newEntity();
protected abstract E newEntity(KE e);
}
Collection
API 서버 개발에는 쓰다보니 딱 두개만 쓰게 되더군요.
HashMap
주로 URL 파라미터 설정할 때 씁니다.
Map<String, String> map = new HashMap<>();
map.put("searchKeyword", "아이폰케이스");
map.put("pageNumber", "1");
map.put("pageSize", "20");
System.out.println(map.get("searchKeyword"));
ArrayList
주로 검색결과를 반환할 때 사용합니다.
List<Object> results = Arrays.asList(toDto(list).toArray());