쿼카러버의 기술 블로그
대규모 LLM API 호출 처리를 위해 Spark + Hive 활용한 경험기 공유 본문
이번에 회사에서 대량의 데이터와 이미지에 대한 분석 정보를 추출하고 분석하기 위해 LLM API를 호출해야 했다. 구체적인 내용은 회사와 관련 돼 있어 언급하기 어렵지만 설명, 특성, 리뷰, 이미지 등의 데이터를 LLM에 전달하고, 구조화된 정보나 개선된 콘텐츠를 생성받아야 한다. 이런 대규모 작업에서는 데이터 관리부터 API 호출, 결과 저장까지 효율적으로 처리할 수 있는 아키텍처가 필요하다.
여기서 한 가지 분명히 짚고 넘어가고 싶은 점이 있다. 이 글은 "Spark와 Hive로 데이터를 어떻게 분석하고 처리하는가"에 초점을 맞춘 글이 아니다. 나는 데이터 엔지니어도 아니고 Spark를 활용해본 경험이 많지 않다. 다만 우리 회사에는 이미 Spark와 Hive 인프라가 갖춰져 있었고, 나는 이 기존 인프라를 LLM API 호출이라는 다소 이질적인 용도에 어떻게 활용할 수 있을지, 그리고 활용하기 전에 어떤 고민들을 했는지를 공유하고자 한다.
읽다보면 "왜 Spark, Hive를 API 호출에 써?"라는 의문이 들 수 있다. 나도 처음에는 그런 의문을 가졌다. 그런데 회사 환경에서는 빠른 개발도 중요하고, 이미 검증되고 안정화된 인프라를 활용하는 것이 여러모로 이점이 있다고 느꼈다. S3에 저장된 파일 관리, 파티셔닝된 데이터 효율적 조회, 작업 상태 추적 등 많은 부분이 기존 인프라에서는 이미 잘 구축되어 있었기 때문이다. 개인적으로는 오버엔지니어링을 경계하는 편이지만 기존 인프라를 활용하는 것도 실무에서는 매우 중요한 접근이라고 생각한다.
앞으로 LLM 호출은 IT 회사라면 불가피하게 많이 사용하게 될 것 같아서, 경험기를 공유하고 이 글을 읽고 조언이나 의견을 주시는 분들이 있으면 너무 감사할 것 같다.
자 그럼 시작!!!
우리 회사에는 이미 Spark와 Hive 인프라가 구축되어 있다. 이 기존 인프라를 어떻게 활용할 수 있을까? 아니면 완전히 다른 접근법이 더 효과적일까? 여러 가능성을 고려해 볼 수 있다:
- Spark + Hive 조합 활용: 기존 인프라를 최대한 활용하는 방법
- Hive만 사용: Spark 없이 Hive와 직접 연동하는 Airflow Operator 활용
- 완전히 다른 접근법: Python 비동기 프로그래밍, 메시지 큐 등 더 가벼운 대안
사실 이 선택은 단순하지 않았다. 데이터 처리 프레임워크인 Spark가 외부 API 호출이라는 I/O 바운드 작업에 적합한지, Hive 없이도 효율적인 데이터 관리가 가능한지, 아니면 완전히 새로운 접근법이 필요한지 신중히 고려해야 한다.
이런 고민에 답하기 위해, 먼저 Spark와 LLM API 호출 간의 얼핏 보이는 불일치를 살펴보고, 그럼에도 불구하고 Spark가 제공할 수 있는 이점을 분석해 보자. 그런 다음 Hive의 역할과 대안적 접근법들을 검토하여, 다양한 상황에서 최적의 아키텍처를 선택할 수 있는 기준을 얘기해보자.
데이터 처리용 Spark를 LLM API 호출에 사용하는 모순된 상황
Spark는 대규모 데이터 처리를 위한 분산 컴퓨팅 프레임워크다. 주로 테라바이트 규모의 데이터를 변환하고, 집계하고, 분석하는 등 계산 집약적인 작업에 사용된다. 반면, LLM API 호출은 외부 서비스에 HTTP 요청을 보내고 응답을 기다리는 네트워크 I/O 위주의 작업이다. 이 두 가지는 본질적으로 다른 성격을 가진 작업이다.
이런 차이점 때문에 의문이 생긴다:
"데이터 처리용 Spark를 외부 API 호출에 사용하는 것이 정말 합리적일까?"
Spark와 LLM API 호출의 근본적 불일치
Spark가 가진 주요 특성과 LLM API 호출의 특성을 나란히 놓고 비교해보면 불일치가 명확하게 드러난다:
- 계산 vs 대기 시간:
- Spark: 대규모 데이터 변환과 계산에 최적화되어 있으며, CPU 집약적 작업을 여러 노드에 분산한다.
- LLM API 호출: 데이터 처리보다 네트워크 대기 시간이 지배적이며, 실제 계산은 외부 서비스에서 수행된다.
- 메모리 관리와 최적화:
- Spark: 대량 데이터의 효율적인 메모리 관리에 중점을 두며, 데이터 파티셔닝과 캐싱 전략을 통해 성능을 최적화한다.
- LLM API 호출: 처리할 데이터가 상대적으로 작아 메모리 관리보다 네트워크 연결 관리가 더 중요하다.
- 확장성과 병렬화:
- Spark: 데이터 처리 작업에서 노드를 추가하면 처리 능력이 확장된다.
- LLM API 호출: 외부 API의 전역 rate-limit으로 인해 노드를 추가해도 전체 처리량은 API 공급자의 제한을 넘을 수 없다.
I/O 바운드 작업과 병렬 처리의 관계
LLM API 호출은 전형적인 I/O 바운드 작업이다. I/O 바운드 작업의 핵심 특성은 프로세서가 실제 계산을 수행하는 시간보다 외부 시스템의 응답을 기다리는 시간이 훨씬 길다는 점이다. LLM API 호출 과정에서는 요청을 보내고 응답이 도착할 때까지 대부분의 시간은 네트워크 전송과 외부 서버의 처리를 기다리는 데 소비된다. 이 대기 시간 동안 로컬 CPU는 사실상 아무런 작업도 수행하지 않는다.
이러한 I/O 바운드 작업의 특성은 처리 방식에 중요한 영향을 미친다:
- 단일 스레드 처리의 비효율성: 한 번에 하나의 API 호출만 처리하는 방식에서는 프로세서가 대부분의 시간을 응답 대기에 낭비한다. 이로 인해 처리해야 할 항목이 많을수록 총 소요 시간이 선형적으로 증가하며 효율성이 크게 떨어진다.
- 병렬 처리의 효율성: 여러 API 호출을 동시에 진행하면 한 호출이 응답을 기다리는 동안 다른 호출을 시작하고 처리할 수 있다. 이를 통해 CPU 유휴 시간을 활용하여 전체 처리 시간을 대폭 줄일 수 있다.
이것만 보면 꼭 병렬처리를 해야할 것 같지만 당연히도 병렬 처리의 이상과 현실 사이에는 중요한 간극이 존재한다. 이론적으로는 병렬 처리를 통해 처리 시간이 획기적으로 줄어들 수 있지만, 실제로는 여러 제약 요소들이 이를 제한한다:
- API 제공자의 속도 제한: 대부분의 LLM API 제공자는 일정 시간 내에 처리할 수 있는 요청 수를 제한한다. 이는 서버 부하 관리와 공정한 리소스 분배를 위한 것으로, 분당 최대 요청 수나 초당 토큰 수 등의 형태로 제한된다. 이로 인해 병렬성을 늘려도 총 처리량이 특정 임계값을 넘지 못한다.
- 네트워크 혼잡과 대역폭 제약: 대량의 동시 요청은 네트워크 혼잡을 유발하여 각 요청의 지연 시간을 증가시킬 수 있다. 또한 네트워크 대역폭이 제한되어 있어 요청 수가 증가할수록 개별 요청의 전송 속도가 감소할 수 있다.
- 시스템 리소스 제한: 각 연결은 메모리와 파일 디스크립터와 같은 시스템 리소스를 소비한다. 연결 수가 증가함에 따라 이러한 리소스 소비도 증가하며, 시스템 한계에 도달하면 더 이상의 병렬화가 불가능해진다.
따라서 이러한 제약을 고려할 때, 최적의 병렬 처리 수준은 단순한 이론적 계산이 아닌 동시성 N = (제한 RPS × 평균 응답 시간) 과 같은 실측 기반 모델로 산정해야 한다. API 제한, 네트워크 특성, 시스템 리소스를 고려하여 실제 환경에서 달성 가능한 최적의 처리량 예측이 필요하다.
따라서 I/O 바운드 작업에서 병렬 처리는 성능을 극적으로 향상시킬 수 있지만, 이를 효과적으로 구현하기 위해서는 다양한 제약 조건과 최적화 요소를 고려한 신중한 설계가 필요하다.
Python으로 대규모 API 호출 처리를 구현할 때의 복잡성
Python으로 대량의 API 호출을 효율적으로 처리하기 위해서는 적절한 병렬화와 작업 관리가 필요하다. 이 과정에서 다음과 같은 기술적 도전에 직면하게 된다:
- 동시성 관리: 모든 요청을 한꺼번에 병렬 처리하는 것이 아니라, 적절한 수준의 동시 요청을 유지하며 점진적으로 처리해야 한다. 이때 ThreadPoolExecutor나 asyncio를 사용할 수 있지만, 시스템 리소스와 API 제한을 고려한 최적의 동시 실행 수준을 결정하는 것이 중요하다.
- 오류 처리와 재시도 메커니즘: 장시간 실행되는 대규모 처리 과정에서는 네트워크 오류, 타임아웃, 일시적인 API 서버 장애가 필연적으로 발생한다. 이러한 일시적 오류에 대응하는 지수 백오프(exponential backoff) 같은 재시도 로직을 구현해야 하며, 영구적 오류와 일시적 오류를 구분하는 능력도 필요하다.
- 작업 진행 상태 관리: 10만 개 항목 중 어떤 것이 처리되었고, 대기 중이며, 실패했는지 추적하는 시스템이 필요하다. 특히 처리 도중 프로그램이 중단된 경우 재개할 수 있도록, 진행 상태를 디스크에 주기적으로 저장하는 체크포인트 메커니즘이 중요하다.
- 속도 제한 관리: API 제공자의 속도 제한(예: 분당 600개 요청)을 지키기 위한 조절 메커니즘이 필요하다. 토큰 버킷 알고리즘과 같은 속도 제한 기법을 구현해야 하며, 요청량이 한도에 근접할 때 자동으로 속도를 조절하는 능력이 필요하다.
- 자원 효율적인 결과 처리: 각 API 호출의 결과를 메모리에 모두 보관하려 하면 금방 메모리 한계에 도달한다. 따라서 처리 결과를 점진적으로 디스크에 기록하면서, 실패한 항목에 대한 정보도 유지하는 효율적인 스토리지 전략이 필요하다.
이러한 요소들을 모두 고려한 견고한 Python 시스템을 직접 구현하는 것은 생각보다 복잡하며, 대규모 처리에서 발생할 수 있는 모든 엣지 케이스를 처리하기 위한 많은 시간과 테스트가 필요하다. 또한 시스템 규모가 커질수록 코드의 복잡성과 유지보수 난이도도 함께 증가한다.
Spark가 I/O 바운드 작업에서 제공하는 실질적 가치와 구현 메커니즘
Spark는 본래 데이터 처리에 최적화되어 있지만, 대규모 LLM API 호출과 같은 I/O 바운드 작업에서도 상당한 이점을 제공한다. 이러한 이점은 특히 10만 개 이상의 대량 호출을 처리할 때 더욱 두드러진다.
Spark의 핵심 이점
(1) 내장된 분산 처리 프레임워크: Spark는 데이터를 자동으로 파티셔닝하고 클러스터 전체에 분산시켜 병렬로 처리한다. 이를 통해 10만 개의 API 호출을 효율적으로 분배하고 관리할 수 있다. 개발자는 분산 시스템의 복잡성을 걱정할 필요 없이 비즈니스 로직에 집중할 수 있어, Python으로 직접 구현하는 것보다 훨씬 적은 코드로 대규모 작업을 처리할 수 있다.
(2) 시스템 수준의 장애 복원력: Spark는 executor 실패, 노드 장애, 네트워크 중단과 같은 시스템 수준의 문제를 자동으로 처리한다. 작업이 실패하면 다른 executor에서 자동으로 재시도되므로, 대규모 처리 과정에서 발생하는 인프라 문제에 대한 복원력이 높다. 이는 특히 장시간 실행되는 대규모 API 호출 작업에서 큰 가치가 있다.
(참고: Spark의 자동 재시도는 executor/태스크 실패나 예외로 종료된 스테이지에 국한된다. HTTP 429·5xx 등 '정상 종료된' API 오류는 Spark가 자동 재시도하지 않으므로, HTTP 실패·rate-limit 재시도 로직은 사용자 코드로 직접 구현해야 한다.)
(3) 리소스 관리의 효율성: Spark는 메모리, CPU 등의 시스템 리소스를 효율적으로 관리한다. YARN이나 Kubernetes와 통합되면 리소스 할당, 격리, 우선순위 지정 등의 기능을 활용할 수 있어 시스템 안정성이 향상된다. 이를 통해 LLM API 호출과 같은 외부 리소스에 최적화된 방식으로 내부 리소스를 할당할 수 있다.
(4) 확장성과 대규모 처리: Spark 클러스터에 노드를 추가하면 내부 데이터 처리 능력이 증가하여 데이터 준비 및 결과 처리 단계의 성능이 향상된다. 이는 전체 파이프라인의 처리량을 개선하는 데 도움이 된다.
(참고: 외부 LLM API는 전역 rate-limit이 있으므로, 노드 수를 늘려도 API 호출 처리량은 공급자 제한을 넘지 못한다. 노드 증설 전에 글로벌 호출 한도에 맞춰 작업 스케줄을 교정하거나 토큰-버킷형 rate-limitter같은 리미터를 구현/활용해야 한다.)
(5) 통합 데이터 파이프라인: Spark를 사용하면 데이터 추출, 전처리, API 호출, 결과 저장까지 하나의 통합된 파이프라인으로 구성할 수 있다. 이는 여러 시스템 간 데이터 이동을 최소화하고, 전체 프로세스의 모니터링과 관리를 단순화한다. 기존 Spark 인프라가 있는 환경에서는 이 이점이 특히 중요하다.
실제 구현 메커니즘과 최적화 방법
이러한 이점을 최대한 활용하기 위해 실제 구현에서는 다음과 같은 메커니즘이 중요하다:
(1) 효율적인 데이터 파티셔닝: Spark는 데이터를 파티션으로 나누어 병렬 처리한다. 예로 10만 개 상품을 100개 파티션으로 나누면, 각 파티션은 약 1,000개 상품을 처리한다. 파티션 크기를 적절히 조정하여 작업 분배와 API 호출 효율성 사이의 균형을 맞출 수 있다. 너무 작은 파티션은 오버헤드를 증가시키고, 너무 큰 파티션은 병렬성을 제한한다.
(2) 연결 풀 최적화: 각 파티션에서 HTTP 클라이언트 연결 풀을 관리하여 네트워크 성능을 향상시킬 수 있다. 이는 반복적인 연결 설정/해제 오버헤드를 줄여 처리량을 높인다.
(참고: PySpark UDF는 별도 Python 프로세스에서 실행되므로 JVM 단일 풀만으로는 재사용이 제한될 수 있다. JVM-측 HTTP 클라이언트를 사용하거나 실행자 로컬 싱글톤 패턴으로 풀을 공유해야 효과적이다.)
(3) API 속도 제한 관리: API 호출의 속도와 패턴을 세밀하게 제어하여 속도 제한을 준수하면서도 최대 처리량을 달성할 수 있다.
(참고: Spark는 파티션 단위 병렬만 제공하며 클러스터 전체에서 공유되는 중앙 rate-limiter/패턴 제어는 내장돼 있지 않다. 전역 토큰-버킷 또는 큐 기반 스로틀러를 별도 구현하여 모든 executor가 공유하도록 설정해야 세밀한 제어가 가능하다.)
(5) API 오류 처리 강화: 맞춤형 재시도 로직을 구현하여 HTTP 429(과다 요청) 또는 5xx와 같은 API 오류에 효과적으로 대응할 수 있다. 지수 백오프 전략과 같은 재시도 메커니즘을 통해 일시적인 API 장애에도 안정적으로 처리할 수 있다.
Spark를 활용한 LLM API 호출 시스템은 이러한 이점과 메커니즘을 결합하여, 대규모 처리에 필요한 인프라 복잡성을 크게 줄이면서도 효율적이고 안정적인 처리를 가능하게 한다. 특히 이미 Spark 기반 데이터 파이프라인이 구축된 환경에서는, 이 접근법이 새로운 시스템을 도입하는 것보다 빠르게 가치를 제공할 수 있다.
Hive 활용에 대하여
S3 직접 사용 vs Hive 활용의 핵심 차이
S3에 데이터를 직접 저장하고 접근하는 방식과 Hive를 통해 데이터를 관리하는 방식 사이에는 몇 가지 중요한 차이가 있다:
(1) 데이터 필터링과 접근 효율성(참고: S3 프리픽스·객체 키 파티셔닝, S3 Select / Glacier Select 등을 사용하면 전체 스캔 없이 부분 조회가 가능하다. 하지만 이를 위해서는 상위 디렉터리 구조(카테고리=값/날짜=값/)를 처음부터 잘 설계하거나 추가 구현이 필요하다. Hive는 이러한 최적화를 자동으로 처리해준다.)
S3에서 "특정 카테고리의 상품만" 또는 "지난 주에 업데이트된 상품만" 처리하려면 기본적으로는 모든 파일을 일일이 다운로드하고 파싱한 후 필터링해야 한다. 이 과정에서 불필요한 데이터를 처리하느라 시간과 비용이 낭비된다.
(2) 파일 경로 관리의 복잡성
"상품 ID 52631의 데이터는 어디 있지?"라는 질문에 답하려면 파일 명명 규칙을 정확히 알고 있어야 한다. 여러 개발자가 작업하거나 시간이 지나면 이런 규칙은 쉽게 망가지거나 잊혀진다. 결국 코드 곳곳에 하드코딩된 경로나 복잡한 경로 생성 로직이 생겨나고, 유지보수가 악몽이 된다.
(3) 처리 실패 시 재시작 문제
10만 개 상품 처리 중 90,000번째에서 오류가 발생했다면, S3 기반 솔루션에서는 어디까지 처리했는지 추적하기 위한 별도의 상태 관리 시스템을 직접 구현해야 한다. 이는 간단해 보이지만, 실패 지점 추적, 부분 완료 처리, 중복 방지 등의 로직을 요구한다.
Hive의 주요 가치는 이러한 문제들을 해결할 수 있는 표준화된 메타데이터 관리 계층을 제공하는 것이다. 물론 S3만으로도 효율적인 데이터 관리가 불가능한 것은 아니지만, Hive는 이러한 최적화와 관리 기능을 기본적으로 제공하여 개발자의 부담을 크게 줄여준다.
Hive가 해결하는 핵심 문제들
Hive는 단순한 SQL 인터페이스가 아니라, 데이터에 대한 메타데이터 관리 계층이다. Hive는 원래 Hadoop 생태계의 일부로, HDFS를 기본 스토리지로 사용하도록 설계되었지만, S3를 포함한 다양한 스토리지 시스템과 함께 작동할 수 있다. 이 메타데이터 계층은 다음과 같은 구체적인 가치를 제공한다:
(1) 데이터를 논리적 구조로 관리
Hive는 물리적 저장소(파일 경로)와 논리적 구조(테이블, 컬럼) 사이의 매핑을 관리한다. 이로 인해 SQL로 특정 조건의 데이터만 쉽게 추출할 수 있으며, 해당 조건에 맞는 파일만 효율적으로 스캔한다. 10만 개 상품 중 필요한 1만 개만 처리할 수 있어 비용과 시간이 90% 절감된다.
(2) 파티셔닝을 통한 성능 최적화
카테고리별로 데이터를 파티셔닝하면, 특정 카테고리 상품만 처리할 때 다른 모든 파티션을 완전히 무시할 수 있다. 이는 SQL 쿼리만으로 자동 처리되며, 파일 경로를 직접 계산할 필요가 없다. 예를 들어, 전자제품 카테고리 2만 개 상품만 처리하려면 다른 8만 개 상품 데이터는 아예 읽지 않는다.
(3) 증분 처리의 단순화
날짜별로 파티셔닝된 테이블에서는 간단한 조건으로 최근 업데이트된 상품만 추출할 수 있다. 이를 통해 매일 전체 10만 개를 다시 처리하는 대신, 새로 추가된 100개 상품만 효율적으로 처리할 수 있다.
1안: Spark + Hive 조합 활용
지금까지 살펴본 Spark의 분산 처리 능력과 Hive의 메타데이터 관리 기능을 결합하면, 대규모 LLM API 호출을 위한 강력한 솔루션을 구축할 수 있다. 이 접근법은 특히 이미 두 기술이 모두 구축되어 있는 환경에서 가장 효과적이다.
Spark와 Hive의 시너지
Spark와 Hive의 조합은 다음과 같은 방식으로 시너지를 발휘한다:
- 효율적인 데이터 필터링과 분산 처리: Hive의 메타데이터 관리 기능을 통해 필요한 데이터만 정확히 추출하고, 이를 Spark의 분산 처리 엔진으로 전달한다. 예를 들어 특정 카테고리나 날짜 범위의 상품만 처리하고자 할 때, Hive의 파티션 프루닝을 통해 필요한 데이터만 효율적으로 선택한 후 Spark로 전달하여 병렬 처리할 수 있다.
- 처리 상태 관리와 재시작 용이성: Spark로 처리 중인 데이터의 상태를 Hive 테이블에 주기적으로 기록하면, 작업이 중간에 실패하더라도 이미 처리된 항목을 추적하고 나머지만 재개할 수 있다. 이는 10만 개와 같은 대규모 작업에서 특히 중요한 기능이다.
- 결과의 통합 관리: LLM API 호출 결과를 Hive 테이블에 저장함으로써, 다른 분석 작업이나 다운스트림 프로세스에서 쉽게 활용할 수 있다. 원본 데이터와 처리 결과가 모두 Hive에서 관리되므로 데이터 계보(lineage)를 명확하게 유지할 수 있다.
실제 구현 방법
Spark와 Hive를 활용한 LLM API 호출 파이프라인은 다음과 같은 단계로 구현할 수 있다:
- 데이터 준비 단계: Hive 테이블에서 처리할 데이터를 Spark DataFrame으로 로드한다. 이때 필요한 필터링 조건을 적용하여 처리 대상 데이터만 선택한다.
- 병렬 처리 단계: Spark의 분산 처리 기능을 활용하여 데이터를 파티션 단위로 병렬 처리한다. 각 파티션에서는 LLM API를 호출하고 결과를 수집한다. 이 과정에서 앞서 언급한 API 속도 제한 관리와 오류 처리 로직이 적용된다.
- 결과 저장 단계: 처리 결과를 다시 Hive 테이블에 저장한다. 필요에 따라 파티션을 나누어 결과를 효율적으로 관리할 수 있다.
- 모니터링 및 관리: 전체 파이프라인은 Airflow나 다른 워크플로우 관리 도구를 통해 스케줄링하고 모니터링할 수 있다. 실패 시 재시도 정책과 알림 설정을 통해 안정적인 운영이 가능하다.
고려해야 할 사항
이 접근법을 사용할 때 몇 가지 고려해야 할 사항이 있다:
- 클러스터 리소스 관리: Spark 클러스터의 리소스(메모리, CPU 코어)를 LLM API 호출의 특성에 맞게 조정해야 한다. I/O 바운드 작업이므로 CPU 코어당 더 많은 병렬 태스크를 실행하도록 설정하는 것이 유리할 수 있다.
- 전역 Rate Limiter 구현: Spark는 클러스터 전체에서 공유되는 rate limiter를 기본 제공하지 않으므로, 모든 executor가 공유할 수 있는 전역 rate limiter를 구현해야 한다. 이는 Redis와 같은 외부 서비스를 활용하거나, 커스텀 솔루션을 개발하여 해결할 수 있다.
- API 오류 처리: HTTP 429(속도 제한 초과) 또는 5xx(서버 오류)와 같은 API 응답은 Spark에서 자동으로 재시도되지 않으므로, 사용자 코드에서 적절한 재시도 로직을 구현해야 한다. 지수 백오프 전략과 최대 재시도 횟수 설정은 필수적이다.
- 비용 최적화: LLM API 호출은 비용이 발생하므로, 중복 처리를 최소화하고 필요한 경우에만 API를 호출하도록 설계해야 한다. 이를 위해 이미 처리된 항목을 추적하고, 캐싱 전략을 구현하는 것이 중요하다.
Spark와 Hive의 조합은 초기 설정과 관리에 다소 복잡성이 있지만, 대규모 LLM API 호출을 안정적이고 효율적으로 처리할 수 있는 강력한 프레임워크를 제공한다. 특히 기존에 두 기술이 모두 구축되어 있는 환경에서는 추가 인프라 없이 빠르게 솔루션을 구현할 수 있는 장점이 있다.
2안 : Spark 없이 Hive만 사용하는 접근법
Spark와 Hive의 조합이 강력하지만, Spark 없이 Hive만 직접 활용하는 방법도 가능하다. 특히 회사에서 Hive는 구축되어 있지만 Spark는 아직 도입하지 않았거나, Spark 클러스터 운영이 부담스러운 경우에 고려할 수 있는 접근법이다.
Airflow의 HiveOperator를 통한 직접 접근
Airflow에서는 HiveOperator를 통해 Hive 쿼리를 직접 실행할 수 있다. 이 접근법은 다음과 같이 작동한다:
- 데이터 추출 단계: HiveOperator로 필요한 상품 데이터를 추출한다. 이때 카테고리나 업데이트 날짜 등의 조건으로 필터링하여 필요한 상품만 가져올 수 있다.
- Python 처리 단계: PythonOperator로 추출된 데이터에 대해 LLM API 호출을 수행한다. 이 단계에서는 Python의 비동기 처리 기능이나 스레드 풀을 활용해 병렬 처리를 구현할 수 있다.
- 결과 저장 단계: 다시 HiveOperator로 처리 결과를 Hive에 저장한다. 이렇게 저장된 결과는 다른 시스템에서도 쉽게 활용할 수 있다.
이 방식의 장점은 Spark 클러스터를 관리할 필요 없이 Hive의 데이터 관리 기능을 활용할 수 있다는 점이다. 또한 Airflow의 재시도 메커니즘과 스케줄링 기능을 활용해 안정적인 파이프라인을 구축할 수 있다.
컴퓨팅 분리와 병렬 처리 전략
Hive만 사용할 경우 대규모 병렬 처리에 제한이 있을 수 있다. 이를 보완하기 위한 몇 가지 전략이 있다:
- 데이터 분할과 여러 워커: Hive 쿼리로 데이터를 여러 부분으로 나누고, 각 부분을 별도의 Airflow 태스크에서 처리할 수 있다. 예를 들어, 카테고리별로 상품을 나누어 여러 Python 태스크에서 병렬로 처리할 수 있다.
- 작업 큐 활용: Celery나 RabbitMQ와 같은 작업 큐 시스템을 활용하여, 추출된 상품 데이터를 작업 큐에 넣고 여러 워커에서 병렬로 처리할 수 있다.
- 상태 추적을 위한 Hive 테이블: 각 상품의 처리 상태(대기 중, 처리 중, 완료, 실패 등)를 Hive 테이블에 기록하여, 실패한 항목만 선별적으로 재처리할 수 있다.
이러한 접근법은 Spark의 내장된 병렬 처리 기능에 비해 더 많은 수동 구성이 필요하지만, Hive의 데이터 관리 기능을 활용하면서도 효율적인 병렬 처리를 구현할 수 있다.
3안 : 기존 인프라 없이 시작하는 경우
만약 회사에 Spark와 Hive 인프라가 없는 경우라면, 이러한 무거운 인프라를 새로 구축하는 것은 단지 LLM API 호출 처리만을 위해서는 과도한 투자일 수 있다. 이런 상황에서는 다음과 같은 가볍고 효율적인 대안을 고려할 수 있다:
현대적인 Python 솔루션
Python의 비동기 프로그래밍 기능을 활용하면, 적은 리소스로도 효율적인 병렬 처리가 가능하다:
- asyncio + aiohttp: 비동기 I/O를 통해 단일 프로세스에서도 수천 개의 API 호출을 동시에 관리할 수 있다. 이 방식은 특히 I/O 바운드 작업에 매우 효율적이다.
- ThreadPoolExecutor: 더 단순한 접근법으로, 스레드 풀을 사용해 병렬 API 호출을 관리할 수 있다. GIL(Global Interpreter Lock)의 제약이 있지만, I/O 바운드 작업에서는 여전히 효과적이다.
메시지 큐 기반 시스템
복잡성은 높아지지만, 좀 찾아봤는데 아래와 같이 확장성과 안정성을 위해 메시지 큐 시스템을 활용할 수 있다고 한다:
- Celery + Redis/RabbitMQ: 작업 큐에 API 호출 작업을 넣고, 여러 워커에서 이를 병렬로 처리한다. 이 방식은 재시도 로직과 모니터링 기능을 기본적으로 제공한다.
- Amazon SQS + Lambda: 서버리스 아키텍처를 활용하면, 인프라 관리 부담 없이 확장 가능한 병렬 처리를 구현할 수 있다. 특히 클라우드 환경에서는 이런 관리형 서비스가 매력적인 옵션이다.
데이터 관리를 위한 가벼운 대안
Hive 없이도 효율적인 데이터 관리가 가능하다:
- SQLite + 파티셔닝된 파일: 간단한 로컬 데이터베이스와 잘 구성된 파일 구조를 조합하여, 기본적인 메타데이터 관리와 상태 추적을 구현할 수 있다.
- DuckDB: 분석적 쿼리 기능을 갖춘 임베디드 데이터베이스로, 파일 기반 데이터에 대한 SQL 쿼리를 지원한다. 이는 Hive의 일부 기능을 더 가볍게 대체할 수 있다.
LLM API 호출의 특수한 고려사항
어떤 아키텍처를 선택하든, LLM API 호출에는 다음과 같은 특수한 고려사항이 있다:
(1) 속도 제한 관리
대부분의 LLM API는 초당 요청 수를 제한한다. 예를 들어 분당 600개 요청만 허용한다면, 10만 개 처리에 최소 167분이 필요하다. 어떤 아키텍처에서든 속도 제한을 고려한 처리량 조절 메커니즘이 필요하다.
(2) 비용 효율성과 실패 관리
각 LLM API 호출은 비용이 발생하므로, 실패한 호출의 재시도와 결과 캐싱이 중요하다. 이미 처리된 항목을 추적하고, 실패한 항목만 선별적으로 재시도하는 메커니즘을 구현해야 한다.
(3) 처리 결과의 저장과 활용
LLM이 생성한 상품 설명이나 속성을 어디에 저장할지도 고려해야 한다. 원본 상품 데이터와 LLM 처리 결과를 논리적으로 연결하여 저장하고, 추후 분석이나 애플리케이션에서 쉽게 활용할 수 있는 구조가 필요하다.
결론: 상황에 맞는 최적의 선택
대규모 LLM API 호출 처리를 위한 최적의 접근법은 회사의 기존 인프라와 팀의 전문성에 크게 의존한다고 생각한다. 내 환경을 기준으로만 생각해보면:
- Spark + Hive가 이미 있는 경우: 이 인프라를 활용하여 대규모 LLM 처리 파이프라인을 구축하는 것이 비용 효율적이고 시간을 절약하는 방법이다. 기존 시스템의 모든 장점을 활용하면서 빠르게 솔루션을 구현할 수 있다.
- Hive만 있는 경우: Spark 없이도 Hive의 데이터 관리 기능과 Airflow의 워크플로우 관리 기능을 조합하여 효율적인 시스템을 구축할 수 있다. 병렬 처리를 위한 추가 전략이 필요하지만, 구현 가능한 접근법이다.
- 아무것도 없는 경우: 새로운 환경에서는 Python 비동기 프로그래밍이나 서버리스 기능과 같은 가벼운 도구를 활용하는 것이 빠르게 시작하는 좋은 방법이다. 필요에 따라 점진적으로 더 복잡한 인프라로 확장할 수 있다.
이처럼 다양한 접근법이 가능하며, 각각 장단점이 있다고 생각한다. 중요한 것은 처리해야 할 데이터의 규모, 요구되는 안정성, 기존 인프라, 팀의 전문성 등을 종합적으로 고려하여 상황에 가장 적합한 아키텍처를 선택하는 것이라고 생각한다. 회사 내 기존 인프라를 활용하는 것이 가장 효율적이지만, 그렇지 않은 경우 문제 해결에 더 적합한 도구를 선택하는 유연한 접근이 필요하다.
'[Infra & Server]' 카테고리의 다른 글
[Airflow] Airflow 개념 중요한 것들만 골라서 정리 (0) | 2025.05.09 |
---|---|
[Airflow] Airflow 온보딩 가이드: 뭐부터 배워야할지 학습 순서만 다룸 (0) | 2025.05.09 |