콘텐츠로 이동

bluetape4k-dependencies 1.3.0 활용기 Part 2: 서비스 경계에 맞는 모듈 조합

작은 로봇 작업자들이 projects, exposed, aws, image, text, leader, graph, javers 모듈 블록과 의존성 BOM 보드를 점검하는 3D 작업대 일러스트
BOM은 시작점입니다. 실제 서비스에서는 데이터베이스, AWS, 리더 선출, 토크나이저, 이미지 처리 경계에 필요한 모듈을 선택해야 합니다.

bluetape4k-dependencies를 적용하면 버전 관리는 단순해집니다. 그러나 서비스 경계에 맞는 모듈은 애플리케이션이 선택해야 합니다.

서비스가 책임지는 경계에는 어떤 모듈이 필요한가?

서비스에 따라 Exposed만 사용하거나 AWS까지 함께 사용할 수 있습니다. 정기 작업을 한 인스턴스에서만 실행해야 한다면 Leader가 필요하고, 사용자 입력을 분석한다면 Text가 필요합니다. 이미지 파일을 처리하는 서비스에는 Image 모듈을 추가합니다.

dependencies 1.3.0을 가져온 뒤에는 실제 서비스에서 어떤 모듈을 함께 고를지가 더 중요합니다. 여기서는 사용 측의 선택에 집중합니다. 핵심은 버전을 선언하는 위치가 아니라 기능 경계에 맞는 모듈을 선택하는 방법입니다.

Gradle에서는 BOM을 platform으로 적용하고, bluetape4k 모듈에는 버전을 선언하지 않습니다.

dependencies {
implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.3.0"))
implementation("io.github.bluetape4k:bluetape4k-spring-boot-core")
implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-spring-boot-jdbc")
implementation("io.github.bluetape4k.aws:bluetape4k-aws-spring-boot")
}

핵심은 1.3.0을 여러 모듈에 반복해서 선언하지 않는다는 점입니다. projects, exposed, aws의 릴리스 번호가 서로 달라도 애플리케이션은 BOM이 검증한 조합을 사용합니다.

application
-> bluetape4k-dependencies 1.3.0
-> projects 1.11.0
-> exposed 1.11.0
-> aws 0.4.0
-> image 0.3.0
-> text 0.2.1
-> leader 0.4.0

개발자가 직접 선택할 대상은 개별 버전이 아니라 기능 경계입니다. 데이터베이스와 AWS 사용 여부, 분산 작업의 단일 실행 요구사항, 입력 텍스트 처리 여부에 따라 모듈을 선택합니다.

하나의 bluetape4k-dependencies BOM 아래에서 Spring Boot 워커, Ktor 검색 API, 이미지 업로드 API가 각 서비스 경계에 맞는 모듈을 선택하는 구조도
세 서비스는 같은 BOM을 사용하지만 데이터베이스 작업, HTTP 입력 검증, 이미지 스트리밍처럼 서로 다른 책임과 자원 수명 주기에 맞춰 모듈을 선택합니다.

첫 번째 예제는 Spring Boot 기반 워커입니다.

요구사항은 다음과 같습니다.

  • DB에 쌓인 작업을 읽는다.
  • S3에 결과 파일을 쓴다.
  • 같은 작업을 여러 인스턴스에서 동시에 실행하지 않는다.
  • 작업 이름이나 입력 텍스트를 토크나이저로 전처리한다.
  • write-behind 캐시의 상태를 상태 점검 엔드포인트에서 확인한다.

이 경우 의존성은 다음과 같이 구성할 수 있습니다.

dependencies {
implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.3.0"))
implementation("io.github.bluetape4k:bluetape4k-spring-boot-core")
implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-spring-boot-jdbc")
implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-cache")
implementation("io.github.bluetape4k.aws:bluetape4k-aws-spring-boot")
implementation("io.github.bluetape4k.leader:bluetape4k-leader-spring-boot")
implementation("io.github.bluetape4k.leader:bluetape4k-leader-redis-lettuce")
implementation("io.github.bluetape4k.leader:bluetape4k-leader-micrometer")
implementation("io.github.bluetape4k.text:tokenizer-korean")
implementation("io.github.bluetape4k.text:text-search")
}

다음 코드는 모듈 조합과 처리 경계를 설명하는 의사코드입니다. 실제 저장소 API로 오해하지 않도록 애플리케이션 고유 이름을 사용했습니다.

@Scheduled(fixedDelayString = "PT30S")
fun runPendingJobs() {
leader.runIfLeader("invoice-worker") {
transaction {
val jobs = jobRepository.findPending(limit = 100)
jobs.forEach { job ->
val tokens = koreanTokenizer.tokenize(job.title)
val result = processor.render(job, tokens)
s3Client.putObject(job.resultKey, result)
jobRepository.markDone(job.id)
}
}
}
}

