DaehunGwak / study-start-ddd

'도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지' 책 스터디
5 stars 3 forks source link

6주차 - 06. 응용 서비스와 표현 영역 #7

Open DaehunGwak opened 1 year ago

DaehunGwak commented 1 year ago

진도

  1. 응용 서비스와 표현 영역

일정

DaehunGwak commented 1 year ago

느낀점

전반적으로 실제적으로 구현했을 때 사소한 고민이라고 생각했던 ‘도메인에 구현해도 괜찮을까?’, ‘DTO는 어디까지 경계를 가져야할까?’, ‘검증은 어디까지 해야할까?’ 등에 대해서 다시 생각해보고 정리할 수 있게됨

개인 정리 노트

https://first-diadem-378.notion.site/6-13dcece951a34aec8dd839a0998c1126

kdg0209 commented 1 year ago

표현 영역과 응용 영역


스크린샷 2023-01-29 오후 4 36 21


응용 서비스의 역할


도메인 로직이 노출된 응용 서비스

public class UserService {

    public void changePassword(String oldPassword, String newPassword) {
        User user = userRepository.findById(1L).orElseThrow();

        // 도메인 로직 노출
        if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
            throw new IllegalArgumentException();
        }

        user.changePassword(oldPassword, newPassword);
    }
}

생각해본 코드 하지만 PasswordEncoder의 위치를 어디에 둬야하는가에 대한 의문이 있습니다.

@Entity
@Table
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Embedded
    private Password password;

    public User(Password password) {
        this.password = password;
    }

    public void changePassword(String oldPwd, String newPwd) {
        password.changePassword(oldPwd, newPwd);
    }
}

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Password {

    private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    private String password;

    public Password(String password) {
        setPassword(password);
    }

    public void changePassword(String oldPwd, String newPwd) {
        if (!matchPassword(oldPwd)) {
            throw new IllegalArgumentException();
        }
        setPassword(newPwd);
    }

    private void setPassword(String password) {
        if (!StringUtils.hasText(password)) {
            throw new IllegalArgumentException();
        }
        this.password = passwordEncoder.encode(password);
    }

    private boolean matchPassword(String inputPassword) {
        return passwordEncoder.matches(inputPassword, this.password);
    }
}

public class UserService {

    public void changePassword(String oldPassword, String newPassword) {
        User user = userRepository.findById(1L).orElseThrow();
        user.changePassword(oldPassword, newPassword);
    }
}


응용 서비스의 구현


---> 하나의 응용 서비스에 도메인의 모든 기능(CRUD)을 구현하면 중복되는 코드를 private 메서드를 사용하여 메서드 추출 기법을 통해 중복 코드를 제거할 수 있습니다. 하지만 하나의 클래스는 여러 책임을 가지게 되고 코드의 크기가 증가하게 됩니다.

---> 하나의 클래스는 2~3개의 기능을 구현하며 해당 기능 구현에 필요한 의존 객체만 필요로 함으로 다른 기능을 구현한 코드에 영향을 받지 않습니다. 다만 클래스의 개수가 많아지고, 중복 코드 발생시 유틸 클래스 등을 활용해야 합니다.

---> UserService 인터페이스와 이를 구현한 UserService가 필요한지에 대한 논쟁입니다. 인터페이스가 필요한 상황은 구현 클래스가 여러개 존재하며 런타임 시에 구현 클래스를 교체해야할 때입니다. 그런데 응용 서비스는 런타임에 교체하는 경우가 거의 없으며, 인터페이스와 구현 클래스를 따로 관리하면 클래스만 많아지고 전체적인 구조가 복잡해집니다.

chilling1000 commented 1 year ago
  1. 표현 영역(Presentation)과 응용 영역(Application)은 우리가 흔히 알고 있는 Controller, Service 이다.

  2. 도메인 로직은 코드의 응집성을 위해, 도메인에만 있어야 한다. 만일, 응용 영역에도 같이 있다면, 코드가 복잡해진다. 한편으로는 DDD가 아닐 때는 응용 영역에 전부 넣는 게 유지보수 하기에 더 좋기도 하다.

  3. 응용 서비스에서 구현할 메소드의 크기가 너무 크다면, 유즈 케이스를 최대한 나눠서 작성하거나, 따로 최대한 분리시켜서 하는 게 좋다.

  4. 중복 로직이 발생할 때는 private 메소드로 분리시켜서 필요한 곳에 사용할 수 있다. 대표적인 예가 find() 메소드를 사용할 때이다. 우리가 흔히 알고 있는 ArrayList 이런 자료구조를 보면, get(index)를 여러 메소드 Ex) Set() 등등 에서 사용되는 것을 볼 수 있다. 그 이유는 보일러 플레이트 코드를 줄이기 위함이기도 하다. 하지만, 단순히 한줄로만 실행되는 코드라면, 굳이 분리시켜야 하는 합리적인 이유가 있을까 라고 봤을 때, 그렇게 나쁘다고 생각되진 않는다. 왜냐하면, 그 한줄을 네이밍해서 사용한다면, 비록 메소드 스택을 한 번씩 더 타야하는 수고로움이 발생하지만, 코드를 읽는 관점에서는 조금 더 효율적일 수 있기 때문이다.

  5. 응용 서비스단에서 인터페이스를 구현한 구체 클래스를 본 적이 많지만, 1:1로 대응된다면 도저히 사용할 가치가 없다고 판단되며, 구현 클래스가 여러 개일 경우에 사용하는 의미가 있다고 생각한다.

    
    public TestInterface<T> {
    void create(T t);
    T retrieve(int id);
    int remove(int id);
    int update(int index, T t);
    }
    위처럼, 다양한 기능을 수행하는 인터페이스이다.
    반면에, 아래는 1:1로만 기능을 수행하는 인터페이스이다.

