본문 바로가기

사용자 정의 스타터

게시판 관측 라이브러리를 핵심·자동 구성·스타터로 분리하고 사용자 빈 우선 규칙을 적용합니다.

자체 스타터의 목표는 의존성 몇 개를 모아 두는 데서 끝나지 않습니다. 사용자는 스타터 하나와 속성만 추가해 안전한 기본값을 얻고, 필요하면 자신의 빈으로 바꾸며, 선택적인 통합 라이브러리가 없어도 클래스 로딩 실패 없이 시작할 수 있어야 합니다.

이를 위해 핵심 API, 자동 구성, 의존성을 모으는 스타터를 별도의 모듈로 나누면, 각 모듈이 맡는 일과 버전 호환성이 분명해집니다. 이 글에서는 게시판 관측 라이브러리를 이 세 모듈로 나눠 보고, 사용자 빈을 우선하는 규칙과 배포 산출물의 검증 방법을 살펴봅니다.

CUSTOM STARTER · MODULE DEPENDENCIES

스타터는 core와 자동 구성을 모으고 소비자가 사용한다

화살표는 모듈 의존 방향입니다. core는 공개 계약, auto-config는 Spring 연결, starter는 의존성 집계이며 배포된 소비자는 이 조합을 검증합니다.

소비자가 들여오는 세 모듈의 의존성화살표는 모듈의 의존성 선언이다. 소비자→Starter, Starter→Auto-config, Starter→Core, Auto-config→Core이며 Core에서 Spring 구성으로 돌아가는 의존성은 없다. AutoConfiguration.imports는 자동 구성 산출물에 넣는다. 실제 런타임 실행 순서나 자동 AOP 계측을 뜻하지 않는다. 모바일도 같은 4노드4관계이며 원 SVG는 숨김 지원으로 보존한다.소비자 프로젝트스타터 의존성Starter의존성 집계Auto-configSpring 연결Core공개 API
소비자가 들여오는 세 모듈의 의존성화살표는 모듈의 의존성 선언이다. 소비자→Starter, Starter→Auto-config, Starter→Core, Auto-config→Core이며 Core에서 Spring 구성으로 돌아가는 의존성은 없다. AutoConfiguration.imports는 자동 구성 산출물에 넣는다. 실제 런타임 실행 순서나 자동 AOP 계측을 뜻하지 않는다. 모바일도 같은 4노드4관계이며 원 SVG는 숨김 지원으로 보존한다.소비자 프로젝트스타터 의존성Starter의존성 집계Auto-configSpring 연결Core공개 API
starter는 auto config와 core에 의존하고 auto config도 core에 의존한다왼쪽 starter에서 가운데 auto config 및 오른쪽 core로 의존한다. auto config는 core의 API를 사용한다. core는 Spring auto config에 의존하지 않는다. 소비자는 배포된 starter 좌표를 사용해 검증한다.Starter의존성 집계Auto-config조건부 Spring 연결Core API프레임워크 없는 계약소비자 → 배포된 Starter 좌표 · 선택 통합은 별도 모듈
  1. Starter → Auto-config + Core

    스타터는 핵심과 자동 구성 의존성을 모읍니다. 자동 구성도 core API를 사용하며 core가 Spring 연결에 의존하지 않습니다.

  2. 소비자 → 배포된 Starter

    소스 프로젝트 의존성뿐 아니라 배포 POM/모듈 메타데이터와 자동 구성 imports를 실제 소비자에서 확인합니다.

빈마다 독립적으로 양보

사용자 수집 대상만 교체해도 기본 시계는 유지하도록 각 빈에 조건을 둡니다. 선택 통합의 클래스 부재가 core 시작을 깨뜨리지 않아야 합니다.

예제와 배포 계약의 차이

원문 예제에는 속성 레코드 검증과 선택 Micrometer 통합이 아직 연결되지 않았습니다. 기본값·비활성·재정의·클래스 부재를 소비자 계약으로 검증합니다.

AutoConfiguration.imports는 자동 구성 JAR에 둡니다. 메타데이터·공개 호환성·SBOM·서명·체크섬과 실제 소비자 컴파일/패키징을 함께 확인합니다.

