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이 그림에서 개발자가 직접 고르는 것은 오른쪽 버전 숫자가 아니라 왼쪽 기능 경계입니다. DB를 쓰는가, AWS를 쓰는가, 분산 작업을 한 번만 실행해야 하는가, 입력 텍스트를 다뤄야 하는가. 그 질문에 따라 모듈을 고르면 됩니다.
예제 1: Spring Boot worker 서비스
섹션 제목: “예제 1: Spring Boot worker 서비스”첫 번째 예제는 Spring Boot 기반 worker입니다.
상황을 이렇게 잡아보겠습니다.
- DB에 쌓인 작업을 읽는다.
- S3에 결과 파일을 쓴다.
- 같은 작업이 여러 인스턴스에서 동시에 실행되면 안 된다.
- 작업 이름이나 입력 텍스트를 tokenizer로 전처리한다.
- write-behind cache를 쓴다면 health에서 상태를 확인하고 싶다.
이 경우 의존성은 대략 이렇게 시작할 수 있습니다.
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는 장식이 아닙니다. worker가 세 대 떠 있는데 세 대가 같은 row를 집어가면, 그날의 배포는
기술 블로그가 아니라 반성문 소재가 됩니다. Leader를 쓰면 “이 작업은 지금 누가 대표로 실행하는가”를 코드 경계에
드러낼 수 있습니다.
Exposed cache를 같이 쓴다면 health도 같이 봅니다.
{ "status": "OUT_OF_SERVICE", "components": { "exposedCache": { "status": "OUT_OF_SERVICE", "details": { "mode": "WRITE_BEHIND", "queueDepth": 128, "flushJobRunning": false, "lastFlushError": "RedisConnectionFailureException" } } }}write-behind cache는 실패를 늦게 알수록 위험합니다. 겉으로는 요청이 성공한 것처럼 지나가는데, 뒤에서 flush가 멈추면 데이터 일관성이 깨질 수 있습니다. 아무 일도 없는 것처럼 보이는 상태가 오히려 더 위험할 때가 있습니다. health에 이 정보가 나오면 운영자는 cache 장애를 애플리케이션 증상으로만 추리하지 않아도 됩니다.
예제 2: Ktor API 서비스
섹션 제목: “예제 2: Ktor API 서비스”두 번째 예제는 Ktor API입니다.
Ktor API는 worker와 확인할 경계가 다릅니다.
- HTTP API가 사용자 요청을 받는다.
- R2DBC나 JDBC로 데이터를 읽고 쓴다.
- AWS CloudWatch에 metric과 log를 보낸다.
- 관리 API나 scheduled task에서 leader election이 필요하다.
- 입력 텍스트를 tokenizer나 blockword 검사에 통과시킨다.
의존성은 다음처럼 잡을 수 있습니다.
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 쪽에서 특히 조심할 부분은 lifecycle입니다. AWS client를 만들고 닫는 책임이 애플리케이션 곳곳에 흩어지면, 테스트에서는 티가 덜 나도 운영에서는 connection pool이나 background thread가 남을 수 있습니다.
의사코드로 보면 원하는 구조는 이쪽입니다.
fun Application.module() { install(BluetapeAwsCloudWatch) { namespace = "billing-api" serviceName = "billing" }
install(BluetapeAwsCloudWatchLogs) { logGroupName = "/bluetape4k/billing-api" logStreamName = environment.config.property("deployment.instanceId").getString() }
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를 “log를 한 줄 더 보내는 코드”로 보지 않는 것입니다. metric namespace, log group, stream 이름, client lifecycle이 애플리케이션 경계 안에 들어옵니다. 이 부분을 plugin으로 모으면 장애가 났을 때 “어떤 인스턴스가 어떤 이름으로 metric을 냈는지”를 따라가기 쉬워집니다.
예제 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 기반 large-file 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 option보다 파일이 메모리에 올라오는 지점입니다. OCR 품질을 올리는 것도 중요하지만, 큰 파일 하나 때문에 서버가 메모리 압박이나 timeout에 걸리면 클라이언트 요청에 OCR 결과를 돌려줄 수 없습니다.
모듈 선택 기준
섹션 제목: “모듈 선택 기준”모듈을 고를 때는 저장소 이름보다 서비스 경계를 먼저 보면 편합니다.
| 서비스 경계 | 먼저 볼 모듈 | 같이 확인할 것 |
|---|---|---|
| Spring Boot 공통 기반 | bluetape4k-spring-boot-core | Spring Boot major line |
| Ktor 기반 API | bluetape4k-ktor-core, bluetape4k-ktor-observability | Ktor major line, plugin lifecycle |
| Exposed JDBC | bluetape4k-exposed-spring-boot-jdbc, bluetape4k-exposed-jdbc | transaction helper, datasource 설정 |
| Exposed R2DBC | bluetape4k-exposed-spring-boot-r2dbc, bluetape4k-exposed-r2dbc | coroutine 경계, connection lifecycle |
| AWS 연동 | bluetape4k-aws-spring-boot, bluetape4k-aws-ktor | client lifecycle, emulator/runtime |
| 분산 작업 대표 선출 | bluetape4k-leader-spring-boot, bluetape4k-leader-ktor | provider/storage 선택, metric |
| 한국어/일본어 텍스트 | tokenizer-korean, tokenizer-japanese, text-search | 입력 길이 제한, error response |
| 이미지/OCR | bluetape4k-images, bluetape4k-images-ocr | 파일 크기, memory, native runtime |
이 표는 “무조건 다 넣으라”는 뜻이 아닙니다. 오히려 반대입니다. 서비스가 책임지는 경계를 먼저 고르고, 그 경계에 필요한 모듈만 가져오면 됩니다. 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모든 프로젝트에서 dependencyInsight를 다 볼 필요는 없습니다. 하지만 DB, AWS SDK, Ktor, Spring Boot처럼
서비스 경계에 가까운 dependency가 움직였을 때는 한 번 확인할 가치가 있습니다. 문제가 생겼을 때 “BOM을 올린 뒤
실제로 어떤 runtime classpath가 되었는지”를 바로 볼 수 있기 때문입니다.
마무리
섹션 제목: “마무리”BOM을 가져왔다는 것은 버전 숫자 고민을 줄였다는 뜻입니다. 하지만 서비스 설계가 자동으로 끝났다는 뜻은 아닙니다.
dependencies 1.3.0에서는 Exposed, AWS, Leader, Image, Text 쪽 조합이 실제 서비스에서 쓰기 좋게 정리되었습니다.
좋은 출발점은 이 질문입니다.
- 내 서비스는 어떤 DB 경계를 갖고 있나?
- AWS client와 log/metric lifecycle은 누가 관리하나?
- scheduled job은 여러 인스턴스에서 동시에 돌아도 되나?
- 사용자 입력은 tokenizer나 blockword 검사 전에 어디서 제한하나?
- 이미지 파일은 어느 순간 메모리에 올라오나?
이 질문에 답하면 필요한 모듈이 꽤 자연스럽게 정해집니다. 버전은 BOM에 맡기고, 사람은 서비스 경계를 보면 됩니다.
시리즈 글
섹션 제목: “시리즈 글”처음 bluetape4k-dependencies를 가져오는 방법부터 보고 싶다면
사용 가이드를 먼저 읽으면 됩니다.
- bluetape4k-dependencies 1.3.0 활용기 Part 1: 새 기능도 넣고 똥도 치우고
- bluetape4k-dependencies 1.3.0 활용기 Part 2: 서비스 조합하기
- bluetape4k-dependencies 1.3.0 활용기 Part 3: 운영에서 티 나는 변화
- bluetape4k-dependencies 1.3.0 활용기 Part 4: 입력 경계에서 치운 똥들
댓글
GitHub 계정으로 의견을 남기거나 reaction을 남길 수 있습니다.