Spring/Spring Boot

Repository는 어느 모듈에 위치해야할까? (DIP. 고수준모듈, 저수준 모듈)

민돌v 2024. 7. 9. 01:20
레이어드 아키텍쳐를 사용하는 단일 모듈 서비스를 멀티모듈로 나누는 과정에서,,, 시작된  Repository 는 과연 어디 모듈에 위치해야하는 가에 대한 주저리주저리 고민 정리 글입니다.

코드 예시는 깃허브에 있습니다.

 

우선 다른 레퍼런스들을 보며, 현재 상황에 맞게 구성해본 구조는 아래와 같습니다,,
이런 설계에서 Repository 는 core:domain 모듈에 들어가야하는가 core:Infra 에 들어가야하는가에 대한 매우 심도깊은 고민을 하고있습니다..ㅎ

저의 가소로운 고민입니다

 

 

[궁금한 것]

  1. 고수준 모듈은 뭐고 저수준모듈은 무엇인가.
  2. 왜 고수준 모듈이 저수준 모듈의 의존성을 가지면 안되는가
  3. 그래서 Repository 는 어떤 모듈에 위치해야하는가

 

~~ 에 대해 정리해봅니다!

 


1. 고수준 모듈과 저수준 모듈의 정의는 무엇일까??

멀티모듈에서 서로 다른 수준의 공통 코드 분리하기 

❓왜 멀티모듈 구조에서 서로 다른 수준을 분리해야 할까요?

✔️서로 수준(Level)이 다르다

  • 다른 변경의 속도를 가진다.
  • 즉, 수준이 다른 모듈의 의존성을 분리해야 변경에 유연해진다.

✔️그럼 수준(Level) 이란 도대체 무엇일까요

수준이란, 입력과 출력까지의 거리

  • 입력 : HTTP, 웹소켓
  • 출력 : 캐시. 데이터베이스 등

✔️ 그렇다면 고수준과 저수준이란!!

  • 고수준 - Domain 영역 (비지니스 요구사항에 의해 수시로 변경됨)
  • 저수준 - 운영중인 데이터베이스의 변경 등,,


✔️ 모듈관점에서의 고수준과 저수준

고수준 모듈

  • 의미 있는 단일 기능을 제공하는 모듈
  • ex - Service 단의 응용 계층, Domain 비지니스 로직

저수준 모듈

  • 고수준 모듈의 기능을 구현하는데 필요한 하위 기능을 실제로 구현한 것
  • ex - JPA 로 데이터를 불러오는 것, 특정 부가 기능을 수행하는 것

저의 구조에서는
"Domain 이 고수준 모듈"
"Infra 가 저수준 모듈"
이 되겠네요

 


2. 왜 고수준 모듈이 저수준 모듈의 의존성을 가지면 안되는가

 

✔️ 고수준 모듈 : 의미 있는 단일 기능을 제공하는 모듈
✔️ 저수준 모듈 : 고수준 모듈의 기능을 구현하는데 필요한 하위 기능을 실제로 구현한 것 

→ 고수준 모듈이 제대로 동작하려면, 저수준 모듈을 사용해야 한다.

❗️그러나, 고수준 모듈이 저수준 모듈을 사용하면 구현 변경과, 테스트가 어려워진다.

 

(예를들어)

회원가입 기능을 제공하는 고수준 모듈 JoinService 가 있다가 했을 때 다음과 같은 하위 기능이 필요합니다.

  1. 가입하려는 회원의 정보가 기존에 존재하는지 확인하는 UserRepository 가 필요
  2. 회원가입 완료 메세지를 전달하는 외부 써드파티 API 를 사용하는 MessageUtls
@Service
public class JoinService{

    private final UserRepository userRepository;
    private final MessageUtils messageUtils;
    
    //생성자 생략
    public String join(Object 어떤정보){
    
    	//중복 가입 DB 확인
       	boolean isUser = userRepository.중복인가요?();
        
        //회원가입하면 메세지 전달
        if(!isUser){
            userRepository.회원가입!(유저정보);
            messageUtils.가입축하메세지전달(유저정보);
        }
    }
}

 → 이런 경우 저수준 모듈에 해당하는 UserRepository 나 MessageUtils 등의 기능을 직접 불러와서 생성자로 객체를 생성하고, 고수준 모듈에서 바로 사용한다면.. 추후 DB가 변경되거나, 써드파티 API 를 다른 서비스로 변경할 때 변경포인트가 매우 많아지겠죠?

 

