본문 바로가기
Trouble Shooting

납득이 가는 멀티모듈 구조 도입기

by saniii 2024. 7. 6.

JDON은 멀티모듈로 구성되어있다. 

 

1. 멀티 모듈을 도입하게 된 이유...

JDON은 원티드에서 스크래핑한 JD를 기반으로 요즘 JD에 많이 언급되는 기술스택을 제공한다. 따라서 스크래핑 로직이 필요했다. 

스크래핑을 구현할 때 고려할 점은 다음과 같았다. 1. 자동? or 수동?   2. API 서버와 분리? or 통합? 

1-1. 자동 ? or  수동 ?

우리의 결론은 '둘 다!' 였다. 

우선 관리자의 편의성을 위해서 주기적으로 스크래핑을 할 수 있도록 구성하고자 했다. 그러나 주기적으로 도는 스크래핑만 구현한다면 우리가 정한 주기에 너무 종속적이고 그 사이에 스크래핑으로 데이터를 업데이트하고 싶어도 할 수 없기 때문에 수동으로 스크래핑할 수 있는 기능 또한 필요하다는 결론을 내렸다. 

1-2. API 서버와 분리 ?  or  통합 ?

결론적으로 많은 이유로 '서버 분리'를 결정했다. 

  1. API 와 스크래핑을 분리하면 각 서버가 독립적으로 동작함으로써 리소스 사용을 최적화할 수 있다. 특히 스크래핑을 할 때 수많은 데이터를 가져와서, 하나하나 가공해서 DB에 비교해서 넣기 때문에 부하가 있을텐데 서버를 분리하면 API에 영향을 주지 않을 수 있다. 
  2. 스크래핑은 특성상 데이터를 가져오는 로직이 자주 변경될 확률이 높다. API에는 영향을 주지 않고 스크래핑에 대한 로직만 업데이트하여 배포하고 싶다. 
  3. 추후에 확장성을 생각했을 때 스프래핑 작업이 증가하면 스크래핑을 위해서 서버를 확장하고, 사용자가 증가하면 API를 위해서 확장할텐데 스크래핑과 API를 분리해서 관리하는 것이 더 효율적인 서버자원 관리가 가능할 것으로 생각된다. 

1-3. 초기에 아주 잠시 API 서버와  Crawler 서버가 머물렀다...

2번의 과정을 통해서 API와 Crawler의 서버를 분리하기로 했다. (우리가 하는 일은 스크래핑이지만 흔히 통틀어 칭하는 크롤러라는 이름이 더 익숙하고, 명시적이라고 생각하여 Crawler라는 이름을 선택하였다.) 하지만 바로 난감한 일이 발생했으니...... 그것은 바로 똑같은 Entity를 가짐에도 API, Crawler에 같은 Entity를 중복해서 사용해야한다는 것이다. 만일 Entity 자체나 Entity가 가진 규칙에 변경이라도 생긴다면, 양쪽 코드에 달려가 같은 수정을 반복해야할테다... 만일 휴먼에러로 하나라도 누락한다..? 것도 모르고 개발이 진행되었다...? 대참사가 발생할지도 모른다. 

 

우리는 그렇다면 Entity Class를 따로 분리하여 API 서버와 Crawler서버가 같은 코드를 사용할 수 있진 않을지 방법을 찾아보게 되었고, 멀티모듈을 알게되었다. 

1-4. 멀티 모듈이란 무엇입니까?

  • 모듈 : 코드와 데이터를 캡슐화하는 단위로, 패키지 한 단계 위의 집합체로써 관련된 패키지와 리소스들에 대해 외부로 공개되는 인터페이스를 정의한다. 
    • 모듈은 독립적으로 개발, 빌드, 배포가 가능하다. 
  • 멀티 모듈 : 의존 관계가 있는 여러 개의 모듈
    • 특징
      1. 모듈 독립성
        • 각 모듈은 독립적으로 개발, 빌드, 테스트
        • 모듈 간의 의존성을 명확하게 정의
      2. 재사용성:
        • 모듈화된 코드는 다른 프로젝트에서도 재사용 -> Entity를 API 모듈과 Crawler 모듈에서 같이 사용할 수 있다. 
        • 공통 기능을 별도의 모듈로 분리하여 여러 애플리케이션에서 사용
      3. 유지보수성:
        • 코드베이스가 작고 집중된 단위로 나뉘기 때문에 유지보수에 용이
        • 각 모듈은 특정 기능이나 책임을 가지므로, 변경 사항의 영향 범위를 최소화
      4. 협업 용이성:
        • 여러 개발자가 동시에 다른 모듈에서 작업할 수 있어 협업이 용이
        • 충돌을 줄이고, 빌드 및 배포 프로세스를 독립적으로 관리

멀티 모듈의 이런 특징을 통해서 멀티 모듈을 도입한다면 우리가 원하던대로 Entity 코드를 따로 분리해두고, 다른 모듈은 이 Entity 모듈의 코드를 가져다쓸 수 있으며, Entity에 발생하는 변경사항 또한 관리하기 용이할 것으로 생각되었다. 그렇다면! 바로 도! 입!

1-5. 그런데.... 어떻게 나누지...?

멀티 모듈을 도입하기 위해서 여러 멀티 모듈 도입기를 읽게 되었는데, 가장 많이 언급되었던 부분이 'Common 이름 아래 뭉쳐진 의존성 덩어리' 라는 부분이었다. 선례를 통해 주의할 점을 알았으니.. 이를 고려해서 멀티 모듈을 잘 구성하고 싶었다. 

우리가 고려한 점은 다음과 같다. 

  1. 각 모듈이 명확한 역할과 책임을 가지도록 하자.  
  2. 쓸데없는 의존성을 가지지 않도록 유의하자. 
  3. Common 모듈에는 공통으로 쓸 것 같은 코드 말고 정말 공통으로 써야하는 코드만 위치시키자.

2. 멀티 모듈 구조_최종,  멀티 모듈 구조_최최종,,,  멀티 모듈 구조_이게찐찐막

제목에서 느껴지듯이 우리는 멀티 모듈 구조를 정말 많이 수정했다. 흠.. 구조라기보다는 어떤 클래스를 어떤 모듈에 위치시킬 것인지를 많이 고민하고, 많이 수정했다. 

모듈은 Domain, Common, API, Crawler, Batch(Crawler)로 두었고 1-5에서 언급한 우리의 약속을 지키기 위해서 Common에 어떤 클래스를 두어야 적절할지 고민했다. 

 

2-1. 초기에는 멀티모듈에 대한 감이 없어서 그냥 우리가 단일 모듈을 만들 때 모든 도메인에 적용되기에 global이라는 패키지로 빼두었던 모든 것을 common에 넣으면 되지 않을까? 라고 놀랍게도 생각했다.

이런 생각은 configuration 파일때문에 곧 깨지게 되었는데, 각 모듈에 대한 설정인 configuration이 Common 모듈에 존재한다는 것이 다행히도 빠르게 느껴졌기 때문이다. 각 모듈에 종속적인 파일이 Common 모듈에 존재한다고 생각되었다.

 

다음으로 Exception 또한 애매한 부분이 있었는데, 각자 다른 모듈이지만 에러의 원인은 같은 경우가 생겼고, Common에 Exception, ErrorCode(ErrorCode는 Domain 별로 나누기 때문에 Domain 모듈도 고려했다.)를 두고 가져다 써야할까? 라는 생각을 했다. 하지만 좀 더 고민을 해봤을 때 ErrorCode 역시 지금, 간단한 기획 아래 시작되었을 때는 모듈이 다름에도 같은 원인의 에러처럼 보일 수 있지만 점차적으로 각 모듈에서 발생하는 ErrorCode가 모듈에 더욱 특성화될 것이며, 사실상 지금도 하나의 case만 원인이 겹칠 뿐 그 외의 다른 에러들은 겹치지 않았다. 

 

