사용자 정의 스타터
게시판 관측 라이브러리를 핵심·자동 구성·스타터로 분리하고 사용자 빈 우선 규칙을 적용합니다.
자체 스타터의 목표는 의존성 몇 개를 모으는 데 그치지 않습니다.
사용자가 스타터 하나와 속성만 추가해 안전한 기본값을 얻고, 자신의 빈으로 교체하며, 선택적 통합이 없을 때 클래스 로딩 실패 없이 시작해야 합니다.
핵심 API, 자동 구성, 의존성 집계 모듈을 분리하면 각 책임과 버전 호환성이 보입니다.
CUSTOM STARTER · MODULE DEPENDENCIES
스타터는 core와 자동 구성을 모으고 소비자가 사용한다
화살표는 모듈 의존 방향입니다. core는 공개 계약, auto-config는 Spring 연결, starter는 의존성 집계이며 배포된 소비자는 이 조합을 검증합니다.
- Starter → Auto-config + Core
스타터는 핵심과 자동 구성 의존성을 모읍니다. 자동 구성도 core API를 사용하며 core가 Spring 연결에 의존하지 않습니다.
- 소비자 → 배포된 Starter
소스 프로젝트 의존성뿐 아니라 배포 POM/모듈 메타데이터와 자동 구성 imports를 실제 소비자에서 확인합니다.
빈마다 독립적으로 양보
사용자 수집 대상만 교체해도 기본 시계는 유지하도록 각 빈에 조건을 둡니다. 선택 통합의 클래스 부재가 core 시작을 깨뜨리지 않아야 합니다.
예제와 배포 계약의 차이
원문 예제에는 속성 레코드 검증과 선택 Micrometer 통합이 아직 연결되지 않았습니다. 기본값·비활성·재정의·클래스 부재를 소비자 계약으로 검증합니다.
AutoConfiguration.imports는 자동 구성 JAR에 둡니다. 메타데이터·공개 호환성·SBOM·서명·체크섬과 실제 소비자 컴파일/패키징을 함께 확인합니다.
핵심 모듈 공개 규칙
관찰 이벤트와 수집 대상 인터페이스, 마스킹 정책 같은 라이브러리의 핵심 요소는 프레임워크 애노테이션 없이 표현할 수 있습니다.
자동 구성 모듈이 이 API의 기본값 구현과 Spring 연결을 제공합니다.
사용자는 내부 구성 클래스가 아니라 핵심 규칙을 주입받습니다.
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입니다.
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가 더 적합합니다.
위 콘솔 기본값은 operation과 outcome을 그대로 출력합니다. 운영용 수집 대상에는 허용된 작업 코드·결과만 전달하고 필요한 마스킹과 출력량 제한을 추가해야 합니다.
예시 사용자 빈이 없고 enabled 속성을 생략하면 matchIfMissing=true가 적용되어 이 구성의 기본 Clock과 Sink가 각각 한 개 생길 조건입니다. 사용자 Sink만 넣으면 Sink 메서드만 양보하고 Clock 기본값은 남습니다. enabled=false로 구성 전체를 제외하면 두 기본 메서드 모두 실행되지 않습니다. 사용자가 별도로 등록한 Sink를 삭제한다는 뜻은 아닙니다.
record(Event)를 호출할 주체도 별도로 필요합니다. 위 코드는 메서드 호출을 자동으로 가로채거나 소요 시간을 측정하는 AOP를 등록하지 않습니다. 시간·작업·결과를 채운 이벤트를 누가 전달할지 소비자 설계에서 연결하세요.
스타터 모듈
스타터 산출물은 핵심과 자동 구성 모듈을 가져오고 컴파일 코드가 거의 없을 수 있습니다.
선택적 외부 연동은 기본으로 끌어오지 않고 별도 스타터로 나눕니다.
사용자가 웹 애플리케이션이 아니어도 핵심 관찰을 사용할 수 있게 웹 스타터에 강제 결합하지 않습니다.
자동 구성 JAR 파일의 AutoConfiguration.imports 메타데이터에 구성 클래스를 등록하고 빌드 과정에서 해당 항목을 읽을 수 있는지 확인합니다.
스타터 JAR 파일이 아니라 자동 구성 JAR 파일에 메타데이터를 두면 직접 의존성을 추가한 사용자에게도 일관되게 동작합니다.
구성 처리기로 속성 메타데이터를 생성하되 처리기는 런타임 산출물에 포함하지 않습니다.
배포된 POM과 Gradle 모듈 메타데이터가 선택적 의존성 의미를 제대로 전달하는지 확인합니다.
사용자 재정의·비활성화
기본값, 비활성화 속성, 사용자 시계, 사용자 수집 대상, 선택적 클래스 부재, 유효하지 않은 속성을 각각 작은 컨텍스트에서 실행합니다.
빈 개수뿐 아니라 동일성과 실패 메시지를 확인합니다.
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 통합이 아직 연결되어 있지 않습니다. 전체 예제 애플리케이션은 배포된 산출물을 리포지토리에서 받아 컴파일·패키지합니다.
다중 프로젝트 소스 의존성만 쓰면 배포 메타데이터 오류를 놓칩니다.
공개 호환성 정책
핵심 인터페이스 메서드 추가는 사용자 구현을 깨뜨릴 수 있습니다.
기본 메서드 추가, 새 하위 인터페이스 도입, 주 버전 상향 가운데 적합한 방법을 선택합니다.
속성 이름 변경은 사용 중단 예정 메타데이터와 마이그레이션 기간을 제공합니다.
자동 구성 내부 빈 이름과 조건 배치는 공개 규칙으로 약속하지 않습니다.
사용자가 재정의하는 타입, 속성 키, 기본값 동작, 필수 Java·Boot 버전을 문서화합니다.
Boot 주 버전별로 별도 스타터가 필요한지 호환성 행렬을 유지합니다.
무리한 리플렉션으로 여러 비호환 API를 한 바이너리에서 지원하면 테스트 표면이 커집니다.
공급망·배포 산출물 검증
소스·Javadoc, 라이선스, SBOM, 시그니처, 체크섬을 발행하고 의존성 혼동 공격을 막는 그룹 소유권을 사용합니다.
릴리스 후보를 빈 사용자 프로젝트에 설치해 AutoConfiguration.imports 메타데이터, 속성 메타데이터, 네이티브/AOT 구동 여부를 확인합니다.
스타터가 사용자 로깅 공급자나 서버를 강제로 바꾸지 않게 그래프를 검토합니다.
의존성 개수와 JAR 크기가 릴리스마다 급증하면 선택적 통합이 핵심 모듈에 스며들었는지 봅니다.
운영 진단 정보
정보 엔드포인트나 시작 로그에 낮은 카디널리티 라이브러리 버전과 활성화 여부를 기록할 수 있습니다.
속성 전체와 비밀 정보는 노출하지 않습니다.
수집 대상 실패가 핵심 요청을 막는지 실패 허용 정책을 명시합니다.
연습 문제
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");
}
}
}외부 전송 수집기는 제한된 대기열과 실패 메트릭을 별도 통합 모듈에서 제공합니다.
독립 확인: enabled=false이고 사용자 Clock 하나·사용자 Sink 하나를 별도 구성에서 등록했습니다. 위 자동 구성 때문에 추가되는 기본 Clock·Sink 수와 남는 사용자 빈 수를 쓰세요. ObservationDefaults의 failOpen=true만 생성하면 Sink.record의 예외가 자동으로 무시되나요?
풀이 확인
기본 Clock 0·기본 Sink 0, 사용자 Clock 1·사용자 Sink 1입니다. 비활성화는 이 자동 구성을 건너뛰는 조건입니다. ObservationDefaults는 길이 범위만 검사하는 레코드이며 failOpen을 실제 호출 경로에 연결하지 않았으므로 예외 무시 기능이 생기지 않습니다. 실패 허용 래퍼와 그 예외·큐 포화 시험을 따로 구현해야 합니다.