멀티모듈 환경에서 공통 모듈을 설계하는 방법: 유스케이스 분리와 인프라 설정 제어 with Spring Boot

2026. 6. 18. 15:49·CS/Architecture

도메인 로직과 유스케이스

핵심 비즈니스 로직

  • 현실 세계의 비즈니스 규칙 그 자체

유스케이스

  • 사용자가 시스템을 통해 어떤 목적을 달성하는 흐름

예시

도메인 로직:
- 게시글 제목은 비어 있을 수 없다.
- 삭제된 게시글에는 댓글을 달 수 없다.
- 작성자만 게시글을 수정할 수 있다.

유스케이스:
- 게시글 작성하기
- 게시글 수정하기
- 게시글 삭제하기
- 게시글 목록 조회하기

즉, 유스케이스는 도메인 로직을 사용해서 하나의 기능 흐름을 완성하는 계층이다.

유스케이스는 공통모듈에 담지 않는 것이 좋다

  • 유스케이스를 공통 코드로 뺀다면, 사이드 이펙트가 발생하기 쉬울 뿐만 아니라 예외 케이스를 위한 분기 처리도 들어가게 되면서 많은 문제가 발생한다
  • 또한 유스케이스는 서비스마다 요구 사항이 다르기 쉽기 때문이다

예를 들어 게시글 작성 이라는 이름은 같아도,

board-service:
- 게시글 저장
- 작성자 검증
- 카테고리 검증

community-service:
- 게시글 저장
- 알림 발송
- 포인트 지급
- 신고 제한 검증

이렇게 흐름이 달라질 수 있다

이걸 공통 모듈에 넣으면 결국 아래와 같은 코드가 생기기 쉽게 된다

if (serviceType==BOARD) {
    ...
}else if (serviceType==COMMUNITY) {
    ...
}

그러면 공통 모듈이 오히려 더러워지고, 한 서비스 수정이 다른 서비스에 영향을 주는 사이드 이펙트가 생김.

그래서 정리하면:

공통 모듈에 넣어도 되는 것:
- 공통 예외
- 공통 응답 형식
- 공통 유틸
- 공통 인증 객체
- 공통 타입
- 정말 안정적인 shared kernel

공통 모듈에 넣으면 위험한 것:
- 유스케이스
- 서비스별 비즈니스 흐름
- 특정 서비스의 Repository
- 특정 서비스의 Entity
- 특정 서비스의 Application Service

자동 빈 등록 방식을 피해야한다

공통 인프라 설정 모듈에서는 컴포넌트 스캔에 의존한 자동 등록을 피하고, 각 서비스가 필요한 설정만 명시적으로 선택하도록 하는 것이 좋다.

주의점

모든 @Component, @Service, @Repository를 쓰지 말라는 뜻이 아니다

  • 일반적인 서비스 내부에서는 여전히 이렇게 써도 됨
@Service
publicclassCreateBoardUseCase {
    ...
}
@Repository
publicinterfaceBoardRepositoryextendsJpaRepository<Board,Long> {
}

다만 문제는 공통 모듈에 있는 인프라 설정 클래스

  • 컴포넌트 스캔 기반의 자동 등록 방식을 사용해서는 인프라 구성을 제어할 수 없다.
  • 또한 해당 방식은 어떤 인프라 구성들이 존재하는지, 무엇이 현재 모듈에 적용되는지 명시적으로 파악하기가 어렵다

예를 들어 공통 모듈에 이런 설정이 있다고 해보자

@Configuration
@EnableJpaRepositories(...)
public class JpaConfig {
}

이게 컴포넌트 스캔으로 자동 등록된다면, 어떤 서비스에서는 JPA가 필요없는데도 JPA 설정이 들어올 수 있다.

또는 어떤 서비스는 Redis만 필요한데, JPA, Async, Jasypt까지 딸려올 수도 있다

따라서 우리는 인프라 구성에 대한 제어권을 획득할 수 있는 방법을 사용하는 것이 좋은데, 이 때 Spring 프레임워크에서 제공하는 ImportSelector를 이용하여 이를 구현해보도록 하자


인터페이스를 구성하자

각 서비스가 필요한 인프라 설정만 선택해서 사용한다.

먼저, 인프라 구성 타입을 추상화한 인터페이스를 생성해야한다.

해당 인터페이스를 구현한 클래스는 다른 모듈에서 적용될 수 있음을 나타낸다

public interface BoardInfraConfig {
}

모든 인프라 구성들은 해당 인터페이스를 구현한다. 구성 클래스들에는 자동 빈 스캔 방식에 의해 등록되지 않도록 @Component 를 포함한 하위 어노테이션들이 존재하지 않는다.

설정 클래스의 의미 붙이기

@Configuration(proxyBeanMethods = false)
@EnableAsync
public class AsyncConfig implements BoardInfraConfig {

    ...

}

@Configuration(proxyBeanMethods = false)
@EnableTransactionManagement
@EntityScan("com.mangkyu.appstore")
@EnableJpaRepositories("com.mangkyu.appstore")
public class JpaConfig implements BoardInfraConfig {

    ...

}

@Configuration(proxyBeanMethods = false) 를 붙이는 게 의도를 명확히 드러내기에 안전하다

하지만 이렇게 하면 @Configuration도 결국 내부적으로는 @Component 계열이라서 컴포넌트 스캔 대상이 될 수 있다.

그래서 자동 등록을 피하려면 아래와 같다

1. 해당 설정 클래스를 컴포넌트 스캔 범위 밖에 두거나
2. 메인 애플리케이션의 scanBasePackages에 포함되지 않게 하거나
3. @Import / ImportSelector로만 가져오도록 구조를 잡아야 함

