인생을 코딩하다.

[Spring] 레이어 구조에 관한 고찰 본문

Spring

[Spring] 레이어 구조에 관한 고찰

Hyung1 2020. 12. 27. 21:17
728x90
반응형

Domain 레이어란?

 

1. Domain

- 저장소와 밀접한 중심 도메인을 다루는 계층은 더 견고하고 특별하게 격리되고 관리되어야 하기 때문에 반드시 분리되어야 한다. 즉, JPA를 기준으로 한다면 테이블과 맵핑되는 Class들

- 이 계층은 오로지 도메인에만 집중하고, 어떠한 도메인이든 그 도메인이 가져야할 서비스와 무관한 도메인의 비즈니스가 있다. 

 

2. Repository

- 도메인의 CRUD 역할을 한다. 여기서 주의할 점은 모든 CRUD 역할을 이곳에서 하는 것이 아니다. 이 모듈은 시스템에서 가장 보호받아야 하며 가장 견고해야 할 모듈이므로, 이 모듈에서 CRUD에 관한 정의를 작성할때 많은 고민을 해야한다.

- 예를 들어 시스템 도메인에 관한 통계를 기능으로 추가한다고 했을 때, 이는 시스템이 갖는 중심 역할에 따라 달라진다. 이 기능이 시스템에서 중심 역할로 볼 수 있다면 도메인 모듈에 작성할 것이고, 그렇지 않다면 사용을 하는 측에서 작성이 되야한다. 만일 이 시스템이 주문이라는 중심 도메인을 갖는다면 통계기능은 사용하는 측(Application Module)에 작성해야 한다.

 

3. Domain Service

- 이 계층은 도메인의 비즈니스를 책임진다. 그렇기 때문에 도메인이 갖는 비즈니스가 단순하다면 이 계층은 생기지 않을 수 도 있다. 

 

양방향 vs 단방향

 

A클래스와 B클래스가 연관관계 일 때, A 클래스가 B 클래스를 사용하게 되면 방향성을 갖는다. 그 반대도 마찬가지도. 하지만 이러한 방향성을 남발하게 되면 성능상의 큰 문제가 발생할 수 있다. 만약 게시판을 구현한다고 할 때, 한 명의 유저가 수 많은 게시글을 작성했고, 회원클래스가 게시물 클래스의 방향을 갖는 경우, 해당 회원을 불러오는 순간 DB상의 큰 부담이 생길 수 있다. 이러한 문제가 생기는 경우를 생각해보고, 부담이 생기는 방향성은 배제하도록 신중하게 설계해야 한다.

 

데이터베이스 간의 관계와 엔티티의 방향성은 별개로 염두해야 한다. 데이터베이스 상에서는 이미 양방향 참조가 가능한 상태고, 엔티티의 방향성은 객체간 참조 방향을 말하는 것이다.

 

가능한 단방향으로만 구현을 해보고 양방향이 필요한 경우에 성능을 고려해 양방향을 추가하는 방식이 옳다고 생각한다.

 

 

Service 레이어란?

 

 

- 코로나 프로젝트와 관련해서 Report라는 객체로 일일 확진자 조회, 주간 확진자 조회, 월간 확진자 조회 등의 모든 행위가 가능하지만, 그것을 영속화시켜야 하기 때문에 별도의 레이어가 필요하고 이것을 서비스 레이어라고 부른다. 서비스 레이어에서는 대표적으로 데이터베이스에 관한 트랜잭션을 관리한다.

 

- 서비스 영역은 도메인의 핵심 비즈니스 코드를 담당하는 영역이 아니라 인프라스트럭쳐(데이터베이스) 영역과 도메인 영역을 연결해주는 매개체 역할이다. 

 

- Report라는 객체에 대한 제어는 Report 스스로 제어해야 한다.

 

