일급 컬렉션

김주형·2022년 11월 14일
0

자라기

목록 보기
10/22

🙇🏻‍♂️ Reference


소트웍스 앤솔로지 - 객체지향 생활체조

규칙 8 : 일급 콜렉션 사용
이 규칙의 적용은 간단하다.
콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 콜렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된 셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다.
필터는 또한 스스로 함수 객체가 될 수 있다,
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 드으이 동작을 처리할 수 있다.
이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
콜렉션은 실로 매우 유용한 원시타입이다.
많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에 의미적 의도나 단초는 거의 없다.

아래의 코드를

Map<String, String> map = new HashMap<>();
map.put("1","A");
map.put("2","B");
map.put("3","C");

아래와 같이 Wrapping 하는 것을 얘기한다고 한다.

public class GameRanking P
	private Map<String, String> ranks;
    
    public GameRanking(Map<String, String> ranks) {
    	this.ranks = ranks;
    }
}

일급 컬렉션?
컬렉션을 Wrapping하면서, 그 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라 한단다.. 근데.. 이걸 왜 할까?

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

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

다음과 같은 조건의 로또 복권 게임을 만든다고 가정

  • 6개의 번호가 존재
  • 서로 중복되지 않는 번호
public class LottoService {

	private static final int LOTTO_NUMBERS_SIZE = 6;
    
    public void createLottoNumber() {
    
    	List<Long> lottoNumbers = createNonDuplicateNumbers();
        validateSize(lottoNumbers); 
        validateDuplicate(lottoNumbers);
        
        ...
        
     }
     
