지속가능한 개발을 위한 멀티모듈

김정인·2022년 11월 12일

coffee-chat

목록 보기
1/2

Overview


멀티모듈이라 할 때 가장 먼저 생각나는 키워드는 무엇일까? 바로 스파게티 소스이다. 특히나 자바 개발자로서 의존성 덩어리의 모듈을 바라보았을 때는 그저 기피하고 회피하고 싶은 구성이다.

그러나 애플리케이션마다 여러 레파지토리로 분리하는 것은 너무 많은 번거로움을 만들어낸다.
예를 들어, API 어플리케이션과 ADMIN 어플리케이션이 있다고 하자. 동일하게 사용하는 도메인 로직을 변경할 때 각 레파지토리에서 로직간의 충돌을 고려해야 한다.

특히 JPA 를 사용할 때의 Entity 클래스는 변경 소요가 있을 때마다 모든 레파지토리를 신경써야한다.

물론 처음 개발할 때부터 기능명세를 명확히 하고, 도메인 변경 소요를 최소화하여 개발하면 이 번거로움은 줄어들 것이다. 이 말은 모두에게 익숙하면서 꺼려지는 폭포수 개발 방법론이다. 서비스는 복잡하고, 시시각각 급변하며, 특히나 서비스의 시작점은 무수한 피벗으로 인해 변경소요가 다분하다.

때문에 서비스가 성숙해지면서 각 기능이 견고해지면서 독립적일 수 있을 때까지 하나의 레파지토리에서 개발을 진행하다, 분리에 대한 소요가 비즈니스적인 판단으로 필요할 때, 우리는 그에 맞춰 진행하는 것이 이상적이다.

따라서 우리는 변화를 수용할 수 있는 확장성결합도가 낮으며, 응집도가 높은 개발을 할 수 있어야한다.

그렇다면 이 멀티모듈 구성은 어떻게 해야 할까?



Layerd Architecture


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

Presentation Layer (표현계층 , Controller)

  • 사용자 요청에 대해 해석하고 응답하는 일을 책임지는 계층
  • 기능
    • Client 로부터 Request 를 받고, Response 를 응답하는 API 정의
    • 요청 방식에 따른 파라미터 검증

Application Layer (응용 계층, Service)

  • 비즈니스 로직을 정의하고 정상적으로 수행될 수 있도록 도메인 계층과 infrastructure 계층을 연결해주는 역할을 하는 계층
  • 실질적인 데이터의 상태 변화 등의 처리는 도메인 계층에서 진행할 수 있도록 위임하는 것이 중요
  • 기능
    • 트랜잭션의 단위
    • DTO 변환
    • Entity 조회 및 저장
    • 비즈니스 로직에 따른 Parameter 검증

Domain Layer (도메인 계층, Model)

  • 비즈니스 규칙, 정보에 대한 실질적인 도메인에 대한 정보를 가지고 있으며, 이 모든 것을 책임지는 계층

Infrastructure Layer (인프라 계층, Repository)

  • 외부와의 통신 (DB, 메시징 시스템 등)을 담당하는 계층
  • 해당 계층에서 얻어온 정보를 응용 계층 또는 도메인 계층에 전달하는 것이 주 역할이다.



이러한 Layer 계층을 볼 때, 우리는 변경소요를 예측해볼 수 있다.

  • Infrastructure 계층은 기술적인 선택으로 인한 변경소요가 있겠구나.
    • 추상화를 통해 여러 기술적인 방법을 선택할 수 있도록 고려해보자.
  • Domain Layer
    • 비즈니스 로직에 대한 변경소요가 있을 때 , (비즈니스적인 판단) 변경소요가 있겠구나.
    • 이는 하나의 서비스이기 때문에 어떤 애플리케이션이던지 일관된 비즈니스로직을 제공해야하구나
  • Application Layer
    • 사용자 요청에 따른 서비스를 호출하는 방식에서 변화가 생기면 변경될 수 있겠구나.
    • client 의 하나의 요청으로 우리 서비스에서 어떤 로직들을 처리하는지 한눈에 볼 수 있는 것이 중요하겠다.
  • Presentation Layer
    • 사용자에게 어떤식으로 보여줄지에 대한 변경이 있으면 변경소요가 있겠구나.
    • 각 애플리케이션마다 호출하는 데이터가 달라질 수 있겠군



MultiModule 을 만들고자 할 때의 목표

  • 그럼, Domain Layer 는 각 도메인별로 비즈니스로직을 구성하고, 모든 애플리케이션에 일관된 로직을 수행해야 하는 거네? 추후 분리되기 용이하게
  • Application Layer 는 애플리케이션을 사용하는 사용자에 따라 도메인 데이터를 선택해서 보여줄 수 있어야하고,
  • Infrastructure Layer 는 기술적 선택으로 전환하기 용이하게 만들어야겠다.

만들고자 하는 모듈 구조


  • app-service-api
    • 프레젠테이션 계층으로서 내부 App API 를 제공한다.
  • app-admin-api
    • 프레젠테이션 계층으로서 내부 Admin API 를 제공한다.
  • app-external-api
    • 공공데이터 API 와 통신 후 데이터 후처리 작업을 진행한다.
  • app-batch
    • Batch 작업을 진행한다.
  • domain
    • 하나의 모듈은 최대 하나의 인프라스트럭처에 대한 책임만 갖는다.
    • 도메인 모듈을 조합한 더 큰단위의 도메인 모듈이 있을 수 있다.
  • common
    • Type, Util 등을 정의한다.
    • 가능하면 사용하지 않는다.

우리는 domain 모듈을 공유하여 선택적으로 프레젠테이션 계층과 결합하여 배포를 진행할 것입니다.

