티스토리 뷰
☝ 일급 컬렉션
일급 컬렉션이란 하나의 컬렉션을 감싸는 클래스를 만들고 해당 클래스에서 컬렉션과 관련된 비즈니스 로직을 관리하는 패턴이다. Wrapping된 컬렉션 이외의 다른 멤버 변수는 없어야한다.
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
this.orders = orders;
}
}
여기서 Order List 컬렉션을 감싼 Orders 클래스가 일급 컬렉션이다.
왜 사용하는가?
1️⃣ 비즈니스에 종속적인 자료구조
일급 컬렉션에 비즈니스 로직을 작성하면 중복 제거, 안정성 향상, 유지보수성 증가 등의 이점을 얻을 수 있다.
Orders라는 일급 컬렉션이 Order 클래스를 관리한다고 해보자. 그리고 다음과 같은 시나리오가 있다.
1. 주문 데이터 관리 : Order는 개별 주문을 나타내고, Orders는 다수의 Order를 담는 컬렉션이ㅏㄷ.\
2. 비즈니스 규칙
- 주문 목록에는 반드시 한 개 이상의 주문이 있어야 한다.
- 주문 목록은 변경 불가하다.
- 전체 주문 금액을 계산하는 로직이 필요하다.
public class Order {
private final long amount; // 주문 금액
public Order(long amount) {
if (amount <= 0) {
throw new IllegalArgumentException("주문 금액은 0보다 커야 합니다.");
}
this.amount = amount;
}
public long getAmount() {
return amount;
}
}
기본적인 Order 클래스이다.
💡 먼저 Orders라는 일급 컬렉션을 사용하지 않고 비즈니스 로직을 구현해보자. 구현은 OrderService 클래스에서 한다.
public class OrderService {
public void processOrders(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
}
// 주문 처리 로직
long totalAmount = calculateTotalAmount(orders);
System.out.println("총 주문 금액: " + totalAmount);
}
private long calculateTotalAmount(List<Order> orders) {
return orders.stream().mapToLong(Order::getAmount).sum();
}
}
어떤 문제가 있을까?
- 중복 로직의 위험 : orders가 null인지, 비어있는지 확인하는 검증 로직은 주문 객체가 필요한 모든 곳에서 필요하다. 즉, 예외 처리 코드가 중복된다.
- 데이터 무결성 보장 어려움 : List<Order>가 매개변수로 사용되기 때문에 외부에서 데이터를 변경할 수 있다.
- 비즈니스 로직 분산 : calculateTotalAmount()같은 총 주문 금액 계산 비즈니스 로직은 Orders 클래스에 더 적합하다.
💡 이를 해결하기 위해 Orders라는 일급 컬렉션을 사용해보자
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
}
this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
}
public long calculateTotalAmount() {
return orders.stream().mapToLong(Order::getAmount).sum();
}
public List<Order> getOrders() {
return orders;
}
}
이렇게 Orders 클래스에서 비즈니스 로직을 처리해줄 수 있다. 그렇게 되면 검증 로직이 Orders 생성자에 존재하므로 Orders 객체는 항상 유효한 상태를 가진다. 물론 검증 로직의 중복도 제거된다.
또한 내부에서 불변 컬렉션으로 만들어 주어서 객체의 불변성이 보장된다.
마지막으로 총 주문 금액 계산 같은 비즈니스 로직이 Orders 클래스 안에 존재하므로 코드의 응집도가 높아지고 OrderService 클래스의 책임도 줄어든다.
public void processOrders(Orders orders) {
long totalAmount = orders.calculateTotalAmount();
System.out.println("총 주문 금액: " + totalAmount);
}
이렇게 OrderService의 processOrders() 메소드가 한층 깔끔해졌다.
2️⃣ 컬렉션의 불변성 보장
일급 컬렉션은 wrapping된 컬렉션의 불변성을 보장한다.
단순히 final 키워드를 선언하는 것과는 다르다. final 키워드는 재할당을 금지하지만 한 번 선언하고 값을 변경하는 것은 가능하다. 즉, 불변성을 완전히 보장하지는 않는다.
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
}
this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
}
public long calculateTotalAmount() {
return orders.stream().mapToLong(Order::getAmount).sum();
}
public List<Order> getOrders() {
return Collections.unmodifiableList(orders);
}
}
위의 코드에서 생성자와 getOrders() 메소드는 Collections.unmodifiableList() 를 사용해 불변 리스트로 변환하고 그 값을 반환함으로써 불변성을 보장하고 있다. 즉, Order객체는 생성 후 값을 변경할 수 없다.
코드로 예시를 보자
💡 먼저, Orders 일급 컬렉션을 사용하지 않은 코드이다.
public class FirstCollectionMain {
public static void main(String[] args) {
Order order1 = new Order(10);
Order order2 = new Order(20);
List<Order> orders = Arrays.asList(order1, order2);
OrderService orderService = new OrderService();
orderService.processOrders(orders);
try {
orders.set(0, new Order(20)); // 맨 처음 order 객체 수정 시도
} catch (Exception e) {
System.out.println("데이터 수정 시도 실패: " + e.getMessage());
}
// 다시 서비스 메소드 호출
orderService.processOrders(orders);
}
}
- 생성한 order 객체들을 orders라는 List에 담고 orderService의 processOrders() 메소드의 인자로 담아 호출했다.
그럼 맨 처음 processOrders(orders)는 총 주문 금액 : 30 이라고 출력된다. - 그런데 그 이후 orders라는 List에 담긴 맨 처음 order 객체의 주문량을 20으로 수정했다. 즉, 외부에서 객체 값을 수정한 것이다. 그 이후 processOrders(orders)를 호출하면 총 주문 금액 : 40 이라고 출력된다. 객체 값이 변경된 것이다.

