들어가며
개발자로 성장하기 위해서는 기술을 익히는 것도 중요하지만
배우고 느낀 것들을 글로 잘 녹여내는 능력도 중요하다는 걸 새삼 깨닫고 있다..
그런 의미에서, 이번에는 내가 공부한 일급 컬렉션에 대해서 정리해보고자 한다.
일급 컬렉션이란?
일급 컬렉션이란 컬렉션을 하나의 클래스로 감싸 관리하는 객체를 말한다.
단순히 List, Set 같은 자료구조를 직접 사용하는 것이 아니라, 그 안에 비즈니스 로직과 제약 조건을 부여하는 것이 핵심
일급 컬렉션의 규칙
- 컬렉션 외에 다른 필드가 없어야 한다.
- 컬렉션 자체의 불변성을 유지해야 한다.
- 비즈니스 로직을 포함해 의미 있는 메서드를 제공해야 한다.
규칙을 말로 보는 것보다는 예제와 함께 보는 것이 이해하기 훨씬 수월하기 때문에, 예제와 함께 보자.
예제 코드
먼저, 학생의 점수를 여러 개 관리해야 하는 경우를 생각해 보자.
점수를 리스트로 직접 관리하면서 평균 점수를 계산하는 Student 클래스가 존재할 수 있다.
일급 컬렉션을 사용하지 않은 경우
import java.util.List;
public class Student {
private final List<Integer> scores;
public Student(List<Integer> scores) {
validateScores(scores); // 직접 검증 로직 포함
this.scores = scores;
}
private void validateScores(List<Integer> scores) {
for (int score : scores) {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("점수는 0에서 100 사이여야 합니다.");
}
}
}
public double calculateAverage() {
return scores.stream().mapToInt(Integer::intValue).average().orElse(0);
}
public int getScoreCount() {
return scores.size();
}
}
이 방식에서는 Student 클래스가 점수 리스트 scores를 직접 관리하고, 검증하고, 평균 점수 계산 및 점수 개수를 반환하는 역할도 수행하고 있는 것을 알 수 있다.
일단 이 코드는 확실한 문제점이 존재한다.
- 과도한 책임 부여 - Student 클래스가 점수 리스트를 직접 관리하고 검증 로직과 점수 관련 로직까지 모두 책임지고 있다.
- 응집력 부족 - Student 클래스가 학생 정보뿐만 아니라 점수 리스트의 검증 및 계산까지 책임지고 있어 역할이 분리되지 않다.
- 확장성 부족 - 만약 점수 관련 로직이 더 필요해지면 Student 클래스가 불필요하게 복잡해질 가능성이 있다.
일급 컬렉션을 사용한 경우
import java.util.Collections;
import java.util.List;
public class Scores {
private final List<Integer> scores;
public Scores(List<Integer> scores) {
validateScores(scores);
this.scores = Collections.unmodifiableList(new ArrayList<>(scores));;
}
private void validateScores(List<Integer> scores) {
for (int score : scores) {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("점수는 0에서 100 사이여야 합니다.");
}
}
}
public double calculateAverage() {
return scores.stream().mapToInt(Integer::intValue).average().orElse(0);
}
public int size() {
return scores.size();
}
}
Scores라는 일급 컬렉션 클래스를 도입하면, 점수 리스트와 관련된 모든 책임을 Scores에게 분리시킬 수 있다.
확실히 '점수'와 '학생'이라는 관점에서, 점수의 책임은 점수 그 자체가 가지게 하는 것이 자연스럽고 잘 분리된 설계라는 것을 느낄 수 있을 것이다.
이쯤에서 일급 컬렉션의 규칙을 보고 해당 클래스가 일급 컬렉션에 해당하는지 확인해보자.
일급 컬렉션의 규칙
- 컬렉션 외에 다른 필드가 없어야 한다. -> scores 리스트만 사용됨.
- 컬렉션 자체의 불변성을 유지해야 한다. -> 반환할 때 unmodifiableList(new ArrayList<>())을 사용.
- 비즈니스 로직을 포함해 의미 있는 메서드를 제공해야 한다. -> 점수 자체 검증 로직, 평균 계산, 점수 개수 반환
even한 일급 컬렉션이다.
또한, 점수를 반환해주는 메서드에서 unmodifiableList 라는 메서드를 발견할 수 있다. 이는 리스트의 내부가 변경되는 메서드(add(), set(), remove())들이 사용되면 예외를 던져주게끔 하는 '방어적 복사' 를 의미한다.
하지만 이 unmodifiableList 가 항상 불변성을 보장해주는 것은 아니다.
해당 내용은 블로그를 참조하자.
쉽게 말하자면,
단순히 unmodifiableList 만 사용할 경우, 원본 리스트를 바라보는 복사된 리스트가 생성되는 것인데, 복사된 리스트에 대한 접근을 차단할 수 있을지는 몰라도 원본 리스트의 접근을 차단할 수는 없기 때문에 문제가 발생하는 것이다.
원본 리스트가 변경되면 그 주소를 참조하고 있는 복사된 리스트 또한 변경되기 때문이다.
아무튼, 이렇게 Scores 라는 점수와 관련된 컬렉션 객체를 정의했을 때, 아래의 Student 클래스는 점수와 관련된 모든 로직은 Scores에게 위임한채로 학생 정보만 책임질 수 있게 된다.
import java.util.List;
public class Student {
private final Scores scores;
public Student(List<Integer> scores) {
this.scores = new Scores(scores);
}
public double getAverageScore() {
return scores.calculateAverage();
}
public int getScoreCount() {
return scores.size();
}
}
해당 예제에서 일급 컬렉션의 개념을 도입함으로써 얻을 수 있는 이점은 다음과 같다.
- 단일 책임 원칙(SRP) 준수 - Scores 클래스가 점수와 관련된 모든 로직을 책임지고, Student는 학생 정보에 집중할 수 있어 응집력이 높아진다. 즉, 점수와 관련된 도메인의 독립성은 강화되고 학생을 관리하는 비즈니스 로직에서는 도메인 로직에 대한 책임이 없어지기 때문에 도메인 의존성이 감소하게 된다.
- 중복 로직 제거 - 점수 관련 검증, 계산 로직을 Scores에서 일관되게 관리할 수 있어, 다른 곳에서도 Scores를 재사용하기 용이해진다.
- 불변성 유지 - Scores에서 Collections.unmodifiableList로 불변성을 보장하여, 외부에서 점수를 수정하는 등의 접근을 차단할 수 있게된다.
- 확장성 - 점수에 대한 기능이 추가되더라도 Scores 클래스 내에서 처리하면 되기 때문에 Student 클래스는 수정이 필요 없어지며 유지보수를 쉽게 할 수 있게 된다.
예제를 읽어도 '일급 컬렉션' 의 의미를 정확하게 분간하지 못할 것 같아서 부연 설명 해보자면,
예제를 작성해보니 일급 컬렉션이라는 용어를 정확하게 정의하진 못한 것 같아서..
일급 컬렉션의 의미는 Collection 클래스를 또 하나의 클래스로 Wrapping하는 것을 의미한다.
일급 컬렉션의 장점
흔히들 일급 컬렉션의 장점을 4가지 정도로 뽑는데, 그것은 다음과 같다.
- 도메인의 독립성 강화
- 비즈니스 로직에서 도메인 의존성 감소
- Collection의 불변성을 보장됨
- 상태와 행위를 한 곳에서 관리할 수 있음
- 컬렉션의 이름을 부여해서 관리할 수 있음
해당 내용은, 사실 이것보다 더 잘 정리할 수 있었을까? 라는 생각을 하게됐던 블로그의 링크를 첨부할테니 한 번씩 읽어보고 장점을 피부로 느꼈으면 좋겠다.
왜 일급 컬렉션을 사용할까?
그래서, 내가 이해한 일급 컬렉션의 사용 이유는 무엇일까?
그건 바로, 도메인 객체가 본질적으로 해야 할 일과 역할을 보다 명확히 하고, 그에 따라 코드의 응집력과 유지보수성을 높이기 위해서이다.
또한 장점에 맞춰서 설명해보면 다음과 같다.
- 단일 책임 원칙(Single Responsibility Principle, SRP) 준수 가능
- 일급 컬렉션을 사용하면 컬렉션과 관련된 책임을 컬렉션 객체(Scores)에게 위임하고, 다른 도메인 객체(Student)는 그 책임에서 벗어날 수 있다.
- 이를 통해 각 클래스가 자신이 맡은 역할에 집중할 수 있게 되어 응집력이 강화된다.
- 도메인의 독립성 강화 및 비즈니스 로직 의존성 감소
- 컬렉션과 관련된 모든 행위가 일급 컬렉션 내에서 수행되기 때문에 비즈니스 로직은 더 이상 도메인의 세부 사항을 알 필요가 없다.
- 이렇게 하면 도메인이 비즈니스 로직과 분리되어 독립성을 유지할 수 있으며, 비즈니스 로직은 컬렉션이나 도메인의 세부 구현을 알지 않고도 작업을 처리할 수 있다.
- 불변성 보장
- 컬렉션을 일급 컬렉션으로 감싸면서 Collections.unmodifiableList 등을 사용해 컬렉션의 변경을 방지할 수 있다. 이렇게 하면 외부에서 컬렉션을 수정하지 못하도록 막을 수 있어 코드의 안정성을 높일 수 있다.
- 중복 로직 제거 및 코드 재사용성 증대
- 점수와 관련된 검증과 계산 등의 로직이 Scores 클래스 내에 집중되기 때문에 중복을 줄이고 일관성을 유지할 수 있다.
- 다른 클래스나 기능에서 동일한 점수와 관련된 컬렉션 로직이 필요할 때, 일관성을 가지는 Scores 클래스를 재사용할 수 있다.
- 상태와 행위를 한 곳에서 관리 가능
- 컬렉션과 관련된 상태(데이터)와 그 데이터에 대한 행위(비즈니스 로직)를 일급 컬렉션으로 한 곳에 모으면서 코드의 관리가 쉬워진다.
- 각 도메인 객체는 자신이 담당해야 할 데이터와 로직을 포함하고 있기 때문에, 유지보수가 수월해지고 필요할 때마다 특정 기능을 한 곳에서 확장하거나 수정할 수 있다.
- 컬렉션에 의미 있는 이름을 부여
- 컬렉션 자체가 도메인에서 중요한 개념일 때, 일급 컬렉션으로 감싸면 그 의미를 명확히 나타낼 수 있다.
- 예를 들어 Scores라는 이름은 단순히 list라는 리스트를 넘기는 것보다 점수와 관련된 역할을 분명하게 전달할 수 있다.
번외 - 내가 짠 코드는 일급 컬렉션이었을까?
package racingcar.Domain;
import java.util.HashMap;
import java.util.List;
public class CarManager {
HashMap<String, Integer> carList = new HashMap();
public CarManager(List<String> carNames) {
for (String carName : carNames) {
carList.put(carName, 0);
}
}
public void moveForward(String carName, Integer randomNumber) {
if (randomNumber >= 4) {
Integer currentPosition = carList.get(carName);
carList.put(carName, currentPosition + 1);
}
}
public HashMap<String, Integer> exportCarList() {
return carList;
}
}
위는 내가 프리코스 2주차에 작성했던 코드이다. 그런데 사실 일급 컬렉션에 대해서 잘 모르고 작성한 코드였는데, 거의 일급 컬렉션에 근접한 코드를 작성했었다는 사실이 놀라웠다.
내 코드는 무슨 문제가 있을까?
그건 바로, 컬렉션의 불변성을 보장하지 못했다는 점.
HashMap을 그대로 반환하는 exportCarList() 메서드는 외부에서 컬렉션을 수정할 수 있었다. 이는 불변성을 보장하지 못하는 설계.
'Java > TIL' 카테고리의 다른 글
| [JAVA-TIL] JWT, JJWT 구현 (0) | 2025.10.30 |
|---|---|
| [Java] 안드로이드와 웹 서버 간 REST API 통신하기 – Retrofit 활용 (1) | 2024.10.24 |