•
늘상 각 계층의 모델을 매핑하는 곳에서는 문제가 발생한다.
매핑에 찬성하는 개발자
•
두 계층 간에 매핑을 하지 않으면 양 계층에서 같은 모델을 사용해야 하는데 이렇게 하면 두 계층이 강하게 결합됩니다.
매핑에 반대하는 개발자
•
두 계층 간에 매핑을 하게 되면 보일러플레이트 코드를 많이 만들게 된다. 많은 유스케이스들이 오직 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 계층간 매핑은 과하다.
‘매핑하지 않기’(No Mapping) 전략
•
포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면 두 계층간의 매핑할 필요가 없어진다.
•
웹 계층과 애플리케이션에서 모두 Account를 사용해서 두 계층이 같은 모델을 사용하는것이다. 영속성 계층에서도 같은 관계다.
◦
각 계층에서 모델에 대한 특별한 요청이 있을 수 있다.
◦
웹계층에서는 JSON 직렬화를 위한 애너테이션을 사용해야 할 수 있다.
◦
ORM을 사용하기 위해서는 데이터베이스 매핑을 위해 특정 애너테이션이 필요할 수 있다.
◦
Account 클래스가 웹, 애플리케이션, 영속성 계층에서 사용되면 단일 책임 원칙을 위반한다.
•
모든 계층이 정확히 같은 구조와 정보를 필요로 한다면 ‘매핑하지 않기 전략’은 완벽한 선택지이다.
•
그러나 애플리케이션 계층, 도메인 계층에서 웹과 영속성 문제를 다루게 되면 곧바로 다른 전략을 취해야한다.
‘양방향’(Two-Way) 매핑 전략
•
각 계층이 전용 모델을 가진 매핑 전략을 양방향 매핑 전략이라고 한다.
각 어댑터가 전용 모델을 가지고 있어서 해당 모델을 도메인 모델로 도메인 모델을 해당 모델로 매핑할 책임을 가지고 있다.
•
웹 계층에서는 웹 모델을 인커밍 포트에서 필요한 모델로 매핑하고 인커밍 포트에 의해 반환된 도메인 객체를 다시 웹 모델로 매핑한다.
•
영속성 계층은 아웃고잉 포트가 사용하는 도메인 모델과 영속성 모델 간의 매핑과 유사한 매핑을 담당한다.
•
두 계층 모두 양방향으로 매핑하기 때문에 ‘양방향’ 매핑이라고 부른다.
•
각 계층이 전용 모델을 가지고 있어 각 계층이 전용 모델을 변경하더라도 다른 계층에는 영향이 없다.
•
웹 모델은 데이터를 최적으로 표현할 수 있는 구조를 가지고 도메인 모델은 유스케이스를 잘 구현할 수 있는 구조를 가진다. 영속성 모델은 데이터베이스에 객체를 저장하기 위해 ORM에서 필요로 하는 구조를 가질 수 있다.
장점
•
웹이나 영속성 관심사로 오염되지 않는 깨끗한 도메인 모델로 이어진다. 단일 책임 원칙을 만족한다.
•
‘매핑하지 않기’ 전략 다음으로 간단한 전략이다.
•
매핑 책임이 명확하다.
•
바깥쪽 계층/어댑터는 안쪽 계층으로 매핑하고 다시 반대 방향으로 매핑한다. 안쪽 계층의 모델만 알면 되고 매핑대신 도메인 로직에 집중할 수 있다.
단점
•
너무 많은 보일러플레이트 코드가 생긴다.
•
매핑 프레임워크를 사용하더라도 시간이 많이 든다.
◦
매핑 프레임워크가 리플렉션을 사용하는 경우 디버깅하기 힘들다.
•
도메인 모델이 계층 경계를 넘어서 통신하는 데 사용된다.
◦
인커밍 포트와 아웃고잉 포트는 도메인 객체를 입력 파라미터와 반환값으로 사용한다.
◦
도메인 요구가 아닌 바깥쪽 계층의 요구에 따른 변경에 취약해진다.
•
은총알(silver bullet)이 아니다.
‘완전’(Full) 매핑 전략
각 연산이 전용 모델을 필요로 하기 때문에 웹 어댑터와 애플리케이션 계층 각각이 자신의 전용 모델을 각 연산을 실행하는 데 필요한 모델로 매핑한다.
•
각 연산마다 별도의 입출력 모델을 사용한다. 계층 경계를 넘어 통신할 때 도메인 모델을 사용하는 대신 SendMoneyUseCase포트의 입력 모델로 동작하는 SendMoneyCommand처럼 각 작업에 특화된 모델을 사용한다.
•
이 모델을 ‘커맨드(command)’, ‘요청(request)’ 혹은 이와 비슷한 단어로 사용한다.
•
웹 계층은 입력을 애플리케이션 계층의 커맨드 객체로 매핑할 책임을 가지고 있다.
◦
커맨드 객체는 애플리케이션 계층의 인터페이스를 해석할 여지 없이 명확하게 만들어준다.
◦
각 유스케이스는 전용 필드와 유효성 검증 로직을 가진 전용 커맨드를 가진다. 어떤 필드를 채울지 어떤 필드를 비워두는 게 더 나은지 추측할 필요가 전혀 없다. 값을 비워둘 수 있는 필드를 허용할 경우 현재의 유스케이스에서는 필요없는 유효성 검증이 수행될 수도 있다.
•
애플리케이션 계층은 커맨드 객체를 유스케이스에 따라 도메인 모델을 변경하기 위해 필요한 무언가로 매핑할 책임을 가진다.
•
한 계층을 다른 여러 개의 커맨드로 매핑하는 데 하나의 웹 모델과 도메인 모델 간의 매핑보다 더 많은 코드가 필요하다.
•
여러 유스케이스의 요구사항을 함께 다뤄야하는 매핑에 비해 구현하고 유지보수하기 쉽다.
•
전역으로 사용하기보다는 웹 계층 → 애플리케이션 계층 사이의 상태 변경 유스케이스의 경계를 명확하게 할 때 빛이난다. 하지만 애플리케이션 → 영속성 사이는 매핑 오버헤드가 발생 할 수 있다.
•
매핑 전략은 여러가지 섞어써야만 한다. 어떤 매핑 전략도 모든 계층에 전역적일 필요는 없다.
‘단방향’(One-Way) 매핑 전략
동일한 ‘상태’ 인터페이스를 구현하는 도메인 모델과 어댑터 모델을 이용하면 각 계층은 다른 계층으로부터 온 객체를 단방향으로 매핑하기만 하면 된다.
•
모든 계층의 모델들이 같은 인터페이스를 공유한다.
•
관련 있는 특성(attribute)에 대한 getter 메서드를 제공해서 도메인 모델의 상태를 캡슐화한다.
•
도메인 객체를 바깥 계층으로 전달하고 싶으면 매핑 없이 할 수 있다. 인커밍/아웃고잉 포트가 기대하는 대로 상태 인터페이스를 구현하고 있기 때문이다.
•
도메인 모델은 풍부한 행동을 구현할 수 있고 애플리케이션 계층 내의 서비스에서 이러한 행동에 접근할 수 있다.
•
바깥 계층에서 상태 인터페이스를 이용할지 전용 모델로 매핑해야할지 결정할 수 있다.
•
바깥 계층에서 애플리케이션 계층으로 전달하는 객체들도 이 상태 인터페이스를 구현하고 있다. 애플리케이션 계층에서는 이 객체를 실제 도메인 모델로 매핑해서 도메인 모델의 행동에 접근할 수 있게 된다.
•
팩토리(factory)라는 DDD 개념과 잘 어울린다. DDD 용어인 팩토리는 어떤 특정한 상태로부터 도메인 객체를 재구성할 책임을 가지고 있다.
•
매핑이 계층을 넘나들며 퍼져있어서 이 전략은 다른 전략에 비해 어렵다.
•
계층 간의 모델이 비슷할 때 가장 효과적이다.
◦
읽기 전용 연산의 경우 상테 인터페이스가 필요한 모든 정보를 제공하기 때문에 웹 계층에서 전용 모델로 매핑할 필요가 없다.
언제 어떤 매핑 전략을 사용할 것인가?
•
언제 어떤 전략을 사용할지 결정하려면 팀 내에서 합의할 수 있는 가이드라인을 정해둬야 한다.
•
가이드라인 예제
◦
변경 유스케이스를 작업하고 있다면 웹 계층과 애플리케이션 계층 사이에서는 유스케이스 간의 결합을 제거하기 위해 ‘완전 매핑’ 전략을 첫 번째 선택지로 택해야한다. 유스케이스별 유효성 검증 규칙이 명확해지고 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다.
◦
변경 유스케이스를 작업하고 있다면 애플리케이션과 영속성 계층 사이에서는 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해서 ‘매핑하지 않기’ 전략을 첫 번째 선택지로 둔다. 하지만 애플리케이션 계층에서 영속성 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꿔서 영속성 문제를 영속성 계층에 가둘 수 있어야 한다.
◦
쿼리 작업을 하면 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해 ‘매핑하지 않기’ 전략이 웹 → 애플리케이션 → 영속성 계층 사이에서 첫 번째 선택지가 되어야한다. 하지만 애플리케이션에서 영속성 문제나 웹 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꾼다.
•
팀 차원에서 지속적으로 논의하고 수정해야 한다.
유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?
•
인커밍 포트와 아웃고잉 포트는 서로 다른 계층이 어떻게 통신해야 하는지를 정의한다.
•
각 유스케이스에 대해 좁은 포트를 사용하면 유스케이스마다 다른 매핑전략을 사용할 수 있고 다른 유스케이스에 영향을 미치지 않으면서 코드를 개선할 수 있기 때문에 특정 상황 특정 시점에 최선의 전략을 선택할 수 있다.
Question
•
어떤 매핑 전략을 선호하는지?
•
어떤 매핑 프레임워크를 선호하는지?
◦
ModelMapper
◦
MapStruct
◦
직접 코드 구성
◦
리플렉션