//
Search
Duplicate
📖

[도메인 주도 개발 시작하기] - 표현 영역과 응용 영역

상태
완료
담당자
시작일
2023/09/15 09:44
최종편집일
2023/12/10 11:50

표현 영역과 응용 영역

표현 영역은 사용자의 요청을 해석
실제 사용자가 원하는 기능은 응용영역에서 제공
@PostMapping("/member/join") public ModelAndVeiw join(HttpServletRequest request) { String email = request.getParameter("email"); String password = request.getPassword("password"); //사용자 요청을 응용 서비스에 맞게 변환 JoinReuqest joinReq = new JoinRequest(email, password); // 변환한 객체(데이터)를 이용해서 응용 서비스 실행 JoinService.join(joinReq); ... }
Java
복사

응용 서비스의 역할

사용자(클라이언트)가 요청한 기능을 실행
도메인 객체를 사용해서 사용자의 요청을 처리하는 것
주로 도메인 객체 간의 흐름을 제어하기 때문에 아래와 같이 구현
public Result doSomeFunc(SomeReq req) { // 1. 리포지터리에서 애그리거트를 구한다. SomeAgg agg = someAggRepository.findById(req.getId()); checkNull(agg); // 2. 애그리거트의 도메인 기능을 실행한다. agg.doFunc(req.getValau()); // 3. 결과를 리턴한다. return createSuccessResult(agg); }
Java
복사
public Result doSomeCreation(CreateSomeReq req) { // 1. 데이터 중복 등 데이이터가 유효한지 검사한다. validate(req); // 2. 에그리거트를 생성한다. someAgg newAgg = createSome(req); // 3. 리포지터리에 에그리거트를 저장한다. someAggRepository.save(newAgg); // 4. 결과를 리턴한다. return createSuccessResult(newAgg); }
Java
복사
응용 서비스가 복잡하다면 응용 서비스에 도메인 로직의 일부를 구현하고 있을 가능성이 높다.
응용 서비스는 트랜잭션 처리도 담당한다.
public void blockMembers(String[] blockIds) { if (blockingIds == null || blockingIds.length == 0) return; List<Member> members = memberRepostiory.findByIdIn(blockIds); for (Member member : members) { mem.block(); } }
Java
복사
blockMembers() 메서드가 트랜잭션 범위에 실행되지 않으면 반영 도중 문제가 생길때 일부 Member만 차단 상태가 되어 데이터 일관성이 께지게 된다. 때문에 응용 서비스의 트랜잭션 범위에서 실행해야 된다.
도메인 로직 넣지 않기
public class changePasswordService { public void changePassword(String memberId, String oldPw, String newPw) { Member member = memberRepository.findById(memberId); checkMemberExists(member); member.changePassword(oldpw, newPw); } ... }
Java
복사
Member 애그리거트는 암호를 변겨하기 전에 기존 암호를 올바르게 입력했는지 확인하는 로직을 구현한다.
public class Member { public void changePasswrod(String oldPw, String newPw) { if (!matchPassword(oldPw)) throw new badPasswordException(); setPassword(newPw); } // 현재 암호와 일치하는지 검사하는 도메인 로직 public boolean matchPassword(String pwd) { return passwordEncoder.matches(pwd); } private void setPasswrod(String newPw) { if (isEmpty(newPw)) throw new IllegalArgumentException("no new password"); this.password = newPw; } }
Java
복사
기존 암호를 올바르게 입력했는지 확인하는 것은 도메인의 핵심 기능 이기때문에 응용 서비스에 구현하면 안된다.
public class ChangePasswordService { public void changePassword(String memberId, String oldPw, String newPw) { Member member = memberRepository.findById(memberId); if (!passwordEncoder.matches(oldPw, member.getPassword())) { throw new badPasswordException(); } member.setPassword(newPw); } }
Java
복사
도메인 로직을 도메인 영역과 응용 서비스에 구현하면
1.
코드의 응집성이 떨어진다.
2.
여러 응용 서비스에 동일한 도메인 로직을 구현할 가능성이 높아진다.
이러한 문제 (응집도가 떨어지고 코드 중복 발생)은 결과적으로 코드 변경을 어렵게 만든다.

응용 서비스의 구현

응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역할을 한다.
응용 서비스의 크기
회원 도메인을 생각 해보자. 보통 두가지 방법 중 한가지 방식으로 구현한다.
한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
한 응용서비스 클래스에 모두 구현
public class MemberService { // 각 기능을 구현하는 데 필요한 리포지터리, 도메인 서비스 필드 추가 private MemberRepository memberRepostiory; public void join(MemberJoinRequest joinRequest) {...} public void changePassword(String memberId, String curPw, String newPw) {...} ... }
Java
복사
장점
동일 로직에 대한 코드 중복을 제거할 수 있다.
단점
클래스의 크기가 커진다. 연관성 없는 코드가 한 클래스에 위치 할 수 있다.
엄연히 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워 넣게 된다.
별도 클래스 구현
public class ChangePasswordService { private MemberRepostiory memberRepostiory; public void changePassword(String memberId, String curPw, String newPw) { Member member = memberRepostory.findById(memberId); if (member == null) throw new NoMemberException(memberId); member.changePassword(curPw, newPw); } ... }
Java
복사
//각 응용 서비스에서 공통되는 로직을 별도 클래스로 구현 public final class MemberServiceHelper { public static Member findExistingMember(MemberRepositry repo, String memberId) { Member member = repo.findById(memberId); if (member == null) throw new NoMemberException(); return member; } } // 공통 로직을 제공하는 메서드를 응용 서비스에서 사용 public class ChangePasswordService { private MemberRepositry memberRepostiory; public void changePassword(String memberId, String curPw, String newPw) { Member member = findExistingMember(memberRepostiory, memberid); member.changePassword(curPw, newPw); } }
Java
복사
메서드 파라미터와 값 리턴
응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는 데 필요한 값을 파라미터로 전달 받아야 한다.
public class ChangePasswordService { // 암호 변경 기능 구현에 필요한 값을 파라미터로 전달 받음 public void changePassword(String memberId, String curPw, String newPw) { ... } }
Java
복사
다음 코드처럼 별도 클래스 전달
public class ChangePasswordRequest { private String memberId; private String currentPassword; private String newPassword(); .. get 메서드 등 생략 }
Java
복사
public class ChangePasswordService { // 암호 변경 기능 구현에 필요한 값을 파라미터로 전달 받음 public void changePassword(ChangePasswordRequest req) { Member member = findExistingMember(req.getMemberId()); member.changePassword(req.getCurrentPassword(), req.getNewPassword()); } ... }
Java
복사
표현 영역에 의존하지 않기
응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안된다는 점이다.
@Controller @RequestMapping("/member/changePassword") public class MemberPasswordController { @PostMapping public String submit(HttpSergiceRequest request) { try{ // 응용 서비스가 표현 영역을 의존하면 안됨 changePasswordService.changePassword(request); } catch (NoMemberException ex){ // 알맞은 익셉션 처리 및 응답 } } }
Java
복사
응용 서비스가 표현영역에 대한 의존이 발생하면
1.
응용 서비스 단독으로 테스트하기 어렵다
2.
표현영역이 변경되면 응용영역도 변경해야된다.
3.
응용서비스가 표현 영역의 역할까지 대신 하는 상황이 발생할 수 있다.
public class AuthenticationService { public void authenticate(HttpServletRequest request) { ... // 응용 서비스에서 표현 영역이 상태 처리 HttpSession session = request.getSession() ... } }
Java
복사
문제점
코드만으로 표현 영역의 상태가 어떻게 변경되는지 추적이 어려워진다.→ 응집도가 깨진다.→ 유지보수하기 어려워진다.
해결 방법
철저하게 응용 서비스가 표현 영역의 기술을 사용하지 않도록 해야 한다.

표현영역

사용자가 시스템을 사용할 수 있는 흐름을 제공하고 제어한다.
사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다.
사용자의 세선을 관리한다.
@PostMapping() public String changePassword(HttpServletRequest requeset, Errors errors) { // 표현 영역은 사용자 요청을 응용 서비스가 요구하는 형식으로 변환한다. String curPw = requeset.getParameter("curPw"); String newPw = requeset.getParameter("newPw"); String memberId = SecurityContext.getAuthentication().getId(); ChangePasswordRequest chPwdReq = new ChangePasswordRequest(memberId, curPw, newPw); try { // 응용 서비스를 실행 changePasswordService.changePassword(chPwdReq); return successView; } catch (BadPasswordExcetption | NoMemberException ex) { // 응용 서비스의 처리 결과를 알맞은 응답으로 변환 erros.reject(""idPasswordNotMatch); return fomeView; } }
Java
복사

값 검증

표현 영역과 응용서비스 모두 가능
모든 값에 대한 검증은 응용서비스에서 한다.
public class JoinService { @Transcation public void join(joinRequest joinReq) { // 값 형식 검사 checkEmpty(request.getId(), "id"); .. // 로직 검사 checkDuplicateId(joinReq.getId()); } private void checkDuplicateId(String id) { int count = memberRepsoitory.countsById(id); if(count > 0) { throw new DuplicationException(); } } }
Java
복사
응용 서비스에서 에러 코드를 모아 하나의 익셉션으로 발생시키는 방법
@Transacional public OrderNo placeOrder(OrderRequest orderRequest) { List<ValidationError> errors = new ArrayList<>(); if (orderRequest == null) { errors.add(ValiddationError.of("emtpy")); } else { if (orderRequest.getOrdererMemberID() == null) { errors.add(ValiddationError.of("ordererMemberId", "emtpy")) } if (orderRequest.getOrderProducts() == null) { errors.add(ValiddationError.of("getOrderProducts", "emtpy")) } if (orderRequest.getOrderProducts().isEmpty()) { errors.add(ValiddationError.of("getOrderProducts", "emtpy")) } } // 응용 서비스가 입력 오류를 하나의 익셉션으로 모아서 발생 if (!erros.isEmpty()) throw new ValidationErrorExceptions(errors); }
Java
복사
public class ValidationErrorException extends RuntimeException { private List<ValidationError> errors; public ValidationErrorException(List<ValidationError> errors) { this.errors = errors; } public List<ValidationError> getErrors() { return errors; } }
Java
복사
필수 값과 값의 형식을 검사
표현 영역 : 필수 값, 값의 형식 범위 등을 검증한다.
응용 서비스 : 데이터의 존재 유무와 같은 논리적 오류를 검증한다.
@Controller public class Controller { @PostMapping("/member/join") public Strig join(JoinReuqest joinRequest, Erros erros) { new JoinRequestValidator().validate(joinRequest, erros); if (erros.hasErros()) return forView; try { } catch (DuplicatedIdException ex) { erros.rejectValue(ex.getPropertyName(), "duplicate"); return formView; } } }
Java
복사

권함 검사

AOP를 활용해서 권한 검사
public class BlockMemberService { private MemberRepostiory memberRepository; @PreAuthorize(hasRole('ADMIN')) public void block(String memberId) { Member member = memberRepository.findById(memberId); if (Objects.isNull(member)) throw new NoMemberExcetpion(); member.block(); } }
Java
복사
개별 도메인 객체 단위 검사
public class DeletedArticleService { public void delete(String userId, Long articleId) { Article article = articleRepository.findById(articleId); checkArticleExistence(article); permissionService.checkDeletePermission(userId, articleId); } }
Java
복사
도메인 객체 수준의 권한 검사 로직은 도메인별로 다르므로 도메인에 맞게 보안 프레임워크를 확장하려면 프레임 워크에 대한 이해도가 높아야된다. 이해도가 높지 않다면 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지보수에 유리하다.