- 뷰는 자신이 요청할 컨트롤러만 알고 있으면 되고, 컨트롤러는 화면에서 넘어오는 매개변수들을 이용해 서비스 객체를 호출하는 역할을 한다. Service는 불필요하게 Http 통신을 위한 HttpServlet를 상속 받을 필요가 없는 순수한 자바 객체로 구성되어야 한다. Service에 request나 response와 같은 객체를 매개 변수로 받아선 안된다. 그걸 사용하는 작업은 컨트롤러에서 해야한다. 그렇기 때문에 자신을 어떤 컨트롤러가 호출하든 상관없이 필요한 매개변수만 준다면 자신의 로직을 처리하게 된다. 즉 모듈화를 통해 어디서든 재사용이 가능한 클래스 파일이라는 뜻

 

- Service는 자기 역할만 충실히 해야한다. 예를 들면 Service의 위치한 비즈니스 로직에는 무결성 검사, 데이터 유효성 검사 등 꼭 필요한 로직들이 들어가야 하는데 이것을 domain에서 조작한다면 데이터 무결성을 보장 할 수 없다.

DTO에서 글자 수, null 값 등의 형식을 검사하고 (애플리케이션 내부) domain에서 데이터베이스에 접근하여 service에서 중복된 값들 인지의 여부를 검증해야 한다.  

 

서비스의 적절한 책임의 크기 부여하기

 

책임이란 것은 외부 객체의 요청에 대한 응답이다. 이러한 책임들이 모여 역할이 되고 역할은 대체 가능성을 의미한다.

그렇게 때문에 대체가 가능할 정도의 적절한 크기를 가져야 한다.

 

1. 행위 기반으로 네이밍하기

 

행위 기반으로 서비스를 만든다. MemberService라는 네이밍은 많이 사용하지만 정말 좋지 않은 패턴이다. 우선 해당 클래스의 책임이 분명하지 않으므로 모든 로직이 MemberService으로 모이게 된다. 그러면 외부 객체에서는 MemberService 객체를 의존하게 된다. findById 메서드 하나를 사용하고 싶어도 MemberService를 주입받아야 하기 때문에 메서드의 라인 의 수도 방대해 지고 테스트 코드를 작성하기도 더욱 어려워진다. 그래서 Member에 대한 조회 전용 서비스 객체인 MemberFindService으로 네이밍을 하면 자연스럽게 객체의 채임이 부여된다. 객체를 행위 기반으로 네이밍을 주어 자연스럽게 책임을 부여하는 것이 좋다.

 

2. 책임의 크기가 적절해야 하는 이유

public interface MemberService {

    Member findById(MemberId id);
    Member findByEmail(Email email);
    void changePassword(PasswordDto.ChangeRequest dto);
    Member updateName(MemberId id, Name name);
   
}
@Service
@Transactional
@AllArgsConstructor
public class MemberServiceImpl implements MemberService {

    private MemberRepository memberRepository;


    @Override
    public Member findById(MemberId id) {
        final Member member = memberRepository.findOne(id);
        if (member == null) throw new MemberNotFoundException(id);
        return member;
    }

    @Override
    public Member findByEmail(Email email) {
        final Member member = memberRepository.findByEmail(email);
        if (member == null) throw new MemberNotFoundException(email);
        return member;
    }

    @Override
    public void changePassword(PasswordDto.ChangeRequest dto) {
        final MemberId id = dto.getId();
        final Member member = findById(id);
        final String newPassword = dto.getNewPassword().getValue();
        final String password = dto.getPassword().getValue();

        if (!member.getPassword().isMatched(password))
            throw new IllegalArgumentException("password is not matched");

        member.changePassword(newPassword);
    }

