Gradle 멀티 모듈 설계

dragonappear·2022년 12월 22일
0

Gradle

목록 보기
4/5


참고

- https://spring.io/guides/gs/multi-module/
- https://techblog.woowahan.com/2637
- https://cjw-awdsd.tistory.com/55
- https://jojoldu.tistory.com/123

글 작성 계기

  • 같은 DB 엔티티를 공유하는 어플리케이션 2개를 돌리는 프로젝트를 해봤었다.
  • 코드중복으로 인한 (CRUD + 그와 관련된 작업) 수정 X2 해야하니까 너무 귀찮았다. 진짜 너무 귀찮았다.
  • 그래서 멀티모듈에 관련된 글들을 읽고 글을 쓴다

멀티모듈 프로젝트란?

  • 예를 들어, 내가 회원 시스템을 개발한다고 하면

member internal api
member external api
member batch

와 같이 서로 독립된 프로젝트 단위로 가지고 있다고 해보자.

이런 구조를 가지고 있을 때 가장 큰 문제점은 시스템의 중심 Domain 이 가져야할 구조와 규칙등을 동일하게 보장해주는 메커니즘이 없다는 것이다.

개발자는 동일한 Domain을 가지고 있는 위 3가지 어플리케이션을 열심히 복붙을 하며 개발을 한다. 진짜 얼마나 귀찮은 일인지 모른다

멀티 모듈 프로젝트는 기존의 단일 프로젝트를 프로젝트 안의 모듈로써 갖을 수 있는 구조를 제공한다

위처럼 하나의 시스템에서 중심 도메인을 모듈로 분리하여 위와 같은 보장 메커니즘을 제공받을 수 있다.

더 자세한 내용은 https://jojoldu.tistory.com/123 참고해보자

상기 내용으로 멀티 모듈 프로젝트는 공통으로 사용하는 코드들을 모아놓고 공용으로 같이 쓸 수 있게 해주는구나! 라고 생각을 하게 된다.

하지만, 그게 전부가 아니다.


실패한 멀티 모듈 프로젝트

아래와 같은 어플리케이션들을 가지는 시스템을 멀티 모듈로 바꾸어보았습니다.

  • Kingbbode System
    • internal api
    • external api
    • batch

공통으로 사용하는 코드들을 모았다.

모듈 이름을 무엇으로 정하지? common 으로 정하자
그리고 아래와 같이 구조를 짜면 이제 common의 저주가 시작한다.

이제 common 모듈에서만 수정을 하면 된다. 더이상 노가다하지 말자.


공통(Common) 모듈의 저주

공통 모듈에 대부분의 핵심 또는 공통 코드들이 다 들어가게 해보자. 그리고 많은 문제점들을 발견해보자

A 어플리케이션에서 기능을 추가한다. 이 때 코드는 어디에 작성하게 될까? 개발자는 고민을 하게 되고 코드는 어떠한 선택에 의해 빈번하든 아니든 공통 모듈에 점점 추가가 된다.

B 어플리케이션에서 에서도 마찬가지일 것이다.

이제 C 어플리케이션에서 기능을 추가한다. 공통 모듈에 작성된 유용해보이는 코드들이 있다. 사용하게 된다.

사실 그 기능은 A 어플리케이션을 위해 작성된 코드이다.

위 과정이 반복되면 어느새 공통 모듈은 걷잡을 수 없이 커져있을 것이다. 그리고 어플리케이션에서 하는 일이 점점 줄어들고 공통 모듈에서 점점 더 많은 일을 하게 된다.

1. 스파게티 코드

모든 프로젝트들은 주기적인 청소가 반드시 필요하다. 리팩토링을 통해서.

그러나 공통 모듈의 저주가 이를 방해한다.

A: "오래된 기능이 F/O 되었다. 코드를 정리해볼까?", "이쪽 흐름이나 코드가 효율적이지 않네. 리펙토링 좀 해볼까?"
관리자 : "영향 범위는?"
A: "시스템 전체.."

