First Class Collection
일급 컬렉션(First Class Collection)은 언어가 제공하는 원시 컬렉션(List, Map, Set 등)을 한 번 래핑하고, 그 외의 다른 멤버 변수를 가지지 않는 클래스를 말한다. 이 클래스는 내부 컬렉션을 조작하고 검증하는 모든 비즈니스 규칙과 로직을 독점적으로 관리하며, 객체지향 프로그래밍과 도메인 주도 설계(DDD)에서 원시 컬렉션을 다루는 로직이 서비스 계층에 흩어지는 문제를 막기 위한 설계 패턴이다.
왜 필요한가: Service에 흩어지는 컬렉션 로직
List<Car>같은 원시 컬렉션을 그대로 노출해 사용하면, 우승자 판별처럼 특정 조건에 맞는 요소를 추출하거나 전체 이동처럼 모든 요소를 조작하는 도메인 로직이Service클래스 이곳저곳에 중복 작성된다.[1]- 원시 컬렉션을
Cars라는 일급 컬렉션으로 래핑하면 컬렉션 관련 비즈니스 로직을Cars내부로 이동시킬 수 있고, 그 결과Service계층에서 컬렉션 관리 로직이 제거되어 응집도가 높아진다.[1:1] - 원문 설계 예시도 우승자 판별과 최대 위치 계산을
Cars에 두고, 내부 컬렉션에 대한 외부 직접 접근을 막는 구성을 제시한다.[2]
// 문제 상태: Service에 컬렉션 로직이 분산
public class RacingService {
public List<Car> getWinners(List<Car> cars) {
int max = cars.streamgetPosition).max().orElse(0;
return cars.stream().filter(c -> c.getPosition() == max).toList();
}
}
// 해결: 일급 컬렉션 Cars 도입으로 로직 캡슐화
public class Cars {
private final List<Car> cars;
public Cars(String[] names) {
this.cars = Arrays.stream(names)
.maptrim
.mapnew
.toList();
}
public List<Car> getWinners() {
int max = cars.streamgetPosition).max().orElse(0;
return cars.stream().filter(c -> c.getPosition() == max).toList();
}
}
구조 규칙
일급 컬렉션을 만드는 절차는 다음 순서로 정리할 수 있다.
- 원시 컬렉션(
List,Set등)을 유일한 인스턴스 변수로 가지는 클래스를 만든다. - 생성자에서 컬렉션 요소의 유효성을 검증한다(크기, 중복, 특정 상태 조건 등).
- 외부로 컬렉션을 노출하지 않거나, 노출해야 할 경우 불변 컬렉션으로 반환해 외부의 임의 조작을 막는다.
- 컬렉션 요소를 다루는 비즈니스 로직(필터링, 상태 업데이트 등)을 일급 컬렉션의 메서드로 구현해, 외부(
Service)로 로직이 새어나가지 않게 한다.
이 구조는 "원시값을 포장(wrapping primitives)한다"는 원칙을 컬렉션으로 확장한 것이라고 볼 수 있다. 원시 컬렉션 자체는 도메인의 의미나 제약 조건을 표현할 수 없기 때문에, 이를 다루는 쪽(주로 Service나 Controller)이 검증·로직을 떠안는 논리적 누수가 생긴다는 것이 이 페이지의 해석이다.[3]
이웃 개념과의 경계
- DTO와의 차이: DTO는 데이터 전달만을 목적으로 하고 로직을 가지지 않지만, 일급 컬렉션은 상태를 검증·조작하는 도메인 로직을 포함한다.[3:1]
- 일반적인 래퍼 클래스와의 차이: 일급 컬렉션은 원시 컬렉션 단 하나만을 멤버 변수로 가져야 하며, 다른 부가적인 상태 변수를 가지지 않는 것이 원칙이다.[3:2]
절충점과 실패 모드
- 장점: 비즈니스에 종속적인 자료구조를 만들 수 있고(
List<Car>→Cars), 컬렉션 상태 조작과 관련된 불변성을 통제할 수 있으며, 상태와 행위를 한 곳에서 관리해 응집도가 높아진다. - 단점: 클래스 수가 늘어나 구조가 다소 무거워질 수 있고, 로직이 전혀 없는 단순 보관용 컬렉션까지 래핑하면 오버엔지니어링이 될 위험이 있다.
- 실패 모드
- 내부 상태 참조 노출:
get()등으로 내부 원시 컬렉션의 참조를 그대로 반환해, 외부에서add/remove로 컬렉션을 임의 조작하게 만드는 경우. - 껍데기뿐인 래핑: 일급 컬렉션을 만들었지만 주요 로직은 여전히
Service에 있고, 단순히 데이터를 전달하는 역할만 하는 경우. - 상태의 오염: 원시 컬렉션 외에 다른 부가 변수를 일급 컬렉션 내부에 추가해 객체의 책임이 불분명해지는 경우.
- 내부 상태 참조 노출:
관련
- Console MVC Design: Controller에서 컬렉션 조작 로직을 밀어내고 Domain 객체에 책임을 두는 구조와 직접 연결된다.
- Custom Exception Design: 일급 컬렉션 생성자나 메서드에서 도메인 검증 실패를 명시적으로 표현할 때 함께 쓰인다.
출처
테스트 질문
- 일급 컬렉션을 도입할 때 클래스가 가져야 하는 핵심적인 구조적 제약 조건은 무엇인가?
- 원시 컬렉션을
Service에서 직접 다룰 때 발생하는 논리적 누수란 무엇이며, 일급 컬렉션은 이를 어떻게 해결하는가? - 일급 컬렉션에서 컬렉션의 상태 변이를 막고 캡슐화를 유지하기 위해 주의해야 할 실패 모드는 무엇인가?
일급 컬렉션 도입 — Cars.md — "여러 대의 자동차(Car) 객체를 관리하는 로직과 상태 변이가 Service 클래스 이곳저곳에 중복 생성되며 코드가 복잡해짐.",
List<Car>를 포장하는Cars일급 컬렉션으로 컬렉션 관련 로직을 이동시키는 리팩토링 예시. ↩︎ ↩︎객체지향 설계 실전 —
Cars에 우승자 판별·최대 위치 계산을 모으고 외부의 내부 컬렉션 직접 접근을 차단하는 예시를 제시한다. ↩︎출처 매핑 미확인 — DTO와의 차이, 일반 래퍼 클래스와의 차이, "원시값 포장 원칙의 확장"이라는 해석은 이 페이지가 인용하는 소스(Cars 리팩토링 노트)에는 등장하지 않는다. 객체지향 설계·DDD 일반 원칙에서 널리 통용되는 구분이지만, 이 볼트 안에서 별도로 grep 가능한 출처는 아직 없다. ↩︎ ↩︎ ↩︎