이유는 admin-api , service-api 등 처럼 관리자 사이트를 만들던, 앱을 위한 REST API 를 만들던 우리는 같은 도메인에 접근하여 서비스를 제공하기 때문입니다.

각 api 를 레파지토리를 분리하여 개발할 경우, 우리는 도메인의 변경에 따라 2개의 레파지토리에 변경사항을 반영해야 하는 관리포인트의 분산 문제가 있습니다.

멀티모듈을 사용하면 같은 도메인 계층을 import 하여 합쳐진 Jar 파일을 간단하게 뽑을 수 있습니다.

  • service-api , admin-api 의 build.gradle
implementation project(":coffee-chat-domain")



구체화


계층 별로 나눈 WAS 의 역할을 좀 더 구체적인 모듈 단위로 나눠보기 위해 이희창님이 강의에서 설명해주신 Layer 별 특징과 역할에 대한 표를 첨부하였다.

각 Layer 별 위의 역할에 따른 구성을 진행해보겠다.

LayerDescription주요 객체
사용자 인터페이스 (interfaces)사용자에게 정보를 보여주고 사용자의 명령을 해석하는 책임Controller, Dto, Mapper(Converter)
응용 계층(application)수행할 작업을 정의하고, 표현력 있는 도메인 객체가 문제를 해결하게 한다. 이 계층에서 책임지는 작업은 업무상 중요하거나 다른 시스템의 응용 계층과 상호작용하는 데 필요한 것들이다. 이 계층은 얇게 유지되고, 오직 작업을 조정하고 아래에 위치한 계층에 포함된 도메인 객체의 협력자에게 작업을 위임한다.Facade
도메인 계층(domain)업무 개념과 업무 상황에 대한 정보, 업무 규칙을 표현하는 일을 책임진다. 이 계층에서는 업무 상황을 반영하는 상태를 제어하고 사용하며, 그와 같은 상태 저장과 관련된 기술적인 세부사항은 인프라 스트럭처에 위임한다. 이 계층이 업무용 소프트웨어의 핵심이다.Entity, Service, Command, Store, Executor, Factory(interface)
인프라 스트럭처 계층(infrastructure)상위 계층을 지원하는 일반화된 기술적 기능을 제공한다. 이러한 기능에는 애플리케이션에 대한 메시지 전송, 도메인 영속화, UI 에 위챗을 그리는 것 등이 있다.low level 구현체, Spring JPA, RedisConnector, …

위의 표처럼 계층을 역할에 따라 나누고, 각각의 계층은 인터페이스를 통해 추상화가 기반이 됩니다.



Service-Api Layerd


  • Service-Api 는 프레젠테이션 계층에 속합니다.
  • Client 에게 요청을 전달받아 그 요청을 도메인 계층에서 사용될 수 있도록 Dto 를 VO 를 Convert 작업을 진행합니다.
  • VO 를 해당 도메인 계층으로 넘겨 비즈니스 로직을 수행하게 한 후 응답값을 Dto 로 다시 Convert 작업을 진행하여 Client 에게 내려줍니다.

  • 프레젠테이션 계층인 Service-Api 는 Controller , dto , facade 로 구성합니다.

SignUpController.java

@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);
}
  • 위는 sign-up Controller Api 부분입니다.
  • 클라이언트에서 받은 요청은 SignUpDto 로 받습니다.
  • 서비스에서 사용될 수 있는 불변객체인 MemberCommand 로 Facade 로 전달합니다.
  • Facade 로 부터 받은 데이터를 SignUpDto 의 Response 로 Convert 를 진행 후 응답으로 돌려줍니다.

MemberFacade.java

@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;
    }
}
  • facade 에서는 해당 비즈니스 로직을 수행할 service 를 호출합니다.
  • 예를 들어, 회원가입을 진행한다고 하였을 때, 회원가입에 대한 비즈니스 로직을 수행하는 서비스와 회원가입 결과를 이메일로 알려줄 서비스를 호출합니다.
  • 이는 하나의 트랜잭션 단위가 아닌 로직의 단위로 구성됩니다.



Domain Layerd


  • domain 은 비즈니스 로직을 수행하고 Persistence 에 데이터를 요청합니다.
  • 앞선 Presentation 계층의 Facade 에서 호출한 순서대로 서비스를 진행합니다.

SignUpService.java

@FunctionalInterface
public interface SignUpService {
    String signUp(String uuid, MemberCommand.SignUp signUp);
}

MentorSignUpService.java

@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();
    }
}
  • MentorSignUpService 는 SignUpService 의 signUp 을 구현한 서비스이다.
  • validate 로직을 수행한 후 Database 에 요청하기 위해 Member Entity 객체를 생성합니다.
  • 이후 Read 의 요청과 Write 의 요청에 따라 Reader, Store 인터페이스를 호출합니다.

@Component
@RequiredArgsConstructor
public class MemberStoreImpl implements MemberStore {

    private final MemberStoreRepository memberStoreRepository;

    @Override
    public Member store(Member member) {
        return memberStoreRepository.save(member);
    }
}
  • 도메인 계층으로부터 넘겨받은 Entity 를 기준으로 데이터베이스에 접근하여 Insert 작업을 수행합니다.
  • 이후 memberStore 인터페이스를 호출해 데이터베이스에 Insert 작업을 진행하게 한 후 Return 값을 다시 프레젠테이션 계층으로 넘겨줍니다.

이와 같은 구성으로 Presentation Layer → Domain → Persistence Layer → Database → Persistence Layer → Domain → Presentation Layer 의 요청의 흐름을 service-api , domain 모듈의 구성으로 진행해보았다.



Reference


profile
Back-end Developer

0개의 댓글