여러 워커 인스턴스가 같은 행을 동시에 처리하면 중복 실행이 발생합니다. Leader를 사용하면 현재 대표 인스턴스만 작업을 실행한다는 경계를 코드에 드러낼 수 있습니다.

Exposed write-behind 캐시를 사용한다면 상태 점검 응답도 함께 확인합니다. 다음 구조는 exposed 1.11.0 릴리스의 JDBC 상태 표시기가 제공한 reports 배열을 반영합니다.

{
"status": "OUT_OF_SERVICE",
"components": {
"exposedCache": {
"status": "OUT_OF_SERVICE",
"details": {
"repositoryCount": 1,
"reports": [
{
"mode": "WRITE_BEHIND",
"queueDepth": 128,
"flushJobRunning": false,
"lastFlushError": "Redis connection failed"
}
]
}
}
}
}

write-behind 캐시는 실패를 늦게 발견할수록 데이터 일관성 위험이 커집니다. 요청이 성공한 뒤 백그라운드 flush가 중단될 수 있기 때문입니다. 상태 응답에 이 정보가 있으면 운영자는 애플리케이션 증상만으로 캐시 장애를 추정하지 않아도 됩니다.

두 번째 예제는 Ktor API입니다.

Ktor API는 worker와 확인할 경계가 다릅니다.

  • HTTP API가 사용자 요청을 받는다.
  • R2DBC나 JDBC로 데이터를 읽고 쓴다.
  • AWS CloudWatch에 메트릭과 로그를 보낸다.
  • 관리 API나 예약 작업에 리더 선출이 필요하다.
  • 입력 텍스트에 토크나이저나 금칙어 검사를 적용한다.

의존성은 다음처럼 잡을 수 있습니다.

dependencies {
implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.3.0"))
implementation("io.github.bluetape4k:bluetape4k-ktor-core")
implementation("io.github.bluetape4k:bluetape4k-ktor-observability")
implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-ktor")
implementation("io.github.bluetape4k.exposed:bluetape4k-exposed-r2dbc")
implementation("io.github.bluetape4k.aws:bluetape4k-aws-ktor")
implementation("io.github.bluetape4k.leader:bluetape4k-leader-ktor")
implementation("io.github.bluetape4k.leader:bluetape4k-leader-redis-lettuce")
implementation("io.github.bluetape4k.text:tokenizer-korean")
implementation("io.github.bluetape4k.text:text-search")
}

Ktor에서 특히 주의할 부분은 수명 주기입니다. AWS 클라이언트의 생성·종료 책임이 여러 위치에 흩어지면 운영 환경에 커넥션 풀이나 백그라운드 스레드가 남을 수 있습니다.

다음 예제는 aws 0.4.0에 공개된 실제 플러그인 이름과 설정 속성을 사용합니다.

fun Application.module() {
install(CloudWatchKtorPlugin) {
namespace = "billing-api"
}
install(CloudWatchLogsKtorPlugin) {
logGroupName = "/bluetape4k/billing-api"
logStreamName = environment.config.property("deployment.instanceId").getString()
flushInterval = Duration.ofSeconds(5)
shutdownFlushTimeout = Duration.ofSeconds(5)
}
routing {
post("/documents/search") {
val request = call.receive<SearchRequest>()
// 애플리케이션이 소유하는 입력 길이 검증 경계
request.query.requireLength(max = 2_000)
val tokens = koreanTokenizer.tokenize(request.query)
val result = documentService.search(tokens)
call.respond(result)
}
}
}

CloudWatch 연동에는 메트릭 네임스페이스, 로그 그룹·스트림 이름, 클라이언트 수명 주기가 포함됩니다. 이 결정을 플러그인 설정에 모으면 어떤 인스턴스가 어떤 이름으로 데이터를 전송했는지 추적하기 쉬워집니다. 플러그인을 설치하는 것만으로 메트릭이나 로그가 자동 전송되는 것은 아니며, 애플리케이션이 명시적으로 게시하거나 버퍼에 추가해야 합니다.

이미지 파일을 받는 API라면 image 모듈이 들어옵니다.

dependencies {
implementation(platform("io.github.bluetape4k:bluetape4k-dependencies:1.3.0"))
implementation("io.github.bluetape4k.image:bluetape4k-images")
implementation("io.github.bluetape4k.image:bluetape4k-images-ocr")
implementation("io.github.bluetape4k.image:bluetape4k-images-vips-api")
implementation("io.github.bluetape4k.image:bluetape4k-images-spring-boot")
}

