아래와 같은 Lotto 클래스가 있다.
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = validate(numbers);
}
private void validate(List<Integer> numbers) {
if (numbers.size() != LOTTO_NUMBER_AMOUNT_MAX.getValue()) {
throw new IllegalArgumentException(LOTTO_AMOUNT_MAX_ERROR_MESSAGE.getMessage());
}
}
}
애플리케이션의 목적은 로또 번호와 정답 번호를 비교해서 당첨 통계를 내는 것이다.
우리가 배운 바, 객체는 외부에서 메시지를 받아 스스로가 상태를 관리하고 관련 로직을 처리해야 한다고 한다.
그렇다면, 만약 이 Lotto
클래스에서 해야 할 것들이 많아지면?
예를 들어, 애플리케이션에서 로또 번호는 오름차순 정렬이 되어있어야 하고, 추가적으로 로또 번호는 중복되지 않는다는 걸 검증할 것을 요구사항으로 추가했다.
그럼 이 Lotto 클래스는 이렇게 작성할 수 있을 것 같다.
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validate(numbers);
this.numbers = sortAscending(numbers);
}
private void validate(List<Integer> numbers) {
Set<Integer> uniqueNumbers = new HashSet<>();
if (numbers.size() != LOTTO_NUMBER_AMOUNT_MAX.getValue()) {
throw new IllegalArgumentException(LOTTO_AMOUNT_MAX_ERROR_MESSAGE.getMessage());
}
for (int number : numbers) {
if (!uniqueNumbers.add(number)) {
throw new IllegalArgumentException(LOTTO_UNIQUE_ERROR_MESSAGE.getMessage());
}
}
}
private List<Integer> sortAscending(List<Integer> numbers) {
try {
Collections.sort(numbers);
} catch (UnsupportedOperationException e) {
List<Integer> mutableNumbers = new ArrayList<>(numbers);
Collections.sort(mutableNumbers);
return Collections.unmodifiableList(mutableNumbers);
}
return numbers;
}
}
여기서 로또에 대한 다른 요구사항이 더 추가된다면?
그 요구사항들을 추가하기 위한 로직들을
Lotto
클래스 내에 구현해야 할까?
그렇게 해서 도메인 객체인 Lotto
클래스가 너무 뚱뚱해진다면?
로또 번호
라는 상태를 객체 스스로가 다루기 위해 수많은 메서드들을 가지게 된다면?
이 때 일어나는 클래스 분리도 해서는 안 되는걸까?
만약 클래스 분리를 하면 상태를 다른 객체가 관리하게 되는 것 같다는 생각이 든다. 예를 들어 너무 많아진 검증 조건을 LottoValidator
라는 클래스를 만들어 이 객체에서 검증 후 Lotto
클래스의 생성자에 값을 전달해준다면, 로또 번호라는 상태를 Lotto
객체 스스로가 관리하지 못하는게 아닌가?
갑자기 '상태를 객체 스스로가 관리해야 한다', '객체에 메시지를 보내 스스로가 일하게 해야 한다' 라는 말이 너무 헷갈렸다.
그래서 위의 의문을 한번 더 학습하고 정리해봤다.
1. 적절한 분리가 필요할 수도 있다.
도메인 클래스가 자신의 상태를 관리하기 위해 필요한 모든 메서드를 가지면 좋지만, 로직이 복잡해지고 클래스의 책임이 많아지면 *클래스를 적절히 분리해야 할 수도 있다.
예를 들어, 번호의 유효성을 검사하는 로직은 LottoValidator
와 같은 별도의 클래스로 분리될 수 있다. 그러나, 이 분리는 상태 관리를 다른 객체에 위임하는 것이 아니라, 단지 유효성 검사라는 행위를 분리하는 것이다. 유효한 상태는 여전히 Lotto
객체에 의해 관리되어야 한다.
2. 도메인 객체 내부의 로직이 다른 곳에서도 재사용될 필요가 있다면, 클래스 분리가 일어나야 할 시점일 수도 있다.
예를 들어, 지금은 로또 하나에서만 검증을 하지만, 이 애플리케이션이 로또와 비슷한 연금복권, 파워볼, 일본 로또들도 구매할 수 있게 된다면? 그리고 검증할 것이 Lotto
객체와 비슷하다면?
이 로또 객체는 적어도 '검증' 항목에 대해서는 스스로 로직을 가지지 않게 될 것이다.
3. 적절한 분리는 캡슐화를 강화시킬 수 있다.
LottoValidator
클래스를 사용하여 유효성 검증 로직을 분리하는 것은 Lotto
클래스가 자신의 상태를 '직접' 관리하지 않는 것처럼 보일 수 있지만, 이는 객체 지향 설계의 원칙 중 하나인 '캡슐화'를 강화하는 것으로 볼 수 있다.
Lotto
객체의 생성자에서 LottoValidator
를 호출하는 것은, 유효하지 않은 상태로 Lotto
객체가 생성되는 것을 방지하기 위한 것이다. 여기서 중요한 점은 Lotto
객체가 생성된 후에는 항상 유효한 상태를 유지한다는 것이며, 이 상태는 Lotto
객체가 스스로 관리한다. 검증 로직은 객체 생성 과정에서만 사용되며, 일단 Lotto
객체가 생성되면 해당 객체는 스스로의 상태를 관리하는 책임을 가진다.
4. 분리가 일어나더라도 객체의 상태 관리의 영역을 침범하지 않을 수도 있다.
LottoValidator
는 단순히 생성 프로세스를 돕는 도구로서 작동하며, Lotto
객체가 생성된 이후의 생명주기에서는 Lotto
객체 스스로가 자신의 상태를 관리한다. 이는 Lotto
객체의 자율성을 해치지 않으며, 오히려 생성과 유효성 검사라는 책임을 분리하여 Lotto
클래스를 더욱 견고하게 만들어 준다.
5. 서비스 레이어와 도메인 객체가 다른 일을 하게 한다.
서비스 레이어에서 LottoValidator
를 사용하여 데이터를 검증하고, 이후에 검증된 값을 Lotto
객체의 생성자에 넣어 객체를 생성하는 방식은 매우 합리적이다. 이 접근 방식은 도메인 레이어와 서비스 레이어의 관심사를 분리하는 것으로 볼 수 있다.
그럼, 도메인 객체가 다른 객체를 의존해도 되는가?
Lotto
클래스와 LottoValidator
클래스 사이의 의존성은 특정 조건 하에서는 바람직할 수 있다.
단방향 의존성: Lotto
는 LottoValidator
에 의존할 수 있지만, 그 반대가 되어서는 안 된다. 이것은 LottoValidator
가 더 일반적인 컴포넌트이며, Lotto
클래스보다 낮은 수준의 상세 구현에 속한다는 것을 의미한다.
인터페이스를 통한 의존성: Lotto
가 LottoValidator
의 구체적인 클래스가 아닌 인터페이스에 의존하도록 할 수 있다. 이렇게 하면 검증 로직을 다양하게 교체할 수 있게 되어, 코드의 유연성과 테스트 용이성이 향상된다.
의존성 주입: LottoValidator
인스턴스를 Lotto
생성자나 메서드를 통해 주입해줄 수 있다. 이를 통해 Lotto
클래스가 특정 LottoValidator
구현에 강하게 결합되는 것을 피할 수 있고, 의존성을 외부에서 관리할 수 있게 된다.
도메인 객체가 다른 객체에 의존하는 것은 괜찮지만, 의존성이 과도하거나 복잡해질 경우 코드를 유지보수하기 어려워질 수 있음을 주의하며 설계를 해야 한다.
결론적으로, LottoValidator
와 같은 유효성 검사 클래스는 Lotto
의 비즈니스 로직을 깨끗하게 유지하는 동시에 유효성 검사를 재사용할 수 있는 방법을 제공하기 때문에, 이 도메인 객체 내에서 해당 의존성을 갖는 것은 가능하다.
다만, Lotto
객체의 일관성을 보장하는 것이 중요하다. Lotto
객체가 생성되기 전에 모든 필요한 검증이 이루어져야 하며, 유효하지 않은 상태의 Lotto
객체가 생성되는 것을 방지해야 한다.
그래서 검증 로직은 항상 객체 생성 전에 실행되어야 하며, 이를 위해 서비스 레이어에서 LottoValidator
를 호출하는 것이 적절할 수 있다!
결론
절대적인 건 없다.
규칙에 얽매이기 보다는 객체 중심적, 행위 중심적 관점을 갖는 것이 필요하다.
애플리케이션의 요구사항과 니즈에 따라 Lotto
객체 스스로가 검증을 하게 할 수도 있고, 외부 클래스에서 검증을 하게 할 수도 있다.
한 번에 완벽한 결과물을 만드는 경우는 정말 드물다.
결국엔 개발자가 자율적으로 판단하고 결정하며, 시행착오를 겪어 보며 수정하는 것이다!
+ 선배 개발자분들의 조언
OKKY에 내가 질문했던 내용이다.
| (초보) Java / 객체지향에서 도메인 객체의 상태 관리에 대해 질문드립니다!
'우아한테크코스' 카테고리의 다른 글
[우테코 6기 프리코스] 4주차 - 2일차 회고 (0) | 2023.12.20 |
---|---|
[우테코 6기 프리코스] 4주차 - 1일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 3주차 전체 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 3주차 - 6일차 회고 (0) | 2023.12.20 |
[우테코 6기 프리코스] 3주차 - 5일차 회고 (0) | 2023.12.20 |