본문 바로가기

책리뷰/도메인 주도 개발 시작하기(DDD핵심 개념 정리부터 구현까지)

도메인 주도 개발 (DDD) 시작하기 응용서비스와 표현영역

728x90
반응형

목차

    public class Member {
    
        public void changePassword(String oldPw, String newPw) {
            if(!matchPassword(oldPw)) throw new BaddPasswordException();
            setPassword(newPw);
        }
    
        // 현재 암호와 일치하는지 검사하는 도메인 로직
        public boolean matchPassword(String pwd) {
            return passwordEncoder.matches(pwd);
        }
    
        private void setPassword(String newPw) {
            if(isEmpty(newPw)) throw new IllegalArgumentException("no new password");
        }
    }

     

    표현영역과 응용영역

    • 도메인 영역이 제 기능을 하려면 사용자와 도메인을 연결해주는 매개체가 필요함 

    • 표현영역은 사용자 요청을 해석함
    • 사용자가 원하는 기능 판별 후 응용서비스 실행 
    • 응용서비스 : 실제 사용자가 원하는 기능을 제공
     
    @PostMapping("/member/join")
    public ModelAndView join(HttpRequestServlet request) {
        String email  =request.getParameter("email");
        String password = request.getParameter("password");
    
        // 사용자 요청을 응용 서비스에 맞게 변환
        JoinRequest joinReq = new JoinRequest(email, password);
        //변환한 객체(데이터)를 이용해서 응용 서비스 실행
        joinService.join(joinReq);
    }
    • 사용자가 입력한 파라미터를 응용서비스가 요구하는 객체를 생성해서 응용서비스 호출
    • 응용서비스 실행 후 사용자에게 알맞은 형식으로 응답(HTML or JSON)

    응용 서비스의 역할

    • 사용자가 요청한 기능을 실행
    • 리포지터리에서 도메인 객체를 가져와서 사용함 
    • 응용서비스는 도메인 영역과 표현영역을 연결해주는 창구 역할 
    public Result doSomeFunc(SomeReq req) {
        // 1. repository에서 애그리거트 구함
        SomeAgg agg = someAggRepository.findById(req.getId());
        checkNull(agg);
    
        // 2. 애그리거트의 도메인 기능 실행
        agg.doFunc(req.getValue());
    
        // 3. 결과를 리턴한다.
        return createSuccessResult(agg);
    
    }
    • 주로 도메인 객체 간의 흐름을 제어함 
    • 응용서비스가 복잡할 경우 도메인 로직의 일부를 구현하고 있을 가능성이 높음 
      • 코드 중복, 로직 분산 등 코드 품질에 안 좋은 영향을 줄 수 있음 
    • 트랜잭션 처리담당


    public void blockMembers(String[] blockingIds) {
        if(blockingIds == null || blockingIds.length == 0) return;
        List<Member> members = memberRepository.findByIdIn(blockingIds);
        for(Member mem : members) {
            mem.block();
        }
    }
    • 한 번에 닫수 회원을 차단 상태로 변경하는 응용서비스 
    • 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);
            }
        }
    • 암호 변경 기능을 위한 응용서비스는 Member애그리거트와 관련 리포지터리를 이용해 도메인 객체간 실행 흐름을 제어함
    public class Member {
    
        public void changePassword(String oldPw, String newPw) {
            if(!matchPassword(oldPw)) throw new BaddPasswordException();
            setPassword(newPw);
        }
    
        // 현재 암호와 일치하는지 검사하는 도메인 로직
        public boolean matchPassword(String pwd) {
            return passwordEncoder.matches(pwd);
        }
    
        private void setPassword(String newPw) {
            if(isEmpty(newPw)) throw new IllegalArgumentException("no new password");
            this.password = newPw;
        }
    }
    • 기존 암호를 올바로 입력했는지 확인하는 것은 도메인 핵심로직이므로 도메인영역에 구현해야함. 
    • 응용서비스에 구현할 경우 
      • 코드 응집성이 떨어짐 (데이터 조작이 도메인과 응용서비스 두 군데 위치)
      • 여러 응용서비스에서 동일한 도메인 로직 구현 가능성이 높음 

    응용서비스의 구현

    • 응용서비스의 크기 
      • 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
      • 구분되는 기능 별로(가입, 탈퇴, 암호변경, 초기화 등) 클래스 따로 구현하기
    • 한 클래스에 구현시 중복제거가 가능하지만 클래스 크기가 커짐 
      • 연관성이 없는 코드가 함께 위치할 가능성이 높아서 코드 이해가 어려움 
    • 기능별로 서비스 클래스 구현 시 클래스 내 2~3개 기능 구현 

    • 클래스 개수는 많아지지만 코드 품질을 일정 수준으로 유지하는데 도움이 됨 


    • Helper클래스를 이용해서 동일한 로직을 모아놓고 코드 중복을 방지함 

     

    응용서비스의 인터페이스와 클래스

    • 인터페이스가 필요한 상황
      • 구현 클래스가 여러개인 경우(런타임에 구현 객체 교체 )
      • 대부분 응용서비스는 런타임 교체 경우가 거의없고 대부분 구현 클래스가 하나임
      • TDD 개발을 하고 표현 영역부터 개발할 경우 응용클래스가 존재하지 않으므로 필요할 수 있음 
    • 도메인 영역이나 응용영역 개발을 먼저할 경우 클래스가 먼저 만들어짐
      • 표현영역 단위 테스트시 Mockito같은 테스트 도구 클래스에 대해서도 대역 객체 만들 수 있으므로 I/F가 없어도 되긴함 

     

    메서드 파라미터와 값 리턴

    • 파라미터로 전달받을때 2개 이상이면 개별값보다는 클래스를 만들어서 전달 
    • 응용서비스에서 애그리거트 자체를 리턴하면 코딩은 편하지만 도메인의 로직실행을 응용서비스와 표현영역 두 곳에서 할 수 있게 됨
      • 응용서비스, 표현영역에 기능 실행 로직을 분산시켜 코드 응집도를 낮추는 원인이됨 
    • 응용서비스는 표현 영역에서 필요한 데이터만 리턴 하는 것이 응집도를 높이는 확실한 방법 

     

    표현영역에 의존하지 않기

    • 응용서비스 파라미터 타입을 결정할 때 표현 영역과 관련된 타입 사용X

    • HttpServletRequest 나 HttpSession을 응용서비스 파라미터로 전달하면 안됨 
    • 응용서비스에서 표현영역에 대한 의존이 발생하면 응용서비스만 단독 테스트가 어려워짐 
    • 응용서비스가 표현영역 역할까지 대신하는 상황이 생길 수 있음 


    • HttpSession, 쿠키는 표현영역인데 응용서비스에서 변경할 경우 표현영역의 코드만으로 상태추적이 어려워짐 

     

    트랜잭션 처리

    • 스프링 프레임워크가 제공하는 트랜잭션 관리 기능 이용시 트랜잭션 처리를 쉽게 할 수 있음 
    • @Transactional 적용된 메서드가 RuntimeException 발생시 롤백, 아니면 커밋 

     


    표현영역

    • 표현영역의 책임
      • 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어
      • 사용자의 요청을 알맞은 응용서비스에 전달하고 결과를 사용자에게 제공
      • 사용자의 세션을 관리 


    값 검증

    • 값 검증은 표현영역과 응용서비스 두 곳에서 모두 수행할 수 있음 
    • 표현영역 : 필수 값, 값의 형식, 범위 등을 검증한다.
    • 응용서비스 : 데이터의 존재 유무와 같은 논리적 오류 검증 

    • 표현 영역에서 필수값 검증 

     

     

    • 스프링에서는 Validator 인터페이스를 제공하기 때문에 간결하게 줄일 수 있음 

     

     


    권한 검사

    • 표현영역, 응용 서비스, 도메인 세 곳에서 권한 검사 수행 가능 
    • 표현 영역 : 인증된 사용자인지 아닌지 검사 (ex: 회원 정보 변경)
      • URL을 처리하는 컨트롤러에 요청 전에 인증된 사용자의 웹요청만 컨트롤러에 전달
      • 인증된 사용자가 아니면 로그인 화면으로 리다이렉트 
      • 서블릿 필터에서 사용자 인증 정보생성, 인증여부 검사 

    • URL 만으로 접근 제어할 수 없을 경우 응용서비스 메서드 단위로 권한 검사 수행 
      • 스프링 시큐리티는 AOP를 활용해 애너테이션으로 서비스 메서드 권한 검사 수행 기능 제공

     

     

    • 도메인 객체 단위로 검사할 경우 (게시글작성자가 본인인지 확인하려면)
    • 애그리거트 먼저 로딩 > 직접 권한 검사 로직을 구현해야함.


    조회 전용 기능과 응용 서비스

     

    • 응용서비스 코드가 단순히 조회기능만 호출할 경우 


    • 표현 영역에서 바로 조회기능을 사용해도됨 

     

    728x90
    반응형