[특화 프로젝트] HDFS에 사용자 로그 적재

2026. 3. 16. 11:21📚 빅데이터 분산

빅데이터 분산 처리 프로젝트 - HDFS에 사용자 로그 적재

목표

Spring Boot에서 HDFS에 사용자 행동 로그를 저장하는 파이프라인을 구현하는 것이 목표다.

전체 흐름

클라이언트 → POST /api/logs → Spring Boot → HDFS에 CSV 파일로 저장

1. Hadoop 라이브러리 추가

Spring Boot에서 HDFS에 접근하려면 Hadoop Client 라이브러리가 필요하다.
build.gradledependencies에 추가했다.

dependencies {
    // 기존 의존성 생략...

    // HDFS 연동을 위한 Hadoop 라이브러리
    implementation 'org.apache.hadoop:hadoop-client:3.2.1'
}

버전은 Docker로 띄운 Hadoop(3.2.1)과 동일하게 맞췄다.


2. 프로젝트 패키지 구조

코드를 역할별로 분리하기 위해 4개의 패키지를 생성했다.

com.travel.bigdata
├── config/         ← 설정 클래스 (HDFS 연결 등)
├── controller/     ← API 엔드포인트
├── dto/            ← 데이터 구조 정의
├── service/        ← 비즈니스 로직
└── BigdataApplication.java

3. 로그 데이터 구조 정의 (DTO)

사용자 행동 로그에 담길 데이터를 정의하는 클래스다.
프로젝트 기획서에 명시된 필드(국적, 나이, 성별, 여행목적, 라이프스타일 등)를 그대로 반영했다.

package com.travel.bigdata.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class UserLogDto {
    private String userId;          // 사용자 ID
    private String nationality;     // 국적
    private int age;                // 나이
    private String gender;          // 성별
    private String travelPurpose;   // 여행 목적
    private String lifestyle;       // 라이프스타일 (여행성향)
    private String action;          // 행동 (VIEW, GO, RE_RECOMMEND)
    private String placeId;         // 장소 ID
    private String timestamp;       // 로그 생성 시간
}

Lombok 어노테이션

어노테이션 역할
@Getter 모든 필드의 getter 메서드 자동 생성
@Setter 모든 필드의 setter 메서드 자동 생성
@ToString toString() 메서드 자동 생성

Lombok을 사용하면 보일러플레이트 코드를 직접 작성하지 않아도 된다.


4. HDFS 연결 설정 (Config)

Spring Boot에서 HDFS에 접속하기 위한 설정 클래스다.

package com.travel.bigdata.config;

import org.apache.hadoop.fs.FileSystem;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HdfsConfig {

    private final String hdfsUri = "hdfs://localhost:9000";

    @Bean
    public FileSystem fileSystem() throws Exception {
        org.apache.hadoop.conf.Configuration configuration = new org.apache.hadoop.conf.Configuration();
        configuration.set("fs.defaultFS", hdfsUri);
        configuration.set("dfs.client.use.datanode.hostname", "true");

        // HDFS에 root 사용자로 접속
        System.setProperty("HADOOP_USER_NAME", "root");

        return FileSystem.get(configuration);
    }
}

주요 설정 설명

설정 설명
@Configuration 이 클래스가 Spring 설정 파일임을 선언
@Bean 이 메서드의 반환 객체를 Spring이 관리하도록 등록
fs.defaultFS HDFS의 주소. Docker의 NameNode 포트(9000)와 매핑
dfs.client.use.datanode.hostname DataNode 접근 시 컨테이너 IP 대신 hostname을 사용
HADOOP_USER_NAME HDFS에 root 권한으로 접속 (권한 문제 방지)

5. HDFS 저장 로직 (Service)

로그 데이터를 받아서 HDFS에 CSV 파일로 저장하는 핵심 로직이다.

package com.travel.bigdata.service;

import com.travel.bigdata.dto.UserLogDto;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.springframework.stereotype.Service;

