현재 내가 하고 있는 프로젝트에서 매우 큰 요구사항이 생겼다. 바로 실시간 5분단위의 차속(차량의 속도)데이터 1년치를 DB에 적재시키고 활용해야 된다는 것이다.
용량만 대충 계산해봐도 ... 365*24*12*500MB(압축풀면 6GB) == ?
일단 활용은 둘째치고 올리는거 부터 문제라는 생각에 어떤식으로 아키텍처를 구축해야되나.. 고민하기 시작했다.
이런 조건에서 PL님의 생각은
Bat 파일로 만들어서 저녁마다 돌리거나, 그냥 일반 배치로 가자. 왜냐하면 기존부터 써왔고, 그게 추후 유지보수나 이전할때도 편하니까!
라고 하셨다. 물론 SI회사의 입장에서는 충분히 이해가 가는 상황이지만... 여기엔 치명적인 단점들이 매우 많았는데,
1년치를 1개씩 넣으려면 8~10일 동안 배치만 돌려야함배치 작업이 실패하면 롤백을 어떻게 하고 결측치를 어떻게 보완하지?1번과 연결되는 문제이러한 문제들이 예상되는 상황에서 아무 대처도 안해보고 넘어갈 수는 없었고, PL님께 의견을 말해본 결과!
결론 부터 말하자만 결국은 SI의 특성때문에 어쩔 수 없이 배치로 갈 것 같지만...
너가 해보고 싶은대로 데이터 적재부분 시스템을 구축해보라고 약 3일간 시간을 주셨다!!!!!!!!!!

