Spring

[SpringBoot - 파일 다루기] DB에 파일 직접 저장하기 (BLOB / Base64)

MoveForward 2025. 2. 1. 02:13

0. 개요

- 이전 포스팅에서 구현한 파일 업로드 방식은 "백엔드에 파일 저장 + DB에 경로 저장" 이다.

- 이번 포스팅에서 구현하는 방식은 "변환한 파일을 DB에 직접 저장" 이다. 

 

 

1. 업로드 방식

- 변환한 파일을 DB에 직접 저장할 것이다.

- 변환 방식은 'BLOB' 와 'Base64' 가 있다.

- 두가지 방식 모두 구현할 것이다.

 

 

2. 구현

- Entity

[FileBlob.java]

package study.imageHandlerTest.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;

@Entity
@Getter
@Setter
public class FileBlob {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String contentType;

    @Lob //스프링 부트가 String / char 이 아니면 알아서 BLOB 타입으로 설정
    @Column(columnDefinition = "LONGBLOB", nullable = false)
    private byte[] data; //BLOB 필드

    @Column(nullable = false)
    private long size; // 파일 크기

    @CreationTimestamp
    private Timestamp createDate;
}

- @Lob 어노테이션

 : DB의 컬럼 타입인 BLOB / CLOB를 매핑하기 위해 표현

data 속성은 DB에서 BLOB 타입의 컬럼으로 매핑되어야 한다.

 

• CLOB: String, char[], java.sql.CLOB

• BLOB: byte[], java.sql. BLOB

 

여기서 data 속성은 'byte[]' 타입이므로 DB에서 'BLOB' 타입으로 매핑된다. 

DB Table을 살펴보면 data 필드가 'BLOB' 타입인 것을 확인할 수 있다.

 

[FileBase64.java]

package study.imageHandlerTest.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;

import java.sql.Timestamp;

@Entity
@Getter
@Setter
public class FileBase64 {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(nullable = false)
    private String fileName;

    @Column(nullable = false)
    private String contentType;

    @Lob //스프링 부트가 String / char 이 아니면 알아서 BLOB 타입으로 설정
    @Column(nullable = false, columnDefinition = "LONGTEXT") //Base64 문자열이 크므로 LONGTEXT 지정
    private String base64Data; //BLOB 필드

    @Column(nullable = false)
    private long size; // 파일 크기

    @CreationTimestamp
    private Timestamp createDate;
}

- @Column 의 'columnDefinition = "LONGTEXT" ' 속성

 : Base64 필드에 저장할 문자열의 크기가 매우 크기 때문에 많은 메모리를 할당하기 위해 지정

 

 

- Repository

[FileBlobRepository.java]

[FileBase64Repository.java]

- JPA 를 이용하여 구현

 

 

- Service

[DBUploadService.java]

package study.imageHandlerTest.service;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.imageHandlerTest.entity.FileBase64;
import study.imageHandlerTest.entity.FileBlob;
import study.imageHandlerTest.repository.FileBase64Repository;
import study.imageHandlerTest.repository.FileBlobRepository;

@Service
@Transactional(readOnly=true)
@RequiredArgsConstructor
public class DBUploadService {

    private final FileBlobRepository fileBlobRepository;
    private final FileBase64Repository fileBase64Repository;

    /**
     * 저장 로직
     */
    @Transactional
    public FileBlob saveBlob(String fileName, String contentType, byte[] data) {
        FileBlob fileBlob = new FileBlob();
        fileBlob.setFileName(fileName);
        fileBlob.setContentType(contentType);
        fileBlob.setData(data);
        fileBlob.setSize(data.length);
        return fileBlobRepository.save(fileBlob);
    }

    @Transactional
    public FileBase64 saveBase64(String fileName, String contentType, String data64Data) {
        FileBase64 fileBase64 = new FileBase64();
        fileBase64.setFileName(fileName);
        fileBase64.setContentType(contentType);
        fileBase64.setBase64Data(data64Data);
        fileBase64.setSize(data64Data.length());
        return fileBase64Repository.save(fileBase64);
    }


    /**
     * 파일 불러오기
     */
    public FileBlob getBlobById(Long id) {
        return fileBlobRepository.findById(id).orElse(null);
    }

    public FileBase64 getBase64ById(Long id) {
        return fileBase64Repository.findById(id).orElse(null);
    }

}

 

 

- Controller

[DBUploadController.java]

package study.imageHandlerTest.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import study.imageHandlerTest.entity.FileBase64;
import study.imageHandlerTest.entity.FileBlob;
import study.imageHandlerTest.service.DBUploadService;

import java.io.IOException;
import java.util.Base64;

