DDD 도메인 주도 설계 6장
in Development on DDD
DDD - 도메인 주도 설계
앞으로 적어갈 글은 최범균님의 저서 DDD START!를 읽고 요약 - 정리한 글입니다.
제 6장 : 응용 서비스와 표현 영역
제 6장 응용 서비스와 표현 영역
표현 영역과 응용 영역
도메인 영역을 잘 구현해야만 사용자의 요구 사항을 충족하는 제대로 된 소프트웨어를 만들 수 있다.
도메인이 제 기능을 하려면 사용자와 도메인을 연결해 주는 매개체가 필요하다.
표현 영역과 응용 영역은 이 매개체 역할을 한다.
표현 영역은 쉽게 말해 웹 브라우저를 떠올리면 된다.
실제 사용자가 원하는 기능을 제공하는 비즈니스 로직은 응용 영역에서 수행한다. (e.q. DB 조회)
응용 서비스의 역할 & 주의점
응용 서비스는 사용자가 요청한 기능을 실행하는 영역을 담당한다.
이 때 리포지터리로부터 도메인 객첼르 구하고, 도메인 객체를 사용한다.
주의 사항은 응용 서비스에서 도메인 로직을 넣어서는 안된다.
응용 서비스에서 도메인 로직을 넣으면 두 가지 단점이 존재한다.
- 코드의 응집도가 떨어진다.
- 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다. (이는 실제로 몇 번 경험한 이슈)
응용 서비스의 구현
응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 하는데 이는 디자인 패턴에서 facade(파사드)와 같은 역할을 한다.
응용 서비스의 구현은 어렵지 않지만 몇 가지 고려해야할 사항들이 있다.
응용 서비스의 크기
가령 회원 도메인에서 회원 가입하기, 회원 탈퇴하기, 회원 암호 변경하기, 비밀번호 초기화하기와 같은 기능을 구현한다 가정해보자.
일반적인 응용 서비스는 다음 두 가지 방법 중 하나로 개발 된다.
한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
- 장 : private 메소드를 사용해 중복 코드 제거 가능
- 단 : 코드 크기가 커져서 관련 없는 코드가 뒤섞여 코드를 이해하는데 방해가 될 수 있음
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
- 장 : 1번의 단, 코드가 쪼개져서 관리되므로 코드 품질을 유지하기 쉬움
- 단 : 1번의 장과 반대, 중복 코드가 많이 생길 수 있음
응용 서비스의 인터페이스와 클래스
XXXService라는 Inerface와 XXXServiceImpl이라는 구현 클래스의 조합을 많이 보았을 것이다.
국내 스프링 코드에서는 사실상 이게 국룰처럼 여겨지고 있다.
우선 책의 내용을 적기 이전에 필자는 이 관례를 굉장히 싫어하는 편이다.
인터페이스가 정말로 여러 구현 클래스로 표현해야할 때 도메인 이름으로 Service 인터페이스를 만들어버리다 보니, 다형성을 갖기가 어려워지는 현상을 종종 경험했었다. (결국 이름을 다시 뜯어 고쳤다.)
물론 이것이 무조건적으로 잘못됐다고만도 볼 수만은 없는게, 저자의 내용에서도 나오지만 TDD의 경우 테스트 코드를 먼저 작성하기 때문에 인터페이스를 이용해서 컨트롤러의 구현을 완성해나가게 된다.
도메인 영역이나 응용 영역의 개발을 먼저 시작하면 응용 서비스가 먼저만들어지므로 인터페이스의 사용을 안할 수도 있다.
이렇게 되면 표현 영역의 단위 테스트를 위해서는 가짜 객체가 필요한데 이는 Mockito같은 테스트용 객체가 테스트용 객체를 만들어주므로 응용 서비스에 대한 인터페이스가 없어도 표현 영역을 테스트할 수 있다.
이는 결과적으로 응용 서비스에 대한 인터페이스 필요성을 약화시킨다.
메서드 파라미터와 값 리턴
표현 영역은 사용자로부터 데이터를 입력받아 응답을 반환해준다.
이 때 응답을 도메인 모델로 할 수 있으나, 이럴 경우 도메인 로직 실행을 응용 서비스와 표현 영역 두 곳에서 모두 가능하게 된다. (물론 표현 영역에서 쓰지말라는 정책을 세울 수는 있지만.. 글쎄?)
그러니 가급적이면 응용 서비스 영역의 반환 값은 표현영역의 반환값인 편이 낫다.
표현 영역에 의존하지 않기
응용 서비스의 파라미터 값은 표현 영역과 관련된 파라미터 값이면 안된다.
응용 서비스에서 표현 영역에 대한 의존이 발생하는 순간 응용 서비스만 단독으로 테스트하기가 어려워진다.
심지어 표현 영역이 바뀌면 응용 서비스도 바뀌어야함 (OMG)
트랜잭션 처리
회원가입에 성공했따고 하면서 회원 정보를 DB에 저장하지 않으면 고객은 로그인을 할 수 없다.
이 때 프레임워크가 제공하는 트랜잭션 기능을 적극 사용하는 것이 좋다.
스프링으 경우
@Transactional
이 그 대표적인 예이다. (동시에 AOP이기도 하다.)
도메인 이벤트 처리
public class Member {
private Password password;
public void initializePassword() {
String newPassword = generateRandomPassword();
this.password = new Password(newPassword);
Events.raise(new PasswordChangedEvent(this.id, password);
}
}
위 코드는 암호 초기화 기능을 수행한 이후 암호 변경 이벤트를 발생시키는 코드이다.
응용 서비스는 이벤트를 받아 처리할 수 있는데 이러한 이벤트 기반의 개발은 코드의 복잡도가 올라가지만 도메인 간의 의존성이나 외부 시스템에 대한 의존도를 낮추는 장점을 얻을 수 있다. (이것이 이벤트 주도 개발?)
표현 영역
표현 영역의 책임은 크게 다음과 같다.
사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어한다.
사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
사용자 세션을 관리한다.
- MVC 프레임워크는 HTTP 요청 파라미터로부터 자바 객체를 생성하는 기능을 제공하기 때문에 손쉽게 HTTP 요청을 자바 객체로 바인딩할 수 있다.
값 검증
@Controller
public class Controller {
@RequestMapping
public String join(JoinRequest joinRequest, Errors errors) {
checkEmpty(joinRequest.getId(), "id", errors);
checkEmpty(joinRequest.getName(), "name", errors);
... // 나머지 값 검증
// 모든 값의 형식을 검증한 뒤, 에러가 존재하면 다시 폼을 보여줌
if(errors.hasErrors()) return formView;
...
}
private void checkEmpty(String value, String property, Errors errors) {
if(isEmpty(value)) erros.rejectValue(property, "empty");
}
값 검증은 표현 영역과 응용 서비스 모두 수행할 수 있다.
원칙적으로 모든 값에 대한 검증은 응용 서비스에서 담당한다.
사용자 요청의 데이터는 표현 영역에서 검증하는 편이 낫다. (응용 서비스의 검증과 표현 영역의 검증의 분리)
권한 검사
권한에 대한 검사는 다음 영역에서 수행할 수 있다.
표현 영역
인증된 사용자 여부 검사
접근 제어를 하기에 좋은 위치가 서블릿 필터이다. (스프링 시큐리티가 접근 제어를 위해 이 필터를 사용한다.)
응용 서비스 영역
- 스프링 시큐리티는 AOP를 활용해서 다음과 같이 애노테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다.
public class BlockMemberService {
private MemberRepository memberRepository;
@PreAuthorize("hasRole('ADMIN')")
public void block(String memberName) {
Member member = memberRepository.findByName(memberName);
if(member == null) throw new NoMemberException();
member.block();
}
...
}
도메인 영역
public class DeleteArticleService {
public void delete(String userId, Long articleId) {
Article article = articleRepository.findById(articleId);
checkArticleExistence(article);
permissionService.checkDeletePermission(userId, article);
article.markDeleted();
}
...
도메인 단위로 권한 검사를 하려는 경우 다소 복잡해질 수 있다.
검증 로직을 직접 구현해야한다.
스프링 시큐리티는 만능
스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수도 있겠지만 그럴려면 프레임워크 자체에 대한 이해도가 높아야한다.
프레임워크를 확장할 만큼 이해도가 높지 않다면 권한 검사 기능을 직접 구현하는 것이 코드 유지보수에 유리할 수 있다. (스프링 시큐리티가 계속 변화하고 있어서 이걸 쫓아가는 것도 이슈 중 하나)
조회 전용 기능과 응용 서비스
public class OrderListService {
public List<OrderView> getOrderList(String ordererId) {
return orderViewDao.selectByOrderer(ordererId);
}
}
- 때때로 서비스 조회만 하는 경우가 종종 있다.
public class OrderController {
private OrderViewDao orderViewDao;
@RequestMapping("/myorders")
public String list(ModelMap model) {
String ordererId = SecurityContext.getAuthentication().getId();
List<OrderView> orders = orderViewDao.selectByOrderer(ordererId);
model.addAttribute("orders", orders);
return "order/list";
}
...
만약 서비스에서 수행하는 추가적인 로직이 없고 조회 전용이라 트랜잭션이 필요 없을 경우 표현 영역에서 바로 조회 전용 기능을 사용해도 된다.
즉, 저자는 응용 서비스가 사용자 요청 기능을 실행하는데 별 기여를 하지 못한다면 굳이 서비스를 만들지 않아도 된다고 말한다.
이는 논쟁의 여부가 있을 것 같은데, 개인적으로는 그래도 서비스를 만드는게 낫다고 생각한다.
왜냐하면 추후에 해당 응용 서비스에 조회 외에 다른 비즈니스 로직들이 추가될 여지가 분명히 있을 것이므로…
Reference
DDD START! - 최범균님 -
그림 및 코드 참조 : https://incheol-jung.gitbook.io/docs/study/ddd-start/1