일급 컬렉션과 값 객체(VO)
이번 과제를 하면서 Car나 TryCount를 어떻게 관리할지 고민이 많았다. Car는 Cars라는 클래스로 감싸서 사용했는데 이가 일급 컬렉션과 관련이 있음을 알게 되었고 처음 들어보는 이 용어에 대해 학습했다.
그리고 TryCount 역시 일급 컬렉션으로 관리할 수 있을지 궁금했는데 글 끝에선 이는 값 객체와 관련됨을 알 수 있었다.
내가 좋아하는 이동욱님 글을 참고해 이번 과제에 적용해보며 생각해보았다.
소트웍스 앤솔로지 의 객체지향 생활체조 파트 내 규칙 8: 일급 콜렉션 사용 에 아래와 같은 내용이 나온다.
콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 콜렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다. 필터는 또한 스스로 함수 객체가 될 수 있다.
새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다. 이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
콜렉션은 실로 매우 유용한 원시 타입이다. 많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에 의미적 의도나 단초는 거의 없다.
한 줄로 요약하자면, 콜렉션을 포장하면서 그 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라 일컫는다.
이걸 이번 과제에 적용해보면 콜렉션이 Car고, Cars가 콜렉션을 포함한 클래스가 될 것이다. 코드로 보여주면 아래와 같다.
•
일급 컬렉션 사용
public class Cars {
private List<Car> cars;
Java
복사
•
일급 콜렉션을 안 썼으면 아래와 같은 코드였을 것이다.
List<Car> cars = new ArrayList<>();
cars.add(new Car());
Java
복사
그렇다면 일급 콜렉션을 써서 얻는 이득이 뭘까? 코드로 보여주겠다.
컬렉션(Car)의 불변 보장
•
일급 컬렉션 사용 : 값 추가/변경 안됨
public class Cars {
private final List<Car> cars;
public Cars(List<Car> cars) {
this.cars = cars;
}
public void move() {
cars.stream().forEach(Car::move); // Method Reference 사용
}
public List<Long> getPositions() {
return cars.stream().map(Car::getPosition).toList();
}
}
Java
복사
•
리스트 사용 : final로 재할당만 금지할 수 있을 뿐, 값 추가/변경이 가능함
@Test
public void final도_값변경이_가능하다() {
//given
final List<Car> cars = new ArrayList<>();
//when
cars.add(new Car());
//then
assertThat(cars.size()).isEqualTo(1); // 성공
}
Java
복사
상태와 행위를 한 곳에서 관리
•
일급 컬렉션 사용 : 값과 로직이 함께 존재함
public class Cars {
private final List<Car> cars; // 값
public Cars(List<Car> cars) {
this.cars = cars;
}
public List<Long> getPositions() {
return cars.stream().map(Car::getPosition).toList(); // 로직
}
}
List<Car> cars = Arrays.asList(new Car("빨간차"), new Car("파란차"));
Cars cars = new Cars(cars);
List<Long> carPosition = cars.getPositions();
Java
복사
•
리스트 사용 : 값과 로직이 흩어져 존재함
List<Car> cars = Arrays.asList(new Car("빨간차"), new Car("파란차")); // 값
List<Long> carPosition = cars.stream().map(Car::getPosition).toList(); // 로직
Java
복사
비즈니스에 종속적인 자료 구조
•
자동차 경주 게임의 조건 1
◦
여러 대의 자동차가 존재
◦
자동차들의 이름은 중복되지 않아야 함
→ 여러 대의 자동차로 이루어지고 자동차 이름이 중복되지 않는 자료구조 만들어 검증 로직을 정밀하게 관리
•
일급 컬렉션 사용
public class Cars {
private final List<Car> cars;
public Cars(List<Car> cars) {
validateNameDuplication(cars);
this.cars = cars;
}
private void validateNameDuplication(List<Car> cars) {
List<String> carName = cars.stream().map(Car::getName).toList();
Set<String> nonDuplicateName = new HashSet<>(carName);
if(nonDuplicateName.size() != cars.size()) {
throw new IllegalArgumentException("자동차 이름들은 중복될 수 없습니다");
}
}
}
Java
복사
•
자동차 경주 게임의 조건 2
◦
이동 시도 횟수(int)가 존재
◦
이동 시도 횟수는 0 이상 이어야 함
→ 이동 시도 횟수로 이루어지고 그 값이 0 이상인 자료 구조 만들어 검증 로직 관리
•
값 객체(VO) 사용
public class TryCount {
private final int count;
public TryCount(int count) {
validateTryCount(count);
this.count = count;
}
private void validateTryCount(int count) {
if(count < 0) {
throw new IllegalArgumentException("이동 시도 횟수는 0 이상이어야 합니다.");
}
}
public int getCount() {
return count;
}
}
Java
복사
마치며
Car와 TryCount가 다른 듯 비슷해서 두 개를 표현할 방법을 찾아나가기 위한 여정 속에 새로운 클래스들을 알게 되어 기쁘다.
처음엔 일급 컬렉션이란 용어만 알고서 미션에 적용해보다가, 값 객체(VO)라는 것도 자연스레 알게 되었다.
미션을 하며 느낀 둘의 공통점과 차이점을 간략히 정리해보며 글을 마무리하겠다.
다만,,, 이제 와서 든 생각은 내 로직에선 TryCount의 경우 값을 줄이고 늘리면 좋을 거 같아 VO로 쓰긴 적합하지 않을 거 같다.. 만일 본인의 로직에선 TryCount를 변경하지 않고 사용할 거라면 VO로 사용해도 좋지 않을까 싶다.
일급 컬렉션 vs 값객체
•
공통점
공통 |
값 변경 불가 (불변성) |
생성자에서 유효성 검증 (자가 유효성 검사) |
•
차이점
일급 컬렉션 | 값 객체 | |
의미 | 하나의 컬렉션만 멤버 변수로 가지는 클래스 | 변경 불가한 객체 |
목적 | 컬렉션 관련한 비즈니스 로직 한 곳에서 관리 | 특정 값을 표현해 값에 대한 로직과 유효성 캡슐화 |
구성 | 하나의 멤버 변수 + 컬렉션 다루는 메서드 | 하나 이상의 멤버 변수 + 값 다루는 메서드 |
특징 | 동등한 속성 가지면 객체 간 동등성 보장 |
record와 dto
이번 과제에서 Car의 이름과 위치를 쌍으로 전달할 일이 많았다. 이를 map으로 표현하는 방법도 있지만, 가독성이 현저히 떨어져 나만 알아보는 코드가 될 가능성이 있기에 이럴 땐 dto를 쓰는 게 좋다. dto를 만들고 놨더니 인텔리제이가 record로 바꿀 수 있다고 알려줬다.
record에 대해 처음 들어봤는데 dto와 비슷하되 가장 큰 차이점은 필드들이 불변으로 선언된단 점이다. dto는 급진적으로 봤을 땐 모든 필드를 public으로 둬도 괜찮다는 의견이 있을 정도로 변경에 취약하다. 하지만 record를 쓰면 데이터를 온전하게 전송할 수 있다.
Today in 프리코스
TIL 작성하기
몰입
구현 완성하기
테스트코드 짜기
Search