[도메인 주도 설계 철저 입문] 6. 어플리케이션 서비스 - 도메인 서비스와의 분리!
이 글을 읽고 깨달아,, 적용한 커밋. 아래 PR 중에 있다.
[refactor/member] 회원 서비스 리팩토링 by Dong-Hyeon-Yu · Pull Request #94 · InhaBas/Inhabas.com-api
학년 정보 없애기 학기 -> 기수 중복검사로직을 도메인 영역으로 분리 (링크) 회원가입 서비스 리팩토링 회원종류 생성 #93 회원가입 가능 기간 및 인터뷰 기간 db 설정 기능 추가 팀 생성 #77 resolve
github.com
이 책에서 말하는 서비스는 크게 두가지로 나뉘는 듯 하다.
하나는 도메인 서비스, 다른 하나는 어플리케이션 서비스.
- 도메인의 특성에 관한 활동은 도메인 서비스.
- 클라이언트에게 제공할 UseCase 에 대한 활동은 어플리케이션 서비스.
기존에는 layered architecture 의 서비스에서 말하는 어플리케이션 서비스 밖에 몰랐는데,
이번 챕터에서 정확히 도메인서비스와 어플리케이션 서비스를 구분하는 사례가 있어서 말끔하게 정리가 되었다.
동시에 기존 프로젝트에서 비슷한 안티패턴으로 코드를 작성했던게 기억나서, 여기서 직접 한번 리팩토링 해보겠다!
아래는 회원가입하는 코드이다. (어플리케이션 서비스)
package member.service;
public class MemberApplicationService {
private final MemberRepository memberRepository;
(...생략...)
@Override
@Transactional
public Member saveSignUpForm(StudentSignUpDto signUpForm) {
(...생략...)
checkDuplicatedMemberId(signUpForm.getMemberId());
checkDuplicatedMemberPhoneNumber(signUpForm.getPhoneNumber());
Member member = Member.builder.
(...)
.build()
return memberRepository.save(member);
}
private void checkDuplicatedMemberId(Integer id) {...}
private void checkDuplicatedMemberPhoneNumber(PhoneNumber number) {...}
}
(1) 회원 중복 검사를 진행하고,
(2) 회원 엔티티를 생성한 후에,
(3) 저장한다.
문제는 서비스 코드에 도메인의 성격을 다루는 코드들이 섞여 있다는 점이다.
중복검사를 하는 코드는 바꾸어 말하면 "회원 id 와, 핸드폰 번호는 회원마다 유일해야한다." 는 도메인의 특성이다.
또 해당 코드 안에서는, 중복되는 경우 해당 케이스에 맞는 오류를 던지도록 하고 있다.
오류를 던지는 것은 어플리케이션의 기술적인 부분이지 도메인의 특성은 아니다.
따라서 리팩토링을 진행할 때, 예외를 던지는 부분은 어플리케이션 서비스에 남겨둬야한다.
package members.domain;
// 도메인의 특성을 나타내는 서비스
class MemberDuplidationChecker {
private final MemberRepository memberRepository;
public MemberDuplicationChecker(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public boolean ok(Integer memberId) {
return !memberRepository.existById(memberId);
}
@Transactional
public boolean ok(PhoneNumber number) {
return !memberRepository.existByPhoneNumber(number);
}
@Transactional
public boolean ok(SignUpForm form) {
if (!this.ok(form.getId()) || !this.ok(form.getPhoneNumber()))
return false;
return true;
}
}
package member.service;
public class MemberApplicationService {
private final MemberRepository memberRepository;
private final MemberDuplicationChecker duplicationChecker;
(...생략...)
@Override
@Transactional
public Member saveSignUpForm(StudentSignUpDto signUpForm) {
(...생략...)
if (!duplicationChecker.ok(signUpForm)) {
throw new CannotCreateMember("해당 필드가 이미 존재합니다.")
}
Member member = Member.builder.
(...)
.build()
return memberRepository.save(member);
}
}
이렇게 바꿀 수 있다.
꼭 이렇게 바꿔야하나? 별로 다른 거 같지 않은데? 라고 생각할 수 있다. 그렇다면 다음과 같은 상황을 생각해보자.
사실 회원의 중복검사를 하는 로직은 꽤 많은 곳에 흩어져 있다.
(1) 처음 회원가입을 할 때,
(2) 회원정보를 수정할 때,
(3) 기타 회원이 꼭 존재해야만하는 메서드,
특히 회원 시스템이 있는 곳이라면 (3) 번의 로직에 굉장히 많을 수 있다.
이렇게 많은 부분에 memberRepository.findById(memberId) 라는 코드가 직접 사용되었다고 생각해보자.
그런데 나중에 로그인 로직이 변경되면서, email 로 회원의 중복 검사를 해야한다고 가정해보자.
(사실 pk 인 id 로 중복로직을 검사하니까, 웬만하면 그럴일은 없겠지만 서비스는 항상 변화하므로, 요구사항이 그렇게 변했다고 가정해보자)
그러면 이때 중복 검사 로직을 시행하는 모든 곳을 찾아서 memberRepository.findByEmail(email) 이렇게 바꿔주어야한다.
하지만 중복 검사를 MemberDuplicationChecker 에게 위임함으로써, 비지니스 어디에서나 동일한 인터페이스로 일관되게 코드를 작성할 수 있고, 나중에 수정을 하더라도 MemberDuplicationChecker의 코드만 수정하면 된다.
또 다른 장점은 언급했다시피, 도메인의 성격을 잘 나타낼 수 있다는 것이다.
예시코드의 패키지를 잘 보자.
members.domain 폴더에는 회원 엔티티와 값 객체 뿐 아니라, 도메인 서비스(MemberDuplicationChecker)가 있다.
이제는 domain 폴더를 보면 id와 핸드폰 번호가 유일해야함을 추가적으로 더 알 수 있게 된 것이다.
이런 중복로직이 비지니스 코드에 산재되어 있으면, 해당 도메인의 속성을 한 눈에 파악하기가 어렵다.
결과적으로는 "회원 도메인의 중복 여부" 라는 특성을 도메인 폴더로 빼버리면서
처음보다는 더 도메인의 특성을 잘 분리해낼 수 있었다!