본문 바로가기
기초 및 언어/▶ Spring

09. Spring Boot 📂파일 업로드 + DB 저장 정리

by 류딩이 2025. 9. 26.

📂 Spring Boot 파일 업로드 + DB 저장 정리

사용자가 업로드한 파일을 서버 디렉토리에 저장하고, 파일 정보(이름/크기 등)를 DB에 기록하는 기능


1. 기본준비

🔽 사용한 sql문

더보기
더보기
drop sequence file_seq;
create sequence file_seq;

drop table file_info;
CREATE TABLE file_info (
    id NUMBER PRIMARY KEY,
    name varchar2(20),
    filename VARCHAR2(255) NOT NULL,
    file_size NUMBER NOT NULL
);

col id for 99
col name for a10
col filename for a20
select * from file_info order by id;

 

🔽 application.properties에 작성 -- db연결

spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=sqlid
spring.datasource.password=sqlpw
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

mybatis.mapper-locations=classpath:mapper/*.xml

 

🔽 <properties> 아래에 추가

<repositories>
        <repository>
            <id>oracle</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
</repositories>

 

🔽 <dependency> 아래에 추가  + 유효성검증 <dependency> 

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.5</version> 
</dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

 


🌈 2. 파일 업로드 기본 구조 흐름

Controller → Service → Interface(Mapper) → Mapper(XML, SQL)
  • Controller → 요청 받고 검증 후 Service 호출
  • Service → 파일 저장 + DB 저장 로직 처리
  • Interface (Mapper) → 자바 ↔ MyBatis 연결 다리
  • Mapper(XML) → 실제 SQL 실행

🌈 3. 작성순서 

 

1. DB 테이블 & 시퀀스 생성
→ file_info(id, name, filename, file_size)

2. DTO 작성 (FileInfo)
→ 업로드 정보 담을 객체

3. Mapper 인터페이스 & XML 작성
→ SQL 정의 및 연결

4. Service 작성
→ 파일 저장 + DB 저장 로직

5. Controller 작성
→ 요청 수신, 검증, Service 호출

6. View 작성 (form.html, list.html)
→ 업로드 폼 & 업로드 목록 출력

 


🌈 4. 파일 업로드 전체코드

📁 controller

더보기
더보기
package com.example.Ex02.controller;

import com.example.Ex02.dto.FileInfo;
import com.example.Ex02.service.FileService;
import jakarta.validation.Valid;
import org.apache.catalina.connector.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import javax.imageio.IIOException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@Controller
public class FileUpLoadController {

    @Autowired
    FileService fileService;
    public static final String UPLOAD_DIR = System.getProperty("user.dir") + "/uploads/";

    @GetMapping(value = "/form")
    public String form(Model model){
        model.addAttribute("fileInfo", new FileInfo());
        return "form";
    }

    // 업로드 post처리
    @PostMapping("/upload")
    public String upload(@Valid FileInfo fileInfo, BindingResult br, Model model){

        // 파일 유효성 검사 ==> 파일은 수동으로 체크 필요
        if(fileInfo.getFile()==null || fileInfo.getFile().isEmpty()){
            br.rejectValue("file", "file.empty", "파일을 선택하세요");
        }

        if(br.hasErrors()){
            model.addAttribute("fileInfo", fileInfo);
            model.addAttribute("message", br.getAllErrors().get(0).getDefaultMessage());
            return "form";
        }
        // controller - service - interface - mapper

        // 파일 저장 시도
        try {
            fileService.uploadFile(fileInfo);
            model.addAttribute("message", "업로드 성공" + fileInfo.getFilename());
        }catch (IOException e){
            model.addAttribute("message", "업로드 실패");
        }

        return "form";
    }

    // 이미지 띄우기 a태그 --> 전체 이미지 보기
    @GetMapping(value = "/images")
    public String images(Model model){
        List<FileInfo> lists = fileService.getAllFiles();
        System.out.println("lists.size()" + lists.size()); // 넣은 값 개수 확인

        model.addAttribute("lists",lists);
        return "list";
    }

    // 이미지 띄우기 get방식 요청
    // {filename:.+} → 확장자(.) 포함된 파일명도 PathVariable로 받기 가능
    @GetMapping(value="/images/{filename:.+}")
    public ResponseEntity images(@PathVariable String filename) throws MalformedURLException {

        Path file = Paths.get(UPLOAD_DIR).resolve(filename).normalize(); // UPLOAD_DIR 경로에 filename에 접근
        Resource resource = new UrlResource(file.toUri());
        // inline → 브라우저에서 바로 열림
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
                .body(resource);

    }
}

 

📁 fileInfo (dto)

더보기
더보기
package com.example.Ex02.dto;


import jakarta.validation.constraints.NotBlank;
import org.springframework.web.multipart.MultipartFile;

public class FileInfo {
    private int id;

    @NotBlank(message = "이름을 입력하세요")
    private String name;

    private String filename;
    private  int fileSize;

    public MultipartFile getFile() {
        return file;
    }

    public void setFile(MultipartFile file) {
        this.file = file;
    }

    private MultipartFile file; // 파일(데이터) 생성  ==> 업로드된 파일 자체가 들어오는 필드

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public int getFileSize() {
        return fileSize;
    }

    public void setFileSize(int fileSize) {
        this.fileSize = fileSize;
    }
}

 

📁 FileMapper(mapper)

더보기
더보기
package com.example.Ex02.mapper;

import com.example.Ex02.dto.FileInfo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface FileMapper {

    void saveFileInfo(FileInfo fileInfo);
    List<FileInfo> getAllFiles();
}

 

📁 Service

더보기
더보기
package com.example.Ex02.service;

import com.example.Ex02.dto.FileInfo;
import com.example.Ex02.mapper.FileMapper;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

@Service
public class FileService {

    @Autowired
    private FileMapper fileMapper;

    // user.dir 유저 현재 프로젝트
    @Value("${upload.dir:${user.dir}/uploads}")
    private String uploadDir; // c:Ex09/uploads

    @PostConstruct
    public void init() throws IOException {
        System.out.println("init");

        // 폴더 자동생성
        Files.createDirectories(Paths.get(uploadDir));
    }

    public void uploadFile(FileInfo fileInfo) throws IOException {
        String name = fileInfo.getName();
        MultipartFile file = fileInfo.getFile();
        String name2 = file.getName();
        String original = file.getOriginalFilename();

        System.out.println("name :" + name); // 사용자가 작성한 이름
        System.out.println("name2 :" + name2); // HTML 폼에서 <input type="file" name="file"> 의 name="file" 속성
        System.out.println("original :" + original); // 사용자가 업로드한 파일의 원래 파일명

        Path target = Paths.get(uploadDir, original); // 올릴경로, 올릴파일
        file.transferTo(target.toFile()); // 전송 (폴더 업로드)

        fileInfo.setFilename(original);
        fileInfo.setFileSize((int)file.getSize());

        fileMapper.saveFileInfo(fileInfo);

    }

    public List<FileInfo> getAllFiles() {
        return fileMapper.getAllFiles();
    }
}

 

 

 

🗂️Resources

  📁FileMapper.xml

더보기
더보기
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.Ex02.mapper.FileMapper">

<!--    <resultMap id="fileResultMap" type="com.example.Ex02.dto.FileInfo">
            <id property="id" column="id"></id>
            <result property="name" column="name"></result>
            <result property="filename" column="filename"></result>
            <result property="fileSize" column="file_size"></result>
    </resultMap>-->

    <!--업로드된 파일 메타데이터 저장-->
    <insert id="saveFileInfo" parameterType="com.example.Ex02.dto.FileInfo">
        insert into file_info (id, name, filename, file_size)
        values(file_seq.nextval, #{name}, #{filename}, #{fileSize})
    </insert>

    <!--이미지 조회-->
    <select id="getAllFiles" parameterType="com.example.Ex02.dto.FileInfo">
        select id, name, filename, file_size as fileSize <!--일치하지 않은 컬럼만 as 별칭-->
        from file_info
        order by id desc
    </select>
</mapper>

 

🗂️templates

  📁form.html

더보기
더보기
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>form</title>
</head>
<body>
        form.html<br>
        <h1>파일 업로드</h1>
        <form th:action="@{/upload}" method="post" th:object="${fileInfo}" enctype="multipart/form-data">
            <input type="text" th:field="*{name}" placeholder="이름을 입력하세요"><br>
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" style="color:red;"></p>

            <input type="file" th:field="*{file}"><br>
            <p th:if="${#fields.hasErrors('file')}" th:errors="*{file}" style="color:red;"></p>

            <br>
            <button type="submit">업로드</button>
        </form>

        message : <p th:text="${message}"></p> <!--컨트롤러 에러메시지 model로 보냄-->
        <a th:href="@{/images}">업로드된 이미지 보기</a>

</body>
</html>

 

  📁list.html

더보기
더보기
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>list</title>
</head>
<body>
        list.html<br>
        <table border="1">
            <tr>
                <th>번호</th>
                <th>이름</th>
                <th>파일이름</th>
                <th>파일 사이즈</th>
                <th>이미지 보기</th>
            </tr>

            <tr th:each="info:${lists}">
                <td th:text="${info.id}"></td>
                <td th:text="${info.name}"></td>
                <td th:text="${info.filename}"></td>
                <td th:text="${info.fileSize}"></td>

                <!--컨트롤러에서 getMapping-->
                <td><img th:src="@{'/images/' + ${info.filename}}" style="max-height:50px"></td>
            </tr>

        </table>


</body>
</html>

 

🌈 5. 코드 분석 및 작성순서

1️⃣ DTO (Data Transfer Object) 작성

  • id → DB 구분자 (PK)
  • name → 업로드한 사람 이름 (사용자 입력)
  • filename → 저장된 파일명 (DB 저장용) 예) abc.jpg
  • fileSize → 저장된 파일 크기 (DB 저장용)
  • file → 실제 업로드된 파일 데이터 (폼에서 넘어옴, 서버 저장에 사용)
public class FileInfo {
    private int id;

    @NotBlank(message = "이름을 입력하세요")
    private String name;

    private String filename;   // 저장될 파일명
    private int fileSize;      // 파일 크기

    private MultipartFile file; // 실제 업로드된 파일
}

 

🌰 MultipartFile

: 일반적인 자바 타입이(int, String 등) 아닌, 스프링에서 제공하는 특별한 타입

private MultipartFil file;

 

🔽 MultipartFile이 특이한 이유

1. 스프링 전용 타입(인터페이스)

  • Spring MVC가 업로드 요청을 처리할 때 자동으로 채워주는 객체

2. 폼에서 <input type="file"> 로 넘어오는 "파일" 자체를 담는 컨테이너

✅ 파일(데이터) 생성  ==> 업로드된 파일 자체가 들어오는 필드