일급 컬렉션이란?
규칙 8 : 일급 컬렉션 사용
이 규칙의 적용은 간단하다.
컬렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 컬렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된 셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다.
필터는 또한 스스로 함수 객체가 될 수 있다.
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.
이는 인스턴스 변에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
컬렉션은 실로 매우 유용한 원시 타입이다.
많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에게 의미적 의도나 단초는 거의 없다.
- 소트웍스 앤솔로지 객체지향 생활체조편
한 마디로 Collection을 Wrapping하면서, 그 외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 합니다.
아래의 코드를
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의 불변성을 보장
- 상태와 행위를 한 곳에서 관리
- 이름이 있는 컬렉션
+ 가독성과 유지보수성 향상, 유효성 검증 용이 등등...
1. 비즈니스에 종속적인 자료 구조
<로또 게임 예시>
로또 복권은 다음의 조건이 있습니다.
- 6개의 번호가 존재
- 보너스 번호는 이번 예제에서 제외
- 6개의 번호는 서로 중복되지 않아야 함
일반적으로 이런 일은 서비스 메소드에서 진행합니다.
이처럼 서비스 메소드에서 비지니스 로직을 처리했을 때 문제들이 생깁니다.
바로 로또 번호가 필요한 모든 장소에선 검증로직이 들어가야만 한다는 점입니다.
- List<Long> 으로 된 데이터는 모두 검증 로직이 필요할까요?
- 이 코드를 처음 본 사람들은 어떻게 이 검증로직이 필요한지 알 수 있을까요?
모든 코드와 도메인을 알고 있지 않다면 언제든 문제가 발생할 여지가 있습니다.
그렇다면 어떻게 해야할까요?
- 6개의 숫자로만 이루어져야만 하고
- 6개의 숫자는 서로 중복되지 않아야만 하는
이런 자료구조를 만들면 됩니다.
아래와 같이 해당 조건으로만 생성 할 수 있는 자료구조를 만들면 위의 문제들이 모두 해결됩니다.
그리고 이런 클래스를 일급 컬렉션이라고 부릅니다.
2. 불변
일급 컬렉션은 컬렉션의 불변을 보장합니다.
Java의 final은 불변을 만들어주는 것은 아니며, 재할당만 금지합니다.
@Test
public void final도_값변경이_가능하다() {
//given
final Map<String, Boolean> collection = new HashMap<>();
//when
collection.put("1", true);
collection.put("2", true);
collection.put("3", true);
collection.put("4", true);
//then
assertThat(collection.size()).isEqualTo(4);
}
@Test
public void final은_재할당이_불가능하다() {
//given
final Map<String, Boolean> collection = new HashMap<>();
//when
collection = new HashMap<>();
//then
assertThat(collection.size()).isEqualTo(4);
}
이 코드는 컴파일 에러가 발생합니다.
final로 할당된 코드에 재할당은 불가하기 때문입니다.
불변 객체는 사이드 이펙트를 최소화시키기 위해 중요합니다.
불변 객체란?
1) 객체를 변경하는 메소드를 제공하지 않는다.
2) 재정의할 수 있는 메소드를 제공하지 않는다.
3) 모든 필드를 final로 만든다.
4) 모든 필드를 private으로 만든다.
5) 가변 객체를 참조하는 필드는 배타적으로 접근해야 한다.
즉, 객체의 상태를 바꿀 수는 없으므로 새로운 상태로 변경해야 할 경우 새로운 불변 객체를 만들어 기존의 불변 객체를 대체 시켜야 한다. 이를 통해 불변객체는 사이드 이펙트 및 동시성 이슈에 노출되지 않는다. 따라서 함수형 프로그래밍에서 Side-effect를 발생시키지 않기 위해 불변객체를 사용하는 것이지, 일급 컬렉션의 특징이 불변성을 보장하는 것은 아니다.
Java에서는 final로 그 문제를 해결할 수 없기 때문에 일급 컬렉션과 래퍼 클래스 등의 방법으로 해결해야합니다.
그래서 아래와 같이 컬렉션의 값을 변경할 수 있는 메소드가 없는 컬렉션을 만들면 불변 컬렉션이 됩니다.
이 클래스는 생성자와 getAmountSum() 외에 다른 메소드가 없습니다.
즉, 이 클래스의 사용법은 새로 만들거나 값을 가져오는 것뿐입니다.
List라는 컬렉션에 접근할 수 있는 방법이 없기 때문에 값의 변경/추가가 안됩니다.
이렇게 일급 컬렉션을 사용하면, 불변 컬렉션을 만들수 있습니다.
3. 상태와 행위를 한 곳에서 관리
일급 컬렉션의 3번째 장점은 값과 로직이 함께 존재한다는 것입니다.
예를 들어 여러 Pay들이 모여있고, 이 중 NaverPay 금액의 합이 필요하다고 가정해보겠습니다.
이 상황에는 문제가 있습니다.
결국 pays 라는 컬렉션과 계산 로직은 서로 관계가 있는데, 코드로 표현이 안됩니다.
Pay타입의 상태에 따라 지정된 메소드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없습니다.
지금은 Pay타입의 List라면 사용될 수 있기 때문에 히스토리를 모르는 분들이라면 실수할 여지가 많습니다.
- 똑같은 기능을 하는 메소드를 중복 생성할 수 있습니다.
- 히스토리가 관리 안된 상태에서 신규화면이 추가되어야 할 경우 계산 메소드가 있다는 것을 몰라 다시 만드는 경우가 빈번합니다.
- 만약 기존 화면의 계산 로직이 변경 될 경우, 신규 인력은 2개의 메소드의 로직을 다 변경해야하는지, 해당 화면만 변경해야하는지 알 수 없습니다.
- 관리 포인트가 증가할 확률이 매우 높습니다.
- 계산 메소드를 누락할 수 있습니다.
- 리턴 받고자 하는 것이 Long 타입의 값이기 때문에 꼭 이 계산식을 써야한다고 강제할 수 없습니다
결국에 네이버페이 총 금액을 뽑을려면 이렇게 해야한다는 계산식을 컬렉션과 함께 두어야 합니다.
만약 네이버페이 외에 카카오 페이의 총금액도 필요하다면 더더욱 코드가 흩어질 확률이 높습니다.
그래서 이 문제 역시 일급 컬렉션으로 해결합니다.
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()));
}
private Long getFilteredPays(Predicate<Pay> predicate) {
return pays.stream()
.filter(predicate)
.mapToLong(Pay::getAmount)
.sum();
}
}
이렇게 PayGroups라는 일급 컬렉션이 생김으로써, 상태와 로직이 한곳에서 관리 됩니다.
4. 이름이 있는 컬렉션
마지막 장점은 컬렉션에 이름을 붙일 수 있다는 것입니다.
같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다릅니다.
그렇다면 이 둘을 구분할려면 어떻게 해야할까요?
가장 흔한 방법은 변수명을 다르게 하는 것입니다.
- 검색이 어려움
- 네이버페이 그룹이 어떻게 사용되는지 검색 시 변수명으로만 검색할 수 있습니다.
- 네이버페이의 그룹이라는 뜻은 개발자마다 다르게 지을 수 있기 때문에, 이 상황에서 검색은 거의 불가능합니다.
- 명확한 표현이 불가능
- 변수명에 불과하기 때문에 의미를 부여하기가 어렵습니다.
- 중요한 값임에도 이를 표현할 명확한 단어가 없는것이죠.
위 문제 역시 일급 컬렉션으로 쉽게 해결할 수 있습니다.
네이버페이 그룹과 카카오페이 그룹 각각의 일급 컬렉션을 만들면 이 컬렉션 기반으로 용어사용과 검색을 하면 됩니다.
https://jojoldu.tistory.com/412
https://tecoble.techcourse.co.kr/post/2020-05-08-First-Class-Collection/
https://brainbackdoor.tistory.com/140
https://dkswnkk.tistory.com/696
'CS Study > 자유주제 스터디' 카테고리의 다른 글
[스터디] IoC : 제어의 역전 (0) | 2023.12.22 |
---|---|
[스터디] 정적 팩토리 메서드(Static Factory Method) (0) | 2023.12.08 |
[스터디] 의존관계 주입(DI : Dependency Injection) (2) | 2023.12.08 |
[스터디] getter, setter 그래서 왜 쓰면 안 되는건데?🤔 (0) | 2023.11.23 |
[스터디] SOLID 원칙 발표 자료 (1) | 2023.11.02 |