코드가 꼬리에 꼬리를 물고 결국 하나의 코드만 수정해도 전체가 영향받는 현상이 발생하게 된다.

심지어 A를 B로, B를 C로 가공한 코드에서 다시 C에서 A로 가공한 코드도 만들어질 수 있다.

class AService {
  private final ARepository aRepository;

  public A act(Long id) {
    ...
    return aRepository.findById(id);
  }
}

class BService {
  private final AService aService;

  public B act(Long id) {
    A a = aService.act(id);
    ...
    return mapToB(a);
  }
}

class CService {
  private final BService bService;

  public C act(Long id) {
    B b = bService.act(id);
    ...
    return mapToC(b);
  }
}

class DService {
  private final CService cService;

  //띠용!!
  public A act(Long id) {
    C c = cService.act(id);
    ...
    return mapToA(c);
  }
}
  • 공통 모듈에 들어간 코드는 더 이상 처음 작성한 의도만을 위한 코드가 아니게 되었다.
  • 공통이기 때문이다.
  • 우리는 혼자 개발하는 것이 아닐 뿐더라 혼자 개발한다고 해도 이러한 실수는 반드시 생기게 된다.

2. 의존성 덩어리

공통 모듈은 어떤 의존성을 가지게 될까? 결국에는 전부라고 확신한다.

  • 웹쪽 설정을 사용하니까 spring-boot-starter-web, JPA를 사용하니까 spring-boot-starter-data-jpa, REDIS도 사용하니까 spring-boot-starter-data-redis 등등등 계속 추가하게 될 것이다.
  • 전부가 아니더라도 프로젝트에서 사용하는 대부분의 의존성이 공통 모듈로부터 시작될 것이다.

  • 문제는 어플리케이션들이 사용하는 의존성은 다를 수도 있다는 것이다.

  • 예를 들어 데이터베이스를 사용하지 않는 어플리케이션에서 공통 모듈을 사용하기 위해 데이터베이스와 커넥션을 맺게 된다.

  • 그 외에 다른 의존성도 마찬가지겠다.

  • 스프링 부트는 의존성을 기반으로 발동되는 AutoConfiguration 들이 아주 많이 있지요.

  • 의존성 덩어리인 공통 모듈로 인해 어디선가 발동되는 스프링 부트의 마법으로 어플리케이션은 최적화되어 돌아가지 못하게 될 것이고, 이것은 곧 장애 포인트가 될 것 이다.

3. 공통 설정

  • 설정까지 공통 모듈로 몰게 되는 경우도 많이 보았다.
  • 고정적으로 공통으로 사용되어야 하는 호스트 정보 등은 경우에 따라 공통으로 보아야할 수도 있지만, 그 외의 Thread Pool, Connection Pool, Timeout 등의 설정도 가장 민감하고 중요한 어플리케이션 기준으로 몰아들어간다.

이 때 발생하는 문제의 대표적인 예로 데이터베이스 커넥션이 있다. 모든 데이터베이스에는 가질 수 있는 최대 커넥션 개수가 정해져 있다. 데이터베이스를 사용하지 않는 어플리케이션에서 공통 모듈을 사용하기 위해 사용되는 커넥션으로 인해 실제로 데이터베이스를 사용하는 어플리케이션과 해당 어플리케이션까지 모두 문제가 생길 수가 있게 된다.

이 외에도 어플리케이션마다 다르게 작성되어야 할 설정을 한 곳으로 몰게 되었을 때 수많은 문제점들이 발생하게 된다.

왜 이런 문제점들이 생기게 되었을까? 멀티 모듈 프로젝트는 독립적으로 실행 가능한 어플리케이션을 1개 이상 가질 수 있기 때문입니다.

멀티 모듈 프로젝트는 하나의 시스템을 단위로 만들진다.
여기서 말하는 시스템 은 아래와 같은 정의를 말한다.