💡 이번엔 Orders라는 일급 컬렉션을 사용해보자
public class FirstCollectionMain {
public static void main(String[] args) {
Order order1 = new Order(10);
Order order2 = new Order(20);
Orders orders = new Orders(Arrays.asList(order1, order2));
OrderService orderService = new OrderService();
orderService.processOrders(orders);
// 외부에서 orders 내부 데이터 수정 시도
try {
orders.getOrders().set(0, new Order(20)); // UnsupportedOperationException 발생
} catch (UnsupportedOperationException e) {
System.out.println("데이터 수정 시도 실패: " + e.getMessage());
}
// 다시 서비스 메소드 호출
orderService.processOrders(orders);
}
}
마찬가지로 맨 처음 order 객체를 수정하려고 했지만 이번에는 UnsupportedOperationException 예외가 발생한다.
결과를 보면

이렇게 맨 처음의 order 객체의 값이 수정되지 않은 것을 알 수 있다.
3️⃣ 객체의 상태와 동작을 한 곳에서 관리
일급 컬렉션은 객체의 상태와 동작을 한 곳에서 관리할 수 있게 해준다.
만약 Order 클래스의 멤버 변수에 주문 타입이 추가된다고 해보자
public class Order {
private final long amount; // 주문 금액
private final String type;
public Order(long amount, String type) {
if (amount <= 0) {
throw new IllegalArgumentException("주문 금액은 0보다 커야 합니다.");
}
this.amount = amount;
this.type = type;
}
public long getAmount() {
return amount;
}
public String getType() {
return type;
}
}
💡 먼저, Orders 일급 컬렉션을 사용하지 않은 코드이다.
type이 "QUICK"인 order 객체의 합을 구하는 코드를 작성하고자 한다.
public static void main(String[] args) {
Order order1 = new Order(10, "QUICK");
Order order2 = new Order(20, "SLOW");
List<Order> orders = Arrays.asList(order1, order2);
long quickOrderAmountSum = orders.stream()
.filter(order -> order.getType().equals("QUICK"))
.mapToLong(Order::getAmount)
.sum();
System.out.println(quickOrderAmountSum);
}
List에 데이터를 담고, 합을 구하는 로직을 수행했다. 어떤 문제가 있을까?
- 로직의 분산 : 현재 "QUICK" 타입의 order 합계 계산이라는 비즈니스 로직이 main 클래스에 존재한다.
- 상태와 동작의 분리 : List<Order>는 단순히 상태를 담고 있을 뿐, 데이터에 대한 동작은 외부에서 처리해야 한다.
- 새로운 요구사항 대응 어려움 : 만약 "SLOW" 타입의 order 합계를 계산하는 로직이라는 요구사항이 추가되거나 다른 타입이 추가되면 관련 로직을 다시 만들거나 일일이 수정해야 한다.
💡 이번엔 Orders라는 일급 컬렉션을 사용해보자
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
if (orders == null || orders.isEmpty()) {
throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
}
this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
}
public long calculateTotalAmount() {
return orders.stream().mapToLong(Order::getAmount).sum();
}
public List<Order> getOrders() {
return Collections.unmodifiableList(orders);
}
public long calculateAmountSumByType(String type) {
return orders.stream()
.filter(order -> order.getType().equals(type))
.mapToLong(Order::getAmount)
.sum();
}
}
이렇게 calculateAmountSumByTpe(String Type)이라는 메소드를 만들었다.
public static void main(String[] args) {
Order order1 = new Order(10, "QUICK");
Order order2 = new Order(20, "SLOW");
Orders orders = new Orders(Arrays.asList(order1, order2));
// QUICK 주문 합계 계산
long quickOrderAmountSum = orders.calculateAmountSumByType("QUICK");
System.out.println("QUICK 주문 합계: " + quickOrderAmountSum);
// SLOW 주문 합계 계산
long slowOrderAmountSum = orders.calculateAmountSumByType("SLOW");
System.out.println("SLOW 주문 합계: " + slowOrderAmountSum);
}
먼저, calculateAmountSumByTpe() 이라는 메소드 하나로 모든 타입의 order 합계를 구할 수 있게 되었다. 즉, 타입이 추가되어도 로직을 중복해서 작성할 필요가 없는 것이다.
그리고 상태와 동작을 한 곳에서 관리할 수 있게 되었다. Orders안에서 order 객체들을 List로 관리하고 관련 동작(행위)도 처리하고 있다. 외부에서 데이터 처리 작업을 이해하지 않아도 Orders 클래스의 메소드를 호출하기만 하면 작업이 수행되는 것이다.
일급 컬렉션을 사용하여 로직과 데이터를 한 곳에서 관리하게 된 것이다. 즉, 코드의 응집도가 높아진다.
일급 컬렉션을 객체지향적인 코드를 작성하기 위한 좋은 선택이라고 할 수 있다.
참고 자료
https://jojoldu.tistory.com/412
일급 컬렉션 (First Class Collection)의 소개와 써야할 이유
최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코
jojoldu.tistory.com
'java' 카테고리의 다른 글
동일성과 동등성 (0) | 2024.12.03 |
---|---|
얕은 복사와 깊은 복사 (0) | 2024.11.27 |
자바의 Checked Exception VS Unchecked Exception (1) | 2024.11.21 |
인터페이스 vs 추상클래스 (0) | 2024.05.16 |
상속과 메모리 구조 (0) | 2023.12.21 |
- Total
- Today
- Yesterday
- NPE
- @Spring
- null
- 생성자
- 이진탐색
- 일급컬렉션
- springboot
- N+1문제
- 동등성
- JPA
- Java
- 메인메소드
- StreamAPI
- 자바
- 백준
- checkedException
- 유효성 검사
- Optional
- Spring
- lowerBound
- @Value
- @ConfigurationProperties
- id생성전략
- ddl-auto
- uncheckedException
- 티스토리챌린지
- upperBound
- @NoArgsConstructor
- Thymeleaf
- 오블완
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |