avatar
Octoping Blog

관습적 레이어 설정 탈피를 통해 프로젝트 개선하기

레이어를 잘 설계해서 프로젝트를 깔끔하게 유지해보자
BackendArchitecturelayer
6 months ago
·
8 min read

우리들의 패키지 구성 방법

여러분의 패키지 구성법은 무엇인가요?

// 도메인형
└─src
    ├─member
    │  ├─controller
    │  ├─repository
    │  └─service
    └─post
        ├─controller
        ├─repository
        └─service
// 레이어형
└─src
   ├─controller
   │  ├─member
   │  └─post
   ├─repostiory
   │  ├─member
   │  └─post
   └─service
      ├─member
      └─post

패키지 구성법은 크게 2가지가 있는데, 이 중 도메인형에 대해 얘기해보려고 한다.

API는 애석히도 여러 도메인에 걸친다

// 도메인형
└─src
   ├─member
   │  ├─controller
   │  ├─repository
   │  └─service
   └─post
      ├─controller
      ├─repository
      └─service

다음과 같이 레이어를 구성할 경우, member와 관련된 api들은 /member/controller에 들어가게 된다. 그런데, api는 애석히도 여러 도메인에 걸치는 경우가 많다.

예를 들어보자. 다음의 api들은 각각 어떤 컨트롤러에 들어가야 할까?

  • 특정 회원의 팔로워들의 글을 최신 순으로 모아서 보여주는 api

  • 특정 글에 '좋아요'를 누른 회원의 목록을 모아서 보여주는 api

특정 '회원'의 '팔로워'의 '글'을 최신 순으로 모아서 보여주는 api의 얘기를 해보자. 이 api는 회원(member), 팔로우(follow), 글(post)라는 세 가지 도메인에 걸쳐있다.

이렇게 된다면, MemberService 혹은 PostService는 3개의 다른 도메인의 의존성을 가지게 된다.

이런 기능들이 쌓이다보면, MemberService는 정작 회원과 관련된 일을 하지 못하고 점점 비대해지게 된다.

API는 도메인과 겸상하지 않는다

API는 클라이언트의 요구사항이자 사용자의 유스케이스를 보여주는 계층이다. 글 작성 api, 글 수정 api 등등… 모두 유스케이스 아닐까?

도메인 로직은 영원하지만, 유스케이스는 바뀔 수 있다. ‘특정 회원의 팔로워들의 글을 최신 순으로 모아 보여주기’라는 유스케이스에 이건 Post와 관련된 기능이라고 적어붙이는 것보다 더 나은 라벨링이 있지 않을까?

왜 우리는 이런 유스케이스에 도메인의 이름을 붙여야 하는걸까?

API를 도메인으로부터 분리해보면 어떨까.

API와 Domain 분리하기

└─src
   ├─api
   │  ├─blog
   │  │      BlogMemberController.java
   │  │      BlogPostController.java
   │  │
   │  └─feed
   │          FeedMainPageController.java
   │          FeedRankingController.java
   │
   └─domain
      ├─member
      │  ├─repository
      │  └─service
      └─post
         ├─repository
         └─service

‘특정 회원의 팔로워들의 글을 최신 순으로 모아 보여주기’는 피드 페이지의 기능이었다. 따라서 해당 api는 feed 폴더에 넣어주었다.

'이 API는 Post의 기능'이라고 적어넣기 보다, 조금 더 실제 유저의 요구사항에 맞게 패키지를 구성하여 더 명시적인 프로젝트 구조를 만들 수 있었다.

컨트롤러에 로직이 쌓여요

public class FeedMainPageController {
    private final MemberService memberService;
    private final PostService postService;

    @GetMapping("/follower-posts")
    public FollowerPostDTO getFollowerPosts(
        @RequestParam("memberId") Long memberId,
    ) {
        if(!memberService.isMemberExist(memberId)) {
            throw new MemberNotFoundException(memberId);
        }
        
        List<Long> = memberService.getFollowerIds(memberId);
        List<Post> posts = postService.getPostsByMemberIds(followerIds);

        return new FollowerPostDTO(posts);
    }
}

이렇게 특정 서비스에서 다른 도메인의 로직을 처리하는 일을 막을 수 있게 되었다. 하지만 이렇게 되니 컨트롤러에서 여러 다른 도메인의 서비스를 호출하는 역할이 생겨버렸다.

명령 api와 같은 경우는 트랜잭션 등의 처리도 필요할텐데, 컨트롤러에서 이를 수행하는 건 좋지 않겠다.

이를 처리해주는 새로운 레이어를 만들어보는 것은 어떨까?

Usecase 레이어로 비즈니스 로직 더 잘 드러내기

└─src
   ├─api
   │  └─feed
   │      ├─controller
   │      │      FeedMainPageController.java
   │      │      FeedRankingController.java
   │      │
   │      └─usecase
   │              FeedUseCase.java
   │
   └─domain
      ├─member
      │  ├─repository
      │  └─service
      └─post
         ├─repository
         └─service

API가 구현하고자 하는 실제 유스케이스의 로직을 나타내는 것이니 usecase라는 이름으로 레이어를 개설했다.

컨트롤러 → 유스케이스 → 서비스 → 레포지토리와 같은, 4단계의 계층이 만들어진 셈이다.

이러면 코드가 어떻게 될까?

public class FeedMainPageController {
    private final FeedMainPageUseCase feedMainPageUseCase;

    @GetMapping("/follower-posts")
    public FollowerPostDTO getFollowerPosts(
        @RequestParam("memberId") Long memberId,
    ) {
        var posts = feedMainPageUseCase.getFollowerPosts(memberId)
        return new FollowerPostDTO(posts);
    }
}

public class FeedMainPageUseCase {
    private final MemberService memberService;
    private final PostService postService;

    public List<Post> getFollowerPosts(Long memberId) {
        if(!memberService.isMemberExist(memberId)) {
            throw new MemberNotFoundException(memberId);
        }

        List<Long> = memberService.getFollowerIds(memberId);
        List<Post> posts = postService.getPostsByMemberIds(followerIds);

        return new posts;
    }
}

멀티 모듈의 초석이 될 수도 있다

지금까지 api와 domain의 패키지 구조를 분리해봤다.

이렇게 서로 성격이 다른 두 패키지를 분리했으니, 추후에 필요할 때에 이를 모듈로 나눠서 멀티 모듈 패키지를 구성해볼 수 있을 것 같다.

멀티모듈이란?

until-428

api, domain과 같은 파트를 각각 한 단위인 모듈로 나눈다면, 우리는 원하는 부분만 따로 jar파일로 빌드하여 한 모듈이 다른 모듈을 라이브러리처럼 사용할 수가 있다.

만약 그냥 api 서버만 있는게 아니라, 어드민 서버가 추가되야 한다면?

  • 어드민 서버 프로젝트 새로 파서 member, post 도메인의 코드를 복붙해오는 것은 조금 무식한 방법 아닐까.

  • 어드민 모듈을 따로 만들어서 도메인 모듈을 라이브러리처럼 참고하면 되지 않을까?

마무리하며

패키지와 레이어의 구성은 항상 정답이 있는게 아니다. 프로젝트가 간단하다면 컨트롤러 하나만으로 구성해볼 수도 있는 것이고, 프로젝트가 훨씬 복잡하다면 더 많은 레이어를 구성해볼 수도 있다.

이 아티클에서 다룬 내용도 꼭 정답임은 아니기 때문에, 이 아티클은 참고 하되 항상 프로젝트의 특성과 일하고 있는 조직의 상황에 맞게 알맞게 구성하는 것이 정답이라고 할 수 있겠다.


- 컬렉션 아티클






반갑습니다 😄