그림의 화살표는 모듈 사이의 의존 방향입니다. 소비자 프로젝트는 스타터에 의존하고, 스타터는 자동 구성과 핵심 모듈에 의존하며, 자동 구성도 핵심 API를 씁니다. 핵심 모듈에서 Spring 구성으로 돌아가는 의존은 없습니다. 아래 칸들은 AutoConfiguration.imports를 자동 구성 JAR에 두어야 한다는 점, 빈마다 따로 사용자 빈에 양보하게 해야 한다는 점, 그리고 이 예제에 아직 속성 검증과 선택적 Micrometer 통합이 연결되지 않았다는 점을 정리합니다. 화살표가 런타임의 실행 순서나 자동 AOP 계측을 뜻하지는 않습니다.


핵심 모듈의 공개 규칙

관측 이벤트, 수집 대상 인터페이스, 마스킹 정책 같은 라이브러리의 핵심 요소는 프레임워크 애노테이션 없이 표현할 수 있습니다. 자동 구성 모듈은 이 API의 기본 구현과 Spring 연결을 제공하고, 사용자는 내부 구성 클래스가 아니라 이 핵심 타입을 주입받습니다.

src/main/java/board/observe/BoardObservationSink.java
package board.observe;

import java.time.Instant;

public interface BoardObservationSink {
    void record(Event event);

    record Event(
            String operation,
            String outcome,
            long elapsedNanos,
            Instant occurredAt
    ) {
        public Event {
            if (operation.isBlank() || outcome.isBlank() || elapsedNanos < 0) {
                throw new IllegalArgumentException("invalid observation event");
            }
        }
    }
}

핵심 모듈이 SLF4J나 Micrometer의 타입을 공개 시그니처에 드러내면, 사용자에게 걸리는 버전 제약이 커집니다. 실제 통합을 별도의 선택적 모듈로 제공할지 결정합니다.


자동 구성 모듈

자동 구성은 속성이 활성화되어 있고 사용자가 수집 대상을 따로 등록하지 않았을 때, 콘솔이나 레지스트리 수집 대상을 만듭니다. 선택적인 Micrometer 클래스는 중첩 구성으로 격리합니다. 구성 속성 레코드는 사용자가 IDE의 메타데이터 지원과 검증을 받을 수 있는 공개 설정 API입니다.

src/main/java/board/observe/autoconfigure/BoardObserveAutoConfiguration.java
package board.observe.autoconfigure;

import java.time.Clock;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;

import board.observe.BoardObservationSink;

@AutoConfiguration
@ConditionalOnProperty(
        prefix = "board.observation",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true)
public class BoardObserveAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    Clock observationClock() {
        return Clock.systemUTC();
    }

    @Bean
    @ConditionalOnMissingBean(BoardObservationSink.class)
    BoardObservationSink observationSink() {
        return event -> System.out.printf(
                "operation=%s outcome=%s%n",
                event.operation(), event.outcome());
    }
}

이 구성은 matchIfMissing = true이므로 속성을 적지 않으면 켜집니다. 기본값으로 켜 두어도 안전한지 검토하고, 외부 네트워크로 전송하는 수집 대상이라면 matchIfMissing = false가 더 알맞습니다. 위 콘솔 기본값은 operation과 outcome을 그대로 출력하므로, 운영용 수집 대상에는 허용된 작업 코드와 결과만 넘기고 마스킹과 출력량 제한을 더해야 합니다.

record(Event)를 호출하는 쪽도 따로 있어야 합니다. 이 코드는 메서드 호출을 자동으로 가로채거나 소요 시간을 재는 AOP를 등록하지 않으므로, 시간, 작업, 결과를 채운 이벤트를 누가 넘길지는 소비자 설계에서 연결합니다.


스타터 모듈

스타터 산출물은 핵심 모듈과 자동 구성 모듈을 가져오며, 컴파일할 코드가 거의 없을 수 있습니다. 선택적인 외부 연동은 기본으로 끌어오지 않고 별도의 스타터로 나눕니다. 웹 애플리케이션이 아닌 사용자도 핵심 관측 기능을 쓸 수 있도록, 웹 스타터에 억지로 묶지 않습니다.

AutoConfiguration.imports 메타데이터는 자동 구성 JAR에 두고, 빌드 과정에서 그 항목을 읽을 수 있는지 확인합니다. 스타터 JAR이 아니라 자동 구성 JAR에 두면, 스타터 없이 자동 구성 모듈에 직접 의존한 사용자에게도 같은 방식으로 동작합니다.

구성 처리기(configuration processor)로 속성 메타데이터를 생성하되, 처리기 자체는 런타임 산출물에 넣지 않습니다. 배포된 POM과 Gradle 모듈 메타데이터가 선택적 의존성의 의미를 제대로 전달하는지도 확인합니다.