import java.time.LocalDate;

@Service
public class HdfsService {

    private final FileSystem fileSystem;

    public HdfsService(FileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void saveLog(UserLogDto log) throws Exception {
        // 날짜별로 폴더를 나눠서 저장
        String date = LocalDate.now().toString();
        String fileName = "log_" + System.currentTimeMillis() + ".csv";
        Path path = new Path("/user-logs/" + date + "/" + fileName);

        // CSV 형태로 한 줄 만들기
        String csvLine = String.join(",",
                log.getUserId(),
                log.getNationality(),
                String.valueOf(log.getAge()),
                log.getGender(),
                log.getTravelPurpose(),
                log.getLifestyle(),
                log.getAction(),
                log.getPlaceId(),
                log.getTimestamp()
        );

        // HDFS에 파일 쓰기
        try (FSDataOutputStream outputStream = fileSystem.create(path, true)) {
            outputStream.writeBytes(csvLine + "\n");
        }
    }
}

설계 포인트

  • 날짜별 폴더 분리: /user-logs/2026-03-16/ 형태로 저장하여 Spark가 특정 날짜의 로그만 선택적으로 읽을 수 있도록 했다.
  • CSV 형태 저장: Spark가 CSV를 쉽게 파싱할 수 있어 분석 단계에서 편리하다.
  • 타임스탬프 기반 파일명: log_1773627176253.csv처럼 밀리초 단위 타임스탬프로 파일명을 생성하여 충돌을 방지했다.

6. API 엔드포인트 (Controller)

외부에서 로그 데이터를 POST 요청으로 보내면 HDFS에 저장하는 API다.

package com.travel.bigdata.controller;

import com.travel.bigdata.dto.UserLogDto;
import com.travel.bigdata.service.HdfsService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/logs")
public class LogController {

    private final HdfsService hdfsService;

    public LogController(HdfsService hdfsService) {
        this.hdfsService = hdfsService;
    }

    @PostMapping
    public ResponseEntity<String> saveLog(@RequestBody UserLogDto logDto) {
        try {
            hdfsService.saveLog(logDto);
            return ResponseEntity.ok("로그 저장 성공");
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("로그 저장 실패: " + e.getMessage());
        }
    }
}

요청-응답 흐름

POST /api/logs (JSON 데이터)
    → LogController가 요청을 받음
    → HdfsService.saveLog() 호출
    → HDFS에 CSV 파일 저장
    → "로그 저장 성공" 응답 반환

7. 테스트

API 호출

curl -X POST http://localhost:8090/api/logs \
  -H "Content-Type: application/json" \
  -d "{\"userId\":\"user001\",\"nationality\":\"KR\",\"age\":25,\"gender\":\"M\",\"travelPurpose\":\"SIGHTSEEING\",\"lifestyle\":\"ADVENTURE\",\"action\":\"VIEW\",\"placeId\":\"place_001\",\"timestamp\":\"2026-03-16T10:30:00\"}"

응답: 로그 저장 성공

HDFS에서 파일 확인

# 파일 목록 확인
docker exec -it travel-namenode hdfs dfs -ls hdfs://namenode:9000/user-logs/2026-03-16/

# 파일 내용 확인
docker exec -it travel-namenode hdfs dfs -cat hdfs://namenode:9000/user-logs/2026-03-16/*.csv

출력 결과:

user001,KR,25,M,SIGHTSEEING,ADVENTURE,VIEW,place_001,2026-03-16T10:30:00

전송한 로그 데이터가 CSV 형태로 HDFS에 정상 저장된 것을 확인했다.


트러블슈팅

1. HADOOP_HOME 경고

HADOOP_HOME and hadoop.home.dir are unset.

Windows에서 Hadoop 라이브러리를 사용하면 발생하는 경고다.
네트워크로 HDFS에 접속하는 방식이므로 로컬에 Hadoop이 설치되어 있을 필요가 없다.
무시해도 정상 동작한다.

2. Permission denied 에러

Permission denied: user=SSAFY, access=WRITE, inode="/":root:supergroup:drwxr-xr-x

HDFS는 root 사용자 권한으로 동작하는데, Spring Boot가 Windows 사용자(SSAFY)로 접속하면 쓰기 권한이 없다.

해결: HdfsConfig에서 System.setProperty("HADOOP_USER_NAME", "root") 설정으로 HDFS에 root 사용자로 접속하도록 했다.

3. DataNode 연결 실패 (UnresolvedAddressException)

java.nio.channels.UnresolvedAddressException

Spring Boot(호스트)가 Docker 컨테이너 내부의 DataNode에 접근하지 못하는 문제다.
세 가지를 수정하여 해결했다.

docker-compose.yml 수정:

