
멀티모듈이라 할 때 가장 먼저 생각나는 키워드는 무엇일까? 바로 스파게티 소스이다. 특히나 자바 개발자로서 의존성 덩어리의 모듈을 바라보았을 때는 그저 기피하고 회피하고 싶은 구성이다.
그러나 애플리케이션마다 여러 레파지토리로 분리하는 것은 너무 많은 번거로움을 만들어낸다.
예를 들어, API 어플리케이션과 ADMIN 어플리케이션이 있다고 하자. 동일하게 사용하는 도메인 로직을 변경할 때 각 레파지토리에서 로직간의 충돌을 고려해야 한다.
특히 JPA 를 사용할 때의 Entity 클래스는 변경 소요가 있을 때마다 모든 레파지토리를 신경써야한다.
물론 처음 개발할 때부터 기능명세를 명확히 하고, 도메인 변경 소요를 최소화하여 개발하면 이 번거로움은 줄어들 것이다. 이 말은 모두에게 익숙하면서 꺼려지는 폭포수 개발 방법론이다. 서비스는 복잡하고, 시시각각 급변하며, 특히나 서비스의 시작점은 무수한 피벗으로 인해 변경소요가 다분하다.
때문에 서비스가 성숙해지면서 각 기능이 견고해지면서 독립적일 수 있을 때까지 하나의 레파지토리에서 개발을 진행하다, 분리에 대한 소요가 비즈니스적인 판단으로 필요할 때, 우리는 그에 맞춰 진행하는 것이 이상적이다.
따라서 우리는 변화를 수용할 수 있는 확장성과 결합도가 낮으며, 응집도가 높은 개발을 할 수 있어야한다.

우리 어플리케이션의 구조는 아래와 같이 계층별로 이루어진다.

Presentation Layer (표현계층 , Controller)
Application Layer (응용 계층, Service)
Domain Layer (도메인 계층, Model)
Infrastructure Layer (인프라 계층, Repository)


우리는 domain 모듈을 공유하여 선택적으로 프레젠테이션 계층과 결합하여 배포를 진행할 것입니다.
이유는 admin-api , service-api 등 처럼 관리자 사이트를 만들던, 앱을 위한 REST API 를 만들던 우리는 같은 도메인에 접근하여 서비스를 제공하기 때문입니다.
각 api 를 레파지토리를 분리하여 개발할 경우, 우리는 도메인의 변경에 따라 2개의 레파지토리에 변경사항을 반영해야 하는 관리포인트의 분산 문제가 있습니다.
멀티모듈을 사용하면 같은 도메인 계층을 import 하여 합쳐진 Jar 파일을 간단하게 뽑을 수 있습니다.
implementation project(":coffee-chat-domain")
계층 별로 나눈 WAS 의 역할을 좀 더 구체적인 모듈 단위로 나눠보기 위해 이희창님이 강의에서 설명해주신 Layer 별 특징과 역할에 대한 표를 첨부하였다.
| Layer | Description | 주요 객체 |
|---|---|---|
| 사용자 인터페이스 (interfaces) | 사용자에게 정보를 보여주고 사용자의 명령을 해석하는 책임 | Controller, Dto, Mapper(Converter) |
| 응용 계층(application) | 수행할 작업을 정의하고, 표현력 있는 도메인 객체가 문제를 해결하게 한다. 이 계층에서 책임지는 작업은 업무상 중요하거나 다른 시스템의 응용 계층과 상호작용하는 데 필요한 것들이다. 이 계층은 얇게 유지되고, 오직 작업을 조정하고 아래에 위치한 계층에 포함된 도메인 객체의 협력자에게 작업을 위임한다. | Facade |
| 도메인 계층(domain) | 업무 개념과 업무 상황에 대한 정보, 업무 규칙을 표현하는 일을 책임진다. 이 계층에서는 업무 상황을 반영하는 상태를 제어하고 사용하며, 그와 같은 상태 저장과 관련된 기술적인 세부사항은 인프라 스트럭처에 위임한다. 이 계층이 업무용 소프트웨어의 핵심이다. | Entity, Service, Command, Store, Executor, Factory(interface) |
| 인프라 스트럭처 계층(infrastructure) | 상위 계층을 지원하는 일반화된 기술적 기능을 제공한다. 이러한 기능에는 애플리케이션에 대한 메시지 전송, 도메인 영속화, UI 에 위챗을 그리는 것 등이 있다. | low level 구현체, Spring JPA, RedisConnector, … |

@PostMapping("{uuid}/sign-up")
@ResponseBody
public ResponseEntity<ApiResponse> signUp(
@PathVariable(name = "uuid") String uuid,
@RequestBody @Valid SignUpDto.SignUp request) {
MemberCommand.SignUp parentCommand = signUpDtoMapper.of(request);
String signUpToken = memberFacade.signUp(uuid, parentCommand);
SignUpDto.SignUpResponse response = signUpDtoMapper.of(signUpToken);
return new ResponseEntity<>(ApiResponse.OK(response), HttpStatus.CREATED);
}
@Service
@RequiredArgsConstructor
public class MemberFacade {
private final SignUpServiceFactory signUpServiceFactory;
private final MemberService memberService;
private final NotificationService notificationService;
public String signUp(String uuid, MemberCommand.SignUp request) {
SignUpService signUpService = signUpServiceFactory.create(request);
String signUpToken = signUpService.signUp(uuid, request);
notificationService.notifySignUpEmail(uuid);
return signUpToken;
}
}

@FunctionalInterface
public interface SignUpService {
String signUp(String uuid, MemberCommand.SignUp signUp);
}
@Service
@RequiredArgsConstructor
public class MentorSignUpService implements SignUpService {
private final MemberReader memberReader;
private final MemberStore memberStore;
@Override
public String signUp(String uuid, MemberCommand.SignUp signUpInfo) {
validateSignUp(uuid, signUpInfo.getEmail(), signUpInfo.getNickname());
MentorDetail mentorDetail = MentorDetail.createMentorDetail(signUpInfo.getMentorDetailInfo());
mentorDetail.validateYear(signUpInfo.getMentorDetailInfo().getYear());
Member newMentor = Member.MentorSignUp(uuid, signUpInfo, mentorDetail);
Member savedMentor = memberStore.store(newMentor);
return savedMentor.getUuid();
}
}

@Component
@RequiredArgsConstructor
public class MemberStoreImpl implements MemberStore {
private final MemberStoreRepository memberStoreRepository;
@Override
public Member store(Member member) {
return memberStoreRepository.save(member);
}
}