더 깊이: 모듈 이름과 속성 키에도 지켜야 할 이름 공간이 있습니다

Spring Boot 문서의 스타터 만들기는 이름에 대한 규칙을 따로 둡니다. 다른 Maven groupId를 쓰더라도 모듈 이름을 spring-boot로 시작하지 말라고 하며, 그 이유로 Spring Boot가 나중에 그 기술을 공식으로 지원할 수 있다는 점을 듭니다. 예를 들어 "acme"라면 자동 구성 모듈은 acme-spring-boot, 스타터는 acme-spring-boot-starter, 테스트용 스타터는 acme-spring-boot-starter-test로 짓고, 두 모듈을 하나로 합친다면 스타터 이름을 따릅니다. 이 글의 게시판 관측 라이브러리라면 board-observation-spring-boot-starter 같은 이름이 이 규칙에 맞습니다.

속성 키도 같습니다. 같은 문서는 스타터가 제공하는 키에 고유한 이름 공간을 쓰고, 특히 server, management, spring처럼 Spring Boot가 쓰는 이름 공간에 키를 넣지 말라고 합니다. 같은 이름 공간을 쓰면 Boot가 나중에 그 공간을 바꿀 때 스타터가 깨질 수 있기 때문입니다. 이 글의 자동 구성이 board.observation을 접두사로 쓰는 것도 이 규칙과 맞습니다. 두 모듈로 나누는 구성이 반드시 필요한 것은 아니어서, 문서는 선택적 기능이 없는 단순한 자동 구성이라면 스타터 하나로 합쳐도 된다고 안내합니다.


사용자 재정의와 비활성화

기본값, 비활성화 속성, 사용자 시계, 사용자 수집 대상, 선택적 클래스의 부재, 잘못된 속성을 각각 작은 컨텍스트에서 실행합니다. 빈 개수뿐 아니라 사용자 빈의 동일성과 실패 메시지도 확인합니다.

starter consumer branch 결과
default -> Clock 1, Sink 1
enabled=false -> Sink 0
custom Sink -> custom 1, default 0
optional registry missing -> core Sink still starts
invalid configuration -> startup failure with property path

이 표는 소비자 테스트에서 확인할 기대 결과입니다. 위 자동 구성 코드에는 속성 레코드 검증이나 선택적 Micrometer 통합이 아직 연결되어 있지 않으므로, 아래 두 줄은 그것을 구현한 뒤에 확인할 항목입니다. 전체 예제 애플리케이션은 저장소에 배포된 산출물을 받아 컴파일하고 패키징합니다. 다중 프로젝트의 소스 의존성만으로 시험하면 배포 메타데이터의 오류를 놓칩니다.


공개 호환성 정책

핵심 인터페이스에 메서드를 추가하면 사용자 구현이 깨질 수 있습니다. 기본 메서드를 추가할지, 새 하위 인터페이스를 도입할지, 주 버전을 올릴지 가운데 알맞은 방법을 고릅니다. 속성 이름을 바꿀 때는 사용 중단 예정 메타데이터와 이전 기간을 제공합니다.

자동 구성 내부의 빈 이름과 조건의 배치는 공개 규칙으로 약속하지 않습니다. 대신 사용자가 재정의하는 타입, 속성 키, 기본값의 동작, 필요한 자바와 Boot 버전을 문서로 남깁니다. Boot의 주 버전마다 별도의 스타터가 필요한지 호환성 표를 유지하고, 무리한 리플렉션으로 서로 호환되지 않는 여러 API를 한 바이너리에서 지원하려 하지 않습니다. 그렇게 하면 시험해야 할 범위가 크게 늘어납니다.


공급망과 배포 산출물 검증

소스와 Javadoc, 라이선스, SBOM, 서명, 체크섬을 함께 발행하고, 의존성 혼동 공격을 막을 수 있도록 그룹 이름의 소유권을 확보합니다. 릴리스 후보를 빈 사용자 프로젝트에 설치해 AutoConfiguration.imports 메타데이터, 속성 메타데이터, 네이티브·AOT 환경에서의 구동을 확인합니다.

스타터가 사용자의 로깅 구현이나 서버를 강제로 바꾸지 않는지 의존성 그래프를 검토합니다. 릴리스마다 의존성 개수와 JAR 크기가 급격히 늘어난다면, 선택적 통합이 핵심 모듈에 섞여 들어갔는지 확인합니다.


운영 진단 정보