출처: https://www.slideshare.net/arawnkr/ss-94475606

  • 멀티 모듈 프로젝트는 독립적으로 실행가능한 어플리케이션 모듈을 1개 이상 가지고 있으며, 사용하는 인프라 자원 역시 1개 이상을 가지고 있다.
  • 독립적으로 실행가능 한 어플리케이션들은 당연히 서로 다른 책임과 역할을 가지기 때문에 스파게티 코드, 의존성 덩어리, 설정 등의 문제를 피하기 위해 하위의 모듈들에 대한 의존성과 사용성에 대한 개방, 폐쇄를 철저히 해야만 한다.

멀티 모듈 구성하기

글을 읽기 전 주의사항
아래 글에서 나오는 용어와 관점을 우리가 많이 바라보는 시점인 레이어 아키텍처 의 시점으로 바라보았을 때 혼동이 올 수 있습니다.

아래 작성된 내용은 기존의 시점과 조금 다른 시점에서 어플리케이션을 바라본 점이라는 것을 인지하여 읽어주시면 감사하겠습니다

  • 사전마다 거의 제각각으로 설명이 되어있지만, 공통적으로 나오는 내용으로 정리했을 때 모듈이라는 개념은 독립적으로 운영될 수 있는 의미를 가지는 구성요소 단위 라고 정의가 된다.

모듈은 여러 가지로 정의될 수 있지만, 일반적으로 큰 체계의 구성요소이고, 다른 구성요소와 독립적으로 운영된다.
출처: wikipedia

  • 모듈이라는 용어는 개발 영역 안에서도 여러 영역에서 쓰이고 있다.
    • 그렇다면 설계해야 할 프로젝트 모듈의 예시가 무엇이 있을까요? 답은 굉장히 간단했고 가까운 곳에 많이 있다.
    • 우리가 제공받고 있는 의존 라이브러리들이 모두 모듈이다. Spring Framework 타사 연동 라이브러리 등등 우리는 굉장히 많은 모듈을 사용하고 있다.
  • 여러오픈 소스 라이브러리 들 중 멀티 모듈 구조를 갖는 프로젝트들이 어떠한 구조로 모듈을 사용하는지 찾아 보았을 때 모듈 자체에 대한 공통적인 특징을 찾아낼 수 있다.
    • 모듈은 독립적인 의미를 갖는다
    • 모듈은 어떠한 추상화 정도에 대한 계층을 가지고 있다.
    • 계층 간 의존관계에 대한 규칙이 있다.

이러한 특징들을 우리가 설계해야할 시스템에 적용하여 고민하여 아래와 같은 시스템에서 가져야할 계층 구조를 정의하였다.

  • 독립 모듈 계층
  • 도메인 모듈 계층
  • 내부 모듈 계층
  • 공통 모듈 계층
  • 어플리케이션 모듈 계층

이러한 정의된 계층 구조를 갖음으로써 우리는 모듈이 어디까지 책임과 역할을 가질 수 있는지를 명확히 할 수 있고, 의존관계 또한 최소화하여 최적화된 프로젝트를 만들어낼 수 있다.

위 계층에 어떠한 독립적으로 의미를 갖는 모듈들이 배치될 수 있는지 살펴보자.


독립 모듈 계층

  • 시스템과 무관하게 어디에서나 사용 가능한 라이브러리 성격의 모듈을 이 계층에 배치하였다.
  • 프로젝트 내에서 가장 프로젝트와 성격이 먼 모듈이 이 계층에 위치하게 된다.
  • 시스템,도메인의 비즈니스와 전혀 별개로 자체 제작한 라이브러리 같은 모듈이 이 계층으로 들어갈 것이고, 경우에 따라 사내 레퍼지토리에 올라갈 수 있다. 특별한 경우가 아니라면 이 계층에 모듈이 생길 일은 거의 없을 것이다.

이 계층은 아래와 같은 원칙을 가진다.

  • 자체로써 독립적인 역할을 갖는다

