Search
📦

공용 라이브러리 개발 — Audit Log / Exposed ORM 페이징

개요

항목
내용
기간
Audit Log: 2025.04, 페이징: 2025.04 (유지보수 2025.11)
역할
설계 및 구현 주도
기술 스택
Kotlin, Spring Boot 2.x/3.x, Spring Cloud Sleuth, Micrometer Tracing, Exposed ORM
영향 범위
100명 규모 개발 조직 전체

배경

커넥트웨이브는 레거시 시스템에서 신규 플랫폼으로의 전환을 진행하고 있었습니다. 이 과정에서 두 가지 조직 차원의 문제가 있었습니다.

Audit Log 문제

레거시 환경에서는 관리자나 배치/실시간 시스템의 CUD 행위에 대한 감사 로그가 부재하거나, 시스템별로 파편화되어 있었음
장애 발생 시 "누가, 언제, 어떤 데이터를 변경했는지" 추적이 어려웠음
신규 플랫폼에서는 모든 API에서 일관된 감사 로그를 남기기로 결정

페이징 문제

Kotlin Exposed ORM을 도입했으나, Spring Data의 Pageable처럼 표준화된 페이징 시스템이 없었음
각 팀이 서로 다른 방식으로 페이징을 구현하고 있었고, 디버깅 및 유지보수에 비용이 발생

문제

Audit Log

Spring Boot 2.x(Spring Cloud Sleuth) 기반의 임시 프로젝트와 Spring Boot 3.x(Micrometer Tracing) 기반의 신규 프로젝트가 동시에 존재
두 환경 모두에서 동작하면서도, 저장소(DB, 파일, 외부 시스템 등)에 독립적인 공용 라이브러리가 필요

페이징

RDBMS(PostgreSQL)와 MongoDB를 모두 사용하는 환경이어서, 특정 DB에 종속되지 않는 설계가 필요
기존 Exposed ORM의 자연스러운 DSL 사용 방식을 깨지 않으면서 페이징을 통합해야 했음

무엇을 했나

Audit Log 라이브러리 설계

flowchart TD
    A[HTTP 요청] --> B[AuditFilter<br>OncePerRequestFilter]
    B -->|헤더를 Baggage로 전파| C[Micrometer Tracing<br>Baggage 저장소]
    B --> D[Controller / DataFetcher]
    
    D -->|"@AuditInfo 어노테이션"| E[ArgumentResolver]
    E -->|tracer.allBaggage| C
    E --> F[AuditInfoDTO로 바인딩]
    
    D -->|"AuditHandler 호출"| G{AuditHandler}
    G -->|tracer.allBaggage| C
    G --> H[LoggingAuditHandler<br>기본 구현체]
    G --> I[커스텀 구현체<br>DB 저장 등]
