표현 영역과 응용 영역
도메인이 제 기능을 하려면 사용자와 도메인을 연결해주는 매개체가 필요하다.
표현 영역과 응용 영역이 사용자와 도메인을 연결해 주는 매개체 역할을 한다.
표현 영역은 사용자의 요청을 해석한다.
웹 브라우저는 요청 파라미터를 포함한 HTTP 요청을 표현 영역에 전달한다.
요청을 받은 표현 영역은 URL, 요청 파라미터, 쿠키, 헤더 등을 이용해서 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행한다.
실제 사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스다.
응용 서비스는 기능을 실행하는 데 필요한 입력 값을 메서드 인자로 받고 실행 결과를 리턴한다.
응용 서비스의 메서드가 요구하는 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에
표현 영역은 응용 서비스가 요구하는 형식으로 사용자 요청을 변환한다.
@PostMapping("/member/join")
public ModelAndView join(HttpServletRequest request) {
String email = request.getParameter("email");
String password = request.getParameter("password");
// 사용자 요청을 응용 서비스에 맞게 반환
JoinRequest joinReq = new JoinRequest(email, password);
// 변한한 객체를 이용해서 응용 서비스 실행
joinService.join(joinReq);
}
응용 서비스를 실행한 뒤에 표현 영역은 실행 결과를 사용자에게 알맞는 형식(Json, HTML 등)으로 응답한다.
사용자와 상호 작용은 표현 영역이 처리하기 때문에, 응용 서비스는 표현 영역에 의존하지 않는다.
응용 영역은 사용자가 웹 브라우저를 사용하는지 REST API를 호출하는지, TCP 소켓을 사용하는지를 알 필요가 없다.
그저 기능 실행에 필요한 입력 값을 받고 실행 결과만 리턴하면 된다.
응용 서비스의 역할
응용 서비스는 클라이언트가 요청한 기능을 실행한다.
응용 서비스는 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다.
응용 서비스가 복잡하다면 응용 서비스에서 도메인 로직의 일부를 구현하고 있을 가능성이 높다.
응용 서비스가 도메인 로직을 일부 구현하면 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있다.
응용 서비스는 트랜잭션 처리도 담당한다.
응용 서비스는 도메인의 상태 변경을 트랜잭션으로 처리해야 한다.
도메인 로직 넣지 않기
도메인 로직은 도메인 영역에 위치하고 응용 서비스는 도메인 로직을 구현하지 않는다.
public class ChangePasswordService {
public void changePassword(String memberId, String oldPw, String newPw) {
Member member = memberRepository.findById(memberId);
checkMemberExists(member);
if(!passwordEncoder.matches(oldPw, member.getPassword()) {
throw new BadPasswordException();
}
member.setPassword(newPw);
}
}
public class Member {
public boolean matchPassword(String pwd) {
return passwordEncoder.matches(pwd);
}
}
기존 암호와 일치하는지 확인하는 것은 도메인에서 처리해야할까 서비스에서 처리해야할까?
기존 암호를 올바르게 입력했는지를 확인하는 것은 도메인의 핵심 로직이기 때문에 서비스에서 해당 로직을 구현해서는 안된다.
도메인 로직을 도메인 영역과 응용 서비스에 분산해서 구현하면 코드 품질에 문제가 발생한다.
첫 번째 문제는 코드의 응집성이 떨어진다.
도메인 데이터와 그 데이터를 조작하는 도메인 로직이 한 영역에 위치하지 않고 서로 다른 영역에 위치한다는 것은 도메인 로직을 파악하기 위해 여러 영역을 분석해야하는 필요성이 생긴다.
두 번째 문제는 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다는 것이다.
ChangePasswordService 말고 다른 서비스에서 기존 암호와 일치하는지 확인하는 로직이 필요하다면 위의 코드처럼 또 작성을 해야한다.
코드 중복을 막기 위해 응용 서비스 영역에 보조 클래스를 만들 수 있지만, 애초에 도메인 영역에 암호 확인 기능을 구현했으면 응용 서비스는 해당 기능을 사용하기만 하면 된다.
응용 서비스의 구현
응용 서비스 자체의 구현은 어렵지 않지만, 몇 가지 고민해야할 지점이 있다.
응용 서비스의 크기
회원 도메인을 생각해보자. 서비스는 회원 가입, 회원 탈퇴, 회원 암호 변경, 비밀 번호 초기화 등의 기능을 구현하기 위해 도메인 모델을 사용한다. 여기서 보통 두 가지 방법 중 한 가지 방식으로 구현한다.
- 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
- 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
만약 회원과 관련된 기능을 한 클래스에 모두 구현한다 가정해보자.
public class MemberService {
private MemberRepository memberRepository;
public void join(MemberJoinRequest joinRequest) { ... }
public void changePassword(String memberId, String curPw, String newPw) { ... }
public void initializePassword(String memberId) { ... }
public void leave(String memberId, String curPw) { ... }
...
}
한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있는 장점이 있다.
public class MemberService {
private MemberRepository memberRepository;
public void changePassword(String memberId, String currentPw, String newPw) {
Member member = findExistingMember(memberId);
...
}
public void initializePassword(String memberId) {
Member member = findExistingMember(memberId);
...
}
public void leave(String memberId, String curPw) {
Member member = findExistingMember(memberId);
...
}
// 기능의 동일 로직에 대한 구현 코드 중복 제거
private Member findExistingMember(String memberId) {
Member member = memberRepository.findById(memberId);
if (member == null) {
throw new NoMemberException(memberId);
}
return member;
}
}
각 기능에서 동일한 로직을 위한 코드 중복을 제거하기 쉽다는 것이 장점이라면 한 서비스 클래스의 크기(코드 줄 수)가 커진다는 것이 해당 방식의 단점이다.
코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 되는데 결과적으로 관련 없는 코드가 뒤섞여 코드를 이해하는데 방해가 된다.
게다가 한 클래스에 코드가 모이기 시작하면 엄연히 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워 넣게 된다. 이것은 코드를 점점 얽히게 만들어 코드 품질을 낮추는 결과를 초래한다.
구분되는 기능별로 서비스 클래스를 구현하는 방식은 한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현한다.
public class ChangePasswordService {
private MemberRepository memberRepository;
public void changePassword(String memberId, String curPw, String newPw) {
....
}
...
}
해당 방식을 사용하면 클래스 갯수가 많아지지만 한 클래스에 관련 기능을 모두 구현하는 것과 비교해서 코드 품질을 일정 수준으로 유지하는 데 도움이 된다. 또한 각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않는다.
각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복해서 동일한 코드를 구현할 가능성이 있다.
이 경우 다음과 같이 별도 클래스에 로직을 구현해서 코드가 중복되는 것을 방지할 수 있다.
// 각 응용 서비스에서 공통되는 로직을 별도 클래스로 구현
public final class MemberServiceHelper {
public static Member findExistingMember(MemberRepository repo, String memberId) {
Member member = repo.findById(memberId);
if (member == null) {
throw new NoMemberException(memberId);
}
return member;
}
}
책에서도 그렇고, 내 개인적인 생각에도 한 클래스가 여러 역할을 갖는 것보다는 각 클래스마다 구분되는 역할을 갖는 것이 좋아 보인다. 그래야 관리도 편하고 책임 측면에서도 그게 맞다고 본다.
응용 서비스의 인터페이스와 클래스
응용 서비스를 구현할 때 논쟁이 될만한 것이 인터페이스가 필요한 지이다.
인터페이스가 필요한 몇가지 상황이 있는데, 그 중 하나는 구현 클래스가 여러 개인 경우이다.
구현 클래스가 다수 존재하거나, 런타임에 구현 객체를 교체해야할 때 인터페이스는 유용할 수 있다.
그런데 응용 서비스는 교체해야하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 두 개인 경우도 드물다.
따라서 인터페이스가 명확하게 필요하기 전까지는 응용 서비스에 대한 인터페이스를 작성하는 것이 좋은 선택이라고 볼 수 없다.
메서드 파라미터와 값 리턴
스프링 mvc와 같은 웹 프레임워크는 웹 요청 파라미터를 자바 객체로 변환하는 기능을 제공하므로 응용 서비스에 데이터로 전달할 요청 파라미턷가 2개 이상 존재하면 데이터 전달을 위한 별도 클래스를 사용하는 것이 편리하다.
응용 서비스에서 애그리거트 자체를 리턴하면 코딩은 편할 수 있지만, 도메인의 로직 실행을 응용 서비스와 표현 영역 두 곳에서 할 수 있게 된다.
이것은 기능 실행 로직을 응용 서비스와 표현 영역에 분산시켜 코드의 응집도를 낮추는 원인이 된다.
응용 서비스는 표현 영역에서 필요한 데이터만 리턴하는 것이 기능 실행 로직의 응집도를 높이는 확실한 방법이다.
표현 영역에 의존하지 않기
응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다는 것이다.
표현 영역과 관련된 타입이라면 어떤 것들이 있을까?
HttpServletRequest나 HttpSession 등이 있다.
public class memberController {
public String submit(HttpServletRequest request) {
// 응용 서비스가 표현 영역에 의존하면 안됨
changePasswordService.changePassword(request);
...
}
}
응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기 어려워진다.
게다가 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제도 발생한다.
가장 심각한 것은, 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다는 것이다.
public class AuthenticationService {
public void authenticate(HttpServletRequest request) {
String id = request.getParameter("id");
String password = request.getParameter("password");
if (checkIdPasswordMatching(id, password)) {
// 응용 서비스에서 표현 영역의 상태 처리
HttpSession session = request.getSession();
session.setAttribute("auth", new Authentication(id));
}
}
}
HttpSession이나 쿠키는 표현 영역의 상태에 해당하는데 이 상태를 응용 서비스에서 변경해버리면
표현 영역의 코드만으로 표현 영역의 상태가 어떻게 변경되는지 추적하기 어려워진다.
그렇기에 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않는 것이 좋다.
값 검증
값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다.
원칙적으로 모든 값에 대한 검증은 응용 서비스에서 처리한다.
그런데 표현 영역은 잘못된 값이 존재한다면 이를 사용자에게 알려주고 값을 다시 입력받아야 한다.
응용 서비스에서 각 값이 유효한지 확인할 목적으로 exception을 사용할 때의 문제점은 사용자에게 좋지 않은 경험을 제공한다는 것이다.
사용자는 폼에 값을 입력하고 전송했는데 입력한 값이 잘못되어 다시 폼에 입력해야 할 때 한 개 항목이 아닌 입력한 모든 항목에 대해 잘못된 값이 존재하는지 알고 싶을 것이다. 그래야 한 번에 잘못된 값을 제대로 입력할 수 있기 때문이다.
그런데 응용 서비스에서 값을 검사하는 시점에 첫 번째 값이 올바르지 않아 exception을 발생시키면 나머지 항목에 대해서는 값을 검사하지 않게 된다.
이러면 사용자는 첫 번째 값에 대한 에러 메세지만 볼 뿐 나머지 항목에 대해서는 값이 올바른지를 알 수가 없다.
이는 사용자가 같은 폼에 값을 여러 번 입력하게 하는 불편함을 초래한다.
좋은 방법은, 표현 영역에서 필수 값을 검증하는 것이다.
표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 ID 중복 여부와 같은 논리적 오류만 검사하면 된다.
- 표현 영역 : 필수 값, 값의 형식, 범위 등을 검증한다.
- 응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류를 검증한다.
필자는 요즘은 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편이라고 말하며
응용 서비스에서 필요한 값 검증을 모두 처리하면 프레임워크가 제공하는 검증 기능을 사용할 때보다 작성할 코드가 늘어나는 불편함이 있지만 반대로 응용 서비스의 완성도가 높아지는 이점도 있다 주장한다.
'책 정리 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
Chapter 8. 애그리거트 트랜잭션 관리 (0) | 2023.12.15 |
---|---|
Chapter 7. 도메인 서비스 (0) | 2023.12.12 |
Chapter 5. 스프링 데이터 JPA를 이용한 조회 기능 (0) | 2023.12.08 |
Chapter 4. 리포지터리와 모델 구현 (0) | 2023.12.05 |
Chapter 3. 애그리거트 (0) | 2023.12.01 |