객체 지향적이고, 리팩토링이 쉬운 코드를 위해 일급 컬렉션의 활용이 필요하다고 한다. 객체 지향 생활 체조 원칙에도 언급되어 있다. 일급 컬렉션이 무엇이고, 어떻게 활용하는 지 알아본다.
일급 컬렉션이란?
Collection을 Wrapping하며 Collection 이외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 한다.
아래 예시를 보자.
1
2
3
4
5
6
7
public class Dice {
private List<Integer> numList;
public Dice(List<Integer> nums) {
this.numList = nums;
}
}
이렇게 어떠한 Collection을 wrapping 하는 것을 뜻한다. 그럼 이걸 사용했을 때 장점이 뭘까?
일급 컬렉션의 사용 이유
- 비즈니스에 종속적인 자료 구조를 만들 수 있다.
- Collection의 불변성을 보장할 수 있다.
- 상태와 행위를 한 곳에서 관리할 수 있다.
- 컬렉션에 이름을 부여할 수 있다.
비즈니스에 종속적인 자료구조
우리가 만들려고 하는 주사위(Dice)에는 조건이 존재한다고 가정하자.
- 주사위는 1에서 10까지의 숫자를 가질 수 있다.
- 6개의 숫자를 가질 수 있다.
- 6개의 숫자는 중복될 수 없다.
1
2
3
4
5
public void createDice() {
List<Integer> diceNumbers = createNonDuplicateNumbers();
validateSize(diceNumbers); // 6개의 숫자를 가지고 있는 것인지 검증.
validateDuplicate(diceNumbers); // 6개의 숫자가 중복되지 않았는지 검증.
}
위와 같이 주사위의 숫자를 만들어내는 메서드를 만들 수 있다.
그런데 이렇게 되는 경우 문제가 발생한다.
- dice가 필요한 모든 장소에서는 검증 로직이 들어가야 한다.
- 코드를 처음 봤을 경우 Integer List의 경우 전부 검증이 필요한가? 라는 생각이 들 수 있다.
이러한 문제는 6개의 숫자로만 이루어지고, 6개의 숫자는 서로 중복되지 않아야 하는 자료구조를 만들면 모두 해결될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Dice {
private static final int DICE_NUMBER_SIZE = 6;
private final List<Integer> diceNumbers;
public Dice(List<Integer> numbers) {
validateSize(numbers);
validateDuplicate(numbers);
this.diceNumbers = numbers;
}
// validateSize()..
// validateDuplicate()..
}
이렇게 검증 로직이 들어가 있는 일급 컬렉션을 만들면 비즈니스에 종속적인 자료구조가 만들어진다.
1
2
3
4
public void createDiceNumber() {
Dice dice = new Dice(createNonDuplicateNumbers()):
//dice를 활용한 로직 구현
}
Collection의 불변 보장
final로는 불변성을 온전히 보장할 수 없다.
final은 재할당을 막아줄 뿐이고 만약 컬렉션을 final로 했다면 컬렉션 내부의 값 변동은 막아줄 수 없다.
1
2
3
4
5
6
final Map<String, Integer> collection = new HashMap<>();
collection = new HashMap<>(); // 불가
collection.put("중복", 0);
collection.put("변할 수 있다", 1);
정상적으로 내부의 값 정보는 변한다는 것을 알 수 있다.
그러나 일급 컬렉션을 이용한다면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}
}
Wrapping할 때 컬렉션의 값을 변경할 수 있는 메소드가 없다면 불변 컬렉션이 되는 것이다.
1
2
3
public Collection<Order> getOrders() {
return Collections.unmodifiableList(orders);
}
불가피하게 getter가 필요한 경우 Collections.unmodifiable~ 메서드를 활용할 수도 있다.
내부의 값을 안전하게 보장한다. 만약 이렇게 반환된 컬렉션의 값을 변경하려고 한다면 예외처리가 된다.
상태와 행위를 한 곳에서 관리할 수 있다.
일급 컬렉션은 값과 함께 로직이 존재한다.
어떤 1,2,3,4학년의 정보들을 하나의 List로 관리하고 각 학년의 평균 수강 학점을 구하려고 한다고 가정하자. 일급 컬렉션 없이 구현한다면 1,2,3,4학년 각각의 정보별로 중복된 메서드를 생성하게 될 것이다.
그렇다면 코드의 길이도 길어지고 중복 코드가 발생함으로써 가독성 및 유지보수성이 떨어지게 된다.
이러한 문제를 일급 컬렉션을 생성하고 해당 메서드들을 일급 컬렉션 내에 만들어두고 외부에서 호출하여 사용하도록 한다면 상태와 행위를 한 곳에서 관리해 해결할 수 있다.
이름이 있는 컬렉션
1
2
List<Car> genesisCars = createGenesisCars();
List<Car> kiaCars = createKiaCars();
같은 자동차이지만 genesis의 CarList와 kia의 CarList는 다르다. 따라서 이 둘을 구분하기 위해 서로 다른 변수명을 지어주게 된다.
이렇게 되면 서비스의 사이즈가 커질 경우 단점들이 생긴다.
- 검색이 어렵다.
- 명확한 표현이 불가능하다.
- 변수 명에 불과하기 때문에 의미를 부여하기 힘들다.
이런 경우 Genesis와 Kia의 일급 컬렉션을 각각 만들면 이 컬렉션을 기반으로 용어 사용과 검색을 할 수 있게 된다.
1
2
GenesisCars genesisCars = new GenesisCars(createGenesisCars());
KiaCars kiaCars = new KiaCars(createKiaCars());