JAVA OOP, Generic, Collection

By | 2021년 12월 9일
Table of Contents

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(List items, 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());

답글 남기기