언제든 이 계층의 모듈을 대처할 탄탄한 라이브러리가 나오거나, 사내 레포지토리 등에 올라간다면 프로젝트에서 이 모듈은 제거될 것이므로, 프로젝트 내에 의존관계를 두지 않아야 한다.


공통 모듈 계층

하나의 프로젝트에서 모든 모듈에서 사용될 수 있는 것들은 나올 수 밖에 없다. 그래서 고민 끝에 이 계층을 정의하였고, 대신에 이 모듈은 가장 큰 제약사항을 걸어두어야 한다.

  • Type,Util 등을 정의한다.
  • 가능하면 사용하지 않는다.

마찬가지로 시스템 내 모든 모듈들이 의존할 수 있을만큼 얇은 의존성을 제공해야 하기 때문에 프로젝트 내 어떠한 모듈도 의존하지 않아야 한다.

심지어 이 모듈에서는 외부의 의존관계도 갖지 않도록 한다.

bootJar 
jar 

dependencies {
}

이 말은 즉 순수 자바 Class만 정의할 수 있다는 이야기이다.

생성된 모듈에서는 시스템에서 정말 많이 쓰이는 Type 성격의 DTO 나 기본적인 Util Class만 배치한다.

그렇다고 모든 Type 이나 Util 격 Class가 이 모듈로 배치되는 것은 아니다.
가능하다면 이 모듈을 사용하지 않는다는 원칙을 두고 있기 때문이다.


도메인 모듈 계층

시스템의 중심 도메인을 다루는 모듈을 이 계층에 배치하였다.

아래의 내부 모듈 계층 과 비슷한 성격에 계층이지만, 저장소와 밀접한 중심 도메인을 다루는 계층은 더 견고하고 특별하게 격리되고 관리되어야 하기 때문에 반드시 분리되어야 한다.

이 계층은 아래와 같은 원칙을 갖는다.

  • 서비스 비즈니스를 모른다.
  • 하나의 모듈은 최대 하나의 인프라스트럭처에 대한 책임만 갖는다.
  • 도메인 모듈을 조합한 더 큰 단위의 도메인 모듈이 있을 수 있다.

이 계층에는 프로젝트 안의 어떠한 실행 가능한 어플리케이션에서도 사용 가능한 모듈이 위치해야 한다.
그러기 위해서는 저장소 외 시스템 특성을 알지 않아야 하기 때문에 내부 모듈을 의존하지 않도록 구성하였다.

이 계층의 모듈은 오로지 도메인에 집중한다. 어떠한 도메인이든 그 도메인이 가져야할 서비스와 무관한 도메인의 도메인의 비즈니스가 있다.

도메인 중심 설계에 대한 내용으로 견고한 도메인에서부터 프로젝트를 만들어가면 자연스럽게 이러한 구조가 나올 수 있기도 하다.

하나의 모듈은 최대 하나의 인프라스트럭처를 갖는 것은 의존성의 전파를 방지하기 위해서서이다.

사용하지 않는 인프라스트럭처에 대한 설정까지 해줘야 했던 경험으로 인프라스트럭처 단위로 최대한 작은 단위부터 작성되어야 한다.

단일 인프라스트럭처 사용 모듈

가장 흔한 케이스로 RDBMS가 중심 도메인을 품고 있는 프로젝트는 이런 도메인 모듈이 하나만 만들어진다.(네이밍은 xxx-domain 혹은 xxx-core 로 작성했다.)