Mermaid
복사
1. AuditHandler — sealed interface 기반 처리 전략 추상화
감사 데이터를 어떻게 처리할지(DB 저장, 로깅, 메시지 큐 등)를 sealed interface로 추상화하고, 비동기 처리도 기본 지원:
sealed interface AuditHandler { fun handle() fun handleAsync(executor: Executor) { CompletableFuture.runAsync({ handle() }, executor) } } abstract class MicrometerTracingAuditHandler( private val tracer: Tracer, ) : AuditHandler { override fun handle() { val auditInfo = tracer.allBaggage // Baggage에서 감사 정보 수집 handle(auditInfo) } abstract fun handle(auditInfo: Map<String, String>) }
Kotlin
복사
@ConditionalOnMissingBean을 활용하여 커스텀 구현체가 없으면 로깅 기반 기본 핸들러를 자동 등록:
@Bean @ConditionalOnMissingBean(AuditHandler::class) fun defaultAuditHandler(tracer: Tracer) = LoggingAuditHandler(tracer)
Kotlin
복사
2. AuditFilter — HTTP 헤더를 Micrometer Baggage로 자동 전파
HTTP 헤더의 감사 정보(who, when, where, what)를 Micrometer Tracing의 Baggage로 자동 설정하여, 서비스 내부 어디서든 tracer.allBaggage로 감사 정보에 접근 가능
3. @AuditInfo + ArgumentResolver — 컨트롤러/리졸버에서 직접 바인딩
GraphQL DataFetcher와 Spring MVC Controller 모두에서 사용 가능하도록 두 종류의 ArgumentResolver를 구현:
// 사용하는 쪽 코드 — 어노테이션 하나로 감사 정보 주입 fun updateItem( @AuditInfo auditInfo: AuditInfoDTO, // who, when, where, what 자동 바인딩 input: UpdateItemInput ) { ... }
Kotlin
복사
Spring Boot 2.x(Sleuth)와 3.x(Micrometer Tracing)를 동시에 지원하기 위해 같은 인터페이스 + 각 라이브러리별 구현체를 별도 모듈로 분리하여 각 프로젝트에서 해당하는 모듈만 import

Exposed ORM 페이징 라이브러리 설계

확장 함수 설계:
// 정렬 조건을 Exposed 쿼리에 매핑하는 람다 타입 typealias SortColumMapper = (Sort.Order) -> Pair<Expression<*>, SortOrder>? // Offset 페이징 확장 함수 — Query에 체이닝하여 사용 fun Query.offsetPagedQuery( page: Pageable, sortColumMapper: SortColumMapper = { null }, ): Query { val sortOrders = page.sort .map { sortColumMapper(it) } .filterNotNull() .toTypedArray() return this .orderBy(*sortOrders) .limit(page.pageSize) .offset(page.offsetAsZeroBase) } // 쿼리 결과를 PageResponse로 변환 — mapper 함수 주입 fun <T> Query.mapToOffsetPageResponse( pageable: Pageable, total: Long, contentMapper: (ResultRow) -> T, ): PageResponse<T> { ... } // 커서 기반 페이징도 동일한 패턴으로 지원 fun Query.cursorPagedQuery( page: Pageable, cursorCondition: SqlExpressionBuilder.() -> Op<Boolean>, sortColumMapper: SortColumMapper = { null }, ): Query { ... }
Kotlin
복사
사용하는 쪽 코드:
val query = ItemTable.selectAll() query.apply { param.categoryId?.let { andWhere { ItemTable.categoryId eq it } } } val totalCount = query.count() // 한 줄로 페이징 적용 + 응답 변환 return query.offsetPagedQuery(pageable).mapToOffsetPageResponse(pageable, totalCount) { it.toItemDTO() }
Kotlin
복사
Query.offsetPagedQuery() / Query.cursorPagedQuery() 확장 함수로 Exposed ORM의 기존 쿼리 빌더에 자연스럽게 체이닝
SortColumMapper typealias로 정렬 조건 변환을 외부에서 주입받아 유연성 확보
Offset 페이징과 Cursor 페이징 모두 동일한 패턴으로 지원 — PageResponse로 일관된 응답 반환
PageResponse.map() 확장 함수로 레이어 간 DTO 변환 시에도 페이징 메타데이터 유지

결과

성과

신규 플랫폼 전체에 Audit Log 라이브러리 적용 — 15개 멀티 모듈에 적용
페이징 라이브러리 도입 후 전체 개발 조직의 페이징 구현 방식 통일
페이징 관련 코드 리뷰 시 "각자 다른 방식" 문제가 사라짐
신규 개발자 온보딩 시 페이징 처리 학습 비용 감소
Audit Log를 통한 장애 추적이 가능해짐

배운 점

각 라이브러리에 KDoc 주석을 상세하게 작성하는 것이 대규모 협업에서 얼마나 중요한지를 체감함 — 100명 규모 조직에서 라이브러리를 배포하면 직접 설명할 수 없는 사람들이 대부분이고, 문서가 코드를 대신함
사용성과 유지보수성 둘 다 포기할 수 없다는 점을 배움. 확장 함수로 쓰기 편하게 만들면서도, 내부 설계를 바꿀 때 기존 사용자에게 영향을 주지 않도록 인터페이스를 설계해야 했음