정보 엔드포인트나 시작 로그에는 카디널리티가 낮은 라이브러리 버전과 활성화 여부를 기록할 수 있습니다. 속성 전체와 비밀 정보는 노출하지 않습니다. 수집 대상이 실패했을 때 핵심 요청까지 막을지, 실패를 허용하고 계속할지도 명시합니다.


정리

  • 스타터는 핵심 API, 자동 구성, 의존성을 모으는 스타터 모듈로 나누며, 핵심 모듈은 Spring 구성에 의존하지 않습니다.
  • 자동 구성은 빈마다 @ConditionalOnMissingBean을 두어, 사용자가 일부 빈만 바꿔도 나머지 기본값을 쓸 수 있게 합니다.
  • AutoConfiguration.imports는 자동 구성 JAR에 두고, 배포된 산출물을 빈 소비자 프로젝트에서 받아 검증합니다.
  • 공개하는 타입과 속성, 기본값 동작은 호환성 정책으로 관리하고, 내부 빈 이름은 약속하지 않습니다.

연습 문제

1. 관측 스타터의 모듈 설계

Board 관측 스타터의 세 모듈 Gradle 그래프, 공개 패키지, AutoConfiguration.imports 메타데이터, 활성화 기본값, 선택적 Prometheus 통합, 사용자 재정의, 배포 검증 절차를 설계하세요.

해설 보기

사용자가 수집 대상만 직접 구현해도, 자동 구성의 나머지 시계 기본값은 계속 쓸 수 있어야 합니다. 그래서 구성 클래스 전체를 @ConditionalOnMissingBean 하나로 막지 않고, 빈마다 사용자 정의를 우선하는 조건을 둡니다. 라이브러리의 기본 설정은 다음과 같은 레코드로 묶을 수 있습니다.

package board.observe;

public record ObservationDefaults(
        boolean enabled,
        boolean failOpen,
        int maximumOperationLength
) {
    public ObservationDefaults {
        if (maximumOperationLength < 8 || maximumOperationLength > 128) {
            throw new IllegalArgumentException("unsafe operation length");
        }
    }
}

외부로 전송하는 수집기는 크기가 제한된 대기열과 실패 메트릭을 갖춰, 별도의 통합 모듈에서 제공합니다.

2. 비활성화와 사용자 빈

board.observation.enabled=false이고, 별도 구성에서 사용자 Clock 하나와 사용자 BoardObservationSink 하나를 등록했습니다. 위 자동 구성 때문에 추가되는 기본 Clock과 Sink의 수, 남는 사용자 빈의 수를 쓰세요. ObservationDefaults를 failOpen=true로 만들기만 하면 Sink.record의 예외가 저절로 무시되나요?

해설 보기

기본 Clock은 0개, 기본 Sink도 0개이고, 사용자 Clock 1개와 사용자 Sink 1개가 남습니다. 비활성화는 이 자동 구성 전체를 건너뛰는 조건일 뿐, 사용자가 등록한 빈을 지우지 않습니다.

ObservationDefaults는 길이의 범위만 검사하는 레코드이며, failOpen 값을 실제 호출 경로에 연결하는 코드가 없습니다. 그러므로 예외를 무시하는 기능은 생기지 않습니다. 실패를 허용하는 래퍼를 따로 구현하고, 예외와 대기열 포화 상황을 시험해야 합니다.

3. 기본값의 세 가지 분기

사용자 빈이 하나도 없는 프로젝트에서 다음 세 경우에 자동 구성이 만드는 기본 Clock과 기본 Sink의 수를 구하세요. enabled 속성을 적지 않은 경우, 사용자 Sink만 등록하고 속성을 적지 않은 경우, enabled=false로 둔 경우입니다.

해설 보기

속성을 적지 않으면 matchIfMissing = true가 적용되어 기본 Clock과 기본 Sink가 하나씩 생깁니다. 사용자 Sink만 등록하면 Sink 메서드만 양보하므로 기본 Sink는 0개, 기본 Clock은 1개입니다.

enabled=false이면 구성 클래스 전체가 제외되어 두 기본 메서드 모두 실행되지 않습니다. 빈마다 조건을 두었기 때문에 둘째 경우처럼 일부만 바꾸는 것이 가능하며, 구성 전체에 누락 조건을 걸었다면 사용자 Sink 하나 때문에 시계까지 사라졌을 것입니다.

다음 글 외부 설정 우선순위에서는 이런 스타터와 애플리케이션이 읽는 속성 값이 명령줄, 환경 변수, 설정 파일 가운데 어디서 오고 어느 쪽이 이기는지를 다룹니다.