본문으로 건너뛰기

안동민 개발노트

본문 시작

사용자 정의 스타터

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

자체 스타터의 목표는 의존성 몇 개를 모으는 데 그치지 않습니다.

사용자가 스타터 하나와 속성만 추가해 안전한 기본값을 얻고, 자신의 빈으로 교체하며, 선택적 통합이 없을 때 클래스 로딩 실패 없이 시작해야 합니다.

핵심 API, 자동 구성, 의존성 집계 모듈을 분리하면 각 책임과 버전 호환성이 보입니다.

자체 starter를 세 artifact 책임으로 나눈다

Spring 없는 core API 위에 조건 wiring을 놓고 최상단 starter는 dependency graph만 집계한다.

  1. Core

    framework와 무관한 기능

  2. Autoconfigure

    조건과 default bean

  3. Starter

    필요 dependency 정렬

  4. Consumer

    property로 선택·재정의


핵심 모듈 공개 규칙

관찰 이벤트와 수집 대상 인터페이스, 마스킹 정책처럼 라이브러리 본질은 프레임워크 애노테이션 없이 표현할 수 있습니다.

자동 구성 모듈이 이 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=false가 더 적합합니다.

콘솔 기본값은 페이로드 마스킹과 출력량 제한을 갖습니다.

Consumer bean이 default integration을 안전하게 교체한다

public sink type을 ownership 경계로 삼고 internal configuration class와 bean name에는 의존하지 않는다.

  1. Consumer settings

    enabled 결정

  2. User sink

    custom owner

  3. MissingBean

    default 협상

  4. Observation caller

    sink 하나 주입


스타터 모듈

스타터 산출물은 핵심과 자동 구성 모듈을 가져오고 컴파일 코드가 거의 없을 수 있습니다.

선택적 외부 연동은 기본으로 끌어오지 않고 별도 스타터로 나눕니다.

사용자가 웹 애플리케이션이 아니어도 핵심 관찰을 사용할 수 있게 웹 스타터에 강제 결합하지 않습니다.

자동 구성 JAR 파일의 AutoConfiguration.imports 메타데이터에 구성 클래스를 등록하고 빌드 과정에서 해당 항목을 읽을 수 있는지 확인합니다.

스타터 JAR 파일이 아니라 자동 구성 JAR 파일에 메타데이터를 두면 직접 의존성을 추가한 사용자에게도 일관되게 동작합니다.

구성 처리기로 속성 메타데이터를 생성하되 처리기는 런타임 산출물에 포함하지 않습니다.

배포된 POM과 Gradle 모듈 메타데이터가 선택적 의존성 의미를 제대로 전달하는지 확인합니다.


사용자 재정의·비활성화

기본값, 비활성화 속성, 사용자 시계, 사용자 수집 대상, 선택적 클래스 부재, 유효하지 않은 속성을 각각 작은 컨텍스트에서 실행합니다.

빈 개수뿐 아니라 동일성과 실패 메시지를 확인합니다.

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

전체 예제 애플리케이션은 배포된 산출물을 리포지토리에서 받아 컴파일·패키지합니다.

다중 프로젝트 소스 의존성만 쓰면 배포 메타데이터 오류를 놓칩니다.

Release artifact마다 검증해야 할 계약이 다르다

source project에서 동작하는 것과 published metadata로 빈 consumer가 시작하는 것을 구별한다.

Artifact공개 계약Release 검증
corebinary APIconsumer compile
autoconfigureimports·propertiescontext branches
starterdependency graphpublished POM smoke

공개 호환성 정책

핵심 인터페이스 메서드 추가는 사용자 구현을 깨뜨릴 수 있습니다.

기본 메서드 추가, 새 하위 인터페이스 도입, 주 버전 상향 가운데 적합한 방법을 선택합니다.

속성 이름 변경은 사용 중단 예정 메타데이터와 마이그레이션 기간을 제공합니다.

자동 구성 내부 빈 이름과 조건 배치는 공개 규칙으로 약속하지 않습니다.

사용자가 재정의하는 타입, 속성 키, 기본값 동작, 필수 Java·Boot 버전을 문서화합니다.

Boot 주 버전별로 별도 스타터가 필요한지 호환성 행렬을 유지합니다.

무리한 리플렉션으로 여러 비호환 API를 한 바이너리에서 지원하면 테스트 표면이 커집니다.


공급망·배포 산출물 검증

소스·Javadoc, 라이선스, SBOM, 시그니처, 체크섬을 발행하고 의존성 혼동 공격을 막는 그룹 소유권을 사용합니다.

릴리스 후보를 빈 사용자 프로젝트에 설치해 AutoConfiguration.imports 메타데이터, 속성 메타데이터, 네이티브/AOT 구동 여부를 확인합니다.

스타터가 사용자 로깅 공급자나 서버를 강제로 바꾸지 않게 그래프를 검토합니다.

의존성 개수와 JAR 크기가 릴리스마다 급증하면 선택적 통합이 핵심 모듈에 스며들었는지 봅니다.

Starter release는 실제 consumer까지 왕복해야 끝난다

local multi-project dependency가 가린 metadata·optional scope 오류는 publication 뒤 깨끗한 프로젝트에서 드러난다.

  1. Publish candidate

    POM과 module metadata를 실제 repository에 게시한다.

  2. Repository 경계

    컬 project dependency 대신 배포된 좌표와 transitive dependency를 해석한다.

  3. Empty consumer

    빈 프로젝트에 starter 하나만 추가해 숨은 의존을 제거한다.

  4. Run branches

    default disable override 자동 설정의 기본·비활성·사용자 대체 경로를 모두 실행한다.

  5. Supply chain

    SBOM과 signature가 게시 artifact를 정확히 설명하는지 확인한다.


운영 진단 정보

정보 엔드포인트나 시작 로그에 낮은 카디널리티 라이브러리 버전과 활성화 여부를 기록할 수 있습니다.

속성 전체와 비밀 정보는 노출하지 않습니다.

수집 대상 실패가 핵심 요청을 막는지 실패 허용 정책을 명시합니다.


연습 문제

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");
        }
    }
}

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