  • DataNode에 hostname: datanode 설정 추가
  • DataNode에 ports: 9864, 9866 포트 매핑 추가
  • HDFS_CONF_dfs_datanode_hostname: datanode 환경변수 추가

HdfsConfig.java 수정:

  • dfs.client.use.datanode.hostname 설정을 true로 추가

Windows hosts 파일 수정:

  • C:\Windows\System32\drivers\etc\hosts127.0.0.1 datanode 추가
  • 관리자 권한 PowerShell에서 아래 명령어로 추가 가능:
Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "127.0.0.1 datanode"

최종 docker-compose.yml

3단계까지의 변경사항이 반영된 전체 파일이다.

services:
  postgres:
    image: postgres:15
    container_name: travel-postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: travel_db
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: admin1234
    volumes:
      - postgres_data:/var/lib/postgresql/data

  namenode:
    image: bde2020/hadoop-namenode:2.0.0-hadoop3.2.1-java8
    container_name: travel-namenode
    ports:
      - "9870:9870"
      - "9000:9000"
    environment:
      CLUSTER_NAME: travel-cluster
      CORE_CONF_fs_defaultFS: hdfs://namenode:9000
    volumes:
      - namenode_data:/hadoop/dfs/name

  datanode:
    image: bde2020/hadoop-datanode:2.0.0-hadoop3.2.1-java8
    container_name: travel-datanode
    hostname: datanode
    depends_on:
      - namenode
    ports:
      - "9864:9864"
      - "9866:9866"
    environment:
      SERVICE_PRECONDITION: "namenode:9870"
      CORE_CONF_fs_defaultFS: hdfs://namenode:9000
      HDFS_CONF_dfs_datanode_hostname: datanode
    volumes:
      - datanode_data:/hadoop/dfs/data

  spark-master:
    image: bde2020/spark-master:3.1.1-hadoop3.2
    container_name: travel-spark-master
    ports:
      - "8080:8080"
      - "7077:7077"
    environment:
      CORE_CONF_fs_defaultFS: hdfs://namenode:9000

  spark-worker:
    image: bde2020/spark-worker:3.1.1-hadoop3.2
    container_name: travel-spark-worker
    depends_on:
      - spark-master
    environment:
      SPARK_MASTER: spark://spark-master:7077
      CORE_CONF_fs_defaultFS: hdfs://namenode:9000

volumes:
  postgres_data:
  namenode_data:
  datanode_data:

최종 프로젝트 구조

bigdata/
├── build.gradle
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/travel/bigdata/
│   │   │       ├── config/
│   │   │       │   └── HdfsConfig.java          ← HDFS 연결 설정
│   │   │       ├── controller/
│   │   │       │   └── LogController.java        ← POST /api/logs API
│   │   │       ├── dto/
│   │   │       │   └── UserLogDto.java           ← 로그 데이터 구조
│   │   │       ├── service/
│   │   │       │   └── HdfsService.java          ← HDFS 저장 로직
│   │   │       └── BigdataApplication.java
│   │   └── resources/
│   │       └── application.yml
│   └── test/
└── gradle/