다만 ErrorCode를 Interface로 두고 이를 구현하여 같은 양식으로 관리하고 있었기 때문에 공통 양식인 ErrorCode Interface만  Common모듈에 위치시켰다. 

public interface ErrorCode {
    HttpStatus getHttpStatus();

    String getMessage();
}
@RequiredArgsConstructor
public enum AuthErrorCode implements ErrorCode, BaseThrowException<AuthErrorCode.AuthBaseException> {
    UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증되지 않은 사용자입니다."),
    UNAUTHORIZED_NOT_MATCH_PROVIDER_TYPE(HttpStatus.UNAUTHORIZED, "다른 소셜 로그인으로 가입된 이메일입니다."),
    ...;

    private final HttpStatus httpStatus;
    private final String message;

    @Override
    public HttpStatus getHttpStatus() {
        return httpStatus;
    }

    @Override
    public String getMessage() {
        return message;
    }

    @Override
    public AuthErrorCode.AuthBaseException throwException() {
        return new AuthErrorCode.AuthBaseException(this);
    }

    public class AuthBaseException extends ApiException {
        public AuthBaseException(AuthErrorCode authErrorCode) {
            super(authErrorCode);
        }
    }
}

 

2-2. Domain 모듈에 Entity와 JPARepository를 위치시켰더니....

Domain으로 두고 는 EntityJPA 영속클래스이 기 때문관련된 Repository 터페이스까지 께 Domain 모듈에 두게 되었다. JPARepository가 Domain 모듈에 있다보니 자연스럽게 모든 모듈에서 Domain의 JPARepository를 가져다가 쓰고, 필요한 쿼리메서드를 Domain 모듈에 정의하게 되었다. 이는 다음의 문제점을 발생시켰는데....

  • Repository 터페이스에 모모듈이 각자 필요하는 든 쿼메서드가 의되었다. 
    • 나의 모듈에서는 사용하지 않는, 남의 모듈에서만 쓰는 메서드까지 의존하게 됨..

 

 

  • QueryDSL을 도입했을 때, 기존의 약속에 따라 DTO는 해당 DTO를 필요로하는 각 모듈에 존재하므로 @QueryProjection 으로 만들어진 Q타입 객체는 Api, Crawler 모듈에 위치하지만 쿼리메서드는 Domain 모듈에 존재하여 역 의존성이 발생하게 되었다. 

 

 

이를 해결하기 위해서 Domain 모듈의 Repository 인터페이스를 모듈에상속하고 필요메서를
모듈 안에 정의하여 모듈간 독립성을 높이고 역 의존관계 또한 제거했다. 

 

 

3. 그래서 'JDON_multimodule_찐찐막찐최종'은 요...

 

 

  • Module-Common : 각 모듈에서 공통적으로 쓰이는 코드, log, Interface를 관리
  • Module-Domain : Entity와 JPARepository 관리
  • Module-Api : 사용자에게 서비스를 제공하는 API 관리
  • Module-Batch : 주기적으로 스크래핑을 수행
  • Module-Crawler : 수동으로 스크래핑을 수행하기 위한 API 관리

 

3-1.  직접 도입해보고 느낀 멀티 모듈의 장점

  1. 모듈의 역할에 따라 독립적인 배포가 가능해졌다. 
    • 모듈을 분리함에 따라 독립적으로 개발, 배포할 수 있어짐
  2. 역할에 따라 모듈을 나누고 책임 분리하였다. 
    • 모듈별로 독립적인 개발, 배포가 가능해짐
  3. 모듈별 공통으로 사용하는 코드 분리하였다. 
    • 코드의 재사용성 증가

 

 

 

댓글