레이어드 아키텍쳐를 사용하는 단일 모듈 서비스를 멀티모듈로 나누는 과정에서,,, 시작된 Repository 는 과연 어디 모듈에 위치해야하는 가에 대한 주저리주저리 고민 정리 글입니다.
코드 예시는 깃허브에 있습니다.
우선 다른 레퍼런스들을 보며, 현재 상황에 맞게 구성해본 구조는 아래와 같습니다,,
이런 설계에서 Repository 는 core:domain 모듈에 들어가야하는가 core:Infra 에 들어가야하는가에 대한 매우 심도깊은 고민을 하고있습니다..ㅎ
[궁금한 것]
- 고수준 모듈은 뭐고 저수준모듈은 무엇인가.
- 왜 고수준 모듈이 저수준 모듈의 의존성을 가지면 안되는가
- 그래서 Repository 는 어떤 모듈에 위치해야하는가
~~ 에 대해 정리해봅니다!
1. 고수준 모듈과 저수준 모듈의 정의는 무엇일까??
멀티모듈에서 서로 다른 수준의 공통 코드 분리하기
❓왜 멀티모듈 구조에서 서로 다른 수준을 분리해야 할까요?
✔️서로 수준(Level)이 다르다
- 다른 변경의 속도를 가진다.
- 즉, 수준이 다른 모듈의 의존성을 분리해야 변경에 유연해진다.
✔️그럼 수준(Level) 이란 도대체 무엇일까요
수준이란, 입력과 출력까지의 거리
- 입력 : HTTP, 웹소켓
- 출력 : 캐시. 데이터베이스 등
✔️ 그렇다면 고수준과 저수준이란!!
- 고수준 - Domain 영역 (비지니스 요구사항에 의해 수시로 변경됨)
- 저수준 - 운영중인 데이터베이스의 변경 등,,
✔️ 모듈관점에서의 고수준과 저수준
고수준 모듈
- 의미 있는 단일 기능을 제공하는 모듈
- ex - Service 단의 응용 계층, Domain 비지니스 로직
저수준 모듈
- 고수준 모듈의 기능을 구현하는데 필요한 하위 기능을 실제로 구현한 것
- ex - JPA 로 데이터를 불러오는 것, 특정 부가 기능을 수행하는 것
저의 구조에서는
"Domain 이 고수준 모듈"
"Infra 가 저수준 모듈"
이 되겠네요
2. 왜 고수준 모듈이 저수준 모듈의 의존성을 가지면 안되는가
✔️ 고수준 모듈 : 의미 있는 단일 기능을 제공하는 모듈
✔️ 저수준 모듈 : 고수준 모듈의 기능을 구현하는데 필요한 하위 기능을 실제로 구현한 것
→ 고수준 모듈이 제대로 동작하려면, 저수준 모듈을 사용해야 한다.
❗️그러나, 고수준 모듈이 저수준 모듈을 사용하면 구현 변경과, 테스트가 어려워진다.
(예를들어)
회원가입 기능을 제공하는 고수준 모듈 JoinService 가 있다가 했을 때 다음과 같은 하위 기능이 필요합니다.
- 가입하려는 회원의 정보가 기존에 존재하는지 확인하는 UserRepository 가 필요
- 회원가입 완료 메세지를 전달하는 외부 써드파티 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> {
}
✔️ 모듈간 의존관계
끝,,이지만
잘못된 내용이나 더 올바른 방향성을 댓글로 알려주신다면 정말 큰 도움이 될 것 같습니다.
참고
- 도메인 주도 개발 시작하기 - 최범균
- 우리는 이렇게 모듈을 나눴어요: 멀티 모듈을 설계하는 또 다른 관점 | 인프콘2023
'Spring > Spring Boot' 카테고리의 다른 글
[Spring Boot] 다중 인스턴스에서 스케줄링 중복 실행 제어 하기 (@Scheduled Lock - shed lock) (0) | 2024.10.10 |
---|---|
동시성 제어 문제에 대한 고찰 (With. Spring, JAVA, MySQL, Redis, Kafka) (0) | 2024.08.30 |
Spring Boot 에서 Redis Cache 사용하기 (2) | 2024.07.02 |
Spring 에서 Redis 사용하기 (설정, In-memory DB, Transaction) (0) | 2024.07.01 |
@Transactional 동작과정 살펴보기 (with. Spring AOP) (1) | 2024.06.16 |