@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class DBUploadController {

    private final DBUploadService dbUploadService;

    /**
     * BLOB 방식으로 업로드
     */
    @PostMapping("/upload/blob")
    public ResponseEntity<FileBlob> uploadBlob(@RequestParam("file") MultipartFile file) {
        try {
            //파일 데이터를 바이너리 형식으로 저장
            FileBlob fileBlob = dbUploadService.saveBlob(
                    file.getOriginalFilename(),
                    file.getContentType(),
                    file.getBytes()
            );
            return ResponseEntity.status(HttpStatus.CREATED).body(fileBlob);
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * Base64 방식으로 업로드
     */
    @PostMapping("/upload/base64")
    public ResponseEntity<FileBase64> uploadBase64(@RequestParam("file") MultipartFile file) {
        try {
            //파일 데이터를 Base64로 인코딩
            String base64Data = Base64.getEncoder().encodeToString(file.getBytes());
            FileBase64 fileBase64 = dbUploadService.saveBase64(
                    file.getOriginalFilename(),
                    file.getContentType(),
                    base64Data
            );
            return ResponseEntity.status(HttpStatus.CREATED).body(fileBase64);
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * Health check 또는 테스트용 엔드포인트
     */
    @GetMapping("/health")
    public ResponseEntity<String> healthCheck() {
        return ResponseEntity.ok("Image upload API is running");
    }

    /**
     * BLOB -> file 변환
     */
    @GetMapping("/view/blob/{id}")
    public ResponseEntity<byte[]> viewBlob(@PathVariable Long id) {
        FileBlob fileBlob = dbUploadService.getBlobById(id);
        if (fileBlob == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        return ResponseEntity.ok()
                .contentType(org.springframework.http.MediaType.parseMediaType(fileBlob.getContentType()))
                .body(fileBlob.getData());
    }

    /**
     * Base64 -> file 변환
     */
    @GetMapping("/view/base64/{id}")
    public ResponseEntity<byte[]> viewBase64(@PathVariable Long id) {
        FileBase64 fileBase64 = dbUploadService.getBase64ById(id);
        if (fileBase64 == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }

        byte[] decodedData = Base64.getDecoder().decode(fileBase64.getBase64Data());
        return ResponseEntity.ok()
                .contentType(org.springframework.http.MediaType.parseMediaType(fileBase64.getContentType()))
                .body(decodedData);
    }
}

 

 

- Front - end

[index.html]

<form action="/api/files/upload/blob" method="POST" enctype="multipart/form-data">
    <h2>BLOB 를 사용해서 DB에 저장</h2>
    <div class="mb-3">
        <label for="file-blob-title" class="form-label">파일 제목</label>
        <input type="text" class="form-control" id="file-blob-title" name="title" required>
    </div>
    <div class="mb-3">
        <label for="file-blob-file" class="form-label">파일 업로드</label>
        <input type="file" class="form-control" id="file-blob-file" name="file" required>
    </div>
    <button type="submit" class="btn btn-primary">업로드</button>
</form>

<form action="/api/files/upload/base64" method="POST" enctype="multipart/form-data">
    <h2>BASE64 형태로 DB에 저장</h2>
    <div class="mb-3">
        <label for="file-base64-title" class="form-label">파일 제목</label>
        <input type="text" class="form-control" id="file-base64-title" name="title" required>
    </div>
    <div class="mb-3">
        <label for="file-base64-file" class="form-label">파일 업로드</label>
        <input type="file" class="form-control" id="file-base64-file" name="file" required>
    </div>
    <button type="submit" class="btn btn-primary">업로드</button>
</form>

 

 

3. 작동시현

1. 웹 서비스 화면

 

2. BLOB 방식 이용하여 업로드

'BLOB' 방식으로 'tiger.jpg' 파일을 업로드한다.

 

DB에 파일이 'data' 필드에 BLOB 타입으로 채워진 것을 확인 할 수 있다.

ID : 1 로서 존재한다.

 

"http://localhost:8080/api/files/view/blob/1" 을 통해 BLOB -> FILE 변환내용을 확인한다.

 

3. Base64 방식 이용하여 업로드

'Base64' 방식으로 'wombat.jpg' 파일을 업로드한다.

 

DB에 파일이 'base64data' 필드에 String 타입으로 채워진 것을 확인 할 수 있다.

ID : 1 로서 존재한다.

 

"http://localhost:8080/api/files/view/base64/1" 을 통해 base64 -> FILE 변환내용을 확인한다.

 

 

4. 마무리

이 방식의 구현한 의의는 실전에서 실용적으로 사용하기 위함 이라기 보다, 이러한 방식을 구현하는 그 자체에 있다.

왜냐하면, 이 방식은 실용적이지 않다. DB에 파일을 직접 저장하는 방식은 DB에 많은 저장공간을 할당해야 하며, 다시 파일을 불러오는 경우에도 많은 리소스를 사용하게 된다.

 

따라서 파일 업로드를 위한 방식은 "백엔드에 파일 저장 + DB에 경로 저장" 혹은, "클라우드 서비스를 이용" 을 사용하는 것이 가장 효율적이라고 생각한다.

 

BLOB / Base64 를 이용하여 DB에 직접적으로 파일을 저장하는 방식을 구현해보면서 파일 저장 기능 구현에 능숙해 질 수 있었고, 이에 자신감이 생겼다.
효율적인 방식이 아니더라도 굳이 구현해 보면서 동작 방식에 대한 사고를 할 수 있는 좋은 계기가 되었다.

만족스러웠다!