섹션 1: 기본 아키텍처: 이벤트 기반 데이터 수집 파이프라인
대규모 데이터를 안정적으로 수집하고 처리하기 위한 시스템을 설계할 때, 아키텍처의 선택은 프로젝트의 확장성, 복원력, 그리고 유지보수성에 지대한 영향을 미친다. 본 섹션에서는 대용량 데이터 수집 플랫폼의 기반이 될 핵심 아키텍처 철학을 정립한다. 초기 개발의 용이성을 제공하는 모놀리식(Monolithic) 아키텍처와 확장성과 유연성에 강점을 보이는 마이크로서비스(Microservices) 아키텍처를 비교 분석하고, 최종적으로 이벤트 기반 마이크로서비스 접근법을 채택하는 이유를 논증한다.
1.1 아키텍처 패러다임: 데이터 수집을 위한 모놀리식과 마이크로서비스 비교
모놀리식 접근법
모놀리식 아키텍처는 API 호출, 데이터 변환, 데이터베이스 저장 등 모든 기능이 단일 애플리케이션 내에 통합된 구조를 가진다. 이 방식은 초기 개발 단계에서 모든 코드가 한곳에 모여 있어 이해와 배포가 비교적 간단하다는 장점이 있다.
그러나 시스템의 규모가 커지고 처리량이 증가함에 따라 여러 단점이 부각된다. 첫째, 시스템의 각 부분이 긴밀하게 결합(tightly coupled)되어 있어 하나의 기능(예: API 호출)이 다른 기능(예: 데이터베이스 쓰기)의 성능에 직접적인 영향을 받는다. 데이터베이스에 부하가 걸려 쓰기 작업이 지연되면 API 호출 로직까지 연쇄적으로 느려지거나 중단될 수 있다. 둘째, 시스템의 특정 부분만 독립적으로 확장하기가 어렵다. API 호출량을 늘리기 위해 전체 애플리케이션을 확장해야 하므로 자원 사용이 비효율적이다. 마지막으로, 코드베이스가 거대해지면서 유지보수와 신규 기능 추가가 점점 더 복잡해지는 경향이 있다.
마이크로서비스 접근법
마이크로서비스 아키텍처는 전체 애플리케이션을 기능별로 작고 독립적인 서비스들로 분해하는 방식이다. 예를 들어, 데이터 수집 시스템을 'API 호출 서비스(api-poller)', '데이터 변환 서비스(data-transformer)', '데이터베이스 쓰기 서비스(db-writer)' 등으로 나눌 수 있다. 이 서비스들은 API나 메시지 버스를 통해 서로 통신한다.
이 접근법의 가장 큰 장점은 서비스의 독립적인 개발, 배포, 확장이 가능하다는 점이다. 예를 들어, 특정 API의 호출 빈도를 높여야 할 경우, 'API 호출 서비스'만 독립적으로 확장(scale-out)하면 되므로 다른 서비스에 영향을 주지 않고 유연하게 대처할 수 있다. 또한, 각 서비스가 자체적인 데이터와 로직을 관리하는 분산 데이터 관리(decentralized data management) 원칙을 따르므로, 서비스 간의 결합도가 낮아진다. 이는 시스템 전체의 복원력을 높이는 데 기여하며, 서킷 브레이커(Circuit Breaker)와 같은 패턴을 적용하여 특정 서비스의 장애가 전체 시스템으로 전파되는 것을 방지할 수 있다.
물론, 마이크로서비스 아키텍처는 운영 복잡성 증가, 서비스 간 데이터 일관성 유지의 어려움, 견고한 서비스 간 통신 메커니즘의 필요성 등 해결해야 할 과제들도 존재한다.
아키텍처 결정
본 보고서에서 목표로 하는 대용량, 고처리량 데이터 수집 시스템의 요구사항을 고려할 때, 마이크로서비스 아키텍처가 모놀리식 아키텍처보다 월등한 선택이다. 특히 다양한 특성을 가진 API들을 처리해야 하는 상황에서 마이크로서비스의 모듈성은 빛을 발한다. 각기 다른 호출 주기, 데이터 형식, 인증 방식을 가진 API 그룹별로 특화된 'API 호출 서비스' 인스턴스를 배포하고 독립적으로 관리할 수 있기 때문이다. 이러한 유연성과 확장성은 모놀리식 구조에서는 구현하기 어려운 수준의 장점이다. 따라서, 이후의 모든 설계는 마이크로서비스 아키텍처를 기반으로 진행한다.
1.2 핵심 패턴: Apache Kafka를 이용한 이벤트 기반 아키텍처
마이크로서비스 간의 통신 방식으로는 동기적인 직접 API 호출 대신, 비동기적인 이벤트 기반 모델을 채택한다. 이벤트 기반 아키텍처(Event-Driven Architecture, EDA)에서 서비스는 다른 서비스의 상태를 알 필요 없이 특정 사건(Event)을 발행(publish)하기만 한다. 예를 들어, 'API 호출 서비스'는 "API X로부터 데이터 수집 완료"라는 이벤트를 발행하고, 이 이벤트에 관심 있는 다른 서비스들이 이를 구독(subscribe)하여 후속 작업을 처리한다. 이러한 방식은 서비스 간의 결합도를 극단적으로 낮춰 시스템 전체의 확장성과 복원력을 크게 향상시킨다.
이벤트 기반 통신의 핵심 요소는 메시지 브로커(Message Broker)이며, 여기서는 대표적인 두 기술인 RabbitMQ와 Apache Kafka를 비교 분석한다.
- RabbitMQ: 전통적인 메시징 시나리오에 최적화된 '스마트' 브로커이다. 복잡한 라우팅 규칙 설정이 가능하고 메시지 전달 보장을 위한 다양한 기능을 제공한다. 생산자(Producer)가 보낸 메시지를 소비자(Consumer)에게 밀어주는 푸시(push) 기반 모델을 사용하며, 비동기 작업 처리와 같은 태스크 큐(task queue) 용도에 탁월하다.
- Apache Kafka: 분산 환경에서의 대용량 이벤트 스트리밍을 위해 설계된 플랫폼이다. Kafka는 본질적으로 분산형 영구 로그(distributed, persistent log) 시스템으로, '멍청한' 브로커와 '똑똑한' 소비자 구조를 가진다. 소비자가 데이터를 당겨오는 풀(pull) 기반 모델을 사용하며, 메시지를 일정 기간 보관하여 데이터를 재처리(replay)할 수 있는 기능은 데이터 파이프라인 구축에 매우 강력한 장점을 제공한다.
결정: 데이터 수집 파이프라인을 위한 Kafka 채택
대용량 데이터 수집 파이프라인의 요구사항을 고려할 때, Apache Kafka가 RabbitMQ보다 더 적합한 선택이다. Kafka는 데이터 수집 단계와 영속화 단계 사이에서 내구성 있는 버퍼(durable buffer) 역할을 수행할 수 있다. 이는 시스템의 안정성에 결정적인 기여를 한다. 예를 들어, 데이터베이스 장애로 인해 데이터 쓰기 작업이 불가능해지더라도, 'API 호출 서비스'는 계속해서 데이터를 수집하여 Kafka 토픽(Topic)에 저장할 수 있다. Kafka의 데이터 영속성 덕분에 이 데이터는 유실되지 않으며, 데이터베이스가 복구된 후 '데이터베이스 쓰기 서비스'는 중단됐던 지점부터 메시지를 다시 소비하여 처리를 재개할 수 있다. 이러한 특성은 잠재적인 데이터 유실이라는 치명적인 장애를 일시적인 소비자 지연(consumer lag)이라는 관리 가능한 운영 이슈로 전환시킨다. 이는 지속적으로 데이터를 수집해야 하는 '무한한 데이터 흐름(unbounded data flow)' 특성을 가진 본 프로젝트에 가장 이상적인 아키텍처 패턴이다.
1.3 시스템 상위 설계: 데이터 흐름 시각화
앞서 논의된 아키텍처 원칙에 따라, 시스템의 전체적인 데이터 흐름을 다음과 같이 설계한다.
!(https://i.imgur.com/example.png)
(주: 시각적 이해를 돕기 위한 예시 다이어그램입니다.)
- 스케줄러/오케스트레이션 서비스 (Scheduler/Orchestration Service)
- 다양한 API의 호출 주기를 관리한다.
- 정해진 스케줄에 따라 'API 호출 서비스'를 트리거하여 데이터 수집 작업을 시작시킨다.
- API 호출 서비스 (API-Poller Service)
- 대상 API에 HTTP 요청을 보내 원시 데이터(raw data)를 수집한다.
- 수집한 원시 데이터(주로 JSON 또는 XML 형식)를 Kafka의 raw-data-topic으로 발행한다.
- 데이터 변환 서비스 (Data-Transformer Service)
- raw-data-topic을 구독하여 원시 데이터를 소비한다.
- OpenAPI 명세에 기반하여 데이터의 유효성을 검증하고, 정제하며, 표준화된 형식(예: Avro, Protobuf)으로 변환한다. 이 과정에서 데이터 정규화 및 보강(enrichment) 작업이 수행될 수 있다.
- 구조화된 데이터를 Kafka의 structured-data-topic으로 발행한다.
- 데이터베이스 쓰기 서비스 (DB-Writer Service)
- structured-data-topic을 구독하여 구조화된 데이터를 소비한다.
- 데이터베이스 커넥션을 관리하며, 최종적으로 RDBMS에 INSERT 또는 UPDATE(UPSERT) 연산을 수행한다.
- 성능 향상을 위해 여러 메시지를 모아 배치(batch) 형태로 데이터베이스에 기록한다.
이러한 이벤트 기반 마이크로서비스 아키텍처는 각 컴포넌트의 역할을 명확히 분리하고, Kafka를 통해 비동기적으로 연결함으로써 대용량 데이터 처리에 필수적인 확장성, 유연성, 그리고 복원력을 확보하는 견고한 기반을 제공한다.
섹션 2: API-우선 워크플로우: RDBMS 스키마 생성 반자동화
본 섹션에서는 사용자의 핵심 요구사항인 OpenAPI 명세를 활용하여 데이터베이스 스키마 생성을 반자동화하는 구체적인 워크플로우를 제시한다. 이 접근법의 핵심 철학은 완전 자동화가 아닌, 개발자의 설계 의도를 반영할 수 있는 '반자동화'에 있다. 즉, OpenAPI 명세를 개발 초기 단계의 청사진으로 활용하여 생산성을 높이되, 최종적인 데이터 모델의 완성도는 숙련된 개발자의 수작업을 통해 보장하는 실용적인 방안을 목표로 한다.
2.1 철학: API 명세에서 데이터베이스 스키마로
API-우선(API-First) 개발 방식에서 OpenAPI 명세는 API 계약의 유일한 진실 공급원(Source of Truth) 역할을 한다. 이 명세의
schemas 섹션에는 API가 주고받는 데이터의 구조가 JSON 스키마의 확장된 형태로 정의되어 있다. 우리의 목표는 이처럼 사전에 정의된 데이터 모델을 활용하여 RDBMS 테이블 구조의 초안을 생성함으로써, 개발 초기부터 데이터 소스와 데이터베이스 모델 간의 정합성을 확보하는 것이다.
그러나 API 명세의 데이터 모델과 RDBMS의 데이터 모델은 근본적으로 다른 목적을 가진다는 점을 명확히 인지해야 한다. API 스키마는 네트워크를 통한 데이터 전송과 직렬화(serialization)에 초점을 맞추는 반면, RDBMS 스키마는 데이터의 영속적 저장, 정규화, 관계 설정, 무결성 보장, 그리고 효율적인 쿼리 성능에 중점을 둔다. 따라서 API 스키마를 RDBMS 스키마로 1:1로 변환하는 것은 종종 비정규화되고 비효율적인 데이터베이스 설계를 초래할 수 있다.
2.2 도구 및 프로세스 분석
OpenAPI 명세를 RDBMS 스키마로 변환하는 데 활용할 수 있는 주요 도구들은 다음과 같다.
- OpenAPI Generator: OpenAPI 명세로부터 클라이언트 SDK, 서버 스텁, 문서 등 다양한 산출물을 생성할 수 있는 강력한 오픈소스 도구이다. 특히 postgresql-schema 및 mysql-schema 생성기를 제공하여 명세의 schemas 정의를 바탕으로 DDL(Data Definition Language) 스크립트를 직접 생성할 수 있다. 이 생성기들은 테이블 및 컬럼 이름의 명명 규칙(예: snake_case)이나 복합 타입(JSON/JSONB) 처리 방식을 옵션으로 제공한다. 하지만 이 기능의 한계는 명확하다. 생성된 DDL은 어디까지나 '시작점'일 뿐이며, 관계형 데이터베이스의 정규화 원칙을 충분히 반영하지 못해 모든 DTO(Data Transfer Object)를 별도의 테이블로 만드는 등, 상당한 수작업을 통한 수정이 필요하다는 평가가 많다.
- OpenAlchemy (Python): Python 생태계에 특화된 라이브러리로, OpenAPI 명세 내에 x-tablename, x-primary-key와 같은 커스텀 확장 속성을 추가하여 SQLAlchemy 모델을 직접 정의하는 방식을 취한다. 이는 명세와 퍼시스턴스 모델을 하나로 통합하는 'spec-as-code' 접근법을 제공하며, 생성된 SQLAlchemy 모델은 Alembic과 같은 마이그레이션 도구와 연동하여 스키마를 관리할 수 있다. 다만, 이 방식은 스키마 정의가 비표준 확장 속성에 종속되고 Python 중심적이라는 한계가 있다.
- Swagger Codegen: OpenAPI Generator의 원형이 되는 도구로, 유사한 기능을 제공한다. 현재는 커뮤니티의 활성도나 OpenAPI 3.x 지원 측면에서 OpenAPI Generator가 더 앞서 있다는 평가를 받는다.
2.3 핵심 경로: 생성과 수동 정제를 결합한 워크플로우
직접적인 DDL 생성의 한계를 극복하고, 개발자의 설계 의도를 반영하기 위해 다음과 같은 간접적이고 점진적인 워크플로우를 제안한다.
- 1단계: DTO (Data Transfer Object) 생성: DDL을 직접 생성하는 대신, openapi-generator와 같은 도구를 사용하여 OpenAPI 명세로부터 목표 프로그래밍 언어의 DTO(Java의 POJO, Python의 Pydantic 모델 등)를 먼저 생성한다. 이 단계는 API의 데이터 구조를 타입-세이프(type-safe)한 애플리케이션 코드로 변환하는 과정이다.
- 2단계: 엔티티(Entity) 수동 설계: 생성된 DTO를 참조하여, 데이터베이스 설계자 또는 개발자가 직접 퍼시스턴스 엔티티(Java의 JPA Entity, Python의 SQLAlchemy Model)를 설계한다. 이 단계가 바로 'human-in-the-loop'가 개입하는 가장 중요한 과정이다. 이 과정에서 다음과 같은 핵심적인 설계 결정이 이루어진다.
- 정규화(Normalization): 여러 DTO에 분산된 정보를 하나의 정규화된 테이블로 통합하거나, 복잡한 DTO를 여러 개의 연관된 테이블로 분리한다.
- 관계 정의: API 명세에서는 암시적으로만 표현되던 테이블 간의 관계를 @ManyToOne, @OneToMany와 같은 어노테이션을 통해 명시적으로 정의한다.
-
- 데이터 타입 매핑: string, number와 같은 JSON 스키마의 일반적인 타입 대신, VARCHAR(255), NUMERIC(10,2), TIMESTAMP WITH TIME ZONE 등 RDBMS에 최적화된 구체적인 데이터 타입을 선택한다.
- 제약 조건 적용: 데이터 무결성을 위해 필수적인 NOT NULL, UNIQUE, CHECK 등의 제약 조건을 데이터베이스 수준에서 정의한다.
-
- 3단계: 매핑 계층 구현: API로부터 수신한 DTO를 수동으로 설계한 엔티티로 변환하는 매핑 계층을 구현한다. Java의 MapStruct와 같은 라이브러리는 이 과정에서 발생하는 상용구(boilerplate) 코드 생성을 자동화하여 DTO와 엔티티 간의 필드 매핑을 손쉽게 처리해준다.
- 4단계: 엔티티로부터 스키마 생성: 최종적으로 퍼시스턴스 프레임워크의 기능을 활용하여 DDL을 생성한다.
- Java/JPA: 개발 환경에서는 spring.jpa.hibernate.ddl-auto=update 설정을 통해 Hibernate가 @Entity 클래스로부터 스키마를 자동으로 생성하거나 업데이트하게 할 수 있다. 하지만 운영 환경에서는 Flyway나 Liquibase와 같은 데이터베이스 마이그레이션 도구를 사용하는 것이 정석이다. JPA Buddy와 같은 도구를 사용해 엔티티로부터 초기 DDL을 생성한 뒤, 이를 버전 관리되는 마이그레이션 스크립트로 관리하는 방식을 권장한다.
-
-
- Python/SQLAlchemy: Alembic을 사용하여 정의된 SQLAlchemy 모델과 현재 데이터베이스 상태를 비교하고, 변경 사항에 대한 마이그레이션 스크립트를 자동으로 생성한다.
-
이 간접적인 워크플로우는 OpenAPI 명세의 진정한 가치를 재정의한다. 명세는 데이터베이스 스키마를 '생성'하는 도구가 아니라, 데이터 소스와 애플리케이션의 경계(DTO) 사이의 '일관성을 강제'하는 수단으로 활용된다. 이를 통해 데이터 매핑 오류를 시스템의 가장자리에서 방지하면서도, 데이터베이스 스키마 설계는 의도적이고 전문적인 아키텍처 설계 활동으로 남겨둘 수 있다.
다음은 이 워크플로우에 사용되는 도구들의 특성을 비교한 표이다.
| 도구/패턴 | 접근 방식 | 지원 언어/프레임워크 | 주요 특징 | 한계 및 트레이드오프 | |
| OpenAPI Generator (*-schema) | 직접 DDL 생성 | PostgreSQL, MySQL 등 | - 명세에서 DDL 스크립트를 빠르게 생성 - 명명 규칙, 데이터 타입 등 일부 옵션 제공 |
- 관계형 모델링 원칙 미반영 - 생성된 스키마의 대대적인 수동 수정 필요 |
- 복잡한 시스템에는 부적합 |
| OpenAlchemy | 모델 생성 (통합 명세) | Python (SQLAlchemy) | - OpenAPI 명세에 x- 확장 속성을 사용해 퍼시스턴스 모델 정의 |
- API 계약과 DB 스키마를 단일 파일에서 관리 - Alembic과 통합 용이 |
- 비표준 명세에 대한 의존성 - Python 생태계에 종속적 - API 모델과 DB 모델이 크게 다를 경우 복잡성 증가 |
| DTO-to-Entity 패턴 (본 보고서 권장) | 간접적/계층적 생성 | Java (JPA, MapStruct), Python 등 범용 | - 관심사의 명확한 분리: API 모델(DTO)과 퍼시스턴스 모델(Entity) |
- 정규화, 관계 설정 등 세밀한 DB 설계 가능 - MapStruct, JPA Buddy 등 보조 도구를 통한 생산성 향상 |
- DTO, Entity, Mapper 등 추가적인 코드 계층 필요 - 초기 설정이 상대적으로 복잡함 |
섹션 3: 기술 스택별 구현 청사진
이전 섹션에서 제시된 추상적인 아키텍처와 워크플로우를 실제 코드로 구현하기 위한 구체적인 청사진을 제공한다. 여기서는 대표적인 엔터프라이즈 기술 스택인 Java/Spring과 Python/FastAPI를 중심으로, 각 생태계의 특성에 맞는 구현 단계를 상세히 설명한다.
3.1 청사진 A: Java 생태계 (Spring Boot, JPA, MapStruct)
Java 생태계는 계층형 아키텍처(Controller-Service-Repository)와 명확한 역할 분리를 중시하는 경향이 있으며, 관련 도구들 역시 이러한 철학을 반영한다.
3.1.1 openapi-generator-maven-plugin을 이용한 DTO 및 API 인터페이스 생성
이 단계의 목표는 OpenAPI 명세를 기반으로 API 계약에 해당하는 Java 코드(DTO 및 API 인터페이스)를 자동으로 생성하는 것이다. 이는 pom.xml 파일에 openapi-generator-maven-plugin을 설정함으로써 이루어진다.
다음은 pom.xml의 설정 예시이다.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.5.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api/spec.yml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>com.example.api</apiPackage>
<modelPackage>com.example.api.model</modelPackage>
<configOptions>
<delegatePattern>true</delegatePattern>
<useSpringBoot3>true</useSpringBoot3>
<interfaceOnly>true</interfaceOnly>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
- inputSpec: OpenAPI 명세 파일의 위치를 지정한다.
- generatorName: spring으로 설정하여 Spring Boot 애플리케이션에 적합한 코드를 생성한다.
-
- apiPackage, modelPackage: 생성될 API 인터페이스와 DTO 클래스의 패키지 경로를 지정한다.
- delegatePattern=true: 이 옵션은 매우 중요하다. API 로직을 직접 구현하는 컨트롤러 대신, PetApiDelegate와 같은 위임(delegate) 인터페이스를 생성한다. 우리의 서비스 클래스가 이 인터페이스를 구현하도록 함으로써, 자동 생성된 API 계약 코드와 실제 비즈니스 로직을 깔끔하게 분리할 수 있다.
-
3.1.2 DTO-to-Entity 패턴: API와 퍼시스턴스 계층 연결
API로부터 받은 데이터를 데이터베이스에 저장하기 위해서는 두 계층을 연결하는 모델이 필요하다.
- 생성된 DTO: 앞선 단계에서 PetDto.java와 같은 DTO 클래스가 modelPackage에 자동으로 생성된다. 이 클래스는 API 명세의 스키마를 그대로 반영한다.
- 수동으로 생성하는 JPA 엔티티: 개발자는 이 DTO를 참조하여 데이터베이스 테이블과 매핑될 Pet.java 클래스를 직접 작성한다. 이 클래스에는 @Entity, @Table, @Id, @GeneratedValue, @Column과 같은 JPA 어노테이션과 @ManyToOne 등의 관계 매핑 어노테이션이 추가된다.
이처럼 DTO와 엔티티를 분리하는 것은 엔터프라이즈 애플리케이션의 모범 사례로 간주된다. DTO는 데이터 전송을 위한 객체로, 불변성(immutability)을 가지는 Java Record로도 구현할 수 있다. 반면, JPA 엔티티는 하이버네이트의 퍼시스턴스 컨텍스트(persistence context) 내에서 상태 변경을 추적해야 하므로 반드시 변경 가능한(mutable) 클래스로 작성되어야 한다. 이 분리 원칙은 각 계층의 역할을 명확히 하고 유연성을 높인다.
3.1.3 MapStruct를 이용한 매핑 자동화
DTO를 엔티티로, 또는 그 반대로 변환하는 코드는 반복적이고 오류가 발생하기 쉽다. MapStruct는 이러한 상용구 코드를 컴파일 시점에 자동으로 생성해주는 어노테이션 프로세서이다.
다음은 PetMapper 인터페이스의 예시이다.
@Mapper(componentModel = "spring")
public interface PetMapper {
@Mapping(source = "id", target = "petApiId") // 필드명이 다른 경우 매핑
@Mapping(target = "id", ignore = true) // DB 자동 생성 ID는 무시
Pet toEntity(PetDto petDto);
@Mapping(source = "petApiId", target = "id")
PetDto toDto(Pet pet);
}
- @Mapper(componentModel = "spring"): MapStruct가 생성할 구현체를 스프링 빈(Bean)으로 등록하도록 지시한다. 이를 통해 서비스 계층에서 의존성 주입(@Autowired)으로 매퍼를 사용할 수 있다.
- @Mapping: DTO와 엔티티 간에 필드 이름이 다르거나 특정 필드를 무시해야 할 때 사용한다.
3.1.4 JPA와 Flyway/Liquibase를 이용한 스키마 관리
- 개발 환경: application.properties 파일에 spring.jpa.hibernate.ddl-auto=update를 설정하면, Spring Boot가 시작될 때 애플리케이션의 엔티티 정의와 데이터베이스 스키마를 비교하여 변경 사항을 자동으로 적용해준다. 이는 개발 단계에서 매우 편리하지만, 데이터 유실의 위험이 있어 운영 환경에서는 절대 사용해서는 안 된다.
- 운영 환경: 운영 환경에서는 Flyway나 Liquibase와 같은 데이터베이스 마이그레이션 도구를 사용해야 한다. 워크플로우는 다음과 같다.
- JPA Buddy와 같은 IDE 플러그인을 사용하여 현재 정의된 엔티티들로부터 초기 DDL 스크립트를 생성한다.
-
- 생성된 DDL을 V1__create_initial_tables.sql과 같이 버전 번호가 포함된 SQL 파일로 저장한다.
- pom.xml에 Flyway 또는 Liquibase 스타터 의존성을 추가하면, Spring Boot가 애플리케이션 시작 시 자동으로 마이그레이션 스크립트를 실행하여 데이터베이스 스키마를 관리한다.
-
3.2 청사진 B: Python 생태계 (FastAPI, SQLAlchemy, OpenAlchemy)
Python 생태계는 '설정으로서의 코드(Configuration-as-Code)' 철학을 바탕으로, 단일 진실 공급원(Single Source of Truth)을 지향하는 보다 직접적인 접근법을 제공한다.
3.2.1 OpenAlchemy를 이용한 SQLAlchemy 모델 직접 생성
OpenAlchemy는 OpenAPI 명세 파일 자체를 확장하여 퍼시스턴스 모델을 정의하는 독특한 방식을 사용한다.
다음은 OpenAlchemy 확장 속성이 포함된 openapi.yml 예시이다.
components:
schemas:
Employee:
type: object
x-tablename: employee # 테이블 이름 정의
properties:
id:
type: integer
x-primary-key: true # 기본 키 정의
x-autoincrement: true # 자동 증가
name:
type: string
x-index: true # 인덱스 생성
division:
type: string
- x-tablename, x-primary-key, x-autoincrement, x-foreign-key 등 비표준 x- 접두사 속성을 사용하여 데이터베이스 관련 정보를 명세에 직접 기술한다.
이 명세 파일을 사용하여 다음과 같이 Python 코드에서 SQLAlchemy 모델을 초기화할 수 있다.
from open_alchemy import init_yaml
# spec.yml 파일을 읽어 SQLAlchemy 모델을 동적으로 생성하고
# open_alchemy.models 모듈에 등록한다.
init_yaml("spec.yml")
# 이제 모델을 임포트하여 사용할 수 있다.
from open_alchemy.models import Employee
이 방식은 Java의 DTO와 엔티티 개념을 하나의 정의로 통합하여 간결함을 제공한다.
3.2.2 FastAPI와의 통합
FastAPI는 Pydantic 모델을 기반으로 OpenAPI 문서를 자동으로 생성하는 것으로 유명하지만 , OpenAlchemy와 같이 명세로부터 코드를 생성하는 방향으로도 통합이 가능하다. OpenAlchemy가 생성한 SQLAlchemy 모델은 FastAPI 애플리케이션 내에서 데이터베이스 연동 및 데이터 유효성 검사에 활용될 수 있다. 또 다른 프레임워크인 Connexion은 OpenAPI 명세로부터 직접 Flask 애플리케이션을 빌드하여 엔드포인트를 Python 함수에 매핑하는 기능을 제공한다.
3.2.3 Alembic을 이용한 데이터베이스 마이그레이션
Alembic은 SQLAlchemy를 위한 표준 데이터베이스 마이그레이션 도구이다. OpenAlchemy로 생성된 모델과 함께 사용하는 절차는 다음과 같다.
- Alembic 초기화: alembic init alembic 명령어로 마이그레이션 환경을 설정한다.
- env.py 수정: Alembic이 SQLAlchemy 모델을 인식할 수 있도록, env.py 파일에 open_alchemy.init_yaml(...) 호출 코드를 추가한다.
- 마이그레이션 스크립트 자동 생성: alembic revision --autogenerate -m "Initial migration" 명령어를 실행하면, Alembic이 env.py를 통해 로드된 모델과 실제 데이터베이스 스키마를 비교하여 변경 사항을 담은 마이그레이션 스크립트를 생성한다.
- 마이그레이션 적용: alembic upgrade head 명령어로 생성된 스크립트를 데이터베이스에 적용한다.
결론적으로, 기술 스택의 선택은 스키마 생성 워크플로우에 강한 경로 의존성을 부여한다. Java/Spring 방식은 계층 분리를 통해 유연성과 명확성을 얻는 대신 더 많은 구성 요소를 필요로 한다. 반면, Python/OpenAlchemy 방식은 단일 명세 파일을 통해 간결함을 추구하지만, 특정 도구와 비표준 확장에 대한 의존성이 높아지는 트레이드오프가 있다. 기술 리더는 단순히 '명세로 스키마를 생성하자'는 목표를 넘어, 팀의 철학과 숙련도에 맞는 아키텍처 패턴과 도구 체인을 신중하게 선택해야 한다.
섹션 4: 고급 데이터베이스 모델링 및 무결성
기본적인 테이블 생성을 넘어, 외부 API로부터 유입되는 실제 데이터의 품질과 무결성을 보장하기 위한 고급 데이터베이스 모델링 기법을 다룬다. 이 섹션에서는 자동화된 스키마 생성만으로는 해결하기 어려운 미묘하지만 중요한 문제들을 집중적으로 분석하고, 이에 대한 구체적인 해결책을 제시한다.
4.1 실제 데이터 제약 조건 모델링
API 명세는 데이터의 구조를 정의하지만, 그 데이터가 담고 있는 비즈니스 규칙이나 고유한 형식까지 완벽하게 표현하지는 못한다. 데이터베이스 스키마는 이러한 암묵적인 규칙을 명시적인 제약 조건으로 변환하여 데이터의 무결성을 최후의 보루로서 지켜야 한다.
한국의 사업자등록번호(BRN)와 법인등록번호(CRN)는 이러한 모델링 전략의 좋은 예시이다.
- 사업자등록번호 (Business Registration Number): XXX-XX-XXXXX 형식의 10자리 숫자로 구성된다. 중간 2자리는 개인/법인, 과세/면세 등 사업자 유형을 구분하는 코드를 포함한다.
- 법인등록번호 (Corporate Registration Number): XXXXXX-XXXXXXX 형식의 13자리 숫자로, 등기소 분류 번호와 법인 종류 분류 번호 등의 정보를 담고 있다.
이러한 식별 번호들을 단순히 BIGINT와 같은 숫자 타입으로 저장하는 것은 부적절하다. 하이픈(-)을 포함한 전체 형식을 보존하고, 데이터의 유효성을 검증하기 위해 VARCHAR(12) 또는 VARCHAR(14)와 같은 문자열 타입으로 저장하는 것이 바람직하다. 더 나아가, 데이터베이스 수준에서 형식을 강제하기 위해 정규 표현식을 사용한 CHECK 제약 조건을 추가해야 한다.
예시 (PostgreSQL):
CREATE TABLE company (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
business_registration_number VARCHAR(12) NOT NULL
CHECK (business_registration_number ~ '^\d{3}-\d{2}-\d{5}$')
);
이러한 CHECK 제약 조건은 애플리케이션 로직을 우회하는 데이터 입력(예: 수동 데이터 수정, 다른 시스템으로부터의 벌크 임포트)으로부터 데이터베이스를 보호하는 강력한 수단이다. 이는 데이터 무결성이 애플리케이션의 책임만이 아니라 데이터베이스 자체의 책임임을 명확히 하는 설계 원칙이다.
4.2 고유성(Uniqueness)의 미묘함: Nullable 컬럼에 대한 UNIQUE 제약 조건 심층 분석
API 응답에는 선택적으로 제공되는 필드가 있을 수 있다. 예를 들어, 사용자의 '별칭(alias)' 필드는 NULL일 수 있지만, 값이 존재할 경우에는 시스템 전체에서 고유해야 하는 요구사항이 있을 수 있다. 이때 단순히 해당 컬럼에 UNIQUE 제약 조건을 추가하는 것은 RDBMS 제품별로 예기치 않은 결과를 초래할 수 있다.
- SQL 표준 및 대부분의 RDBMS (PostgreSQL, MySQL, Oracle): UNIQUE 제약 조건은 NULL 값을 고유성 검사에서 제외한다. 즉, NULL 값은 다른 NULL 값과 중복으로 간주되지 않아 여러 개의 NULL 값이 허용된다. 이는 NULL이 '알 수 없는 값'이라는 개념에 기반한다. 하지만 여기서도 RDBMS 간에 미세한 차이가 존재한다. 복합 고유 키 UNIQUE(col_a, col_b)의 경우, (1, NULL)과 (1, NULL) 같은 행에 대해 MySQL은 이를 중복으로 보지 않지만 , Oracle은 중복으로 간주하여 오류를 발생시킨다. PostgreSQL은 이들을 서로 다른 값으로 취급한다.
- SQL Server (비표준 동작): UNIQUE 제약 조건이 걸린 컬럼에는 단 하나의 NULL 값만 허용된다. 두 번째 NULL 값을 삽입하려고 하면 고유성 제약 조건 위반 오류가 발생한다.
이러한 RDBMS 벤더 간의 구현 차이는 자동화된 스키마 생성 도구를 사용할 때나, 특정 데이터베이스를 염두에 두고 개발된 애플리케이션을 다른 데이터베이스로 이식할 때 심각한 문제를 야기할 수 있는 주요 위험 요소이다.
4.3 조건부 고유성 강제를 위한 RDBMS별 전략
이러한 문제를 해결하고 '값이 있을 경우에만 고유성'을 보장하기 위한 전략은 데이터베이스 제품에 따라 다르다.
- 전략 1: 필터링된 인덱스 / 부분 인덱스 (Filtered/Partial Indexes - PostgreSQL, SQL Server): 가장 우아하고 직관적인 해결책이다. NULL이 아닌 행에 대해서만 고유 인덱스를 생성한다.
- PostgreSQL: CREATE UNIQUE INDEX ON users (alias) WHERE alias IS NOT NULL;
- SQL Server: CREATE UNIQUE INDEX ON users (alias) WHERE alias IS NOT NULL;
- 전략 2: 함수 기반 인덱스 (Function-Based Indexes - Oracle, PostgreSQL): NULL 값을 특정 값으로 변환하는 함수를 인덱스에 적용하여 고유성을 제어한다. Oracle에서는 모든 NULL 포함 키를 인덱싱하지 않는 특성을 교묘하게 활용하는 기법도 가능하다.
- Oracle 예시: CREATE UNIQUE INDEX idx_t_unique ON t (CASE WHEN col1 IS NULL THEN NULL ELSE col1 END, CASE WHEN col2 IS NULL THEN NULL ELSE col2 END); 이 기법은 col1 또는 col2 중 하나라도 NULL이면 인덱스 키 전체를 NULL로 만들어, Oracle이 이러한 키를 인덱싱하지 않도록 유도한다.
- Oracle 예시: CREATE UNIQUE INDEX idx_t_unique ON t (CASE WHEN col1 IS NULL THEN NULL ELSE col1 END, CASE WHEN col2 IS NULL THEN NULL ELSE col2 END); 이 기법은 col1 또는 col2 중 하나라도 NULL이면 인덱스 키 전체를 NULL로 만들어, Oracle이 이러한 키를 인덱싱하지 않도록 유도한다.
- 전략 3: 가상/생성 컬럼 (Virtual/Generated Columns - MySQL 5.7+, PostgreSQL 12+): 조건부 고유성을 위한 별도의 컬럼을 생성하는 방식이다. 예를 들어, 소프트 삭제(soft delete)된 레코드를 제외하고 활성 레코드에 대해서만 고유성을 강제하고 싶을 때 유용하다.
- MySQL 예시: ALTER TABLE articles ADD not_archived INT AS (IF(deleted_at IS NULL, 1, NULL)) VIRTUAL;
- ALTER TABLE articles ADD CONSTRAINT UNIQUE (name, not_archived);
- 이 방식은 deleted_at이 NULL인 (즉, 활성 상태인) 레코드에 대해서만 (name, 1) 조합의 고유성을 강제한다.
이처럼 조건부 고유성 처리는 데이터베이스의 특성을 깊이 이해하고 수동으로 개입해야 하는 영역이며, 이는 OpenAPI 명세로부터의 직접적인 DDL 생성이 왜 불충분한지를 보여주는 대표적인 사례이다.
다음 표는 주요 RDBMS별 UNIQUE 제약 조건의 동작 방식과 권장 구현 패턴을 요약한 것이다. 이 표는 아키텍트와 개발자가 데이터베이스를 선택하고 이식성을 고려할 때 발생할 수 있는 위험을 완화하는 데 중요한 참고 자료가 될 것이다.
| RDBMS | 단일 Nullable 컬럼 동작 | 복합 키 내 Nullable 컬럼 동작 | 권장 구현 패턴 (값이 있을 때만 고유) |
| PostgreSQL | 여러 NULL 허용 | (1, NULL)과 (1, NULL)은 서로 다른 값으로 취급 | 부분 인덱스 (Partial Index) sql\nCREATE UNIQUE INDEX ON tbl (col) WHERE col IS NOT NULL;\n |
| MySQL | 여러 NULL 허용 | (1, NULL)과 (1, NULL)은 서로 다른 값으로 취급 | 가상 컬럼 (Generated Column) sql\nALTER TABLE tbl ADD col_unique INT AS (IF(col IS NULL, NULL, 1)) VIRTUAL;\nALTER TABLE tbl ADD CONSTRAINT UNIQUE (col, col_unique);\n |
| SQL Server | 단 하나의 NULL만 허용 |
(1, NULL)과 (1, NULL)은 중복으로 간주 | 필터링된 인덱스 (Filtered Index) sql\nCREATE UNIQUE INDEX ON tbl (col) WHERE col IS NOT NULL;\n |
| Oracle | 여러 NULL 허용 | (1, NULL)과 (1, NULL)은 중복으로 간주 (오류 발생) |
함수 기반 인덱스 (Function-Based Index) sql\nCREATE UNIQUE INDEX ON tbl (CASE WHEN col IS NULL THEN NULL ELSE col END);\n |
섹션 5: 고처리량 데이터 처리를 위한 아키텍처
이 섹션에서는 '대용량' 데이터 처리라는 핵심 요구사항을 충족시키기 위한 애플리케이션 수준의 구체적인 전략을 다룬다. 대규모의 정해진 데이터를 처리하기 위한 병렬 배치 처리 방식과, 지속적으로 유입되는 데이터를 처리하기 위한 비동기 스트림 처리 방식을 상세히 설명한다. 또한, 시스템 전반의 성능을 최적화하기 위한 캐싱 전략을 함께 제시한다.
5.1 전략 1: Spring Batch 파티셔닝을 이용한 병렬 배치 처리
이 전략은 "지난 한 달간의 모든 기록 가져오기" 또는 "1만 개 기업에 대한 데이터 재수집"과 같이, 처리해야 할 데이터의 전체 범위가 명확한 대규모 배치(batch) 작업에 이상적이다.
핵심 개념: Spring Batch의 파티셔닝(Partitioning)은 하나의 Step을 여러 개의 '슬레이브(slave)' Step으로 분할하고, 이를 별도의 스레드에서 병렬로 실행하는 기능이다. '마스터(master)'
Step이 이 분할 및 실행 과정을 총괄한다.
API 소스를 위한 Partitioner 설계: Partitioner 인터페이스는 각 슬레이브가 처리할 작업 단위를 정의하는 ExecutionContext를 생성하는 역할을 한다. API 데이터 수집 시나리오에서는 다음과 같은 기준으로 파티션을 나눌 수 있다.
- 날짜 범위 기준: 마스터가 전체 데이터의 시작일과 종료일을 파악한 후, 이를 일별 또는 주별로 분할한다. 각 슬레이브는 startDate와 endDate를 ExecutionContext를 통해 전달받는다.
- ID 범위 기준: 마스터가 대상 테이블의 MIN(id)와 MAX(id)를 조회하여 ID 범위를 분할한다. 각 슬레이브는 fromId와 toId를 전달받아 해당 범위의 데이터만 처리한다.
- 엔티티 목록 기준: 마스터가 처리해야 할 모든 엔티티(예: 기업 ID 목록)를 조회한 후, Partitioner가 이 목록을 여러 개의 작은 목록으로 나누어 각 슬레이브에 할당한다.
슬레이브 Step 구현: 슬레이브 Step의 ItemReader는 @StepScope 어노테이션을 사용하여 실행 시점에 마스터로부터 전달받은 ExecutionContext의 파라미터를 주입받아야 한다. 예를 들어,
JdbcPagingItemReader는 이 파라미터를 사용하여 데이터베이스에서 자신이 처리할 범위의 데이터만 읽어오고, 커스텀 ItemReader는 이 파라미터를 기반으로 API 요청을 동적으로 생성할 수 있다.
스레딩 및 확장: 병렬 처리 수준은 TaskExecutor(예: ThreadPoolTaskExecutor)와 grid-size 파라미터를 통해 제어된다.
grid-size는 동시에 실행될 슬레이브의 수, 즉 스레드 풀의 크기를 결정한다.
5.2 전략 2: Kafka 기반 아키텍처를 통한 비동기 스트림 처리
이 전략은 실시간 또는 준실시간으로 지속적인 데이터 수집이 필요한 경우에 사용되는 핵심 패턴으로, 섹션 1에서 제시된 아키텍처의 구체적인 구현 방식이다.
수집과 영속화의 분리: 이 아키텍처의 가장 큰 장점은 'API 호출 서비스'가 자신의 처리 속도에 맞춰 Kafka에 데이터를 쓰는 동안, 'DB 쓰기 서비스'는 데이터베이스의 처리 능력에 맞춰 Kafka로부터 데이터를 소비한다는 점이다. 이 비동기 버퍼링은 시스템 전체의 안정성을 극적으로 향상시킨다.
Reader/Writer 성능 최적화:
- Kafka Producer (API 호출 서비스): Kafka 생산자는 batch.size와 linger.ms 옵션을 조정하여 여러 메시지를 한 번에 묶어 전송함으로써 높은 처리량을 달성할 수 있다.
- Kafka Consumer (DB 쓰기 서비스): Spring Kafka의 @KafkaListener는 spring.kafka.listener.type=batch 설정을 통해 배치 모드로 동작할 수 있다. 이 경우, 리스너는 단일 메시지가 아닌 메시지 목록(List)을 한 번에 수신하게 된다. 이를 통해 서비스는 데이터베이스에 대해 단일 INSERT 문으로 여러 행을 한꺼번에 삽입하는 벌크(bulk) 연산을 수행할 수 있으며, 이는 개별 INSERT를 반복하는 것보다 성능 면에서 압도적으로 우수하다.
Spring Batch와의 결합: Spring Batch 잡을 Kafka 토픽의 소비자로 활용할 수도 있다. Spring Batch가 제공하는 KafkaItemReader를 사용하면 Kafka 토픽으로부터 메시지를 읽고, 이를 청크(chunk) 단위로 처리할 수 있다. 이는 Kafka의 스트리밍 데이터 처리에 Spring Batch의 강력한 트랜잭션 관리 및 재시작 기능을 결합하는 효과적인 방법이다.
Spring Batch의 ItemReader 선택 시, JdbcPagingItemReader와 JdbcCursorItemReader 사이의 트레이드오프를 이해하는 것이 중요하다. JdbcCursorItemReader는 단일 데이터베이스 커넥션을 열고 결과를 스트리밍하여 애플리케이션 메모리 사용량이 적지만 , 스텝이 끝날 때까지 DB 커넥션을 계속 유지한다. 반면,
JdbcPagingItemReader는 각 페이지마다 별도의 쿼리를 실행하여 커넥션을 열고 닫기 때문에 DB 자원을 더 많이 사용하지만 커넥션을 오래 점유하지 않는다. 가장 결정적인 차이는 스레드 안전성이다.
JdbcCursorItemReader는 단일 ResultSet을 여러 스레드가 공유할 수 없으므로 스레드에 안전하지 않다. 따라서, 여러 스레드에서 병렬로 실행되는 Spring Batch 파티셔닝 환경에서는 반드시 스레드에 안전한
JdbcPagingItemReader나 다른 페이징 기반 리더를 사용해야 한다.
5.3 성능 최적화: 캐싱의 역할
API는 대부분 호출 횟수 제한(rate limit)이 있으며, 자주 변경되지 않는 동일한 데이터를 반복적으로 요청하는 것은 비효율적이고 API 제한에 도달할 위험을 높인다.
해결책: Redis를 이용한 Cache-Aside 패턴
- 패턴 로직: 애플리케이션이 데이터를 필요로 할 때, 먼저 Redis 캐시를 확인한다. 데이터가 캐시에 존재하면(cache hit), 즉시 반환한다. 데이터가 없으면(cache miss), 원본 데이터 소스(API)를 호출하여 데이터를 가져온 후, 그 결과를 캐시에 저장하고 사용자에게 반환한다.
- Spring Boot 구현: Spring의 캐시 추상화(Cache Abstraction)를 사용하면 이 패턴을 매우 간단하게 구현할 수 있다.
- pom.xml에 spring-boot-starter-data-redis와 spring-boot-starter-cache 의존성을 추가한다.
- 메인 애플리케이션 클래스에 @EnableCaching 어노테이션을 추가하여 캐싱 기능을 활성화한다.
- API를 호출하는 서비스 메소드에 @Cacheable("myCacheName") 어노테이션을 붙이면, Spring이 cache-aside 로직 전체를 자동으로 처리해준다.
- pom.xml에 spring-boot-starter-data-redis와 spring-boot-starter-cache 의존성을 추가한다.
데이터 수집 시나리오에서의 캐싱 활용:
- 호출 제한 관리: 비용이 비싸거나 호출 횟수 제한이 엄격한 API 엔드포인트의 응답을 캐싱한다.
- 참조 데이터 캐싱: 자주 조회되지만 거의 변경되지 않는 데이터(예: 사용자 정보, 카테고리 목록 등)를 캐싱하여 데이터베이스 조인이나 추가적인 API 호출을 줄인다.
왜 Redis인가?: 간단한 ConcurrentHashMap도 캐시로 사용할 수 있지만, Redis는 분산 캐시를 제공하므로 수평적으로 확장된 마이크로서비스의 모든 인스턴스가 동일한 캐시 상태를 공유할 수 있다. 또한, Redis는 영속성 옵션과 함께 해시(Hash)와 같은 고급 데이터 구조를 지원한다. Redis 해시는 객체를 캐싱하는 데 특히 유용하며, 객체 전체를 다시 가져올 필요 없이 특정 필드만 개별적으로 업데이트할 수 있게 해준다.
결론적으로, Spring Batch 파티셔닝과 Kafka 기반 스트리밍은 경쟁 관계가 아닌, 서로 다른 시나리오를 위한 상호 보완적인 도구이다. 성숙한 데이터 수집 플랫폼은 일상적인 연속 수집을 위해 Kafka 기반 스트리밍 아키텍처를 사용하고, 역사적 데이터 로드나 데이터 보정 같은 특정 작업을 위해 Spring Batch 파티셔닝 잡을 실행하는 하이브리드 접근 방식을 채택해야 한다.
섹션 6: 종합 및 최종 아키텍처 권고
본 보고서의 이전 섹션들에서 논의된 다양한 기술, 패턴, 그리고 철학을 종합하여, 대규모 데이터 API 수집을 위한 최종적인 아키텍처를 권고한다. 이 아키텍처는 확장성, 복원력, 그리고 유지보수성의 균형을 맞추는 하이브리드 모델을 지향한다.
6.1 통합 하이브리드 아키텍처 권고
최종적으로 권고하는 아키텍처는 **'하이브리드 이벤트 기반 마이크로서비스 및 배치 처리 시스템'**이다. 이 시스템은 서로 다른 데이터 처리 요구사항에 대응하기 위해 두 가지 핵심 전략을 결합한다.
- 지속적 데이터 수집 (Continuous Ingestion): 시스템의 주축은 섹션 1에서 제시된 이벤트 기반 마이크로서비스 아키텍처가 담당한다.
- 특정 API 그룹별로 최적화된 API-Poller 서비스들이 데이터를 수집한다.
- Apache Kafka가 시스템의 중추 신경망으로서, 수집된 데이터의 내구성 있는 버퍼 역할을 수행한다.
- Data-Transformer와 DB-Writer 서비스는 Kafka 토픽을 구독하며, 특히 DB-Writer는 배치 소비(batch consumption) 및 벌크 INSERT/UPDATE를 통해 데이터베이스 쓰기 성능을 극대화한다.
- 대규모/과거 데이터 수집 (Bulk/Historical Ingestion): 역사적 데이터의 대량 이관이나 특정 조건에 따른 대규모 재처리 작업은 섹션 5에서 논의된 Spring Batch 파티셔닝 모델을 통해 수행한다.
- 온디맨드(on-demand)로 실행되는 이 배치 잡들은 ID 범위나 날짜 범위 등으로 작업을 분할하여 병렬 처리한다.
- 이 잡들은 API를 직접 호출하거나, 처리할 대상 목록을 데이터베이스에서 읽어올 수 있다.
- 처리된 결과는 지속적 수집 파이프라인과 동일한 Kafka 토픽으로 발행하여, 후속 Transformer 및 DB-Writer 서비스의 로직을 재사용한다. 이는 코드 중복을 피하고 시스템의 일관성을 유지하는 효율적인 방법이다.
- 공통 캐싱 계층 (Caching Layer): 모든 마이크로서비스는 공통의 분산 Redis 캐시를 공유한다.
- Spring의 캐시 추상화를 통해 구현된 Cache-Aside 패턴을 사용하여, 자주 사용되지만 변경이 적은 참조 데이터나 호출 비용이 비싼 API 응답을 캐싱한다.
- 이는 API 호출 횟수를 줄여 속도 제한(rate limiting) 문제를 완화하고, 데이터베이스 부하를 경감시켜 시스템 전반의 응답성과 성능을 향상시킨다.
6.2 자동화 워크플로우에서 도메인 기반 정제의 중심적 역할
OpenAPI 명세를 활용한 RDBMS 스키마 생성 프로세스는 '반자동화'되어야 함을 다시 한번 강조한다. 가장 견고하고 실용적인 워크플로우는 다음과 같은 순서를 따른다.
명세 분석 → DTO 자동 생성 → 엔티티 수동 설계 → DTO-엔티티 매핑 → 엔티티 기반 스키마 생성 및 관리
여기서 '엔티티 수동 설계' 단계는 이 워크플로우의 약점이 아니라 핵심적인 강점이다. 이 단계는 자동화 도구가 할 수 없는, 도메인 지식과 데이터베이스 설계 원칙을 시스템에 적용하는 과정이다. 개발자는 이 단계를 통해 외부 API 계약의 변화로부터 내부 데이터 모델을 보호하는 안정적인 퍼시스턴스 계층을 구축할 수 있다. 즉, 이 수동 개입은 시스템 데이터 자산의 장기적인 건전성을 보장하는 가장 중요한 아키텍처 활동이다.
6.3 최종 아키텍처 다이어그램 및 결론
다이어그램은 지속적 수집을 위한 이벤트 기반 흐름(파란색 화살표)과 대규모 배치를 위한 파티셔닝 잡의 흐름(녹색 화살표)이 어떻게 Kafka를 중심으로 통합되는지를 보여준다. 또한, 모든 서비스가 Redis 캐시를 공유하여 성능을 최적화하는 모습을 나타낸다.
결론적으로, 본 보고서에서 제안하는 아키텍처는 대규모 데이터를 장기간에 걸쳐 안정적으로 수집하고 관리해야 하는 복잡한 요구사항에 대응하기 위해 설계되었다. 초기 구축 비용과 운영 복잡성은 단순한 모놀리식 접근법보다 높을 수 있다. 그러나 마이크로서비스의 독립성, Kafka를 통한 비동기 처리의 복원력, Spring Batch 파티셔닝을 통한 병렬 처리 능력, 그리고 체계적인 데이터 모델링 워크플로우는 장기적인 관점에서 비교할 수 없는 확장성과 유지보수성을 제공할 것이다. 이 아키텍처는 변화에 유연하게 대응하고, 데이터 처리량이 증가함에 따라 선형적으로 확장할 수 있는 견고한 기반이 될 것이다.
'IT기술' 카테고리의 다른 글
| 문서 마크다운 변환 도구 비교 (3) | 2025.07.25 |
|---|