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

bluetape4k-dependencies를 적용하면 버전 관리는 단순해집니다. 그러나 서비스 경계에 맞는 모듈은
애플리케이션이 선택해야 합니다.
서비스가 책임지는 경계에는 어떤 모듈이 필요한가?
서비스에 따라 Exposed만 사용하거나 AWS까지 함께 사용할 수 있습니다. 정기 작업을 한 인스턴스에서만
실행해야 한다면 Leader가 필요하고, 사용자 입력을 분석한다면 Text가 필요합니다. 이미지 파일을
처리하는 서비스에는 Image 모듈을 추가합니다.
dependencies 1.3.0을 가져온 뒤에는 실제 서비스에서 어떤 모듈을 함께 고를지가 더 중요합니다.
여기서는 사용 측의 선택에 집중합니다. 핵심은 버전을 선언하는 위치가 아니라 기능 경계에 맞는
모듈을 선택하는 방법입니다.
먼저 BOM을 적용한다
섹션 제목: “먼저 BOM을 적용한다”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 사용 여부, 분산 작업의 단일 실행 요구사항, 입력 텍스트 처리 여부에 따라 모듈을 선택합니다.

예제 1: Spring Boot 워커 서비스
섹션 제목: “예제 1: Spring Boot 워커 서비스”첫 번째 예제는 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가 중단될 수 있기 때문입니다. 상태 응답에 이 정보가 있으면 운영자는 애플리케이션 증상만으로 캐시 장애를 추정하지 않아도 됩니다.
예제 2: Ktor API 서비스
섹션 제목: “예제 2: Ktor API 서비스”두 번째 예제는 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 연동에는 메트릭 네임스페이스, 로그 그룹·스트림 이름, 클라이언트 수명 주기가 포함됩니다. 이 결정을 플러그인 설정에 모으면 어떤 인스턴스가 어떤 이름으로 데이터를 전송했는지 추적하기 쉬워집니다. 플러그인을 설치하는 것만으로 메트릭이나 로그가 자동 전송되는 것은 아니며, 애플리케이션이 명시적으로 게시하거나 버퍼에 추가해야 합니다.
예제 3: 이미지 업로드 API
섹션 제목: “예제 3: 이미지 업로드 API”이미지 파일을 받는 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.bytesval 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-core | Spring Boot 주 버전 호환선 |
| Ktor 기반 API | bluetape4k-ktor-core, bluetape4k-ktor-observability | Ktor 주 버전, 플러그인 수명 주기 |
| Exposed JDBC | bluetape4k-exposed-spring-boot-jdbc, bluetape4k-exposed-jdbc | 트랜잭션 도우미, 데이터소스 설정 |
| Exposed R2DBC | bluetape4k-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 | 입력 길이 제한, 오류 응답 |
| 이미지/OCR | bluetape4k-images, bluetape4k-images-ocr | 파일 크기, 메모리, 네이티브 런타임 |
서비스가 책임지는 경계를 먼저 선택하고 그 경계에 필요한 모듈만 추가합니다. BOM은 선택한 모듈이 검증된 버전 조합을 사용하도록 제어합니다.
업그레이드 후 확인할 것
섹션 제목: “업그레이드 후 확인할 것”dependencies 1.3.0으로 업그레이드했다면 다음 항목을 확인합니다.
./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을 남길 수 있습니다.