
스프링 부트와 도커 컴포즈로 구현하는 분산 환경 머신러닝 시스템
오늘날의 데이터는 그 규모와 복잡성이 엄청납니다. 단일 서버의 한계를 넘어 데이터를 처리하고 분석하기 위해 분산 시스템은 필수적인 요소가 되었죠.
Apache Spark는 이러한 분산 데이터 처리를 위한 강력한 엔진입니다.
하지만 데이터 처리와 비즈니스 로직이 분리되면 시스템 아키텍처가 복잡해지기 쉽습니다.
그렇다면 비즈니스 로직을 다루는 데 특화된 스프링 부트와 Spark를 함께 사용하면 어떨까요? 스프링의 유연성과 Spark의 분산 처리 능력을 결합하면, 데이터 수집부터 분석, 그리고 분석 결과를 활용하는 비즈니스 로직까지 하나의 애플리케이션 내에서 효율적으로 관리할 수 있습니다. 특히, 대용량 데이터를 기반으로 한 머신러닝 모델을 서비스에 통합하고자 할 때 이 조합은 매우 효과적입니다.
이 포스팅에서는 스프링 부트 애플리케이션에 Apache Spark를 연동하여 대용량 뉴스 데이터를 처리하고 클러스터링하는 간단한 예제를 통해, 분산 머신러닝 시스템을 구축하는 방법을 알아보겠습니다.
로컬 개발 환경에서 분산 Spark 클러스터를 쉽게 구성하려면 Docker Compose가 최적의 도구입니다. 아래 docker-compose-spark.yml 파일을 살펴보세요.
version: '3.8'
services:
spark-master:
image: bitnami/spark:3.4.0
environment:
- SPARK_MODE=master
- SPARK_RPC_AUTHENTICATION_ENABLED=no
- SPARK_RPC_ENCRYPTION_ENABLED=no
- SPARK_LOCAL_STORAGE_ENCRYPTION_ENABLED=no
- SPARK_SSL_ENABLED=no
ports:
- "8080:8080" # Spark UI
- "7077:7077" # Spark Master Port
volumes:
- ./data:/opt/bitnami/spark/data # 데이터 공유를 위한 볼륨
spark-worker-1:
image: bitnami/spark:3.4.0
environment:
- SPARK_MODE=worker
- SPARK_MASTER_URL=spark://spark-master:7077
- SPARK_WORKER_MEMORY=2G
- SPARK_WORKER_CORES=2
depends_on:
- spark-master
spark-worker-2:
image: bitnami/spark:3.4.0
environment:
- SPARK_MODE=worker
- SPARK_MASTER_URL=spark://spark-master:7077
- SPARK_WORKER_MEMORY=2G
- SPARK_WORKER_CORES=2
depends_on:
- spark-master
이 설정은 다음과 같은 역할을 합니다.
spark-master: 클러스터의 마스터 노드로, Spark 애플리케이션의 실행을 조율합니다. 포트 8080을 통해 웹 UI에 접속하여 클러스터 상태를 확인할 수 있습니다.spark-worker-1, spark-worker-2: 워커 노드로, 마스터의 지시에 따라 실제 데이터 처리 작업을 수행합니다.SPARK_MASTER_URL을 통해 마스터 노드에 연결됩니다.터미널에서 이 파일을 실행하면, 로컬 환경에 분산 Spark 클러스터가 손쉽게 구축됩니다.
docker-compose -f docker-compose-spark.yml up -d
이제 http://localhost:8080으로 접속해 Spark 웹 UI를 확인해 보세요.
Spark와의 연동을 위해 스프링 부트 프로젝트에 의존성을 추가해야 합니다.
build.gradle (또는 pom.xml)에 spark-mllib 라이브러리를 추가합니다.
implementation 'org.apache.spark:spark-mllib_2.12:3.4.0'
Spark MLlib은 Spark의 머신러닝 라이브러리로, 클러스터링, 분류, 추천 시스템 등 다양한 알고리즘을 제공합니다.
스프링 부트 애플리케이션이 Spark 클러스터와 통신하려면 SparkSession 객체가 필요합니다.
이 객체는 Spark의 모든 기능을 사용할 수 있는 진입점 역할을 합니다.
@Configuration 클래스를 만들어 SparkSession을 빈(Bean)으로 등록하는 것이 좋습니다.
@Configuration
@ConditionalOnProperty(name = "spark.enabled", havingValue = "true")
public class SparkConfig {
@Value("${spark.master:local[*]}")
private String sparkMaster;
@Value("${spark.app-name:finsight-ml}")
private String appName;
@Bean
public SparkSession sparkSession() {
return SparkSession.builder()
.appName(appName)
.master(sparkMaster)
.config("spark.sql.adaptive.enabled", "true")
.config("spark.sql.adaptive.coalescePartitions.enabled", "true")
.config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
.getOrCreate();
}
@PreDestroy
public void closeSparkSession() {
if (sparkSession() != null) {
sparkSession().stop();
}
}
}
@ConditionalOnProperty: spark.enabled=true일 때만 빈을 생성하여, Spark 사용 여부를 설정으로 제어할 수 있습니다.spark.master: application.properties 파일에서 Spark 마스터의 URL을 설정합니다.local[*], 도커 컴포즈 환경에서는 spark://spark-master:7077로 설정할 수 있습니다.@PreDestroy: 애플리케이션이 종료될 때 SparkSession을 안전하게 닫아 리소스를 해제합니다.이제 설정된 SparkSession을 주입받아 실제 머신러닝 로직을 구현해 봅시다.
아래 SparkMLService는 대용량 뉴스 데이터의 클러스터링을 처리하는 예제입니다.
@Service
@ConditionalOnProperty(name = "spark.enabled", havingValue = "true")
public class SparkMLService {
private final SparkSession sparkSession;
public SparkMLService(SparkSession sparkSession) {
this.sparkSession = sparkSession;
}
// 대용량 뉴스 데이터 클러스터링
public List<NewsCluster> clusterNews(List<News> newsList) {
Dataset<Row> newsDF = sparkSession.createDataFrame(newsList, News.class);
// TF-IDF 벡터화
Tokenizer tokenizer = new Tokenizer()
.setInputCol("content")
.setOutputCol("words");
HashingTF hashingTF = new HashingTF()
.setInputCol("words")
.setOutputCol("rawFeatures")
.setNumFeatures(1000);
IDF idf = new IDF()
.setInputCol("rawFeatures")
.setOutputCol("features");
// K-means 클러스터링
KMeans kmeans = new KMeans()
.setK(10)
.setSeed(1L);
// Pipeline을 이용한 전처리 및 모델 학습
Pipeline pipeline = new Pipeline()
.setStages(new PipelineStage[]{tokenizer, hashingTF, idf, kmeans});
PipelineModel model = pipeline.fit(newsDF);
Dataset<Row> predictions = model.transform(newsDF);
return predictions.collectAsList().stream()
.map(this::convertToNewsCluster)
.collect(Collectors.toList());
}
// 뉴스 추천 시스템 (협업 필터링 예제)
public List<NewsRecommendation> recommendNews(String userId, int limit) {
ALS als = new ALS()
.setMaxIter(10)
.setRegParam(0.01)
.setUserCol("userId")
.setItemCol("newsId")
.setRatingCol("rating");
// 모델 훈련 및 예측 로직은 필요에 따라 구현
// ...
return Collections.emptyList();
}
}
데이터프레임 생성: sparkSession.createDataFrame을 사용해 자바 객체 리스트를 Spark의 Dataset<Row>(데이터프레임)으로 변환합니다.
Spark의 모든 작업은 이 데이터프레임을 기반으로 이루어집니다.
텍스트 전처리 및 벡터화: Tokenizer, HashingTF, IDF를 순차적으로 적용하여 텍스트 데이터를 머신러닝 모델이 이해할 수 있는 숫자 벡터(features)로 변환합니다.
클러스터링 모델: KMeans 모델을 정의하고, 전처리 파이프라인과 함께 **Pipeline**으로 묶어 한 번에 학습(fit)하고 예측(transform)합니다.
분산 처리: 이 모든 과정은 Spark에 의해 분산되어 워커 노드에서 병렬로 실행되므로, 대용량 데이터도 효율적으로 처리할 수 있습니다.
이 포스팅을 통해 Docker Compose로 분산 Spark 환경을 구축하고, 스프링 부트와 Spark를 연동하여 대용량 데이터를 처리하는 방법을 살펴보았습니다.
스프링의 안정적인 아키텍처 위에서 Spark의 분산 처리 능력을 활용하면, 확장성과 유지보수성이 뛰어난 머신러닝 시스템을 구축할 수 있습니다.
이러한 접근 방식은 단순히 뉴스 클러스터링뿐만 아니라, 추천 시스템, 이상 탐지, 자연어 처리 등 다양한 대용량 데이터 기반의 머신러닝 과제에 적용될 수 있습니다.
여러분의 서비스에 맞는 형태로 이 구조를 확장하고 개선해 보세요.