VO (Value Object)와 일급 컬렉션
VO
: 도메인에서 한 개 또는 그 이상의 속성들을 묶어서 특정 값을 나타내는 객체
: 도메인 객체의 일종
: 보통 기본 키로 식별값을 갖는 Entity와 구별해서 사용
VO는 어떤 조건들에 의해 엔티티와 구별될까?
1. equals & hash code 메서드를 재정의해야 한다
타입도 같고, 내부의 속성값도 같은 두 객체가 있으면 당연히 같은 객체로 취급하고 싶을 것임
근데 실제로 값이 같은 두 객체를 생성하고 동일성 비교를 해보면 둘은 서로 다른 객체로 구별됨
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
@Test
void equals() {
Point point = new Point(2, 3);
Point point2 = new Point(2, 3);
// point != point2
assertThat(point == point2).isFalse(); // 동일성 비교
}
분명 같은 위치를 가리키고 있는데 다른 위치라고 판단
이 문제를 해결하기 위해서 우선은 동일성 비교와 동등성 비교의 차이를 알아야함
동일성(==) 비교 : 객체가 참조하고 있는 주솟값을 확인
point와 point2가 참조하고 있는 이 메모리 주솟값은 서로 다르고, 임의로 같게 만들 수 없음
따라서 객체가 포함하고 있는 속성값들을 기준으로 객체를 비교하는 동등성 비교를 통해 객체를 비교해야 함
동등성 비교는 equals 메서드를 재정의함으로써 가능
어떤 속성값들을 기준으로 동등성 비교를 할 것인지는 다음과 같이 직접 equal 메서드의 재정의를 통해 정해야함
// equals & hashcode 재정의
...
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Point point = (Point) o;
return x == point.x &&
y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
...
hashCode는 객체를 식별할 하나의 정숫값을 가리키고 재정의하지 않으면 메모리 주솟값을 사용해서 해시값을 만듦
해시 코드를 재정의해주면 특정 값을 기준으로 같은 해시 코드를 얻을 수 있고,
이는 해시값을 사용하는 컬렉션 등에서 객체를 비교하는 용도로 사용
equals와 hashCode를 재정의하면, VO를 사용할 때 속성값이 같은 객체는 같은 객체임을 보장하면서
VO를 사용할 수 있음
2. 수정자(setter)가 없는 불변 객체여야 한다
Entity :
따로 식별 값을 갖고 있기 때문에 내부 속성값들이 변경된다 하더라도 같은 객체로 계속 인식하고 추적 가능
VO :
값이 바뀌면 다른 값이 되어 추적이 불가하고, 복사될 때는 의도치 않은 객체들이 함께 변경되는 문제를 유발
그래서 VO는 반드시 값을 변경할 수 없는 불변 객체로 만들어야함
// Order.java
public class Order {
private String restaurant;
private String food;
private int quantity;
// Getter, Setter ...
// equals & hashcode
}
// Main.java
public static void main(String[] args) {
Order 첫번째주문 = new Order();
첫번째주문.setRestaurant("황제떡볶이");
첫번째주문.setFood("매운떡볶이");
첫번째주문.setQuantity(2);
// 첫번째주문 = {restaurant='황제떡볶이', food='매운떡볶이', quantity=2}
Order 두번째주문 = 첫번째주문;
// 두번째주문 = {restaurant='황제떡볶이', food='매운떡볶이', quantity=2}
두번째주문.setFood("안매운떡볶이"); //** 주문 변경
두번째주문.setQuantity(3); //** 주문 변경
// 첫번째주문 = {restaurant='황제떡볶이', food='안매운떡볶이', quantity=3}
// 두번째주문 = {restaurant='황제떡볶이', food='안매운떡볶이', quantity=3}
}
Order은 VO
첫 번째 주문 값을 두번째 주문에 그대로 복사한 곳
두 번째 주문은 첫 번째 주문 값을 복사한 것이 아닌
참조하고 있는 메모리 주소를 복사했기 때문에 주문 내용이 바뀌면 메모리 안에 저장된 실제 값이 변경
같은 메모리를 참조하고 있는 첫 번째 주문의 내용도 변경된 값을 가리키게 되는 것 (치명적인 오류)
VO는 중간에 그 값이 변하지 않도록 만들어야 함
즉, 값을 변경할 수 있는 수정자(setter)가 없어야 함
생성자를 통해서 객체가 생성될 때, 값이 한 번만 할당되고 이후로는 변경되지 않도록
Setter이 아닌 생성자를 통해 Order 객체를 불변으로 만들기!!
public class Order {
private String restaurant;
private String food;
private int quantity;
public Order(String restaurant, String food, int quantity) {
this.restaurant = restaurant;
this.food = food;
this.quantity = quantity;
}
// only getter..
}
public static void main(String[] args) {
Order 첫번째주문 = new Order("황제떡볶이", "매운떡볶이", 2);
// 첫번째주문 = {restaurant='황제떡볶이', food='매운떡볶이', quantity=2}
Order 두번째주문 = new Order("황제떡볶이", "매운떡볶이", 2)
// 두번째주문 = {restaurant='황제떡볶이', food='매운떡볶이', quantity=2}
두번째주문 = new Order("황제떡볶이", "안매운떡볶이", 3) //** 주문 변경
// 첫번째주문 = {restaurant='황제떡볶이', food='매운떡볶이', quantity=2}
// 두번째주문 = {restaurant='황제떡볶이', food='안매운떡볶이', quantity=3}
}
이제는 setter가 없으므로 생성자를 통해 객체를 새로 생성하고 재할당
VO를 사용하면 얻을 수 있는 이점
1. 객체가 생성될 때 해당 객체안에 제약사항을 추가할 수 있음
2. 생성될 인스턴스가 정해져 있는 경우에는
미리 인스턴스를 생성해놓고 캐싱하여 성능을 높이는 방법도 고려해볼 수 있음
3. Entity의 원시 값들을 VO로 포장하면 Entity가 지나치게 거대해지는 것을 막을 수 있음
( 테이블 관점이 아닌 객체 지향적인 관점으로 프로그래밍 가능)
컬렉션도 VO의 역할을 한다면, 일급 컬렉션과 같은 불변객체로 만들어서 사용 가능
일급 컬렉션?
"컬렉션을 감싸는 하나의 클래스를 만들어, 컬렉션을 관리하는 객체"
: 컬렉션을 래핑하여 단 하나의 멤버 변수만을 가지는 객체
: Collection을 Wrapping하면서, Wrapping한 Collection 외 다른 멤버 변수가 없는 상태
예시)
💡 [문제점] 일반적인 List 사용
Order
public class Order {
private List<Item> items;
public Order(List<Item> items) {
this.items = items;
}
public List<Item> getItems() {
return items;
}
}
단점
1. Order 클래스에서 List<Item>을 그대로 사용하므로, 외부에서 items.add()를 통해 직접 조작 가능 (불변성 깨짐)
2. List<Item> 관련 비즈니스 로직이 Order 클래스 곳곳에 흩어질 가능성 높음
💡 [해결책] 일급 컬렉션 적용
Items
public class Items {
private final List<Item> items;
public Items(List<Item> items) {
this.items = new ArrayList<>(items); // 방어적 복사
}
public List<Item> getItems() {
return Collections.unmodifiableList(items); // 불변 리스트 반환
}
public int totalPrice() { // 비즈니스 로직 캡슐화
return items.stream()
.mapToInt(Item::getPrice)
.sum();
}
public void addItem(Item item) {
items.add(item);
}
}
Order
public class Order {
private final Items items;
public Order(Items items) {
this.items = items;
}
public int getTotalPrice() {
return items.totalPrice();
}
}
- Order 클래스가 Items 객체만 관리하므로 책임이 명확해짐
- List<Item>을 직접 노출하지 않고, Collections.unmodifiableList()로 불변성 유지
- totalPrice()와 같은 비즈니스 로직이 Items 내부에 위치하여 코드의 응집도가 올라감
예시 2)
public class Person {
private String name;
private List<Car> cars;
// ...
}
public class Car {
private String name;
private String oil;
// ...
}
위 코드에서 List<Car>과 같은 컬렉션 부분을 다음과 같이 바꾸는 것
public class Person {
private String name;
private Cars cars;
// ...
}
// List<Car> cars를 Wrapping
// 일급 컬렉션
public class Cars {
// 멤버변수가 하나 밖에 없다!!
private List<Car> cars;
// ...
}
public class Car {
private String name;
private String oil;
// ...
}
왜 사용할까?
다음과 같은 코드가 있음
// GSConvenienceStore.class
public class GSConvenienceStore {
// 편의점에는 여러 개의 아이스크림을 팔고 있을 것이다.
private List<IceCream> iceCreams;
public GSConvenienceStore(List<IceCream> iceCreams) {
this.iceCreams = iceCreams;
}
...
}
// IceCream.class
public class IceCream {
private String name;
...
}
편의점은 아이스크림의 종류를 10가지 이상 팔지 못할 때,
List<IceCream> iceCreams의 size가 10이 넘으면 안되는 검증이 필요할 것
// GSConvenienceStore.class
public class GSConvenienceStore {
private List<IceCream> iceCreams;
public GSConvenienceStore(List<IceCream> iceCreams) {
validateSize(iceCreams)
this.iceCreams = iceCreams;
}
private void validateSize(List<IceCream> iceCreams) {
if (iceCreams.size() >= 10) {
new throw IllegalArgumentException("아이스크림은 10개 이상의 종류를 팔지 않습니다.")
}
}
// ...
}
위 코드의 문제점
1. 만약 아이스크림뿐만 아니라 과자, 라면 등 여러 가지가 있다고 가정할 때
모든 검증을 GSConvenienceStore class에서 할 것인가?
2. 만약 CUConvenienceStore class에서도 동일한 것을 판다면
GSConvenienceStore class에서 했던 검증을 또 사용할 것인가?
잘못된 코드
// GSConvenienceStore.class
public class GSConvenienceStore {
private List<IceCream> iceCreams;
private List<Snack> snacks;
private List<Noodle> Noobles;
public GSConvenienceStore(List<IceCream> iceCreams ...) {
validate아이스크림(아이스크림);
validate과자(과자);
validate라면(라면);
// ...
}
// ...
}
// CUConvenienceStore.class
public class CUConvenienceStore {
private List<IceCream> iceCreams;
private List<Snack> snacks;
private List<Noodle> Noobles;
public CUConvenienceStore(List<IceCream> iceCreams ...) {
validate아이스크림(아이스크림);
validate과자(과자);
validate라면(라면);
// ...
}
// ...
}
3. List<IceCream> iceCreams의 원소 중에서 하나를 find하는 메서드가 필요할 때
GSConvenienceStore class와 CUConvenienceStore class 같은 메서드(find)를 두번 구현할 것인가?
// GSConvenienceStore.class
public class GSConvenienceStore {
private List<IceCream> iceCreams;
// ...
public IceCream find(String name) {
return iceCreams.stream()
.filter(iceCream::isSameName)
.findFirst()
.orElseThrow(RuntimeException::new)
}
// ...
}
// CUConvenienceStore.class
public class CUConvenienceStore {
private List<IceCream> iceCreams;
// ...
public IceCream find(String name) {
return iceCreams.stream()
.filter(iceCream::isSameName)
.findFirst()
.orElseThrow(RuntimeException::new)
}
// ...
}
편의점 class의 역할이 무거워 지고, 중복코드가 많아짐
위 코드를 일급 컬렉션으로 만든다면?
1. 아이스크림 객체를 따로 생성
// IceCream.class
public class IceCreams {
private List<IceCream> iceCreams;
public IceCreams(List<IceCream> iceCreams) {
validateSize(iceCreams)
this.iceCreams = iceCreams
}
private void validateSize(List<IceCream> iceCreams) {
if (iceCreams.size() >= 10) {
new throw IllegalArgumentException("아이스크림은 10개 이상의 종류를 팔지않습니다.")
}
}
public IceCream find(String name) {
return iceCreams.stream()
.filter(iceCream::isSameName)
.findFirst()
.orElseThrow(RuntimeException::new)
}
// ...
}
2. 편의점 class는 다음과 같이 변경됨
// GSConvenienceStore.class
public class GSConvenienceStore {
private IceCreams iceCreams;
public GSConvenienceStore(IceCreams iceCreams) {
this.iceCreams = iceCreams;
}
public IceCream find(String name) {
return iceCreams.find(name);
}
// ...
}
// CUConvenienceStore.class
public class CUConvenienceStore {
private IceCreams iceCreams;
public CUConvenienceStore(IceCreams iceCreams) {
this.iceCreams = iceCreams;
}
public IceCream find(String name) {
return iceCreams.find(name);
}
// ...
}
// 만약 find메서드 중복되는 것이 신경쓰인다면 부모 클래스를 만들어 상속을 사용하세용:)
과자랑 라면이 생겨도 과자/라면의 일급 컬렉션이 해결해줄 것
이러한 구조는 컬렉션에 대한 조작을 통제하고 비즈니스 로직을 캡슐화하는 데 유용
1) 객체지향 프로그래밍에서 중요한 역할함
→ 객체의 상태와 행위를 한 곳에서 관리할 수 있게 해주며, 불변성을 보장하여 부작용을 줄일 수 있기 때문
2) 코드의 명확성과 안전성, 유지보수성 향상
(코드의 재사용성을 높이고, 오류 가능성을 줄이는 데 도움)
컬렉션에 대한 조작을 제한하고, 관련 로직을 한 곳에서 관리, 테스트가 용이해지기 때문
관련 로직이 한 곳에 집중되어 있어, 변경 사항이 발생할 경우 해당 부분만 수정하면 되기 때문
구현 예제)
컬렉션을 멤버 변수로 가지는 클래스를 정의> 해당 컬렉션에 대한 조작을 제어하는 메소드를 제공
이를 통해 컬렉션에 대한 직접적인 접근을 제한하고, 관련 로직을 캡슐화할 수 있기 때문
public class GameResults {
private final List<GameResult> results = new ArrayList<>();
public void addResult(GameResult result) {
results.add(result);
}
public List<GameResult> getResults() {
return new ArrayList<>(results);
}
}
장점
1. 컬렉션에 대한 조작을 제한하여 코드 안정성을 높일 수 있음
2. 비즈니스 로직을 캡슐화해서 코드의 가독성과 유지보수성을 향상시킬 수 있음
(관련 로직이 한 곳에 집중되어 코드 구조가 명확해짐)
3. 불변성을 보장해서 부작용을 줄일 수 있음
외부에서 컬렉션에 접근할 수 없어서 컬렉션의 상태를 변경할 수 없기 때문
단점
1. 컬렉션에 대한 간단한 조작이 필요한 경우에도 별도의 클래스를 정의해야함
-> 코드의 복잡성이 증가 (컬렉션마다 별도의 클래스를 정의해야하기 때문)
2. 성능 상 이슈 발생
-> 컬렉션의 상태를 변경할 때마다 새로운 객체를 생성해야해서
객체 생성 비용이 증가
단점에도 불구하고 장점이 더 큼
코드의 안정성과 유지보수성을 높이는 데 큰 도움이 되기 때문에
우아한 테크 블로그에서는 다음 정의를 다르게 정의함
일급 컬렉션은 불변성을 보장하지 않는다는 입장
= 일급 컬렉션이 주는 기능의 핵심은 불변이 아니다.
다른 블로그 글
1. setter을 구현하지 않으면 불변 컬렉션이 된다 (x)
→ setter을 안 사용해도 변화를 줄 수 있음
따라서
public class Lotto {
private final List<LottoNumber> lotto;
public Lotto(List<LottoNumber> lotto) {
this.lotto = new ArrayList<>(lotto);
}
public List<LottoNumber> getLotto() {
return Collections.unmodifiableList(lotto);
}
}
1. new ArrayList<>(lotto);로 재할당
2. unmodifiableList()를 사용하면 불변이 됨
참고 문헌
VO
VO(Value Ojbect)란 무엇일까?
프로그래밍을 하다 보면 VO라는 이야기를 종종 듣게 된다. VO와 함께 언급되는 개념으로는 Entity, DTO등이 있다. 그리고 더 나아가서는 도메인 주도 설계까지도 함께 언급된다. 이 글에서는 우선 다
tecoble.techcourse.co.kr
일급 컬렉션
1급 컬렉션의 이해와 실무 적용 사례
1급 컬렉션의 개념, 장단점, 실무 적용 사례 및 구현 예제를 통해 코드의 품질을 높이는 방법을 알아봅니다.
f-lab.kr
일급 컬렉션을 사용하는 이유
일급 컬렉션이란? 본 글은 일급 컬렉션 (First Class Collection)의 소개와 써야할 이유를 참고 했다. 일급 컬렉션이란 단어는 소트웍스 앤솔로지의 객체지향 생활체조 규칙 8. 일급 콜렉션 사용에서 언
tecoble.techcourse.co.kr