728x90
반응형
표현 영역(Presentation Layer)
- 사용자의 요청을 해석
- 사용자가 실행하고 싶은 기능을 판별하고 그 기능을 제공하는 응용 서비스를 실행
- 응용 서비스의 메서드가 요구하는 파라미터와 표현 영역이 사용자로부터 전달받은 데이터는 형식이 일치하지 않기 때문에 표현 영역은 서비스가 요구하는 형식으로 사용자 요청을 변환 필요
응용 영역(Application Layer)
- 실제 사용자가 원하는 기능을 제공
@PostMapping("/member/join")
fun join(req: HttpServletRequest): ModelAndView {
val emal = req.getParameter("email")
val password = req.getParameter("password")
// 요청을 응용 서비스에 맞게 변환
val joinReq = JoinRequest(email, password)
// 응용 서비스 실행
joinService.join(joinReq)
...
}
- Controller에서 HttpServletRequest가 아닌 JoinRequest(Dto)를 받게 되면 응용 서비스계층으로 바로 넘겨도 되지 않을까?
- 그렇게하면 Presentation Layer관련 코드가 섞여있을 수 있지 않은가? (swagger관련 코드라던가..)
- 응용계층에서 적절한 객체로 변환해서 사용해도 문제 없지 않을까?
응용 서비스(Application Layer)의 역할과 구현
- 응용 서비스는 클라이언트가 요청한 기능을 실행한다.
- 응용 서비스는 사용자의 요청을 처리하기 위해 리포지터리에서 도메인 객체를 가져와 사용한다.
- 즉, 응용 서비스의 주요 역할은
도메인 객체를 사용해서 사용자의 요청을 처리
하는 것이다. 응용 서비스는 도메인 영역과 표현 영역을 연결해 주는 창구다. (도메인 객체 간의 흐름만 제어한다.)
도메인 로직 넣지 않기
- 도메인 로직은 도메인 영역에 위치시키자. 응용 서비스는 도메인 로직을 구현하지 않는다.
// Domain Layer
class Member {
fun changePassword(oldPw: String, newPw: String) {
if (!matchPassword(oldPw)) throw BasPasswordException()
setPassword(newPw)
}
// 현재 암호와 일치하는지 검사하는 도메인 로직
fun matchPassword(pwd: String): Boolean = passwordEncoder.matches(pwd)
..
}
// Application Layer
class ChangePasswordService {
// 도메인 로직이 도메인 영역에 존재하는 경우
fun changePaasword(memberId: String, oldPw: String, newPw: String) {
val member = memberRepository.findById(memberId)
checkMemberExists(member)
member.changePassword(oldPw, newPw)
}
// 도메인 로직을 응용계층에서 구현한 경우
fun changePaasword(memberId: String, oldPw: String, newPw: String) {
val member = memberRepository.findById(memberId)
checkMemberExists(member)
if (!passwordEncoder.matches(oldPw, member.password)) {
throw BasPasswordException()
}
member.setPassword(newPw)
}
}
문제점?
1. 코드의 응집성이 떨어진다. (도메인 로직이 도메인/응용 서비스에 분산해서 구현되면 코드 품질이 저하됨)
- 도메인 데이터와 그 데이터를 조작하는 도메인 로직이 한 영역에 위치하지 않으면 도메인 로직을 파악하기 위해 여러 영역을 분석해야한다는 것을 의미2. 여러 응용 서비스에서 동일한 도메인 로직을 구현할 가능성이 높아진다.
- 계정 해지를 위해 암호를 확인하는 경우, 동일한 로직(passwordEncoder.matches)이
해지 메소드
에 들어가야 한다. - 중복 발생을 피하기 위해 보조 클래스를 만들 수 있지만, 애초에 도메인 영역에서 확인 기능을 구현하면 응용 서비스에 코드 중복 문제는 발생하지 않는다.
-> 변경 용이성이 중요. 변경이 어렵다는 것은 그만큼 소프트웨어의 가치가 떨어진다는 것을 의미
-> 도메인 로직을 도메인 영역에 모아서 코드 중복을 줄이고 응집도를 높이자
응용 서비스(Application Layer) 구현
- facade와 같은 역할을 함(feat. design pattern)
- 복잡한 로직을 수행하지 않으며, 응용-도메인 영역을 연결하는 매개체 역할
서비스의 크기 결정하기
- 어느 정도의 크기가 적당한 걸까?
- 회원 도메인을 구현한다면, 가입/탈퇴/암호변경/초기화 등 기능 존재
case1. 한 응용 서비스 클래스에 도메인 모든 기능 구현
- 장점 : 각 기능에서 동일 로직에 대한 코드 중복을 제거 가능 (중복 로직 private 구현)
- 단점 : 클래스의 크기가 커져, 연관성이 적은 코드가 함께 위치할 가능성이 높아짐(뒤섞임)
- 엄연히 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 끼어 넣게 됨(점점 얽히게 되는.. 품질 낮추는 결과 초래)
case2. 구분되는 기능별로 응용 서비스 클래스를 따로 구현(추천)
- 한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현
- ex. 암호 변경 기능만을 위한 응용 서비스 클래스를 별도로 구현
- 장점 : 한 클래스에 관련 기능을 구현하는 것과 비교해서 코드 품질을 일정 수준으로 유지할 수 있음
- 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않음
- 공통되는 로직은 별도의 클래스(Helper)로 구현
- 단점 : 클래스 개수가 많아짐
메서드 파라미터와 리턴 값은 어떻게 구성하는게 좋을까
- 파라미터
- 응용 서비스에 데이터로 전달할 요청 파라미터가 2개 이상 존재하면 별도 클래스를 사용하자
- 리턴 값
- case1. 표현 영역에서 필요한 값을 리턴
-
// controller val orderNo: String = orderService.placeOrder(orerReq) return orderNo
- case2. 애그리거트 객체를 그대로 리턴 & 응용계층에서 필요한 값을 구해 응답화면으로 전달
-
// controller val order: Order = orderService.placeOrder(orderReq) return order // view ${order.number}
- 응용 서비스에서 애그리거트 자체를 리턴하면 코딩은 편하나, 도메인 로직 실행을 응용/표현 영역 두 곳에서 할 수 있게 된다.
- 기능 실행 로직을 분산시켜 응집도를 낮추는 원인이 되므로 필요한 값만 리턴하자
조회 전용 기능과 응용 서비스
- 서비스에서 수행하는 추가 로직이 없고 && 단일 쿼리만 실행하는 조회 기능이라면 서비스를 만들 필요 없이 표현 영역에서 바로 조회 기능을 사용해도 문제가 없다.
class OrderController(
private orderViewDao: OrderViewDao
){
fun list(criteria: Criteria): OrderResponse {
return orderViewDao.selectByOrderer(criteria.ordererId)
}
}
728x90
반응형
'프로그래밍 노트 > 도메인 주도 설계(DDD)' 카테고리의 다른 글
[도메인 주도 설계] 도메인 모델과 바운디드 컨텍스트 (0) | 2023.08.21 |
---|---|
[도메인 주도 설계] 도메인 서비스(Domain Service) (0) | 2023.08.16 |
[도메인 주도 설계] 애그리거트에 대해 (3) | 2023.08.07 |
[도메인 주도 설계] 아키텍처에 관하여 (0) | 2023.07.23 |
[도메인 주도 설계] 도메인이란? (도메인과 친해져보자) (0) | 2023.07.23 |