티스토리 뷰
Java 기반 서버 애플리케이션에서 MongoDB에 저장된 텍스트 데이터를 분석해야 하는 상황은 비교적 자주 발생합니다. 기술 뉴스, 사용자 게시글, 로그 메시지 등 다양한 형태의 텍스트 데이터에서 특정 기간 동안 자주 등장하는 키워드를 추출하거나, 변화 추이를 파악해야 하는 요구는 실무에서도 반복적으로 등장합니다.
이러한 텍스트 빈도 분석을 구현할 때 일반적으로 두 가지 접근을 고려하게 됩니다. 하나는 Java 애플리케이션에서 MongoDB 도큐먼트를 조회한 뒤, Stream API나 컬렉션 기반 로직으로 분석하는 방식입니다. 다른 하나는 MongoDB의 Aggregation Pipeline을 활용하여 데이터베이스 서버 내부에서 집계를 수행하는 방식입니다.
이 글에서는 실제 프로젝트에서 MongoDB Aggregation Pipeline을 활용해 서버사이드 텍스트 분석을 구현하면서 정리한 내용을 바탕으로, 두 접근 방식의 트레이드오프와 Aggregation 기반 설계의 의미를 정리합니다.
Java 클라이언트 처리와 서버사이드 집계의 트레이드오프
Java 애플리케이션에서 직접 처리하는 방식
Java에서 MongoDB 도큐먼트를 조회한 뒤 애플리케이션 코드로 텍스트를 분석하는 방식은 구현 자체만 놓고 보면 비교적 직관적입니다. Spring Data MongoDB의 Repository나 MongoTemplate을 사용해 데이터를 조회하고, Java Stream API를 활용해 문자열을 분리하고 빈도를 집계할 수 있습니다.
이 방식의 장점은 분명합니다. Java 개발자에게 익숙한 문법과 라이브러리를 그대로 활용할 수 있고, 디버깅과 로직 확장이 수월합니다. 형태소 분석이나 동의어 처리처럼 비교적 복잡한 자연어 처리 로직이 필요한 경우에도 Java 생태계의 라이브러리를 적극적으로 활용할 수 있습니다.
다만 분석 대상 도큐먼트 수가 증가할수록 부담이 커집니다. 모든 도큐먼트를 네트워크를 통해 애플리케이션 서버로 전송해야 하고, JVM 힙 메모리에 적재한 뒤 처리해야 합니다. 실제로 수십만 건 수준의 도큐먼트를 대상으로 텍스트 분석을 수행할 경우, 네트워크 전송량과 메모리 사용량이 성능 병목으로 이어질 가능성이 높아집니다.
MongoDB 서버에서 집계하는 방식
MongoDB Aggregation Pipeline은 데이터베이스 서버 내부에서 데이터 변환과 집계를 수행하기 위한 기능입니다. 여러 스테이지를 순차적으로 연결해 도큐먼트를 가공하며, 각 스테이지의 출력이 다음 스테이지의 입력이 됩니다.
서버사이드 집계의 가장 큰 특징은 네트워크 전송량을 최소화할 수 있다는 점입니다. 전체 도큐먼트를 애플리케이션으로 전달하지 않고, 이미 집계가 완료된 결과만 전달합니다. 예를 들어 상위 20개의 키워드만 필요한 경우, 실제로 네트워크를 통해 전달되는 데이터 역시 20개 결과 도큐먼트에 그칩니다.
또한 대량 데이터 처리 시 MongoDB 서버의 메모리 관리 방식과 내부 최적화를 활용할 수 있다는 점도 장점으로 작용합니다. 다만 Aggregation Pipeline의 문법과 실행 흐름은 Java 코드에 비해 직관성이 떨어질 수 있고, 복잡한 파이프라인은 디버깅이 쉽지 않다는 점은 고려해야 할 요소입니다.
어떤 방식을 선택할 것인가
두 접근 방식 중 어느 하나가 항상 우월하다고 보기는 어렵습니다. 도큐먼트 수가 상대적으로 적고, 복잡한 자연어 처리 로직이 필요한 경우에는 Java 애플리케이션에서 처리하는 방식이 더 적합할 수 있습니다. 반대로 수만 건 이상의 데이터를 대상으로 단순한 토큰화와 빈도 집계가 목적이라면, 서버사이드 집계가 시스템 전체 관점에서 더 합리적인 선택이 될 가능성이 큽니다.
실제 프로젝트에서는 약 20만 건의 기술 뉴스 도큐먼트를 대상으로 키워드 빈도 분석이 필요했고, Java 클라이언트 방식에서는 응답 시간이 수십 초까지 증가하는 상황을 경험했습니다. Aggregation Pipeline으로 전환한 이후에는 응답 시간이 수 초 수준으로 줄어들었으며, 네트워크 전송량과 JVM 메모리 사용량 역시 눈에 띄게 감소했습니다. 물론 이러한 수치는 환경에 따라 달라질 수 있으므로, 실제 운영 환경에서 측정하는 것이 가장 정확한 판단 기준이 됩니다.
텍스트 빈도 집계를 위한 Aggregation Pipeline 설계
텍스트에서 단어 빈도를 집계하려면 몇 가지 단계적인 변환이 필요합니다. 먼저 텍스트 필드를 단어 배열로 분리하고, 배열의 각 요소를 개별 도큐먼트로 전개한 뒤, 단어별로 그룹화하여 개수를 집계합니다. MongoDB Aggregation Pipeline에서는 이러한 흐름을 여러 스테이지의 조합으로 표현합니다.
핵심적인 처리 흐름은 다음과 같이 정리할 수 있습니다.
분석 대상 도큐먼트를 조건에 따라 필터링하고, 제목과 요약과 같은 텍스트 필드를 결합한 뒤 소문자로 정규화합니다. 이후 공백 기준으로 문자열을 분리해 단어 배열을 생성하고, 배열을 전개하여 단어 단위로 집계합니다.
이 과정에서 $project, $split, $unwind, $group 스테이지가 중심적인 역할을 합니다. 각 스테이지를 거치며 데이터의 형태가 점진적으로 변환되고, 최종적으로는 고유 단어별 빈도 결과만 남게 됩니다.
불용어 필터링을 Aggregation 내부에서 처리하기
텍스트 빈도 분석에서 불용어 처리는 결과의 품질을 좌우하는 중요한 요소입니다. 관사나 조동사처럼 의미 없는 단어를 제거하지 않으면, 빈도 상위 결과가 실제로 의미 있는 키워드를 반영하지 못하는 경우가 많습니다.
불용어 처리를 Java 코드에서 수행할 수도 있지만, Aggregation Pipeline 내부에서 처리하는 방식도 충분히 고려해볼 만합니다. $unwind 이후 $match 스테이지에서 $nin 연산자를 활용하면, 특정 단어 목록에 포함되지 않는 경우만 집계 대상으로 남길 수 있습니다.
불용어 외에도 숫자만으로 구성된 토큰이나 한 글자짜리 토큰처럼 분석에 적합하지 않은 값들을 함께 걸러낼 필요가 있습니다. 이를 위해 정규식 기반 조건을 $match 스테이지에 함께 적용하면, 의미 있는 토큰만 집계 대상으로 유지할 수 있습니다.
이러한 필터링을 Aggregation 내부에서 수행하면, 애플리케이션으로 전달되는 데이터 자체가 정제된 상태가 됩니다. 다만 불용어 목록은 도메인에 따라 달라질 수 있으므로, 코드에 하드코딩하기보다는 외부 설정으로 관리하는 방식이 운영 측면에서 더 유연합니다.
Spring Data MongoDB Aggregation API 활용
Spring Data MongoDB는 Aggregation Framework를 Java 코드에서 표현할 수 있는 타입 세이프한 API를 제공합니다. MongoDB Shell의 JSON 기반 파이프라인을 그대로 문자열로 관리하는 대신, Java 코드로 단계별 파이프라인을 구성할 수 있습니다.
match, project, unwind, group, sort, limit과 같은 주요 스테이지는 대부분 Aggregation 클래스의 정적 메서드로 제공되며, IDE의 자동 완성과 컴파일 타임 검증을 활용할 수 있습니다. 이는 파이프라인이 길어질수록 유지보수 측면에서 장점으로 작용합니다.
다만 $split, $toLower, $concat, $ifNull과 같이 중첩된 표현식이 필요한 경우에는, Spring Data MongoDB의 빌더 API만으로 표현하기 어려운 상황도 발생합니다. 이 경우 BSON Document를 직접 구성해 $project 스테이지에 포함시키는 방식으로 해결할 수 있습니다. 실제 구현에서는 이 방식이 복잡한 텍스트 전처리를 표현하는 데 가장 현실적인 선택이었습니다.
네트워크 전송량과 대량 데이터 처리 성능 관점
서버사이드 집계의 가장 큰 효과는 네트워크 전송량 감소입니다. 수십만 건의 도큐먼트를 그대로 애플리케이션으로 전달하는 경우와, 상위 N개의 집계 결과만 전달하는 경우의 차이는 매우 큽니다. 이 차이는 응답 시간뿐만 아니라 애플리케이션 서버의 메모리 사용량과 GC 부담에도 직접적인 영향을 미칩니다.
다만 MongoDB Aggregation Pipeline 역시 메모리 사용에 대한 제약을 가지고 있습니다. 공식 문서에 따르면 단일 파이프라인 스테이지는 기본적으로 100MB 메모리 제한이 있으며, 이를 초과하는 경우 에러가 발생하거나 디스크 사용 설정이 필요합니다. $unwind 단계에서 도큐먼트 수가 급격히 증가할 수 있으므로, 분석 대상 범위를 $match로 적절히 제한하는 설계가 중요합니다.
마무리하며
MongoDB Aggregation Pipeline을 활용한 서버사이드 텍스트 분석은 단순히 구현 방식의 차이를 넘어, 시스템 전체에서 비용을 어디에서 지불할 것인지에 대한 선택에 가깝다고 느껴졌습니다. 네트워크, 메모리, 코드 복잡도 중 어떤 요소를 우선적으로 관리할 것인지는 시스템의 성격에 따라 달라집니다.
대량 데이터에서 단순한 빈도 분석이 주된 목적이라면, 서버사이드 집계는 충분히 검토할 만한 선택지입니다. 반면 형태소 분석이나 복잡한 자연어 처리가 필요한 경우에는 여전히 Java 애플리케이션 레벨의 처리가 더 적합할 수 있습니다.
앞으로 확장해볼 만한 주제로는 단일 단어가 아닌 인접 단어 쌍을 분석하는 2-gram 분석이 있습니다. MongoDB Aggregation의 $reduce와 $zip 연산자를 활용하면 서버사이드에서도 구현 가능할 것으로 보입니다.
'TECH AND AI' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Eager Initialization
- Hot Key 문제
- 트랜잭션 관리
- 캐시 성능 비교
- Redis vs DB
- 캐시 장애
- 백엔드 아키텍처
- 백엔드 성능 설계
- spring batch 5
- Double-Checked Locking
- mybatis
- 트래픽 처리
- 캐시와 인덱스
- Spring Batch
- Enum 기반 싱글톤
- Java Performance
- 백엔드 성능
- Redis 성능 개선
- Cache Penetration
- DB 인덱스 성능
- DB 트랜잭션
- 백엔드 성능 튜닝
- Cache Avalanche
- Redis 캐시 전략
- 동시성처리
- 스레드 생명주기
- TTL 설계
- Initialization-on-Demand Holder Idiom
- InterruptedException
- Cache Aside
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

