Bounded Context 기준으로 Kotlin/Spring 도메인 모듈을 나눈 이유
큰 도메인 모듈 하나로는 경계를 지키기 어렵습니다. s-class-backend에서 bounded context 기준으로 domain module을 나누며 배운 점을 정리합니다.
Backend field notes
Kotlin, Spring Boot, NestJS, MongoDB, 분산 시스템을 다루며 남긴 기술 노트입니다. 글마다 가능한 경우 실행 가능한 샘플 코드 저장소를 함께 연결합니다.
Featured
큰 도메인 모듈 하나로는 경계를 지키기 어렵습니다. s-class-backend에서 bounded context 기준으로 domain module을 나누며 배운 점을 정리합니다.
Architecture test는 설계를 대신해주는 도구가 아닙니다. 사람이 매번 확인하던 경계 위반을 빌드에서 먼저 드러내는 안전장치에 가깝습니다.
Querydsl/JPA 기반 persistence를 Exposed DSL로 옮긴 작업은 단순한 ORM 교체가 아니었습니다. Kotlin 빌드에서 kapt와 생성 코드를 걷어내고, persistence 경계를 더 명시적으로 만든 전환이었습니다.
마이크로서비스 환경에서 비즈니스 로직을 외부 의존성으로부터 보호하기 위해 헥사고날 아키텍처를 적용했습니다. Domain, Application, Adapter 계층으로 명확히 분리하고, Port와 Adapter 패턴으로 유연한 구조를 만든 실전 사례를 공유합니다.
마이크로서비스 환경에서 여러 서비스를 거치는 결제 프로세스를 Saga 패턴으로 구현했습니다. PG 승인, Order 생성, 완료 처리의 각 단계에서 실패 시 보상 트랜잭션을 실행하여 데이터 일관성을 유지하는 실전 사례를 공유합니다.
HikariCP 대시보드에서 Connection Timeout이 27회 발생하는 문제를 발견했습니다. 원인은 N+1 쿼리 문제와 작은 Connection Pool 크기였습니다. 배치 조회로 N+1 쿼리를 해결하고 Connection Pool 크기를 5에서 20으로 증가시켜 Timeout을 0으로 만들었습니다. 쿼리 수는 161개에서 3개로 98% 감소했습니다.
Native Query를 Kotlin JDSL로 마이그레이션하면서 확장 함수 패턴을 도입했습니다. 일반 join을 fetchJoin으로 변경하여 N+1 쿼리 문제를 해결하고, 데이터베이스 레벨에서 직접 집계 연산을 수행하여 쿼리 성능을 개선했습니다. Kotlin JDSL의 타입 안전성과 확장 함수 패턴을 활용하여 유지보수성을 크게 향상시켰습니다.
LLM 리포트 생성 파이프라인의 완료 이벤트가 조용히 유실되는 버그를 추적하다가, 7,600줄짜리 오케스트레이터의 복잡함 대부분이 Lambda 15분 타임아웃을 우회하기 위한 코드였음을 알게 됐습니다. 버그를 고치는 대신 실행 기반을 Temporal로 옮기고, 91곳의 정규식 사후 정제를 Spring AI structured output 계약으로 대체한 과정입니다.
2025년 5월, 뭘 만들지도 정하지 못한 채 시작한 프로젝트가 14개월 뒤 결제와 교사 매칭, AI 리포트 생성이 돌아가는 유료 프로덕트가 됐습니다. 그 사이에 아키텍처를 두 번 접었고, 상품 하나를 만들었다 제거했고, 장애를 겪었습니다. 만든 것보다 접은 것 중심으로 정리한 회고입니다.
Topic tracks
도메인 경계, 헥사고날 아키텍처, 분산 트랜잭션을 다룹니다.
JPA, Query, Coroutine, 모듈 설계 경험을 기록합니다.
MongoDB, PostgreSQL, 인덱스, 트랜잭션 문제를 정리합니다.
Recent
LLM 리포트 생성 파이프라인의 완료 이벤트가 조용히 유실되는 버그를 추적하다가, 7,600줄짜리 오케스트레이터의 복잡함 대부분이 Lambda 15분 타임아웃을 우회하기 위한 코드였음을 알게 됐습니다. 버그를 고치는 대신 실행 기반을 Temporal로 옮기고, 91곳의 정규식 사후 정제를 Spring AI structured output 계약으로 대체한 과정입니다.
2025년 5월, 뭘 만들지도 정하지 못한 채 시작한 프로젝트가 14개월 뒤 결제와 교사 매칭, AI 리포트 생성이 돌아가는 유료 프로덕트가 됐습니다. 그 사이에 아키텍처를 두 번 접었고, 상품 하나를 만들었다 제거했고, 장애를 겪었습니다. 만든 것보다 접은 것 중심으로 정리한 회고입니다.
Temporal 워크플로에 Spring AI를 붙이면서 내부를 들여다봤습니다. useProviderStructuredOutput()이 Anthropic 네이티브 outputConfig로 나가는 경로, 프롬프트 캐싱을 켰지만 read가 0이라 손해였던 이유, 그리고 .entity()가 가린 토큰·비용을 CallAdvisor로 거둔 방법입니다. 2부작 중 2편입니다.
진단 리포트 생성을 SQS+Lambda 이벤트 파이프라인으로 돌렸습니다. 동시성 한도, 큐 백로그 지연, 흩어진 오케스트레이션 세 가지가 걸렸고 이걸 Temporal 워크플로로 옮기면서 생성 로직을 한 메서드 안으로 모았습니다. 2부작 중 1편입니다.
Querydsl/JPA 기반 persistence를 Exposed DSL로 옮긴 작업은 단순한 ORM 교체가 아니었습니다. Kotlin 빌드에서 kapt와 생성 코드를 걷어내고, persistence 경계를 더 명시적으로 만든 전환이었습니다.
Architecture test는 설계를 대신해주는 도구가 아닙니다. 사람이 매번 확인하던 경계 위반을 빌드에서 먼저 드러내는 안전장치에 가깝습니다.
큰 도메인 모듈 하나로는 경계를 지키기 어렵습니다. s-class-backend에서 bounded context 기준으로 domain module을 나누며 배운 점을 정리합니다.
Spring Boot의 메타 어노테이션 원리를 활용해 @UseCase, @DomainService, @Adaptor 커스텀 어노테이션을 만들고, 레이어 아키텍처의 역할을 코드에 명시적으로 표현하는 방법을 도입 전/후 비교와 함께 정리합니다.
Spring Quartz(JDBC JobStore, Cluster)로 supporters-service 예약 알림을 구현한 실전기. TaskDeadlineJob 중심으로 등록/취소/실행 흐름과 Pub/Sub 연동 패턴을 코드로 정리합니다.
Spring Quartz 내부 구현을 따라가며 동적 스케줄링이 가능한 이유를 분석합니다. scheduleJob/rescheduleJob/deleteJob 흐름, QRTZ_* 테이블, Cluster 락, misfire 처리까지 실무 관점으로 정리합니다.
BullMQ의 delayed job이 실제로 어떻게 동작하는지 궁금하셨나요? Polling 방식일까요? 아니면 다른 방식일까요? 이 글에서는 BullMQ의 소스코드를 직접 분석하여 delayed job이 event-driven 방식으로 동작하며, Redis의 BZPOPMIN blocking primitive를 사용한다는 것을 확인합니다. TypeScript 코드가 어떻게 컴파일되고 실행되는지, Redis와 어떻게 통신하는지까지 깊이 파고 들어봅니다.
Native Query를 Kotlin JDSL로 마이그레이션하면서 확장 함수 패턴을 도입했습니다. 일반 join을 fetchJoin으로 변경하여 N+1 쿼리 문제를 해결하고, 데이터베이스 레벨에서 직접 집계 연산을 수행하여 쿼리 성능을 개선했습니다. Kotlin JDSL의 타입 안전성과 확장 함수 패턴을 활용하여 유지보수성을 크게 향상시켰습니다.