이미지 처리에서 흔한 실수는 입력 파일 전체를 이른 시점에 ByteArray로 변환하는 것입니다.

// 작은 예제에는 편리하지만 대용량 입력에는 적합하지 않다.
val bytes = multipartFile.bytes
val text = ocr.read(bytes)

작은 샘플 파일만으로는 문제가 드러나지 않지만, 운영 환경에서 80MB TIFF를 처리하면 메모리 사용량이 급증할 수 있습니다.

1.3.0 호환선에서는 Okio 기반 대용량 파일 I/O와 OCR 처리 경계를 함께 확인해야 합니다. 다음은 스트리밍 경계를 설명하는 애플리케이션 의사코드입니다.

fun extractText(path: Path): OcrResult {
return fileSystem.source(path).buffer().use { source ->
imageReader
.read(source)
.resize(maxWidth = 2_000)
.normalizeForOcr()
.runOcr(language = "kor+eng")
}
}

핵심은 OCR 옵션보다 파일이 메모리에 적재되는 지점입니다. 대용량 파일로 서버가 메모리 압박이나 타임아웃에 이르면 OCR 품질과 무관하게 요청을 완료할 수 없습니다.

모듈은 저장소 이름보다 서비스 경계를 기준으로 선택합니다.

서비스 경계우선 검토할 모듈함께 확인할 항목
Spring Boot 공통 기반bluetape4k-spring-boot-coreSpring Boot 주 버전 호환선
Ktor 기반 APIbluetape4k-ktor-core, bluetape4k-ktor-observabilityKtor 주 버전, 플러그인 수명 주기
Exposed JDBCbluetape4k-exposed-spring-boot-jdbc, bluetape4k-exposed-jdbc트랜잭션 도우미, 데이터소스 설정
Exposed R2DBCbluetape4k-exposed-spring-boot-r2dbc, bluetape4k-exposed-r2dbc코루틴 경계, 커넥션 수명 주기
AWS 연동bluetape4k-aws-spring-boot, bluetape4k-aws-ktor클라이언트 수명 주기, 에뮬레이터·런타임
분산 작업 대표 선출bluetape4k-leader-spring-boot, bluetape4k-leader-ktor공급자·저장소 선택, 메트릭
한국어/일본어 텍스트tokenizer-korean, tokenizer-japanese, text-search입력 길이 제한, 오류 응답
이미지/OCRbluetape4k-images, bluetape4k-images-ocr파일 크기, 메모리, 네이티브 런타임

서비스가 책임지는 경계를 먼저 선택하고 그 경계에 필요한 모듈만 추가합니다. BOM은 선택한 모듈이 검증된 버전 조합을 사용하도록 제어합니다.

dependencies 1.3.0으로 업그레이드했다면 다음 항목을 확인합니다.

Terminal window
./gradlew compileTestKotlin
./gradlew test
./gradlew dependencyInsight --dependency exposed --configuration runtimeClasspath
./gradlew dependencyInsight --dependency ktor --configuration runtimeClasspath
./gradlew dependencyInsight --dependency aws --configuration runtimeClasspath

모든 프로젝트에서 모든 의존성을 조회할 필요는 없습니다. 그러나 데이터베이스, AWS SDK, Ktor, Spring Boot처럼 서비스 경계에 가까운 의존성이 변경되었다면 실제 런타임 클래스패스를 확인해야 합니다.

BOM을 가져왔다는 것은 버전 숫자 고민을 줄였다는 뜻입니다. 하지만 서비스 설계가 자동으로 끝났다는 뜻은 아닙니다.

dependencies 1.3.0은 Exposed, AWS, Leader, Image, Text 조합을 하나의 BOM 호환선으로 제공합니다. 모듈 선택 전 다음 질문에 답해야 합니다.

  • 서비스의 데이터베이스 경계는 무엇인가?
  • AWS 클라이언트와 로그·메트릭 수명 주기는 누가 관리하는가?
  • 예약 작업을 여러 인스턴스에서 동시에 실행해도 되는가?
  • 사용자 입력은 토크나이저나 금칙어 검사 전에 어디에서 제한하는가?
  • 이미지 파일은 어느 시점에 메모리에 적재되는가?

이 질문에 답하면 필요한 모듈을 결정할 수 있습니다. 버전 조합은 BOM에 맡기고 서비스 경계는 애플리케이션이 명시적으로 설계합니다.

처음 bluetape4k-dependencies를 적용하는 방법부터 확인하려면 사용 가이드를 먼저 읽으면 됩니다.

댓글

GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.