    @Override
    public Member updateName(MemberId id, Name name) {
        final Member member = findById(id);
        member.updateName(name);
        return member;
    }

 

위 와 같은 인터페이스를 두어서 얻는 이점은 세부 구현체를 숨기고 바라보게 함으로써 클래스 간의 의존관계를 줄이는 것, 다형성을 사용하는 것이 핵심이다. 하나의 인터페이스를 구현하는 여러 구현체가 있고 기능에 따라 적절한 구현체가 들어가서 다형성을 주기 위함이다. 또 인터페이스만 바라보니 의존 관계를 줄 일 수 있다.

 

위의 책임의 문제점은 무엇일까?

인터페이스의 책임이 너무 많은것이 문제이다. 저 인터페이스의 구현체가 두 개 이상이 되려면 해당 구현 체가 다른 기능을 가져야 한다. 하지만 기능이 계속 늘어나게 되면 대체성을 갖지 못할 뿐더러 SOLID 또 한 준수할 수가 없다. 

 

하지만 인터페이스의 하나의 구현체 하나를 두면 의존 관계를 줄이는 효과도 다형성을 주는 효과도 없다. 그렇다면 인터페이스 하나의 구현체 하나는 반드시 나쁜 구조일까? 사실 또 그렇지도 않다.

 

하나의 구현체만 둘 경우에 인터페이스를 둘 필요가 없다는 것으로 결론이 나오는데, 하나의 구현체만 갖더라도 인터페이스를 사용하는 것이 좋을 수도 있다.

public interface CardPaymentService {
    void pay();
}
public class ShinhanCardPaymentService implements Card{
    private ShinhanCard shinhanCard;
    @Override
    public void pay() {
        shinhanCard.pay(); //신한 카드 결제 API 호출
        // 결제를 위한 비지니스 로직 실행....
    }
}

 

위처럼 카드 인터페이스를 두고 신한카드 구현체를 하나만 갖지만 향후 추가적으로 생길 여지가 있으니 인터페이스를 두는 것이 바람직하다.

하지만 앞으로 완전히 추가될 여지가 없다고 판단면 어떻게 해야할까? 이건 사실 인터페이스를 두지 않는 것이 맞다고 생각한다. 

 

하지만 위에 설명한 인터페이스와 구현체를 매핑하고 impl을 네이밍에 쓰는 것은 좋지 않은 방법이라고 한다, MVC 구조 패턴으로 개발할 때 좋지 않은 방법으써 관례적으로 이어져온 방법이다. 

 

위에 관한 이해만을 기반으로써 더 나은 프로그램을 짜는게 맞다고 생각한다.

 

근데 Service에서 다른 Service를 참조하면 어떻게 될까?

 

서비스에서 서비스를 참조할 때, 로직을 가져다 쓸 경우에 의존성 주입을 받게된다. 이 경우에는 순환 참조가 발생한다.

명확하게 서비스끼리 계층이 완벽하다면 하나의 방법이라고는 하는데 단점이 수반된다. 하지만 나는 아직 서비스가 서비스를 참조할 때 문제점에 관해 정확히 이해를 하지 못하여 더 알아보아야 할 듯 하고, 정확한 이해가 됬을때 다시 수정해야겠다.

 

 

Controller란?

 

- 클라이언트가 이용할 엔드 포인트

- 클라이언트이 요청을 어떻게 처리할지 정의함.

- 클라이언트의 요청을 처리하고 어떻게 응답할지 결정하는 곳

 

위의 서비스, 도메인에 관한 설명에 덧붙여,

Controller에서 작동을 하기 위해 Service에서 객체들을 받아 하나의 작동을 하는 것이 가장 이상적라고 생각한다.

 

1. 각각의 레이어에서는 자신의 역할만 명확하게 하면 되기 때문에 재사용성이 높아진다. 

2. Service이 로직이 변경되어도 다른곳에 side effect가 없기 때문에 확장에 용이하다. 또 Service의 교체도 용이하다.

 

 

참고 문헌 :

github.com/cheese10yun/blog-sample/tree/master/service

 

cheese10yun/blog-sample

:octocat: Yun Blog sample code. Contribute to cheese10yun/blog-sample development by creating an account on GitHub.

github.com

woowabros.github.io/study/2019/07/01/multi-module.html

 

멀티모듈 설계 이야기 with Spring, Gradle - 우아한형제들 기술 블로그

멀티 모듈 설계 이야기

woowabros.github.io

 

현재 하고 있는 멀티모듈 기반의 프로젝트에 모듈이 하나 더 추가되면, 내 프로젝트에 입각한 레이어 구조 고찰(2)를 또 포스팅 하겠다. 

728x90
반응형

'Spring' 카테고리의 다른 글

[Spring] AOP(Aspect-Oriented Programming)  (0) 2021.02.10
[Spring] IOC와 DI  (0) 2021.02.09
[Spring] 스프링 테스트 전략  (0) 2020.12.27
[Spring] RestTemplate  (0) 2020.12.17
[Spring] @CrossOrigin  (0) 2020.12.10
Comments