도메인 모듈은 아래와 같은 책임을 갖는다.

  1. Domain
  • Java Class로 표현된 도메인 Class 들이 이곳에 위치한다.
  • JPA를 기준으로 한다면 테이블과 맵핑되는 Class들이다.
  • 도메인 모듈 내부에서는 이 곳에 위치한 도메인들을 사용하여 대화를 하게 된다.
  1. Repository
  • 도메인의 CRUD 역할을 한다.

  • 여기서 주의할 점은 모든 CRUD 역할을 이곳에서 하는 것은 아니라는 점이다.

  • 이 모듈은 시스템에서 가장 보호받아야 하며, 가장 견고해야 할 모듈이므로, 이 모듈에서 CRUD 에 대한 작성을 할 때 많은 고민을 해야 한다.

  • Admin Application 에서 시스템 도메인에 대한 통계를 기능을 추가한다고 했을 때를 가정해보자

    • 이 때에 통계 조회에 대한 정의는 시스템이 갖는 중심 역할에 따라 달라지게 된다.
    • 만들고 있는 시스템에서 통계라는 기능이 중심 역할로 볼 수 있다면 도메인 모듈에 작성할 것이고, 그렇지 않다면 사용을 하는 측에 작성되어야 한다.
    • 만일 이 시스템이 주문 이라는 중심 도메인을 갖는다면 나는 통계 기능은 사용하는 측(Application Module)에 작성할 것이다.
  1. Domain Service
  • 이 계층은 도메인의 비즈니스를 책임진다.
  • 그렇기 때문에 도메인이 갖는 비즈니스가 단순하다면 이 계층은 생기지 않을수도 있다.
  • 이 계층에서 저는 트랜잭션의 단위를 정의했고, 요청 데이터를 검증했으며, 이벤트를 발생하는 일 등 도메인의 비즈니스를 작성했다.

다중 인프라스트럭처 사용 모듈

  • 도메인 계층에서 여러 인프라스트럭처를 사용해야 할 때 꼬이는 의존관계를 많이 보았다.

  • 예를 들어 RDBMS를 사용하는 A라는 도메인 모듈이 있다. 시스템의 요구사항으로 A 도메인의 삽입이 발생할 때 B라는 도메인으로 가공하여 임시 저장시켜야 하는 요구가 생겼다.

  • 이 때 RDBMS를 사용하는 도메인 모듈에 Redis 의존성을 추가한다면 의존 지옥을 맛볼 가능성이 크다.

  • A 도메인에 대한 조회성을 위해서만 도메인 모듈을 사용해야 하는 상황에서도 Redis 에 대한 설정과 의존을 설정해주어야 한다.

  • 앞으로 더 다양한 인프라스트럭처를 사용하게 될 때 점점 더 지옥이 열릴 것이다.

  • 그래서 하나의 모듈은 하나의 인프라스트럭처만 책임지도록 모듈을 작성하자. 그리고 위와 같은 두 인프라스트럭처 사이의 관계가 생길 때 두 모듈을 품는 모듈을 작성하거나 두 인프라스트럭처 모듈을 품는 어플리케이션에서 처리하자

  • 결국 하나의 모듈이 두 인프라스트럭쳐를 책임지는 것이 아니냐고 생각할 수 있지만, 두 인프라스트럭처를 모듈을 품는 모듈에게는 인프라스럭처에 대한 책임이 주어지지 않는다.

  • A,B 도메인이 다른 인프라를 갖지만 절대 떨어질 수 없다고 단정지을 수 있다면 단일 모듈로도 작성하면 된다. 위와 같이 분리된 모듈로 작성하는 것은 추후 분리와 확장을 더 용이하도록 하기 위한 기초 작업이다.


내부 모듈 계층

저장소,도메인 외 시스템에서 필요한 모듈들은 이 계층에 속하게 된다. 독립 모듈 계층은 시스템에도 전혀 관여되지 않았다면, 이 계층은 시스템과는 연관이 있는 모듈을 말한다.

이 계층은 아래와 같은 원칙을 갖는다.

어플리케이션,도메인 비즈니스를 모른다.

이 계층은 시스템 전체적인 기능을 서포트하기 위한 기능 모듈이 만들어질 수 있다. 이 계층 역시 프로젝트 안의 어떠한 실행 가능한 어플리케이션에서도 독립 사용 가능한 모듈이 위치되어야 하므로, 도메인 계층을 의존하지 않는다.

core-web

web 설정을 사용하는 프로젝트에서 사용할 수 있는 모듈이다. 주로 Web Filter 를 이용한 보안, 로깅 등으로 활용되며, 웹에 대한 필수적인 공통 설정을 하기도 한다.

xxx-client

외부의 xxx 시스템과 통신을 책임지는 모듈이며 각 외부 시스템별로 따로 모듈을 만들었다. 이 모듈은 비지니스와 관계없이 요청과 응답을 할 수 있는 사용성을 제공하고, 요청에 대한 설명과 스팩을 책임진다. 어플리케이션 모듈에서는 사용하는 외부 시스템 모듈만 사용하게 된다.

xxx-event-publisher

특정 이벤트에 대한 처리를 담당한다. 여기서 말하는 이벤트는 Spring ApplicationEvent 를 말하며, 이벤트가 발생했을 때 SQS 로 이벤트를 전송하거나, 로그를 남기는 등 특정 행위를 처리한다. 이 모듈 또한 하고 있는 주요 행위의 범위에 따라 따로 모듈이 생성될 수 있다.


어플리케이션 모듈 계층

이 계층은 우리가 잘알고 있는 독립적으로 실행 가능한 어플리케이션 모듈 계층이다.

어플리케이션 모듈은 하위 설계했떤 모듈들을 조립하여 서비스 비즈니스를 완성시킨다.

아래와 같은 모듈들을 배치할 수 있다.

  • xx-app-batch

  • xx-app-worker

  • xx-app-internal-api

  • xx-app-external-api

  • 이 계층의 모듈에게는 app 이라는 네이밍을 넣어서 모듈 내의 실행 가능한 어플리케이션이라는 것을 명확하게 했다.

어플리케이션 모듈은 독립 모듈, 도메인 계층 모듈, 내부 모듈, 공통을 사용성에 따라 의존성을 추가하여 사용하게 된다.

모듈 계층의 의존 관계 흐름


멀티모듈 구성 효과

1. 명확한 추상화 경계

계층에 따라 분리된 모듈로 인해 추상화 레벨을 맞출 수 있다.

기존에는 하나의 모듈에서 다양한 어플리케이션 레이어와 의존성을 책임지고 있었지만, 계층화된 모듈 레이어로 분리됨으로써 아래와 같이 역할과 책임의 선을 명확하게 할 수 있었고, 그로인해 개발의 생산성을 향상시킬 수 있다.

기존의 둘쭉날쭉 했던 기능의 제공 정도와 추상화 정도의 경계가 계층 설계로 인해 명확해짐으로써 얻는 이득이 많다

  • 각 모듈이 갖는 책임과 역할이 명확하여 리팩토링, 기능의 변경의 영향 범위를 파악하기 용이해진다.
  • 경계가 명확해짐으로써 기능의 제공 정도를 예측 가능하여 스파게티 코드 발생 가능성이 줄어든다.
  • 역할과 책임에 대한 애매함이 없어짐으로써 어떤 모듈에서 어느정도까지를 개발되어야할지 명확해진다.

위 세가지의 효과로 개발자는 더 빠르게 책임과 역할에 집중하여 확신을 가지고 개발을 할 수 있다.

2. 최소 의존성

각 모듈들이 각자 필요한 의존성만 가지고 있기 때문에 최소한의 의존성을 가질 수 있게 된다.

의존성 관리가 그렇게 중요할까?

자바에서 성능적으로 본다면 사실 그렇게 큰 성능 차이는 아닐 것이다.

그러나 좋은 개발환경이란 측면과 스프링 부트를 사용하는 측면에서는 손해를 볼 수 있다.

  1. 여지의 개방으로 개발 생산성을 떨어뜨린다. 무엇이든 끌어다 쓸 수 있다는 측면에서는 초기에는 빠르게 개발을 할 수 도 있지만, 그로인해 만들어지는 스파게티 코드로 인해 점차 개발 생산성이 떨어지게 될 것이다.

  2. 스프링 부트의 자동 설정으로 인해 예상치 못했거나, 불필요한 설정이 동작하게 될 수 있다. 스프링 부트는 클래스의 존재만으로도 작동되는 설정들이 아주 많이 있기 때문이다.

이로인해 예상치 못한 설정이 로드될 수도 있고, 혹은 위 잘못된 멀티모듈에서 보았던 사용하지 않는 자원에 대한 설정을 해줘야하는 일들도 발생할 수 있다.

역할과 책임이 명확해진 모듈 설계로 위 두가지 측면에서도 이득을 볼 수 있다.


의존관계 완성하기

1. 모듈 Configuration Property Load

스프링 부트는 몇가지 규칙에 의해 configuration property를 로드하도록 되어있다.

출처: https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config

그래서 대부분 모듈에 속해있는 properties 파일을 작동시키기 위해 application-$ 가 로드되는 방식을 많이들 이용하고 있다.

모듈에는 application-xxx.yml 과 같은 네이밍을 사용하여 변수를 셋팅하고 어플리케이션 모듈의 application.yml 에서 spring.profile.include=xxx 와 같이 xxx profile을 추가하여 파일을 로드하는 방식이다.

이 방식이 마음에 들지 않는 이유는 여러가지 이유가 있지만, 가장 큰 이유는 모듈 사용의 번거로움이다.

모듈 의존성을 추가한뒤에도 properties 혹은 yaml에 profile을 설정해주어야만 정상적으로 동작하는 번거로움이 마음에 들지 않는다.

모듈 의존성만 추가하면 되는 사용성을 만들고 싶다.

그래서 특정 path에 yaml을 스프링 부트의 적절한 사이클에 밀어넣을 수 있는 yaml-importer 모듈을 사용하여, 모듈 의존성만 추가하면 되는 사용성을 이용한다.

2. 모듈 Component Scan

모듈에 있는 Bean(Configuration,Service,Repository)를 로드할 수 있는 방법은 무엇일까?

1. spring.factories 를 통한 로딩 방식

스프링 프레임 워크는 모듈에 있는 META-INF/spring.factories 파일을 통해 특정 사이클에서 로드될 Class를 설정할 수 있다.

org.springframework.boot.env.EnvironmentPostProcessor=
com.baemin.xxx.processor.ThirdPropertiesEnvironmentPostProcessor

위에서 언급한 yaml-importer 모듈은 EnvironmentPostProcessor 라는 스프링 사이클에 설정을 도으하도록 되어있다.

spring.factories 를 사용해도 되겠다고 생각할 수 있지만, 이 방식은 시스템과 관련없는 위와 같은 모듈에 대한 설정을 로딩할 때만 사용하는 것이 맞다.

2. base package 의 확장

스프링 부트는 아래와 같이 스캔 되는 패키지를 확장할 수 있다.

