일급컬렉션의 정의와 필요성

PPakSSam·2022년 1월 9일
1
post-thumbnail

이동욱님의 글을 보고 공부용으로 정리한 글임을 밝힌다.

참고한 레퍼런스는 일급 컬렉션의 소개와 써야할 이유이다.


일급컬렉션이란


간단하게 설명하자면, 아래의 코드를

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

아래와 같이 Wrapping 하는 것을 의미한다.

public class GameRanking {

    private Map<String, String> ranks;

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

정리하면 Collection을 Wrapping하면서, 그 외 다른 멤버변수가 없는 상태를 일급컬렉션이라 한다.

일급컬렉션의 장점


일급 컬렉션의 장점은 다음과 같다.

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

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


예를 들어 다음과 같은 조건으로 로또 복권 게임을 만든다고 하자.

  • 6개의 번호가 존재
  • 6개의 번호는 서로 중복되지 않아야 함

일반적으로 이런 일은 서비스 메소드에서 진행한다.

서비스 메소드에서 비즈니스 로직을 처리했는데 이런 경우 큰 문제가 발생한다.

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

  • List<Long>로 된 데이터는 모두 검증 로직이 필요한가?
  • 신규 입사자분들은 어떻게 이 검증로직이 필요한지 알 수 있을까?

등등 모든 코드와 도메인을 알고 있지 않다면 언제든 문제가 발생할 여지가 있다.

위의 문제 해결방법

  • 6개의 숫자로만 이루어져야만 하고
  • 6개의 숫자는 서로 중복되지 않아야 하는

이런 자료구조가 있어야 하는데 없으니까 직접 만들면 된다!!!
즉 아래와 같이 해당 조건으로만 생성할 수 있는 자료구조를 만들면 문제가 해결된다.
그리고 이런 클래스를 우린 일급 컬렉션이라고 부른다.

이제 로또 번호가 필요한 로직은 이 일급 컬렉션 안에 다 있게 된다.

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

2. 불변


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

여기서 final을 사용하면 안되는지 의문을 제기하는 분들을 위해 설명하고자 한다.
Java의 final은 정확히는 불변을 만들어주는 것이 아닌 재할당만 금지한다.


아래는 final에 대한 테스트 코드와 실행 결과이다.

보다시피 값이 추가되는 것을 확인할 수 있다.
이미 collection비어있는 HashMap으로 선언되었음에도 값이 변경될 수 있다.


아래는 재할당은 불가능함을 보여주는 코드이다.

보다시피 컴파일 에러가 나는데 final로 할당된 코드에 재할당할 수 없기 때문이다.

Java의 final재할당만 금지한다.
이외에 member.setAge(10)과 같은 코드 역시 작동해버리니 반쪽짜리라 할 수 있다.

요즘과 같이 소프트웨어 규모가 커지고 있는 상황에서 불변 객체는 아주 중요하다.
각각의 객체들이 절대 값이 바뀔일이 없다는게 보장되면 그만큼 코드를 이해하고 수정하는데 사이드 이펙트가 최소화되기 때문이다.

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

그래서 아래와 같이 컬렉션의 값을 변경할 수 있는 메소드가 없는 컬렉션을 만들면 불변 컬렉션이 된다.

이 클래스는 생성자와 getAmountSum() 외에 다른 메소드가 없다.
즉, 이 클래스의 사용법은 새로 만들거나 값을 가져오는 것뿐이다.
List라는 컬렉션에 접근할 수 있는 방법이 없기 때문에 값을 변경/추가가 안된다.
이렇게 일급 컬렉션을 사용하면, 불변 컬렉션을 만들 수 있다.

라고 이동욱님 블로그에는 써져있다. 그러나 아래 참고의 내용에 의하면 다음이 맞는 것 같다.

public class Orders {
    private final List<Order> orders;
    
    public Orders(List<Order> orders) {
    	this.order = new ArrayList<>(orders);
    }
    
    public long getAmountSum() {
        return orders.stream()
                     .mapToLong(Order::getAmount)
                     .sum();
    }
}

이렇게 작성해야 불변 컬렉션이 되는 것 같으니 참고하길 바란다.


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


일급 컬렉션의 또하나의 장점은 값과 로직이 함께 존재한다는 것이다.

예를 들어 여러 Pay들이 모여있고, 이 중 NaverPay 금액의 합이 필요하다고 가정해보자.
일반적으로는 아래와 같이 작성한다.

  • List에 데이터를 담고
  • Service 혹은 Util 클래스에서 필요한 로직 수행

Pay타입의 상태에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다. 그러면 다음과 같은 문제가 발생할 수 있다.

