- 도메인 영역을 구현을 잘 한다고 소포트웨어 구현이 끝나는 것은 아니다.
- 도메인이 제 기능을 하려면 사용자와 도메인을 연결해 주는 매개체가 필요하다.
사용자가 원하는 기능을 제공하는 것은 응용 영역에 위치한 서비스다. 회원 가입을 요청했다면 실제 그 요청을 위한 기능을 제공하는 주체는 응용 서비스에 위치한다.
- 사용자가 요청한 기능을 실행한다.
- 사용자의 요청을 처리하기 위해 레포지터리에서 도메인 객체를 가져와 사용한다.
- 표현 영역에서 보았을 때 응용 서비스는 도메인 영역과 표현 영역을 연결해 주는 창구 역할을 한다.
주로 도메인 객체 간의 흐름을 제어하기 때문에 단순한 형태를 갖는다.
fun doSomeFunc(req: SomeReq): Result {
// 1. 레포지터리에서 애그리거트를 구한다.
val agg = someAggRepository.findById(req.id)
checkNull(agg)
// 2. 애그리거트의 도메인 기능을 실행한다.
agg.doFunc(req.value)
// 3. 결과를 리턴한다.
return createSuccessResult(agg)
}
새로운 애그리거트를 생성하는 응용 서비스 또한 간단하다.
fun doSomeCreation(req: CreateSomeReq): Result {
// 1. 데이터 유효성 검사
validate(req)
// 2. 애그리거트 생성
val newAgg = createSome(req)
// 3. 레포지터리에 애그리거트 저장
someAggRepository.save(newAgg)
// 4. 결과 리턴
return createSuccessResult(newAgg)
}
- 도메인 로직은 도메인 영역에 위치하고 응용 서비스는 도메인 로직을 구현하지 않는다.
class ChangePasswordService(private val memberRepository: MemberRepository) {
fun changePassword(memberId: String, oldPw: String, newPw: String) {
val member = memberRepository.findById(memberId)
checkMemberExists(member)
member.changePassword(oldPw, newPw)
}
}
위 코드처럼 응용 서비스는 Member
애그리거트와 관련 레포지터리를 이용해서 도메인 객체 간의 실행 흐름을 제어한다.
class Member(
var pw: String
) {
private var password: String = pw
fun changePassword(memberId: String, oldPw: String, newPw: String) {
if (!matchPassword(oldPw)) throw BadPasswordException()
setPassword(newPw)
}
fun matchPassword(pwd: String): Boolean {
return passwordEncoder.matches(pwd)
}
fun setPassword(newPw: String) {
if (newPw.isEmpty()) throw IllegalArgumentException("no new password")
this.password = newPw
}
}
Member
는 암호를 변경하기 전에 기존 암호를 올바르게 입력했는지 확인한다.
암호를 올바르게 입력했는지를 확인하는 것은 도메인의 핵심 로직이기 때문에 응용 서비스에서는 로직을 구현하면 안된다.
- 도메인 영역과 표현 영역을 연결하는 매개체 역할을 하는 응용 서빗는 디자인 패턴에서 파사드와 같은 역할을 한다.
회원과 관련된 기능을 한 클래스에서 모두 구현하면 다음과 같은 모습을 갖는다.
class MemberService(
private val memberRepository: MemberRepository
) {
fun join(joinRequest: MemberJoinRequest) {...}
fun changePassword(memberId: String, curPw: String, newPw: String) {...}
fun initializePassword(memberId: String) {...}
fun leave(memberId: String, curPw: String) {...}
}
class MemberService(
private val memberRepository: MemberRepository
) {
fun join(joinRequest: MemberJoinRequest) {
val member = findExistingMember(memberId)
member.changePassword(currentPw, newPw)
}
fun changePassword(memberId: String, curPw: String, newPw: String) {
val member = findExistingMember(memberId)
val newPassword = member.initializePassword()
notifier.nofiryNewPassword(member, newPassword)
}
fun leave(memberId: String, curPw: String) {
val member = findExistingMember(memberId)
member.leave()
}
// 중복 코드 제거
private fun findExistingMember(memberId: String): Member {
return memberRepository.findById(memberId).orElseThrow {throw NotFoundException()}
}
}
class ChangePasswordService(private val memberRepository: MemberRepository) {
fun changePassword(memberId: String, oldPw: String, newPw: String) {
val member = memberRepository.findById(memberId).orElseThrow { throw NotFoundException() }
member.changePassword(memberId, oldPw, newPw)
}
}
interface ChangePasswordService {
fun changePassword(memberId: String, curPw: String, newPw: String)
}
class ChangePasswordImpl: ChangePasswordService {...}
- 응용 서비스가 제공하는 메서드는 도메인을 이용해서 사요자가 요구한 기능을 실행하는 데 필요한 값을 파라미터로 전달 받아야 한다.
파라미터로 값을 전달받을 수 있도록 데이터 클래스를 만들어 전달을 받을 수 있다.
data class ChangePasswordRequest(
private val _memberId: String,
private val _currentPassword: String,
private val _newPassword: String
) {
val memberId: String
get() = _memberId.ifEmpty { "" }
val currentPassword: String
get() = _currentPassword.ifEmpty { "" }
val newPassword: String
get() = _newPassword.ifEmpty() { "" }
}
응용 서비스는 파라미터로 전달받은 데이터를 사용해서 필요한 기능을 구현한다.
class ChangePasswordServiceImpl(private val memberRepository: MemberRepository): ChangePasswordService {
override fun changePassword(req: ChangePasswordRequest) {
val memberId = req.getMemberId()
val member = memberRepository.findById(memberId).orElseThrow { throw NotFoundException() }
member.changePassword(memberId, req.getCurrentPassword(), req.getNewPassword())
}
}
- 표현 영역과 관련된 타입을 사용하면 안 된다.
@Controller
@RequestMapping("/member/changePassword")
class MemberPasswordController {
@PostMapping
fun submit(request: HttpServletRequest): String {
try {
// 응용 서비스가 표현 영역을 의존하면 안된다.
changePasswordService.changePassword(request)
} catch (ex: NotFoundException) {
// Error handling
}
}
}
- DB에 데이터가 반영이 되지 않는다면 응용 서비스의 역할을 하지 않는 것이다.
스프링에서 제공하는 트랜잭션 관리 기능을 사용한다.
open class ChangePasswordServiceImpl(private val memberRepository: MemberRepository):
ChangePasswordService {
@Transactional
override fun changePassword(req: ChangePasswordRequest) {
val memberId = req.memberId
val member = memberRepository.findById(memberId).orElseThrow { throw NotFoundException() }
member.changePassword(memberId, req.currentPassword, req.newPassword)
}
}
- 사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어
- 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공
- 사용자의 세션을 관리
- 값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행
- 모든 값에 대한 검증은 응용 서비스에서 처리
open class JoinService(private val memberRepository: MemberRepository) {
@Transactional
open fun join(joinReq: JoinRequest) {
checkEmpty(joinReq.id, "id")
checkEmpty(joinReq.name, "name")
checkEmpty(joinReq.password, "password")
if (joinReq.password.equals(joinReq.getConfirmPassword()))
throw IllegalArgumentException("confirmPassword")
}
private fun checkEmpty(value: String?, propertyName: String) {
if (value.isNullOrEmpty()) throw IllegalArgumentException(propertyName)
}
private fun checkDuplicatedId(id: String) {
val count = memberRepository.countsById(id)
if (count > 0) throw IllegalArgumentException()
}
}
표현 영역은 잘못된 값이 존재하면 이를 사용자에게 알려주고 값을 다시 입력받아야한다. 스프링 MVC는 에러 메세지를 보여주기 위한 용도로 Errors
나 BindingResult
를 사용하는데 컨트롤러에서 응용 서비스를 사용하면 폼에 에러 메세지를 보여주기 위해 번잡한 코드를 작성해야 된다.
Validator
인터페이스를 사용하여 검증기를 따로 구현한다.
- 단순한 시스템은 인증 여부만 검사하면 되는데 반해, 관리자인지에 따라 사용할 수 있는 기능이 달라지기도 한다.
-> 서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부를 검사한다.
open class ChangePasswordServiceImpl(private val memberRepository: MemberRepository):
ChangePasswordService {
@Transactional
@PreAuthorize("hasRole('ADMIN)")
override fun changePassword(req: ChangePasswordRequest) {
val memberId = req.memberId
val member = memberRepository.findById(memberId).orElseThrow { throw NotFoundException() }
member.changePassword(memberId, req.currentPassword, req.newPassword)
}
}
- 응용 서비스에서 전용 기능을 만들면 서비스 코드가 단순히 전용 기능을 호출하는 형태가 된다.
class OrderListService {
fun getOrderList(ordererId: String): List<OrderView> {
return orderViewDao.selectByORderer(ordererId)
}
}