즉 ,@Configuration 을 붙이면 무조건 문제가 아니라, 스캔으로 자동 등록되지 않게 패키지 구조를 잘 잡는 게 중요하다


이제 구성 클래스들을 모아둔 enum 클래스를 하나 생성한다.

이를 통해 인프라 구성들이 존재하는지, 무엇을 적용할지 손쉽게 파악할 수 있다.

@Getter
@RequiredArgsConstructor
public enum BoardConfig{

	ASYNC(AsyncConfig.class),
	JASYPT(JasyptConfig.class),
	JPA(JpaConfig.class),
	;

	private final Class<? extends BoardConfig> configClass;
	
}

여기까지 했다면 적용할 인프라 구성을 명시할 @Enable 어노테이션과 어노테이션에 존재하는 인프라 구성을 적용시켜 줄 ImportSelector를 구현한다. ImportSelector는 애플리케이션이 실행되는 시점에 해당 어노테이션에 붙은 Enum 값들을 기반으로 동적으로 구성 파일들을 불러온다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(BoardConfigImportSelector.class)
public @interface EnableBoardConfig {

    BoardConfigGroup[] value();

}

ImportSelector를 구현할 때는 ImportSelector의 하위 인터페이스인 DeferredImportSelector를 사용하면 된다.

DeferredImportSelector는 모든 @Configuration 빈들이 처리된 후에 동작하는 ImportSelector이다. 선택된 설정들이 @Conditional 조건을 갖고 있는 경에 유용하다.

이를 처리하기 위해 EnableBoardConfig의 value에 할당된 Enum 값들을 모두 불러온 후에 configClass 값을 가져와야한다. 이러한 역할을 담당하는 ImportSelector 구현체는 다음과 같이 만들어줄 수 있다.

public class BoardConfigImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        return Arrays.stream(getValues(metadata))
                .map(value -> value.getConfigClass().getName())
                .toArray(String[]::new);
    }

    private BoardConfigGroup[] getValues(AnnotationMetadata metadata) {
        Map<String, Object> attributes =
                metadata.getAnnotationAttributes(EnableBoardConfig.class.getName());

        if (attributes == null) {
            return new BoardConfigGroup[]{};
        }

        return (BoardConfigGroup[]) attributes.getOrDefault(
                "value",
                new BoardConfigGroup[]{}
        );
    }
}

그러면 이제 다음과 같이 개별 모듈들에서 적용할 인프라 구성을 동적으로 그리고 명시적으로 선택할 수 있다. 이를 통해 인프라 구성에 대한 제어권을 획득할 수 있게 된 것이다.

@Configuration(proxyBeanMethods = false)
@EnableAppStoreConfig({
    BoardConfigGroup.ASYNC,
    BoardConfigGroup.JASYPT,
    BoardConfigGroup.JPA,
})

class InfraConfig {

}

하지만 이렇게 하더라도 엄격하게 공통 모듈을 관리해야할 수도 있다 아래 링크에 자세히 나와있다

https://mangkyu.tistory.com/304


결론

  • 공통 모듈은 최대한 작고 안정적으로 유지한다.
  • 유스케이스는 각 서비스 내부에 둔다.
  • 인프라 설정은 필요한 서비스가 명시적으로 선택하게 한다.
  • 컴포넌트 스캔으로 의도치 않은 Bean이 등록되지 않게 한다.

📚참고자료

https://mangkyu.tistory.com/304

'CS > Architecture' 카테고리의 다른 글

DDD(Domain-Driven Design)란? 핵심 개념과 계층형 아키텍처  (0) 2026.07.11
객체 간의 결합도(Coupling)와 응집도(Cohesion)의 관계  (0) 2026.07.10
[TIL] 객체 지향 프로그래밍 (OOP) 핵심 개념 총정리  (1) 2025.10.15
'CS/Architecture' 카테고리의 다른 글
  • DDD(Domain-Driven Design)란? 핵심 개념과 계층형 아키텍처
  • 객체 간의 결합도(Coupling)와 응집도(Cohesion)의 관계
  • [TIL] 객체 지향 프로그래밍 (OOP) 핵심 개념 총정리
dev_Hyeonjoon
dev_Hyeonjoon
스프링이랑 자바 공부 중 입니다!!
  • dev_Hyeonjoon
    Hyeonjoon's code
    dev_Hyeonjoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (61) N
      • CS (6)
        • Architecture (4)
        • Algorithm (2)
      • Front-end (10)
        • JavaScript (0)
        • React-project (5)
        • Vanilla[JS]-project (5)
        • study (0)
      • Tools (1)
        • GitHub (1)
        • AI (0)
      • Back-end (35) N
        • Java (27)
        • Architecture (2) N
        • JPA (0)
        • Trouble Shooting (1)
        • Spring (2)
        • DevOps (2)
      • Trouble Shooting (2)
        • Python (1)
      • 자격증 (1)
      • 알고리즘 (0)
      • 회고 (6)
        • 기타 (6)
        • 독서록 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Java
    토이프로젝트
    리액트
    자바공부
    OOP
    자바
    자바기초
    backend
    아키텍처
    객체지향
    백엔드
    군대에서코딩
    파이썬
    Til
    사지방코딩
    vanillajs
    코딩
    바닐라js
    형변환
    Spring
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
dev_Hyeonjoon
멀티모듈 환경에서 공통 모듈을 설계하는 방법: 유스케이스 분리와 인프라 설정 제어 with Spring Boot
상단으로

티스토리툴바