로또 미션에서 한 스터원이 당첨 통계를 출력할 때 View가 Domain에 의존해 화면을 출력하고 있어서 View에서 Domain을 의존하지 않게 수정해보는 것을 리뷰로 남겼었습니다.
이와 같은 리뷰를 남긴 이유는 위의 사진에도 나와있듯 View에서 Domain 객체를 이용해 오용할 수 있는 가능성이 있어서 였습니다. 물론 현재는 혼자서 구현을 하는 것이다 보니 오용할 가능성이 낮지만 View가 Domain에 의존하면 오용의 가능성이 생긴다는 것을 스터디원에게도 전달하고 싶었습니다.
이를 통해서 제가 의도한 개선 방향은 View 메소드에 Domain 객체를 파라미터로 받는 대신에 Getter를 통해 필요한 정보만 파라미터로 전달하는 것이었습니다. 간단하게 코드로 설명하면 아래와 같습니다.(실제 코드와는 다르며 한눈에 볼 수 있게 간소화 했습니다.)
public class Lottos {
private final List<Lotto> lottos;
...
public List<Integer> getNumbers() {
return numbers;
}
public Map<Rank, Integer> makeStatistics(final Lotto winningLotto) {
Map<Rank, Integer> statistic = new HashMap<>();
// 통계 생성
...
return statistic;
}
public double makeProfitRate(final Map<Rank, Integer> statistics, final Money purchaseMoney) {
// 수익률을 만드는 로직
...
return profitRate;
}
}
public class OutputView {
// 기존의 코드
public void printLotto(final Lottos lottos) {}
// 개선했으면 하는 방향
public void printLottoResult(final Map<Rank, Integer> statistics, final double profitRate) {}
}
코드에 대해서 간단히 설명을 하면 makeStatistics()
는 당첨 번호와 구매한 로또를 비교해 등수를 정하는 메소드이고 makeProfitRate()
는 로또 구입 비용과 상금을 통해 수익률을 구하는 메소드 입니다.
View가 Domain에 의존하지 않기 위해 printLottoResult()
메소드에서 Lottos 객체를 받는 대신에 Map<Rank, Integer>와 double을 파라미터로 받는 것을 의도했었습니다.
제 의도와 다르게 스터디원은 DTO를 이용해서 view에 의존성을 제거하는 방향을 택했습니다. DTO(Data Transfer Object)는 여러 계층 사이에서 데이터를 전달하는 객체로 그 안에는 비즈니스 로직을 포함하지 않고 순수하게 데이터를 가진다는 특징을 가지고 있습니다.
이와 같은 특징을 보았을 때 DTO를 이용하는 것 역시 view에서 비즈니스 로직의 오용을 막을 수 있는 방안이어서 좋은 방식이라고 생각했습니다. 하지만 스터디원의 수정된 코드를 보면서 의문점이 생기기 시작했습니다.(실제 코드를 간소화 한 것입니다.)
public class Lottos {
private final List<Lotto> lottos;
// 의문을 가진 코드
public StatisticsDto makeStatistics() {
// 통계를 생성하는 로직
return new StatistsDto();
}
}
public class StatisticsDto {
private final Map<Rank, Integer> statistic;
private final double profitRate;
}
public class OutputView {
public void printStatistic(final StatisticsDto statistic) {
// 로또 결과 출력
}
}
제가 의문점을 가진 부분은 domain에서 Dto를 생성하는 팩토리 메소드(makeStatistics())였습니다. 제가 의문섬을 가지기 시작한 이유에 앞서서 전체적인 의존도를 도식화 하면 아래와 같습니다.
도식화한 것의 특징은 domain과 view 모두를 DTO를 의존한다는 것입니다. 이런 상황에서 DTO가 변경되거나 수정되면 Domain의 코드(DTO를 생성하는 메소드)의 수정이 발생합니다. 이 상황이 문제가 된다고 생각했는데 그 이유는 다음과 같습니다.
현재 DTO는 주로 비즈니스 요구사항 보다는 화면에 나타내야하는 정보가 변경되어야 할 때 변경 및 수정이 발생합니다. 즉, 화면 요구사항의 변경 → DTO 수정 → DTO를 생성하는 Domain 코드의 수정으로 이어지게 되어Domain이 View에 직접적으로 의존하는 것은 아니지만 View의 변화에 의해 Domain의 변경이 발생합니다. 이는 MVC의 본질인 관심사 분리
가 명확하게 분리되지 않았음을 의미한다고 생각했습니다.
그러면 DTO의 특징(비즈니스 로직을 포함하지 않고 순수한 데이터를 가지는 객체)을 유지하면서 View의 변화가 Domain까지 이어지지 않게 DTO를 사용하기 위해서는 어떻게 해야할까요? 제 스스로 내린 해결책은 DTO의 생성 책임을 Domain에서 하는 것이 아닌 DTO 스스로가 처리하여 Domain이 DTO의 의존하는 상황을 제거하는 방안입니다.
public class StatisticsDto {
private final Map<Rank, Integer> statistics;
private final double profitable;
public StatisticsDto(final Map<Rank, Integer> statistics, final double profitable) {
this.statistics = statistics;
this.profitable = profitable;
}
public static StatisticsDto of(final Lottos lottos, final Lotto winningLotto, final Money purchaseMoney) {
Map<Rank, Integer> statistics = lottos.makeStatistics(winningLotto);
double profitRate = lottos.makeProfitRate(statistics, purchaseMoney);
return new StatisticDto(statistics, profitRate);
}
}
위의 코드와 같이 DTO의 생성 책임을 스스로 처리한다면 의존 방향에 변화가 생깁니다.
생성을 DTO가 스스로 책임짐으로써 파라미터로 Domain 객체를 받기 때문에 Domain이 DTO에서 의존하던 것에서 DTO가 Domain에 의존하는 방향으로 변경됩니다. 이를 통해서 DTO에 새로운 필드가 추가되거나 삭제가 되더라도 변화가 Domain까지 이어지지 않아 Domain이 화면 변화에 영향을 받지 않게 됩니다.
현재 스터디원이 DTO의 생성 책임을 스스로 처리하도록 리뷰를 남겨놓은 상태입니다.
DTO는 두 영역(Domain, View) 사이에서 의존성을 제거하는데 이용할 수 있다. 하지만 DTO를 잘 못 사용하면 View와 Domain 사이에 직접 적인 의존관계가 아니더라도 한쪽 변화에 의해 다른 한쪽도 영향을 받는 상황이 발생할 수 있다.(DTO의 변화에 Domain 코드가 변경되는 상황) 그렇기에 DTO의 생성은 DTO 스스로가 책임지도록 구성하여 화면상에 변화가 Domain까지 이어지지 않게 해야한다.