도메인 주도 개발 시작하기(6)
6-1. 표현 영역과 응용 영역
•
도메인이 제 기능을 하려면 사용자와 도메인을 연결해주는 매개체가 필요하다
◦
응용 영역과 표현 영역이 사용자와 도메인을 연결해 주는 매개체 역할을 한다
flowchart LR
user --> presentation(표현 영역)
presentation --> application(응용 영역)
application --> domain(도메인 영역)
Mermaid
•
표현 영역은 사용자의 요청을 해석하여 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다
◦
실제 사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스다.
◦
응용 서비스의 메서드가 요구하는 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에 표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다
•
사용자와 상호 작용은 표현 영역이 처리하기 때문에, 응용 서비스는 표현 영역에 의존하지 않는다.
◦
응용 영역은 사용자가 웹브라우저를 사용하는지, REST API 를 사용하는지, TCP 소켓을 사용하는지를 알 필요가 없다.
6-2. 응용 서비스의 역할
•
응용 서비스는 사용자가 요청한 기능을 실행한다.
◦
사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다
◦
주요 역할은 도메인 객체를 사요해서 사용자의 요청을 처리하는 것이므로 표현 영역 입장에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해주는 창구 역할을 한다
•
응용 서비스는 주로 도메인 객체 간의 흐름을 제어하기 때문에 다음과 같이 단순한 형태를 갖는다
예시 코드
•
◦
응용 서비스가 도메인 로직을 일부 구현하면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있다.
•
응용 서비스는 트랜잭션 처리도 담당한다.
◦
도메인의 상태 변경을 트랜잭션으로 처리해야 한다.
6-2-1. 도메인 로직 넣지 않기
•
도메인 로직은 도메인 영역에 위치하고, 응용 서비스는 도메인 로직을 구현하지 않는다.
•
도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다.
1.
코드의 응집성이 떨어진다
•
도메인 데이터와 그 데이터를 조작하는 도메인 로직이 한 영역에 위치하지 않고 서로 다른 영역에 위치한다는 것은 도메인 로직을 파악하기 위해 여러 영역을 분석해야 한다는 의미이다.
2.
여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
•
코드 중복을 위해 응용 서비스 영역에서 별도의 보조 클래스를 만들 수 있지만, 애초에 도메인 영역에 구현되어 있으면 응용 서비스는 그 기능을 이용하기만 하면 된다
•
일부 도메인 로직이 응용 서비스에 출현하면서 두 가지 문제는 결과적으로 코드 변경을 어렵게 만든다.
•
소프트웨어가 가져야 할 중요한 경쟁 요소 중 하는 변경 용이성인데, 변경이 어렵다는 것은 그만큼 소프트웨어의 가치가 떨어진다는 것을 의미한다.
6-3. 응용 서비스의 구현
•
응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 하는데, 이는 디자인 패턴에서 파사드(facade) 와 같은 역할을 한다.
•
응용 서비스 자체는 복잡한 로직을 수행하지 않기 때문에 응용 서비스의 구현은 어렵지 않다.
6-3-1. 응용 서비스의 크기
필자는 한 클래스가 여러 역할을 갖는 것 보다 각 클래스마다 구분되는 역할을 갖는 것을 선호한다.
•
응용 서비스는 보통 2가지 방법 중 한가지 방식으로 구현한다
1.
한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
•
각 기능에서 동일한 로직에 대한 코드 중복을 제거할 수 있는 장점이 있다
◦
중복되는 로직을 private 메서드로 구현하고 이를 호출하는 방법으로 중복 로직을 제거할 수 있다
•
한 서비스 클래스의 크기 (코드 라인 수) 가 커진다는 것은 이 방식의 단점이다
◦
코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 된다
▪
결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하는데 방해가 된다
◦
한 클래스에 코드가 모이기 시작하면 엄연히 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워넣게 된다
▪
이것은 코드를 점점 얽히게 만들어 코드 품질을 낮추는 결과를 초래한다
2.
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
•
구분되는 기능별로 서비스 클래스를 구현하는 방식은 한 응용 서비스클래스에서 1개 ~ 3개의 기능을 구현한다
•
클래스 개수는 많아지지만, 한 클래스에 관련된 기능을 모두 구현하는것과 비교해서 코드 품질을 일정 수준으로 유지하는데 도움이 된다
◦
각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않는다
•
각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있는데, 이는 별도의 클래스에 로직을 구현해서 코드가 중복되는 것을 방지할 수 있다
6-3-2. 응용 서비스의 인터페이스와 클래스
•
응용 서비스를 구현할 때 인터페이스가 필요한 몇가지 상황이 있다
◦
대표적인 예시) 구현 클래스가 여러 개인 경우
▪
구현 클래스가 다수 존재하거나, 런타임에 구현 객체를 교체해야 할 때 인터페이스를 유용하게 사용할 수 있다
▪
하지만, 응용 서비스는 런타임에 교체하는 경우가 거의 없고, 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다.
•
인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고, 구현 클래스에 대한 간접 참조가 증가해서 전체 구조가 복잡해진다.
◦
따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 선택이라고 볼 수는 없다.
6-3-3. 메서드 파라미터와 값 리턴
•
응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는데 필요한 값을 파라미터로 전달받아야 한다.
◦
응용 서비스에 데이터로 전달할 요청 파라미터가 2개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다
•
응용 서비스의 결과를 표현 영역에서 사용해야 하면 응용 서비스 메서드의 결과로 필요한 데이터를 리턴해야 한다.
◦
결과 데이터가 필요한 대표적인 예가 식별자다. ( 유저ID, 주문번호 등 )
•
표현 영역 코드는 응용 서비스가 리턴한 주문 애그리거트 객체에서 주문번호를 구해 사용제에게 보여줄 응답 화면을 생성하면 된다.
•
응용 서비스에서 애그리거트 자체를 리턴하면 코딩은 편할 수 있지만, 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다.
◦
이것은 기능 실행 로직을 응용 서비스와 표현 영역에 분산시켜 코드의 응집도를 낮추는 원인이 된다.
◦
응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다.
6-3-4. 표현 영역에 의존하지 않기
•
응용 서비스 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다.
◦
HttpServletRequest 나 HttpSession, HttpCookie 등을 전달하면 안된다.
•
응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기 어려워진다.
◦
표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제도 발생한다
•
가장 큰 문제점: 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수 있다.
◦
표현 영역의 응집도가 깨진다
◦
결과적으로 코드 유지 보수 비용을 증가시키는 원인이 된다
6-3-5. 트랜잭션 처리
•
트랜잭션을 관리하는 것은 응용 서비스의 중요한 역할이다.
◦
프레임워크가 제공하는 트랜잭션 기능을 적극 사용하는 것이 좋다.
◦
트랜잭션을 시작하여 커밋하고, Exception 이 발생하면 롤백할 수 있다.
6-4. 표현 영역
표현 영역의 책임
1.
사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어한다
•
표현 영역은 응용 서비스를 이용해서 표현 영역의 요청을 처리하고 그 결과를 응답으로 전송한다
2.
사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다
•
표현 영역은 사용자의 요청 데이터를 응용 서비스가 요구하는 형식으로 변환하고, 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 반환한다.
•
표현 영역의 뷰는 이 에러 코드에 알맞은 처리 (해당하는 메시지 출력과 같은)를 하게 된다.
3.
사용자의 세션을 관리한다
•
웹은 쿠키나 서버 세션을 이용해서 사용자의 연결 상태를 관리한다.
•
세션 관리는 권한 검사와도 연결된다.
6-5. 값 검증
•
값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
◦
원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.
◦
사용자 불편을 해소하기 위해 응용 서비스에서 에러 코드를 모아 하나의 익셉션으로 발생시키는 방법도 있다.
◦
표현 영역에서 필수 값을 검증하는 방법도 있다.
•
스프링과 같은 프레임워크에서는 값 검증을 위한 Validator 인터페이스를 별도로 제공하므로 이 인터페이스를 구현한 검증기를 따로 구현할 수 있다.
•
표현 영역과 응용 서비스가 값 검사를 나눠서 수행한다
◦
표현 영역에서 필수 값과 값의 형식을 검증하면 실질적으로 응용 서비스는 ID 중복 여부와 같은 논리적 오류만 검사하면 된다
◦
표현 영역: 필수 값, 값의 형식, 범위 등을 검증
◦
응용 서비스: 데이터의 존재 유무와 같은 논리적 오류 검증
•
응용 서비스에서 값 검증을 모두 처리하게되면 작성할 코드가 늘어나는 단점은 있지만 응용 서비스의 완성도가 높아지는 이점도 있다.
6-6. 권한 검사
•
보통 3곳에서 권한 검사를 수행할 수 있다
1. 표현 영역
•
URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행해야 한다
•
기본적인 검사는 인증된 사용자인지 아닌지 검사한다
◦
URL 을 처리하는 컨트롤러에 웹 요청을 전달하기 전에 인증 여부를 검사해서 인증된 사용자의 웹 요청만 컨트롤러에 전달한다
◦
인증된 사용자가 아닐 경우 로그인 화면으로 리다이렉트 시킨다.
2. 응용 서비스
•
꼭 응용 서비스의 코드에서 직접 권한 검사를 해야 한다는 것을 의미하진 않는다
•
스프링 시큐티리의는 AOP 를 활용해서 Annotation 으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다
3. 도메인
•
개별 도메인 객체 단위로 권한 검사를 해야 하는 경우는 구현이 복잡해진다
•
도메인 확인해야 하는 권한이 다르므로 도메인에 맞게 보안 프레임워크를 확장하거나, 직접 구현해야 한다.
6-7. 조회 전용 기능과 응용 서비스
•
서비스에서 수해앟는 추가적인 로직이 없을 뿐더러 단일 쿼리만 실행하는 조회 전용 기능이어서 트랜잭션이 필요하지 않다.
◦
이 경우라면 굳이 서비스를 만들 필요 없이 표현 영역에서 바로 조회 전용 기능을 사용해도 문제가 없다.
•
응용 서비스를 항상 만들었던 개발자는 컨트롤러와 같은 표현 영역에서 응용 서비스 없이 조회 전용 기능에 접근하는 것이 이상하게 느껴질 수 있다
◦
하지만, 응용 서비스가 사용자 요청 기능을 실행하는 데 별다른 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 괜찮다.
AS-IS
flowchart TB
controller(컨트롤러) --> application(조회 응용 서비스)
application --> dao(조회 전용 기능\nDAO/리포지터리)
Mermaid
TO-BE
flowchart TB
controller(컨트롤러) --> dao(조회 전용 기능\nDAO/리포지터리)
Mermaid