클린 코드, TDD에 대해 관심을 갖게 되다보면서 자연스럽게 일급 컬렉션에 대해 접하게 되는 데요,
도대체 일급 컬렉션이란 무엇일까요?
class Car {
private String name;
private int position;
}
class Cars {
List<Car> cars = new ArrayList<>();
...
}
역시 개념을 이해하는 데는 예제가 제일 쉬운 거 같습니다.
위의 Car
라는 클래스를 여러개 만들어 사용하고 싶은데, 이때 Cars
클래스를 통해 Car
Collection을 Wrapping하면서 그 외 다른 멤버 변수가 없는 상태
를 일급 컬렉션이라고 합니다.
GameController.java
private void inputTimes() {
OutputView.askTimes();
try {
String input = InputView.input();
InputValidator.validTimeInput(input);
time = input;
} catch (NullPointerException e) {
inputTimes();
}
}
제가 처음 자동차 경주 게임을 진행하면서 짰던 횟수 입력 기능 메소드입니다.
횟수의 검증 로직
1. 숫자가 입력되었는가? (특수문자/한글/영어 입력 X)
2. 자연수가 입력되는가? (0 이하는 입력 X)
InputValidator.validTimeInput(input)
는 서비스를 제공하는 코드에서 검증 로직이 있게 됩니다.
만약, 횟수를 GameController
가 아닌 다른 곳에서도 입력 받는 경우 검증 로직이 또 사용 되어야 합니다.
public class Time {
private int time;
public Time(String time) {
Time.validTimeInput(time);
this.time = Integer.parseInt(time);
}
public static void validTimeInput(String time) {
if (TimeValidator.isInteger(time)) {
throw new NotNumberException();
}
if (TimeValidator.isBelowZero(time)) {
throw new InvalidRangeTimeException();
}
}
...
]
횟수에 대한 일급 콜렉션을 만들어주어 그 안에서 검증 로직을 수행할 수 있도록 해줍니다.
이렇게 되면 GameController
는 무엇을 수행할까요?
private void inputTimes() {
OutputView.askTimes();
try {
time = new Time(InputView.input());
} catch (NullPointerException e) {
inputTimes();
}
}
횟수가 생성되었다는 것을 생성자를 통해 확인할 수 있고, 서비스 메소드에서 검증 로직을 수행하지 않고 일급 콜렉션을 통해서 검증 후 값을 가집니다. 👍
Java의 final
은 정확히는 불변을 만들어 주는 것이 아닌 재할당을 금지합니다. (자바의 static과 final)
final List<String> list = new ArrayList<>();
list.add("123");
list.add("456");
list.add("789");
list = new LinkedList<>(); // error
list
는 final
로 선언 되었지만 데이터 추가/삭제가 위와 같이 자유롭습니다.
다만, new
를 통해 재할당 하는 경우에는 컴파일 에러가 발생합니다. 😢 (final은 재할당을 금지한다!
)
소프트웨어 규모가 커짐에 따라 불변 객체는 절대 값이 바뀌지 않는 다는 것이 보장되기 때문에 코드 변경이나 수정에 대한 부작용이 최소화된다고 합니다.
그러나 자바에서는 final
로 이 문제를 해결할 수 없기 때문에 일급 컬렉션과 래퍼 클래스 등의 방법으로 해결해야 한다고 합니다.
이말인즉슨, 생성자를 통해 데이터가 생성된 일급 컬렉션에 Setter
를 두지 않아 값을 변경/추가가 안되는 불변 컬렉션으로 만들 수 있습니다.
일급 컬렉션은 값과 로직이 함께 존재합니다.
위에서 예를 들었던 자동차 경주의 최대 우승자를 구하는 기능을 예시로 보겠습니다.
Cars
는 Car
의 리스트를 담고 있는 일급 컬렉션입니다.
단순히 Car
의 상태(position
, name
)만 담고 있는 것이 아닌 Car
의 행위로 가지게 됩니다.
public class Cars {
final List<Car> carList = new ArrayList<>();
...
public List<String> getWinner() {
return carList.stream()
.filter(car -> car.isMaxPosition(findMaxPosition()))
.map(car -> car.getName())
.collect(Collectors.toList());
}
private int findMaxPosition() {
int maxPosition = -1;
return carList.stream()
.filter(car -> car.aboveMaxPosition(maxPosition))
.mapToInt(car -> car.getPosition())
.max()
.getAsInt();
}
}
우승자를 찾는 getWinner
메소드와 이 우승자를 찾기 위한 최대 거리를 구하는 findMaxPosition
메소드를 가집니다. (행위)
만약, 우승자를 찾는 메소드가 Cars
에 있지 않고 다른 곳에 있다면?
만약 다른 곳에 Cars
를 생성하면, 그땐 또 다시 우승자를 찾는 메소드를 만들어주어야합니다. 🤦♀️
일급 컬렉션 애용합시다!
실제로 사용하진 않았지만, 예시 코드를 들어보겠습니다.
List<Car> BenzList = new BenzCar();
List<Car> BMWList = new BMWCar();
같은 차 종류임에도 벤츠 일급 컬렉션과 BMW 일급 컬렉션이 각각 다르게 생성될 수 있습니다.
의미에 맞게 사용하면 검색이 쉽고 표현을 명백히 할 수 있습니다.
제가 이해한 나름대로 일급 컬렉션을 정리해보고, 그에 맞게 예시를 만들어보았는데요.
최대한 쉽게 풀어보고자 노력했습니댜 ㅠㅠ
쉽게 전달이 되었으면 좋겠네요!!
혹시 잘못된 내용이나 이해가 안 가는 부분이 있다면 댓글 환영입니다