@SpringBootApplication(scanBasePackages = "com.baemin.xxx.domain.shop")
public class XXXApplication {

이 방식이 스프링에서 권장하는 방식이며, 이 방식을 사용하는 것이 스프링의 의도와 잘 맞는 것 같다.

하지만 이 방식을 사용할 떄 불편함이 있다. 모듈 의존성을 추가할 때마다 scanBasePackaegs를 추가로 작성하는 것이 번거롭기 때문이다.

그래서 Application 클래스를 한 패키지 상위에 작성하는 관례를 두는 것이다. 프로젝트 안에서 모든 모듈은 공통적으로 com.baemin.xxx 라는 상위 공통 패키지를 갖고 어플리케이션 모듈에서는 모듈에서는 com.baemin.xxx 패키지에 Application 클래스를 배치한다.

3. 접근 개방/폐쇄

설계와 개발을 하다보면 개방과 폐쇄에 대한 많은 고민들이 있을 것이다.

A라는 코어가 있고 A는 보호받아야 하기 때문에 B라는 서비스를 만들게 되는 경우가 있다.

이럴 때 우리가 할 수 있는 방법 중 하나는 첫번째로 접근 제한자의 활용이다. 패키지나 상속 수준의 접근 제한을 설정하여 A 라는 코어를 보호할 수 있다.
그러나 A와B를 모듈 차원에서 사용할 떄는 쉽지 않다. 모듈이라는 것은 독립적인 의미를 가져야하므로, 이 둘 모듈의 내용을 제한할 수 없다.

두번째는 관례이다. 함께 개발하는 사람들끼리 함께 지켜야하는 관례를 만들어내는 문서, 코드 리뷰 등을 통해 새로운 구성원들에게도 계속 전파되게끔 노력해야하는 방법이다.

세번째는 gradle의 implementation,api의 활용이다.
gradle 이 업데이트되면서 기존 compile 이 deprecated 되고 두 가지 방식을 제공하도록 바뀌었다.

이 두 가지는 매우 유용합니다. api 는 기존의 compile 과 거의 같다고 보면 됩니다.
주목해야할 것은 implementation 이다.
implementation 는 하위 의존 관계를 숨겨준다. 숨겨준다는 것은 즉 하위 의존에 대한 접근을 제한한다는 것 입니다.

A Module

public class A 

B Module

api project(':A')

C Module

implementation project(':B')
public class C {
  public void act() {
    new A() <-- compile error
  }
}

implementation 은 하위 의존을 숨겨주므로, 우리는 보호받아야할 하위 모듈에 대한 접근을 폐쇄할 수 있다.

그래서 어플리케이션 모듈 계층 에서는 implementation 을 사용하고 그 외 계층에서는 api 방식을 사용하여 개발을 하자.

4. README

다양한 Module 을 갖게 될 때 Naming 만으로는 모듈의 의도를 파악하기 힘들다. 모든 ModuleREADME.md 를 반드시 작성하자.

모든 모듈에

  • 역할과 책임
  • 실행방법(사용방법)
  • 관례

등을 작성하고 있다.

자동화된 매커니즘만으로 제공될 수 없는 위의 이야기를 이 프로젝트를 개발하게 될 모든 개발자를 위해 친절하게 꼭 작성하자. README를 통해 더 원활한 개발환경을 만들 수 있다.

5. 확장 및 사용자 정의 여지 개방

하위 모듈에서 정의한 Bean 을 사용하는 모듈에서 확장하거나 변경하고 싶을 때가 있다. 그렇기 때문에 하위 모듈을 개발할 때도 필요한 설정에 한하여 여지를 개방해둘 수 있다.

첫번째 방법은 @ConditionalOnMissingBean 이다.
Spring Framework 에서도 이 방식으로 많은 여지를 개방해주고 있다. @ConditionalOnMissingBeanBean 이 없을 때 발동한다.

이 어노테이션을 하위 모듈에서 설정해놓음으로서 상위 모듈에서 대체할 수 있는 여지를 줄 수 있다.

하위 모듈

@Conficuration
public class Config {
  @Bean
  @ConditionalOnMissingBean
  public A a() {
    return new A("하위"); //로드되지 않음
  }
}

상위 모듈

@Conficuration
public class Config {
  @Bean
  public A a() {
    return new A("상위"); //로드
  }
}

두번째 방법은 Customizer 방식이다.

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(MapperFeature.DEFAULT_VIEW_INCLUSION);
}
Bean 으로 설정된 Customizer를 Configuration class가 주입받아 생성 과정에 관여를 합니다.

@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
    return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.featuresToEnable(MapperFeature.DEFAULT_VIEW_INCLUSION);
}

Bean 으로 설정된 Customizer를 Configuration class가 주입받아 생성 과정에 관여를 하는 방법이다.

@Bean
@ConditionalOnMissingBean
public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(
        List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
    Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
    builder.applicationContext(this.applicationContext);
    customize(builder, customizers);
    return builder;
}

private void customize(Jackson2ObjectMapperBuilder builder,
        List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
    for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
        customizer.customize(builder);
    }
}

위와 같은 방법으로 필요한 곳에 한하여 확장과 변경에 대한 여지까지 제공하면서 더 유연한 모듈이 될 수 있다.

0개의 댓글