     private void validateSize(List<Long> lottoNumbers) {
     	// 6개의 번호 유효성 검사
        }
     }
     
     private void validateDuplicate(List<Long> lottoNumbers) {
     // 중복 검증

서비스 메서드에서 비즈니스 로직을 처리할 경우 다음과 같은 큰 문제가 있다.

로또 번호가 필요한 모든 장소에서 검증 로직이 들어가야만 한다.

  • List으로 된 데이터는 모두 검증이 필요할까?
  • 모든 코드와 도메인을 알고 있지 않다면 어떻게 이 검증로직이 필요한지 알 수 있을까?

해당 조건으로만 생성할 수 있는 자료구조를 만들면 문제가 해결된다.
그렇다면 유효성 검사의 내용을 자료구조 생성과 통합할 순 없을까?
이러한 것이 가능한 클래스를 '일급 컬렉션'이라고 부른다고 한다.

  
public class LottoTicket {

  private static final int LOTTO_NUMBERS_SIZE = 6;
  
  private final List<Long> lottoNumbers;
  
  public LottoTicket(List<Long> lottoNumbers) {
  	validateSize(lottoNumbers); // 6개
  	validateDuplicate(lottoNumbers); // 중복되지 않은 숫자
    this.lottoNumbers = lottoNumbers;
  }
  
  private void validateSize(List<Long> lottoNumbers) {
     	// 6개의 번호 유효성 검사
        }
     }
     
     private void validateDuplicate(List<Long> lottoNumbers) {
     // 중복 검증

이제 로또 번호가 필요한 모든 로직은 이 일급 컬렉션만 있으면 된다.

  public class LotterService {
  
  	public void createLottoNumber() { // 필요한 로직은 모두 LottoTicket으로
  		LottoTicket lottoTicket = new LottoTicket(createNonDuplicateNumbers());
  
  // 로직 ..

비즈니스에 종속적인 자료구조가 만들어져, 이후 발생할 문제가 최소화되었다.


불변

일급 컬렉션은 컬렉션의 불변을 보장한다.

  • java의 final은 정확히는 불변을 만들어주는 것이 아니라, 재할당만 금지한다.

    불변 객체가 왜 중요할까?

  • 요즘같이 소프트웨어 규모가 커지고 있는 상황에서 불변 객체는 아주 중요하다고 한다.

  • 각 객체들이 절대 값이 바뀌지 않는다는게 보장되면 그만큼 코드를 이해하고 수정하는데 사이드 이펙트최소화 되기 때문

  • java에서는 final로 그 문제를 해결할 수 없기 때문에 일급 컬렉션과 래퍼 클래스 등의 방법으로 해결해야만 한다.

  • 컬렉션의 값을 변경할 수 있는 메서드가 없는 컬렉션을 만들면? -> 불변 컬렉션 생성

    public class Orders {
    	private final List<Order> orders;
    
    	public Orders(List<Order> orders) {
    		this.orders = orders;
    	}
    
    	public long getAmountSum() {
    		return orders.stream()
    				.mapToLong(Order::getAmount)
    				.sum();
    	}
    }
  • 이 클래스는 생성자와 getAmountSum() 외에 다른 메서드가 없다.

  • 즉, 새로 만들거나 값을 가져오는 것 뿐

  • List라는 컬렉션에 접근할 수 있는 방법도 없다. -> 값 변경 / 추가가 제한된다.

    일급 컬렉션 사용을 통해 불변 컬렉션 만드는 것이 가능하다.


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

    값과 로직이 함께 존재한다.

    여러 Pay들이 모여있고, 이 중 NaverPay 금액의 합이 필요하다고 가정

    
    @DisplayName("로직이 밖에 있는 상태")
    @Test
    public void 로직이_밖에_있는_상태() {
    
    	List<Pay> pays = Arrays.asList(
    				new Pay(NAVER_PAY, 1000), // 값은 여기에
    				new Pay(NAVER_PAY, 1500),
    				new Pay(KAKAO_PAY, 1200),
    				new Pay(TOSS, 3000L)_;
    
    	Long naverPaySum = pays.stream()  // 계산은 여기에서?
    			.filter(pay -> pay.getPayType().equals(NAVER_PAY))
    			.mapToLong(Pay::getAmount)
    			.sum();
    
    
    	assertThat(naverPaySum).isEqualTo(2500);

문제점

  • 결국 pays라는 컬렉션과 계산 로직은 서로 관계가 있는데, 이를 코드로 표현이 안 된다.

  • Pay타입의 상태에 따라 지정된 메서드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다.

  • 지금은 Pay타입의 List라면 사용될 수 있기 때문에 히스토리를 모른다면 실수할 여지가 많다.

    • 똑같은 기능을 하는 메서드 중복 생성할 수 있다.
      • 계산 메서드를 누락할 수 있다.

    결국 네이버페이 총 금액을 뽑으려면 이렇게 해야한다는 계산식을 컬렉션과 함께 두어야 한다.
    만약 네이버페이 외 카카오페이의 총금액도 필요하다면 더더욱 코드가 흩어질 확률이 높다.

    일급 컬렉션으로 해결하기

    public class PayGroups {
    	private List<Pay> pays;
    
    	public PayGroups(List<Pay> pays) {
    		this.pays = pays;
    	}
    
    	public Long getNaverPaySum() {
    		return pays.stream()
    				   .filter(pay -> PayType.isNaverPay(pay.getPayType()))
    				   .mapToLong(Pay::getAmount)
    				   .sum();
    	}
    }

    다른 결제 수단들의 합이 필요하다면 람다식으로 리팩토링 가능하다고 한다.

    public class PayGroups {
    
    	private List<Pay> pays;
    
    	public PayGroups(List<Pay> pays) {
    		this.pays = pays;
    	}
    
    	public Long getNaverPaySum() {
    		return getFilteredPays(pay -> PayType.isNaverPay(pay.getPayType()));
    	}
    
      public Long getKaKaoPaySum() {
            return getFilteredPays(pay -> PayType.isKaKaoPay(pay.getPayType()));
        }
    
      public Long getFilteredPays(Predicate<Pay> predicate) {
            return pays.stream()
    					.filter(predicate)
    					.mapToLong(Pay::getAmount)
    					.sum();
    
        }
    }

    PayGroups라는 일급 컬렉션 생성을 통해 상태와 로직이 한 곳에서 관리 가능하다.

    
    @DisplayName("로직이 밖에 있는 상태")
    @Test
    public void 로직이_밖에_있는_상태() {
    
    	List<Pay> pays = Arrays.asList(
    				new Pay(NAVER_PAY, 1000), 
    				new Pay(NAVER_PAY, 1500),
    				new Pay(KAKAO_PAY, 1200),
    				new Pay(TOSS, 3000L)_;
    
    	PayGroups payGroups = new PayGroups(pays);
    
    	Long naverPaySum = payGroups.getNaverPaySum();
    
    
    	assertThat(naverPaySum).isEqualTo(2500);

    이름이 있는 컬렉션

    마지막으로 컬렉션에 이름을 붙일 수 있다고 한다.

    같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다르다.
    이 둘을 구분하려면 어떻게 해야 할까?

    흔한 방법 : 변수명으로 구분

    @Test
    public void 컬렉션을_변수명으로() {

    List<Pay> naverPays = createNaverPays();
    List<Pay> kakaoPays = createKakaoPays();

    }

  • 검색이 어렵다.

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

    일급 컬렉션으로 해결하기

    네이버 페이 그룹과 카카오 페이 그룹 각각 일급 컬렉션으로 만든다.
    -> 컬렉션 기반 용어사용과 검색

    
    @Test
    public void 컬렉션을_변수명으로() {
    
    	List<Pay> naverPays = new NaverPays(createNaverPays());
    	List<Pay> kakaoPays = new KakaoPays(createKakaoPays());
    
    }

    사용될 표현을 이제 이 컬렉션에 맞추면 된다고 한다.
    검색 역시 컬렉션 클래스를 검색하면 모든 사용 코드를 찾아낼 수 있다.


    객체지향 코드로 가기 위해 꼭 익혀둬야 할 방법 중 하나라고 한다..
    열심히 배우자!

profile
왔을때보다좋은곳으로

0개의 댓글