ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 일급 컬렉션
    개발/Java 2020. 12. 23. 13:34

    일급 컬렉션

    참고: jojoldu님 블로그
    블로그 원문을 공부하면서 개인적으로 이해를 위해 필기한 글입니다.
    연습하며 작성한 코드

    일급 컬렉션이란?

    public class Station {
        List<String> stations = new ArrayList<>();
        // ...
    }
    • 컬렉션을 wrapping 하는 것을 말한다.
    • 콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
    • 각 콜렉션은 그 자체로 포장되어 있으므로 콜렉션과 관련된 동작 근거지가 마련된 것이다.
    • 필터가 이 새 클래스의 일부가 될음 알 수 있다.
    • 필터는 스스로 객체가 될 수 있다.
    • 두 그룹을 묶는 등 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.

    장점

    • 비즈니스에 종속적인 자료구조
    • Collection의 불변성 보장
    • 상태와 행위를 한 곳에 저장
    • 이름이 있는 컬렉션

    1. 비즈니스에 종속적인 자료구조

    LottoService.java :
    createLottoNumber에서 List<Long>으로 로또 번호를 생성한다.
    이 경우 문제점은 로또번호가 필요한 모든 곳에서 검증이 필요하다. (validate**()) 이 코드를 처음보는 사람은 검증을 해야하는지 모를 수 있다.
    이 문제점을 해결하기 위해 6개의 숫자로 이루어지고, 서로 중복되지 않는 자료구조를 만든다.

    그것이 일급 컬렉션이다.

    LottoTicket.java :
    validate**()를 이 클래스의 생성자에서 수행한다. 로또 번호를 생성한 이 자료구조를 사용할 땐 다음과 같이 사용한다.

    public class LottoService2 {
        public void createLottoNumber() {
            LottoTicket lottoTicket = new LottoTicket(createNonDuplicateNumbers());
        }
    }

    이로써 비즈니스에 종속적인 자료구조가 만들어졌다.

    2. 불변

    컬렉션에는 final 예약어를 붙여도 재할당만 금지되지, 컬렉션 메소드를 통해 값을 추가할 수 있으니 불변이 아니다.

    final List<String> names = new ArrayList<>();
    names.add("홍길동"); // 가능하다.

    그러나 일급 컬렉션을 사용한 경우, set 메소드를 추가하지 않으면 콜렉션은 불변이 된다.

    3. 상태와 행위를 한 곳에서 관리

    PayTest.java:
    Enum과 마찬가지로 값과 로직이 한곳에 위치하는 것이 장점이다.
    예를 들어 여러가지 Pay가 있고, NaverPay의 합이 필요할 때,

    public void test() {
        //given
        List<Pay> pays = new Arrays.List(
                new Pay(NAVER_PAY, 1000), 
                new Pay(NAVER_PAY, 1500),
                new Pay(KAKAO_PAY, 1000),
                new Pay(TOSS, 1500)); 
        //when
        Long naverPaySum = pays.stream()
                .filter(pay -> getPayType().equals(NAVER_PAY))
                .mapToLong(Pay::getAmount)
                .sum();
    }

    위 처럼 list에 담고 Service에서 필요한 로직을 수행한다.
    이 상황에서 문제점은 pays라는 리스트와 그 아래 로직이 관련이 있음이 표현이 안된다.
    이 코드를 모르는 사람은 둘이 관련이 있는지 모르고, 똑같은 기능을 하는 코드를 중복으로 작성할 수도 있다.

    원문 인용

    Pay타입의 상태에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없습니다.
    지금은 Pay타입의 List라면 사용될 수 있기 때문에 히스토리를 모르는 분들이라면 실수할 여지가 많습니다.

    • 똑같은 기능을 하는 메소드를 중복 생성할 수 있습니다.
      • 히스토리가 관리 안된 상태에서 신규화면이 추가되어야 할 경우 계산 메소드가 있다는 것을 몰라 다시 만드는 경우가 빈번합니다.
      • 만약 기존 화면의 계산 로직이 변경 될 경우, 신규 인력은 2개의 메소드의 로직을 다 변경해야하는지, 해당 화면만 변경해야하는지 알 수 없습니다.
      • 관리 포인트가 증가할 확률이 매우 높습니다.
    • 계산 메소드를 누락할 수 있습니다.
      • 리턴 받고자 하는 것이 Long 타입의 값이기 때문에 꼭 이 계산식을 써야한다고 강제할 수 없습니다 .
      • 결국에 네이버페이 총 금액을 뽑을려면 이렇게 해야한다는 계산식을 컬렉션과 함께 두어야 합니다.
      • 만약 네이버페이 외에 카카오 페이의 총금액도 필요하다면 더더욱 코드가 흩어질 확률이 높습니다.

    그래서 이 문제 역시 일급 컬렉션으로 해결합니다.

    List<Pay> pays = Arrays.asList(
                new Pay(NAVER_PAY, 1000),
                new Pay(NAVER_PAY, 1000),
                new Pay(KAKAO_PAY, 1000),
                new Pay(TOSS, 1500));
    
        // 상태와 로직이 한 곳에 위치
        PayGroups payGroups = new PayGroups(pays);
        Long naverPaySum = payGroups.getNaverPaySum(); 

    4. 이름이 있는 컬렉션

    같은 Pay들의 모임이라도 kakaoPaynaverPay는 다르다. 이 둘을 구분하는 가장 쉬운 방법은 변수명으로 구분하는 것이다.

    List<Long> kakaoPay = createNaverPays();
    List<Long> naverPay = createNaverPays();

    이 방식의 단점은 검색이 어렵고, 명확한 표현이 불가능하다는 것이다.

    원문 인용

    위 코드의 단점은 뭘까요?

    • 검색이 어려움
      • 네이버페이 그룹이 어떻게 사용되는지 검색 시 변수명으로만 검색할 수 있습니다
      • 이 상황에서 검색은 거의 불가능합니다.
      • 네이버페이의 그룹이라는 뜻은 개발자마다 다르게 지을 수 있기 때문입니다.
      • 네이버페이 그룹은 어떤 검색어로 검색이 가능할까요?
    • 명확한 표현이 불가능
      • 변수명에 불과하기 때문에 의미를 부여하기가 어렵습니다
      • 이는 개발팀/운영팀간에 의사소통시 보편적인 언어로 사용하기가 어려움을 얘기합니다.
      • 중요한 값임에도 이를 표현할 명확한 단어가 없는것이죠.

    이 문제의 해결 역시 일급 컬렉션으로 가능하다.
    네이버페이, 카카오페이 각각의 일급 컬렉션을 만들어 이 컬렉션을 기반으로 용어 사용과 검색을 한다.

    KakaoPays kakaoPays = new KakaoPays(createKakaoPays());
    ...

    ❓ 공부할 것

    • 함수형 인터페이스 (Predicate)
    public Long getNaverPaySum() {
          return getFilteredPays(pay -> PayType.isNaverPay(pay.getPayType()));
      }
    
    
    private Long getFilteredPays(Predicate predicate) {  
        return pays.stream()  
            .filter(predicate)  
            .mapToLong(Pay::getAmount)  
            .sum();  
    }

    댓글

Designed by Tistory.