"고수준 모듈이 저수준의 의존성을 가지면 안된다"가 이런 의미인 것 같습니다.

 

그렇기 때문에 DIP(Dependency Inversion Principle) 을 사용하여 "저수준 모듈이 고수준 모듈에 의존" 하도록 바꿔야합니다.


저수준 모듈이 고수준 모듈에 의존하게 하기

 💡 모두가 다 알듯이 인터페이스를 사용합니다.

  • 고수준 모듈은 인터페이스를 바라보며, 내부 구현을 알지 못하도록 합니다.
  • 또한 저수준 모듈은 인터페이스를 구현한 구현체가 되므로
    "고수준 모듈은, 저수준 모듈에 의존하지 않고 구현을 추상화한 인터페이스에 의존"하게 됩니다.

 

이렇게되면, DB를 변경하거나 외부 API 를 더 저렴하고 좋은 다른 서비스로 변경하더라도 구현체만 새로 만들어 갈아끼면됩니다.

!즉, 고수준 모듈은 저수준의 변경이 일어나도 영향을 받지않습니다.

 


3. 모듈 관점에서의 DIP

그래서 Repository 는 어떤 모듈에 위치해야하는가

위의 예제를 모듈구조로 분리해보고자 합니다.

  • 비지니스로직을 가지고 요구사항과 밀접한 관계가있는 JoinService 는 Domain 영역에 가야한다고 생각합니다.
  • 단순하게 생각했을때 DB 에 접속하고, 외부 API 를 연동하는 부분은 Infra로 가야하겠죠

📌 하지만, 이렇게 되면 "고수준 모듈인 Domain 이 저수준 모듈인 Infra 를 의존하게 됩니다."

  • 이러한 구조는, UserRepository 인터페이스를 고수준 모듈인 도메인 모듈 관점이 아닌 저수준 모듈 관점에서 도출한 것 입니다.
  • DIP 를 적용할 때, 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출되어야 합니다.

 

 

✨ 즉!
하위 기능을 추상화한 Repository 인터페이스는 도메인 모델 영역에(고수준)에
실제 구현 클래스는 인프라스트럭쳐 영역에(저수준) 에 속하는것이 좋다고 합니다.

더 복잡한 모듈 구조


4. 코드 예시 

도메인 모듈을 분리한 멀티모듈 구조
프로젝트 폴더
├── admin
│   └── {ui}
│   └── {usecase}
├── api
│   └── {ui}
│   └── {usecase}
├── core
│   ├──  domain
│   │     └── {domain}
│   │     └── {service}
│   │     └── {repository}
│   ├──  infra
│   │     └── {entity}
│   │     └── {repository}
│   │     └── {repositoryImpl}
│   │     └── {config}
├── common-utils
│   └── {utils}

✔️ API 모듈

package com.example.api.presentation;

import com.example.infra.MemberDto;
import com.example.infra.MemberRepository;
import com.example.api.presentation.response.MemberResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberUsecase {

    private final MemberRepository memberRepository;

    public MemberResponse findMemberById(Long id) {
        MemberDto memberDto = memberRepository.find(id);

        return MemberResponse.ofDto(memberDto);
    }
}

✔️ Infra 모듈

package com.example.infra;

public interface MemberRepository {

    MemberDto find(Long memberId);
}
package com.example.infra;

import com.example.domain.member.Member;
import com.example.domain.member.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class MemberRepositoryConnector implements MemberRepository {

    private final MemberService memberService;

    @Override
    public MemberDto find(Long memberId) {
        Member member = memberService.findMemberById(memberId);
        return MemberDto.ofEntity(member);
    }
}

✔️ Domain 모듈

예시에서는 Entity 를 Domain 으로 보고 있습니다.
package com.example.domain.member;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberJpaRepository extends JpaRepository<Member, Long> {
}

✔️ 모듈간 의존관계

https://www.youtube.com/watch?v=p5ZMF2bpE6A&t=572s

 


끝,,이지만
잘못된 내용이나 더 올바른 방향성을 댓글로 알려주신다면 정말 큰 도움이 될 것 같습니다.

 

 

 


참고