그래서 그 3일간의 여정을 기록해 보고자 한다
PL님 감사합니다
이러한 문제를 해결하기 위해 내가 선택한 기술은 Apache Kafka와 Apache Spark를 기반으로 한 새로운 아키텍처였다.
| 기술 | 장점 | 단점 | 이유 |
|---|---|---|---|
| Apache Kafka | - 높은 처리량 - 영구 저장 기능 - 메시지 재생 가능 - 분산 시스템 지원 | - 설정 복잡성 - 리소스 요구사항 | 선택: 메시지 손실 방지 및 작업 재시도 기능이 중요 |
| RabbitMQ | - 쉬운 설정 - 다양한 메시징 패턴 | - 상대적으로 낮은 처리량 - 영구 저장 제한적 | 처리량이 낮고 영구 저장이 제한적 |
| ActiveMQ | - JMS 표준 준수 - 쉬운 통합 | - 대규모 처리에 성능 제약 | 대용량 처리에 최적화되지 않음 |
| 전자정부 표준 배치 | - 기존 인프라 활용 - 개발자 친숙성 | - 비동기 처리 부족 - 상태 관리 어려움 | 실시간 모니터링 및 비동기 처리 부족 |
Kafka를 선택한 주요 이유는 메시지 손실 방지와 장애 복구 능력이다. 1억 4천만 행의 데이터를 365 * 24 * 12 개를 처리하는 중 시스템 장애가 발생하더라도, Kafka는 메시지를 디스크에 영구 저장하기 때문에 처리가 중단된 지점부터 재개할 수 있다. 또한 Kafka의 토픽 분할(파티셔닝) 기능은 대량의 메시지를 효율적으로 처리할 수 있게 해준다.
RabbitMQ나 ActiveMQ 같은 대안도 검토했지만, 이들은 주로 트랜잭션 메시징에 최적화되어 있어 대용량 데이터 스트림 처리에는 성능 제약이 있었다. 특히 8~10일 동안 계속해서 메시지를 안정적으로 처리해야 하는 우리 시나리오에서는 Kafka의 내구성이 결정적인 장점이었다.
| 기술 | 장점 | 단점 | 이유 |
|---|---|---|---|
| Apache Spark | - 메모리 기반 처리 - 강력한 병렬화 - SQL, ML 통합 - 배치/스트리밍 지원 | - 리소스 요구량 - 학습 곡선 | 선택: 메모리 내 처리와 병렬화로 최대 성능 |
| Hadoop MapReduce | - 안정성 - 대규모 데이터 처리 | - 디스크 I/O 병목 - 느린 처리 속도 | 처리 속도가 Spark보다 훨씬 느림 |
| 단일 스레드 배치 | - 간단한 구현 - 적은 오버헤드 | - 느린 처리 속도 - 병렬화 불가 | 처리 시간이 8~10일로 비현실적 |
| Spring Batch | - 기존 스택과 통합 - 개발자 친숙성 | - 제한된 병렬화 - 확장성 한계 | 대용량 데이터에 최적화되지 않음 |
| PostgreSQL 내부 처리 | - 추가 기술 불필요 - 간단한 구현 | - DB 서버 부하 - 제한된 병렬화 | DB 서버 부하 증가 및 성능 제약 |
Spark를 선택한 핵심 이유는 메모리 내 처리와 자동화된 병렬화 기능이다. 전통적인 Hadoop MapReduce와 달리, Spark는 중간 결과를 디스크에 쓰지 않고 메모리에 유지하여 처리 속도를 최대 100배까지 향상시킬 수 있다. 우리 서버는 64GB RAM을 갖추고 있어, 이 메모리를 활용한 Spark의 성능 이점을 최대한 활용할 수 있다고 판단했다.
Spring Batch와 같은 전통적인 배치 처리 솔루션도 고려했지만, 이는 단일 서버에서 대규모 병렬 처리를 효율적으로 지원하지 못한다. PostgreSQL 내부에서 처리하는 방식(예: pl/pgsql 함수 사용)도 검토했지만, 이는 데이터베이스 서버에 과도한 부하를 주고, 잘못하다가는 기존 서버까지 병목현상이 일어 날 것 같았다. 또한 CPU 및 메모리 리소스를 효율적으로 활용하지 못한다는 단점이 있었다.
비록 우리 환경은 서버 1대로 제한되어 있지만, Spark는 단일 서버 내에서도 효율적인 멀티코어 활용을 통해 처리 시간을 크게 단축할 수 있다고 판단했다. 실제로 우리 테스트에서는 일반 배치로 8~10일 걸리던 작업을 Spark로 2~3일 내로 단축할 것으로 예상되었다.
Kafka와 Spark의 조합은 단순히 개별 기술의 장점만 제공하는 것이 아니라, 시스템 전체에 추가적인 이점을 가져온다.
+------------------+
| |
| 사용자 인터페이스 |
| (Web Browser) |
| |
+--------+---------+
|
| HTTP 요청
v
+------------------+ 상태 저장 +-------------------+ 작업 요청 +------------------+
| |<-----------+ +----------->| |
| PostgreSQL | | Spring Boot | | Apache Kafka |
| Database | | (SparkJobService)| | Message Broker |
| (상태 테이블) |<-----------+ |<-----------+ |
+------------------+ 상태 조회 +-------------------+ 상태 업데이트 +------------------+
^ ^
| |
| 프로세스 시작 | 메시지 수신
| |
v v
+-----------+-----------+ +---------+---------+
| | | |
| Spark Process #1 (JVM)| | Spark Process #2 |
| (SparkAggregator) | | (SparkAggregator) |
| | | |
+-----------+-----------+ +---------+---------+
| |
| 결과 저장 | 결과 저장
| |
v v
+--------+--------------------------+
| |
| PostgreSQL Database |
| (집계 결과 테이블) |
| |
+-----------------------------------+
우리 시스템의 가장 큰 특징 중 하나는 작업 요청마다 독립적인 Spark 프로세스(JVM)를 실행하는 방식을 채택했다는 점이다. 이는 Spring Boot 애플리케이션(SparkJobService)이 Kafka에서 메시지를 수신하면, 외부 프로세스로 새로운 Spark 애플리케이션(SparkAggregator)을 시작하는 구조이다.
// 외부 프로세스로 Spark 애플리케이션 실행
ProcessBuilder pb = new ProcessBuilder(
"java",
"-Xmx8g",
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED",
// 기타 JVM 옵션들...
"-jar",
sparkJarPath, // Spark 애플리케이션 JAR 파일 위치
"--jobId=" + jobId,
"--fileName=" + fileName,
"--kafkaBrokers=" + kafkaBootstrapServers,
"--statusTopic=" + jobStatusTopic,
// 기타 인자들...
);
// 로그 파일 설정
pb.redirectErrorStream(true);
pb.redirectOutput(ProcessBuilder.Redirect.to(logFile));
// 프로세스 시작
Process process = pb.start();
이러한 방식을 선택한 이유는 다음과 같다.
이러한 구조는 전통적인 상시 실행 Spark 클러스터 방식과는 다소 차이가 있지만, 우리 시스템의 요구사항(비정기적인 대용량 처리, 작업별 격리, 간편한 운영 등)에 더 적합한 방식이었다.
spark-job-requests 토픽: 작업 요청 메시지 전달spark-job-status 토픽: 작업 상태 업데이트 메시지 전달spark_job_status 테이블)vs_its_hourly 테이블)지금까지 대용량 데이터 처리 시스템의 문제 인식과 기술 선택, 그리고 아키텍처 설계에 대해 살펴보았다. 다음 편에서는 실제 구현 과정에서 만난 문제점들과 그 해결 방법, 성능 최적화 여정, 그리고 이 프로젝트에서 얻은 교훈을 공유해보겠다!!
이건정말 경험상 좋아요 누를수밖에 없슴다