  • 똑같은 기능을 하는 메소드를 중복 생성할 수 있다.
    • 히스토리가 관리 안된 상태에서 신규화면이 추가되어야 할 경우 계산 메소드가 있다는 것을 몰라 다시 만드는 경우가 빈번하다.
    • 만약 기존 화면의 계산 로직이 변경 될 경우, 신규 인력은 2개의 메소드의 로직을 다 변경해야하는지, 해당 화면만 변경해야하는지 알 수 없다.
    • 관리 포인트가 증가할 확률이 매우 높다.
  • 계산 메소드를 누락할 수 있다.
    • 리턴 받고자 하는 것이 Long 타입의 값이기 때문에 꼭 이 계산식을 써야한다고 강제할 수 없다.

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

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

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 getKakaPaySum() {
    	return getFilteredPays(pay -> PayType.isKakaoPay(pay.getPayType()));
    }
    
    private Long getFilteredPays(Predicate<Pay> predicate) {
    	return pays.stream()
        	.filter(predicate)
            .mapToLong(Pay::getAmount)
            .sum();
    }

}

이렇게 PayGroups라는 일급 컬렉션이 생김으로 상태와 로직이 한곳에서 관리된다.

4. 이름이 있는 컬렉션

마지막 장점은 컬렉션에 이름을 붙일 수 있다는 것이다.
이 장점에 대해서 크게 메리트를 못느낄 수도 있다.
그런데 이동욱님은 이것 역시 장점이라고 생각되어서 블로그에 글을 올리셨다.

같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다르다.
그렇다면 이 둘을 구분하려면 어떻게 해야될까?
가장 흔한 방법은 변수명을 다르게 하는 것이다.

위 코드의 단점은 다음과 같다.

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

위 문제 역시 일급 컬렉션으로 쉽게 해결할 수 있다.

네이버페이 그룹과 카카오페이 그룹 각각을 일급 컬렉션으로 만들면 이 컬렉션 기반으로 용어사용과 검색을 하면 된다.

개발팀 / 운영팀 내에서 사용될 표현은 이제 이 컬렉션에 맞추면 된다.
검색 역시 이 컬렉션 클래스를 검색하면 모든 사용코드를 찾아낼 수 있다.


참고

일급컬렉션의 장점 중 하나인 불변에 대하여 다른 의견이 있는 글이 있어 추가하려고 한다.
이는 일급컬렉션을 사용하는 이유에서 가져온 글임을 밝힌다.

다음 코드를 보자

public class Lotto {
    private final List<LottoNumber> lotto;
    // ...
    public List<LottoNumber> getLotto() {
        return lotto;
    }
}

위의 코드는 setter가 없으므로 불변 객체이다.

라는 글을 많이 보았을 것이다. 그러나 이는 옳지않은 글이다.
왜냐하면 setter를 사용하지 않아도 Lotto안의 lotto 변수에 변화를 줄 수 있기 때문이다.

public class Lotto {
    private final List<LottoNumber> lotto;

    public Lotto(List<LottoNumber> lotto) {
        this.lotto = lotto;
    }

    public List<LottoNumber> getLotto() {
        return lotto;
    }
}

public class LottoNumber {
    private final int lottoNumber;

    public LottoNumber(int lottoNumber) {
        this.lottoNumber = lottoNumber;
    }
   
    @Override
    public String toString() {
        return "LottoNumber{" +
                "lottoNumber=" + lottoNumber +
                '}';
    }
}

위와 같은 코드가 있다고 가정하자.

@Test
public void lotto_변화_테스트() {
    List<LottoNumber> lottoNumbers = new ArrayList<>();
    lottoNumbers.add(new LottoNumber(1));
    Lotto lotto = new Lotto(lottoNumbers);
    lottoNumbers.add(new LottoNumber(2));
}

위의 코드와 같은 상황에서 lotto를 get 했을 때 어떤 값을 가지고 있을까?
정답은 [LottoNumber{lottoNumber=1}, LottoNumber{lottoNumber=2}]이다.
lottoNumberslotto class의 멤버변수가 주소값이 같기 때문이다.

이 문제를 해결하기 위해서는 다음과 같이 수정하면 된다.

public class Lotto {
    private final List<LottoNumber> lotto;

    public Lotto(List<LottoNumber> lotto) {
        this.lotto = new ArrayList<>(lotto);
    }

    public List<LottoNumber> getLotto() {
        return lotto;
    }
}

이렇게 수정하면 멤버변수에 저장되는 주소값을 재할당하기 때문에 영향을 받지 않는다.

하지만 또 다른 문제가 있다!!

@Test
public void lotto_변화_테스트() {
    List<LottoNumber> lottoNumbers = new ArrayList<>();
    lottoNumbers.add(new LottoNumber(1));
    Lotto lotto = new Lotto(lottoNumbers);
    lotto.getLotto().add(new LottoNumber(2));
}

이러한 상황에서도 [LottoNumber{lottoNumber=1}, LottoNumber{lottoNumber=2}]가 나온다.

이 문제를 해결하기 위해서는 unmodifiableList를 사용한다.

public class Lotto {
    private final List<LottoNumber> lotto;

    public Lotto(List<LottoNumber> lotto) {
        this.lotto = new ArrayList<>(lotto);
    }

    public List<LottoNumber> getLotto() {
        return Collections.unmodifiableList(lotto);
    }
}

unmodifiableList를 사용하면 lotto는 불변이 되고, getter로 return해서 사용될 때 변경이 불가능하다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글