public TestInterface { void doSomething(Objecb obj); }


향후, 여러 기능을 수행할 인터페이스로 발전한다면, 이렇게 선언해서 사용하는 게 합리적이지만, 그냥 관행을 따르는 것이라면, 추천하진 않는다.
추가로, 토비의 스프링의 경우 위와 같이 1:1로 대응되는 Interface Impl 클래스를 선언해서 사용하는 것을 합리적이라고 함.
우리가 저번 챕터에서 배운 Data JPA 기술이 김영한 강의의 내용에 나온 기술에 비하면, 비효율적이라는 것을 알 수 있듯이, 
시간이 지나면서 트렌드가 바뀔 수도 있고, 다시 바뀐 트렌드가 제자리에 오는 경우가 있다.

6. 응용 서비스의 메소드 리턴 타입이 void일 경우, 보통 리다이렉션으로 사용되는 경우가 많다. 예를 들면, 계정 비밀번호를 변경 후,
리다이렉션이 되는데 여기에 추가할 리턴값을 생각해보자면, 거의 없는 게 대부분이다. 그리고 실행 메소드가 실패되더라도,
그것은 예외 발생으로 인한 실패이니, 오류 메시지로 리턴된다.

7. 표현 영역의 책임은 사용자의 값을 검증하고, 응용 서비스에게 검증된 값을 전달 후, 데이터를 받아서 다시 사용자에게 제공하는 역할을 한다. 그래서 HTTP로 넘어온 값들 Ex) HttpServletRequest 등 을 받으면, 곧바로 응용 서비스에 전달하는 것이 아니라, 검증해서 보낸다. 여기서 검증이란, 빈값이 있는지 확인하는 용도이기도 하고, 파라미터로 받은 객체를 그대로 전달하는 것을 방지하는 용도이기도 하다.

추가로, 값 검증을 서비스단에서 하는 방법도 있지만, 개인적으로는 표현 영역에서 처리하는 게 합리적이라고 생각한다.
developer-wonjin commented 1 year ago

6장 응용서비스와 표현영역

1. 표현영역

1.1. 표현영역의 값리턴

강의 실전 스프링부트와 JPA활용2 - API개발과 성능최적화

1.2. 표현영역만 다뤄야하는 자원 : 세션, 쿠키

public class XXXService{
    public void XXXmethod(HttpServletRequest request){
        HttpSession session = request.getSession();
        session.setAttribute("auth", new Authentication(id));
    }
}
  1. 응용영역 -> 표현영역 방향으로 잘못된 방향으로 의존이 생겨버림
    • 서블릿자원을 참조하는 것만으로 의존이 생긴다?
    • Controller가 Service를 멤버필드로 갖는 것과 다른 의미의 의존인가?
  2. 응용서비스가 테스트하기 어려워진다. (서블릿 자원까지도 테스트를 위한 준비를 해놔야함)
  3. 표현영역 소스코드만으로 표현영역의 상태(세션, 쿠키)값의 변경을 파악하기 어려움
    • 응용영역 소스코드까지 들어가봐야 암

1.3. 이벤트

추후 뒷 장에서....

2. 응용영역

2.1 역할

public MemberService{
    public Long register(MemberDTO m){
        Member member = memberRepository.dtoToEntity(m); 
        memberRepository.save(member); //1. 레포지토리를 통한 도메인객체 조회, 저장

        //... 2. 얻은 도메인객체를 사용
        // 3. 도메인 객체간의 흐름제어

        return member.getId();
    }

    //4. 다중 DML 건들에 대한 트랜잭션 처리
    @Transactional
    public void makeBlackConsumer(Long[] ids){
        for(Long id : ids){
            Member member = memberRepository.findById(id);
             member.makeBlackConsumber();
        }
    }
}

2.2. 응용서비스는 되도록 잘게 나누자

2.3. 응용서비스의 인터페이스

3. 유효성검사 (입력값, 비즈니스로직)

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (유효성 파트만 5시간)

4. 접근제어 Access Control

5. 조회전용 기능

progress0407 commented 1 year ago

chapter 6. 응용 서비스와 표현영역

Intro

이번 장에도 특별히 건질 것 같아 보이는 부분이 없었다... ㅠ

6.1 표현 영역과 응용 영역

6.2 응용 서비스의 역할

6.3 응용 서비스의 구현

MemberService의 비밀번호 변경 로직을 ChangePasswordService에 놓았다. 하지만 공감되지 않는다... 이미 코드에 나와있는 데로, Member가 Password를 가지고 있고, member에게 Password 변경 권한을 위임하는 것으로 충분하지 않을까...

6.4 표현 영역

6.5 값 검증

6.6 권한 검사

스프링 시큐리티에 대한 높은 이해도가 없다면... 차라리 직접 구현하는게 나을 것 같다

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

CQRS 얘기다 !