[아이티센 부트캠프] Spring Boot 4

이언덕·2026년 4월 24일

아이티센 부트캠프

목록 보기
73/115
post-thumbnail

Spring MVC 파일 업로드 완전 이해

Spring MVC에서 파일 업로드를 이해할 때 가장 먼저 잡아야 하는 것은 새로운 문법 이름이 아니다.
이전글에서 문자열, 숫자, DTO를 받는 흐름을 먼저 봤다면, 이번에는 그 요청 처리 구조가 파일까지 어떻게 확장되는지를 보면 된다.
즉, 파일 업로드는 완전히 새로운 별개 기능이 아니라, Spring MVC가 요청값을 받는 구조 위에 파일 데이터가 추가된 것이라고 이해하면 된다.


그래서 이 글은 이전글의 RequestParam, ModelAttribute, DTO 바인딩 흐름 다음 단계로 이어진다.
이전글에서는 컨트롤러가 요청값을 어떻게 받는지 정리했다.
이번에는 그 구조 안에서 파일이 어떤 방식으로 들어오고, 컨트롤러가 그 파일을 어떻게 확인하고 다루는지를 차례대로 정리한다.



1. 파일 업로드를 보기 전에 먼저 연결해야 할 흐름

파일도 결국 브라우저가 서버로 보내는 요청 데이터이다.
이 점은 문자열이나 숫자와 같다.
다만 파일은 단순한 글자값 하나가 아니라 이름, 크기, 실제 내용까지 함께 움직여야 하므로 처리 방식이 더 복잡해진다.


즉, 문자열 요청과 파일 요청은 완전히 다른 세계가 아니다.
둘 다 컨트롤러 메서드의 매개변수로 들어오는 요청 데이터이다.
차이는 문자열은 비교적 가볍고 단순한 값이고, 파일은 실제 데이터 덩어리까지 같이 다뤄야 한다는 점이다.
그래서 파일 업로드를 이해할 때도 이전글에서 정리한 Spring MVC 요청 처리 구조를 그대로 떠올리면 된다.


이전글에서는 RequestParam으로 값을 하나씩 받기도 했고, DTO나 VO로 값을 묶어서 받기도 했다.
파일 업로드도 이 흐름을 그대로 이어 간다.
파일 하나를 직접 받을 수도 있고, 업로드용 객체 안에 파일 필드를 넣어서 함께 받을 수도 있다.
즉, 파일 업로드를 갑자기 새 문법 암기처럼 보지 말고, 요청값 받기의 확장판으로 이해하는 것이 가장 자연스럽다.



2. 파일은 왜 일반 요청값처럼 받기 어려운가

문자열이나 숫자는 보통 한 줄 값처럼 다루면 된다.
예를 들어 이름, 나이, 주소는 값 자체가 비교적 단순하다.
브라우저가 서버로 보낼 때도 텍스트 중심으로 보내면 된다.


하지만 파일은 다르다.
파일은 단순히 “사진” 같은 값 하나만 보내는 것이 아니다.
실제로는 파일 이름도 필요하고, 파일 크기도 필요하고, 그 안에 들어 있는 실제 데이터도 같이 움직여야 한다.
즉, 파일은 텍스트 한 줄처럼 가볍게 보낼 수 있는 값이 아니다.
그래서 일반 요청값과는 다른 방식으로 다뤄야 한다.


사용자가 파일을 업로드한다는 것은 단순히 파일을 선택했다는 뜻으로 끝나지 않는다.
서버는 그 파일이 실제로 존재하는지도 확인해야 하고, 이름이 무엇인지도 알아야 하고, 필요하면 저장도 해야 한다.
즉, 파일 하나를 처리하려고 해도 그 주변에 따라오는 정보가 많다.
초보자 기준에서는 이 부분을 값 + 파일 자체 정보 + 실제 내용을 함께 받아야 하는 구조라고 이해하면 된다.


기본 개념예제

아래 예제는 파일 업로드 폼이 왜 일반 폼과 다르게 생기는지를 보여 주는 가장 작은 예제이다.

// exam01.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>upload form</title>
</head>
<body>
<form action="/upload-basic" method="post" enctype="multipart/form-data">
<input type="file" name="file"><!-- 업로드할 파일 선택 -->
<input type="submit" value="전송"><!-- 파일 업로드 요청 전송 -->
</form>
</body>
</html>

이 예제에서 먼저 눈에 들어와야 하는 것은 input type="file"이다.
이 입력칸은 일반 텍스트 입력칸과 다르게 파일 선택 창을 열어 준다.
그리고 enctype="multipart/form-data"도 같이 들어가 있다.
이 설정이 있어야 브라우저가 파일 데이터를 일반 텍스트 요청처럼 보내지 않고, 파일 전송에 맞는 방식으로 보낼 수 있다.
즉, 파일 업로드는 폼 모양부터 이미 일반 요청과 다르게 시작된다고 이해하면 된다.



3. multipart/form-data는 무엇인가

multipart/form-data는 파일과 일반 입력값을 여러 부분으로 나누어 보내는 전송 형식이다.
이름만 보면 어렵지만, 초보자 기준에서는 문자열 입력값과 파일 입력값을 한 번에 묶어서 보내되, 각각을 따로 구분해서 전달하는 방식이라고 이해하면 된다.


여기서 중요한 점은 2번과 역할이 다르다는 것이다.
2번에서는 왜 파일이 일반 문자열처럼 단순하지 않은지를 봤다.
여기서는 그래서 브라우저가 실제로 어떤 전송 형식을 써야 하는지를 보는 것이다.
즉, 파일이 복잡하기 때문에 전송 방식도 일반 폼과 달라진다.


브라우저는 form을 전송할 때 어떤 방식으로 묶어서 보낼지 결정해야 한다.
파일이 없는 일반 폼이면 비교적 단순한 방식으로 보내도 된다.
하지만 파일이 포함되면 일반 텍스트처럼 보내면 안 된다.
그래서 enctype="multipart/form-data"를 반드시 지정해야 한다.
이 설정이 있어야 브라우저가 문자열 입력값과 파일 입력값을 서로 구분해서 전송할 수 있다.


서버 입장에서도 일반 요청 파라미터처럼 파일을 보면 안 된다.
문자열은 요청값 하나로 바로 읽으면 되지만, 파일은 파일명, 비어 있는지 여부, 크기, 실제 저장 같은 처리가 함께 필요하다.
그래서 Spring MVC는 파일 업로드 요청이 들어오면 이를 일반 문자열이 아니라 파일 객체로 다룰 수 있게 준비해 둔다.
그 중심에 있는 것이 MultipartFile이다.


기본 개념예제

아래 예제는 문자열 입력값과 파일 입력값이 한 폼 안에서 함께 전송되는 가장 기본적인 형태이다.

// multipart.html
<!doctype html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>multipart</title>
</head>
<body>
    <h1>멀티파트 학습</h1>
    <hr/>
    <form method="post" action="업로드처리주소" enctype="multipart/form-data">
        성명: <input type="text" name="stname" />
        <br/>
        학번: <input type="text" name="stnum" />
        <br/>
        과제첨부: <input type="file" name="reports" />
        <br/>
        <input type="submit" value="제출" />
    </form>
</body>
</html>

이 예제에서 먼저 봐야 할 것은 enctype="multipart/form-data"이다.
이 설정이 있기 때문에 브라우저는 stname, stnum 같은 일반 입력값과 reports 파일 입력값을 한 번에 보내면서도, 각각을 따로 구분해서 전송할 수 있다.


또 name="reports"도 중요하다.
이 이름은 나중에 서버 쪽에서 파일을 받을 때 연결 기준이 된다.
즉, 브라우저 화면에서 파일 입력칸 이름을 reports로 정하면, 서버도 그 이름을 기준으로 파일 요청값을 받게 된다.


개발자 도구로 보면 무엇이 확인되는가

이 예제는 브라우저 개발자 도구의 Network 창으로 확인하면 더 이해가 쉬워진다.
폼을 제출한 뒤 요청 하나를 클릭해서 보면, Headers에서는 요청 메서드가 POST로 보이고, Content-Type은 multipart/form-data; boundary=... 형태로 나타난다.
여기서 boundary는 여러 부분으로 나뉜 데이터를 구분하는 경계선이라고 이해하면 된다.
즉, 브라우저가 정말 멀티파트 형식으로 요청을 만들었다는 뜻이다.




그리고 페이로드를 보면 stname, stnum, reports가 같이 들어간다.
즉, 일반 입력값 두 개와 파일 입력값 한 개가 하나의 요청 안에서 함께 전송된다는 점을 눈으로 확인할 수 있다.
이게 바로 multipart/form-data를 설명할 때 가장 중요한 장면이다.




원본 데이터까지 열어 보면 각 부분이 실제로 따로 나뉘어 전송되는 모습이 더 분명하게 보인다.
stname, stnum는 각각 별도 파트로 들어가고, reports 부분에는 filename="근로계약서.pdf"처럼 실제 파일명과 Content-Type: application/pdf 같은 파일 종류 정보도 함께 붙는다.
즉, 파일은 단순히 “있다” 정도로만 전송되는 것이 아니라, 일반 입력값과는 다른 파일 정보까지 함께 묶여서 전송된다는 점이 분명하게 드러난다.




그래서 이 예제는 파일 업로드가 단순히 입력칸 하나 더 추가되는 것이 아니라, 브라우저의 전송 형식 자체가 달라지는 구조라는 점을 보여 주기에 아주 좋은 예제이다.
즉, multipart/form-data는 파일만 보내는 형식이 아니라, 문자열 입력값과 파일 입력값을 함께 보내되 각각을 구분해서 전달하는 형식이라고 이해하면 된다.



4. Spring MVC에서 파일을 받는 방식

Spring MVC에서 파일을 받는 가장 기본적인 방식은 컨트롤러 메서드의 매개변수 타입을 MultipartFile로 지정하는 것이다.
여기서 먼저 정확히 잡아야 할 점은 MultipartFile이 예약어가 아니라는 점이다.
MultipartFile은 업로드된 파일 하나를 표현하기 위해 Spring MVC가 제공하는 파일 전용 타입이다.


컨트롤러 메서드의 매개변수는 요청으로 들어온 값을 받는 자리이다.
문자열 요청값을 받을 때는 매개변수 타입을 String으로 지정한다.
숫자 요청값을 받을 때는 매개변수 타입을 int나 Integer로 지정한다.
파일 요청값을 받을 때는 매개변수 타입을 MultipartFile로 지정한다.
즉, MultipartFile을 매개변수 타입으로 적는다는 것은 이 자리에는 업로드 파일을 받겠다고 선언하는 것이다.


예를 들어 문자열 요청값은 아래처럼 받을 수 있다.

// StringParameterExample.java
@PostMapping("/save-name")
public String saveName(@RequestParam("name") String name) {
    System.out.println(name); // 요청으로 들어온 문자열 확인
    return "result";
}

String name에서 String은 타입이고, name은 메서드 안에서 사용할 변수 이름이다.
@RequestParam("name")은 요청에서 name이라는 이름으로 넘어온 값을 찾으라는 뜻이다.
즉, 요청에서 name 값을 찾아 String 타입의 name 변수에 넣는 구조이다.


파일 요청값도 흐름은 같다.
다만 타입이 문자열용 String이 아니라 파일용 MultipartFile로 바뀐다.

// MultipartFileParameterExample.java
@PostMapping("/upload-direct")
public String upload(@RequestParam("file") MultipartFile file) {
    System.out.println(file.getOriginalFilename()); // 요청으로 들어온 파일명 확인
    return "result";
}

MultipartFile file에서 MultipartFile은 타입이고, file은 변수 이름이다.
@RequestParam("file")은 요청에서 file이라는 이름으로 넘어온 값을 찾으라는 뜻이다.
즉, 이 코드는 “요청에서 file이라는 이름으로 넘어온 업로드 파일을 찾아서 MultipartFile 타입의 file 변수로 받겠다”는 뜻이다.


이 흐름은 아래처럼 비교하면 더 분명하다.

// ParameterTypeCompare.java
// 문자열 요청값을 받을 때
@RequestParam("name") String name

// 파일 요청값을 받을 때
@RequestParam("file") MultipartFile file

따라서 “문자열 요청값을 String으로 받듯이, 파일 요청값은 MultipartFile로 받는다”는 말은 단순히 이름을 외우라는 뜻이 아니다.
요청값의 종류에 맞게 컨트롤러 매개변수 타입을 정한다는 뜻이다.


파일 입력칸과 컨트롤러 매개변수가 연결되는 흐름

파일이 컨트롤러 매개변수에 들어오려면 먼저 브라우저가 파일을 파일 업로드 형식으로 보내야 한다.
그래서 파일 업로드 폼에는 enctype="multipart/form-data"가 필요하다.

// uploadForm.html
<form action="/upload-direct" method="post" enctype="multipart/form-data">
<input type="file" name="file"><!-- file이라는 이름으로 파일 전송 -->
<input type="submit" value="업로드"><!-- 업로드 요청 전송 -->
</form>

이 코드에서 input type="file"은 사용자가 파일을 선택하게 해 주는 입력칸이다.
그리고 name="file"은 서버로 보낼 때 이 파일 데이터의 이름을 file로 정하겠다는 뜻이다.


이제 컨트롤러에서 아래처럼 받으면 된다.

// UploadDirectController.java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class UploadDirectController {
    @PostMapping("/upload-direct")
    public String upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) { // 파일을 선택하지 않았는지 확인
            System.out.println("파일이 없습니다");
            return "result";
        }

        System.out.println(file.getOriginalFilename()); // 실제 업로드 파일명 확인
        return "result";
    }
}

이 코드에서 연결 기준은 file이라는 이름이다.
브라우저의 파일 입력칸은 name="file"로 파일을 보낸다.
컨트롤러는 @RequestParam("file")로 요청에서 file이라는 이름의 값을 찾는다.
그리고 그 값을 MultipartFile 타입으로 받는다.


전체 흐름은 아래처럼 이어진다.

// MultipartFileBindingFlow.java
// 1. 브라우저의 파일 입력칸 이름
name="file"

// 2. 사용자가 선택한 파일
test.png

// 3. 브라우저가 file이라는 이름으로 파일 전송
file = test.png

// 4. 컨트롤러가 file이라는 이름의 요청값을 찾음
@RequestParam("file")

// 5. 파일 요청값이므로 MultipartFile 타입으로 받음
MultipartFile file

// 6. 메서드 안에서 file 변수로 업로드 파일 사용
file.getOriginalFilename()

즉, MultipartFile이라는 이름 자체가 자동으로 동작하는 것이 아니다.
브라우저가 multipart/form-data 형식으로 파일을 보내고, Spring MVC가 요청 안에서 file이라는 이름의 파일 데이터를 찾은 뒤, 그 데이터를 MultipartFile 타입의 매개변수에 넣어 주는 구조이다.


어노테이션 없이 아래처럼 쓸 수 있는 경우도 있다.

// UploadDirectControllerSimple.java
@PostMapping("/upload-direct")
public String upload(MultipartFile file) {
    System.out.println(file.getOriginalFilename()); // 실제 업로드 파일명 확인
    return "result";
}

이 코드는 매개변수 이름 file과 파일 입력칸의 name="file"이 맞으면 동작할 수 있다.
Spring MVC가 같은 이름의 파일 데이터를 찾아 MultipartFile file에 넣어 줄 수 있기 때문이다.


하지만 처음 배울 때는 이 방식보다 @RequestParam("file") MultipartFile file처럼 쓰는 편이 더 좋다.
어떤 요청 이름과 어떤 매개변수가 연결되는지 바로 보이기 때문이다.
즉, 초보자 기준에서는 아래처럼 이해하면 된다.

// RequestParamMultipartFileFlow.java
@RequestParam("file") // 요청에서 file이라는 이름의 값을 찾음
MultipartFile // 그 값을 업로드 파일 타입으로 받음
file // 메서드 안에서 file이라는 변수명으로 사용함



파일을 직접 받는 방식과 객체 안에 넣어 받는 방식

파일을 받는 방식은 하나만 있는 것이 아니다.
가장 단순한 방식은 지금처럼 컨트롤러 매개변수에 MultipartFile을 직접 적는 방식이다.
이 방식은 파일 하나를 바로 확인할 수 있어서 처음 이해하기 쉽다.


하지만 실제 화면에서는 파일만 보내는 경우보다 제목, 설명, 작성자 같은 일반 입력값과 파일을 함께 보내는 경우가 많다.
이럴 때는 업로드용 VO나 DTO 안에 MultipartFile 필드를 넣어서 받을 수 있다.

// UploadFileVO.java
import org.springframework.web.multipart.MultipartFile;

public class UploadFileVO {
    private String title; // 일반 문자열 요청값
    private MultipartFile file; // 업로드 파일 요청값
}

이 구조는 이전에 배운 DTO, VO 바인딩과 비슷하다.
여러 문자열 값을 객체 하나로 묶어 받던 것처럼, 파일도 객체 안에 넣어 함께 받을 수 있다.
즉, 파일 업로드는 기존 요청값 처리 구조와 완전히 다른 기능이 아니라, 기존 요청값 처리 구조에 파일 타입이 추가된 흐름이다.


또 여러 파일을 더 직접적으로 다루고 싶을 때는 MultipartRequest를 사용할 수도 있다.
MultipartRequest는 업로드 요청 전체에서 파일 목록을 꺼내는 방식에 가깝다.
다만 처음 파일 업로드를 이해할 때는 MultipartFile을 먼저 잡는 것이 좋다.
왜냐하면 파일 하나가 어떻게 컨트롤러 매개변수로 들어오고, 그 안에서 파일명과 크기를 어떻게 확인하는지가 가장 직관적으로 보이기 때문이다.


이 이미지는 파일 업로드 처리 시 컨트롤러 메서드에서 사용할 수 있는 파라미터 형태를 보여 준다.
MultipartFile 하나를 직접 받을 수도 있고, 여러 파일이면 배열로 받을 수도 있고, 업로드용 객체 안에 넣어서 받을 수도 있다.


기본 개념예제

아래 예제는 컨트롤러가 MultipartFile 하나를 직접 받는 가장 단순한 형태이다.

// UploadDirectController.java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class UploadDirectController {
    @PostMapping("/upload-direct")
    public String upload(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) { // 파일이 비어 있는지 먼저 확인
            System.out.println("파일이 없습니다"); // 업로드 실패 상황 확인
            return "result"; // 결과 화면 이름 반환
        }

        System.out.println(file.getOriginalFilename()); // 업로드한 실제 파일명 확인
        return "result"; // 결과 화면 이름 반환
    }
}
// 출력결과
// 파일이 없습니다
// 또는
// test.png

이 예제에서 중요한 점은 파일도 결국 컨트롤러 메서드의 매개변수로 들어온다는 것이다.
다만 문자열이 아니라 파일이기 때문에 매개변수 타입을 MultipartFile로 지정한다.
그리고 @RequestParam("file")을 붙여서 요청에서 file이라는 이름으로 넘어온 업로드 파일을 이 매개변수에 넣겠다고 명확히 표시한다.


이 단계에서 정리할 핵심은 간단하다.
브라우저는 name="file"이라는 이름으로 파일을 보내고, 컨트롤러는 @RequestParam("file") MultipartFile file로 그 파일을 받는다.
이 흐름이 잡히면 다음 5번에서 MultipartFile이 어떤 역할을 하는지도 자연스럽게 이어진다.



5. MultipartFile은 무엇인가

4번에서 파일 요청값이 MultipartFile 매개변수에 들어오는 흐름을 봤다.
이제 5번에서는 그 매개변수 안에 들어온 MultipartFile이 정확히 무엇을 의미하는지 정리한다.


MultipartFile은 업로드된 파일 하나를 자바 객체처럼 다루게 해 주는 타입이다.
서버는 화면에서 넘어온 파일을 그냥 막연한 데이터 덩어리로 직접 만지는 것이 아니다.
MultipartFile 객체를 통해 업로드 파일에 접근한다.


즉, MultipartFile은 단순히 파일을 담아 두는 상자가 아니다.
업로드된 파일을 확인하고 다루고 저장하기 위한 파일 처리 도구이다.


문자열 요청값을 String으로 받으면 그 안에는 문자열 값이 들어 있다.
파일 요청값을 MultipartFile로 받으면 그 안에는 업로드 파일과 관련된 정보가 들어 있다.
파일은 단순한 글자값이 아니기 때문에 파일명, 크기, 비어 있는지 여부, 실제 내용 같은 정보가 함께 필요하다.
그래서 MultipartFile에는 파일 처리를 위한 여러 메서드가 들어 있다.


MultipartFile 안에 들어 있다고 생각하면 되는 것

MultipartFile file이라는 매개변수 안에는 업로드 파일을 다루기 위한 정보와 기능이 들어 있다.
초보자 기준에서는 아래처럼 이해하면 된다.

  • 실제 업로드한 파일 이름을 확인할 수 있다.
  • 파일 입력칸의 이름을 확인할 수 있다.
  • 파일을 선택했는지 확인할 수 있다.
  • 파일 크기를 확인할 수 있다.
  • 파일 내용을 읽을 수 있다.
  • 파일을 실제 경로에 저장할 수 있다.

즉, MultipartFile은 업로드된 파일 하나를 컨트롤러 안에서 다룰 수 있게 만들어 주는 객체이다.


기본 개념예제

아래 예제는 MultipartFile이 무엇을 대표하는지 가장 직관적으로 보여 주는 예제이다.

// MultipartFileIntroController.java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class MultipartFileIntroController {
    @PostMapping("/upload-name")
    public String upload(@RequestParam("file") MultipartFile file) {
        return file.getOriginalFilename(); // 업로드한 실제 파일명 반환
    }
}
// 출력결과
// test.png

이 코드는 업로드된 파일에서 실제 파일명만 꺼내서 바로 응답한다.
여기서 file은 단순한 문자열 변수가 아니다.
업로드된 파일을 표현하는 MultipartFile 객체이다.
그래서 file.getOriginalFilename()처럼 파일 전용 메서드를 사용할 수 있다.


이 흐름을 실제 값 기준으로 보면 아래와 같다.

// MultipartFileObjectFlow.java
// 1. 사용자가 선택한 파일
test.png

// 2. 컨트롤러 매개변수에 들어온 객체
MultipartFile file

// 3. 파일 객체에서 실제 파일명 꺼내기
file.getOriginalFilename()

// 4. 결과
test.png

즉, MultipartFile을 받으면 이 객체를 통해 업로드된 파일에 접근할 수 있다.
문자열 요청값을 받으면 그 값을 바로 출력할 수 있듯이, 파일 요청값을 받으면 그 파일 객체 안의 정보를 메서드로 읽을 수 있는 것이다.


MultipartFile을 왜 객체처럼 본다고 하는가

객체는 관련된 값과 기능을 함께 가진 구조이다.
MultipartFile도 업로드 파일과 관련된 값과 기능을 함께 가지고 있다.


예를 들어 파일명만 필요하면 getOriginalFilename()을 사용한다.
파일을 선택했는지 확인하려면 isEmpty()를 사용한다.
파일 크기가 필요하면 getSize()를 사용한다.
실제 저장이 필요하면 transferTo()를 사용할 수 있다.


그래서 MultipartFile file은 단순히 파일 이름 하나만 들어 있는 변수가 아니다.
파일 업로드 처리에 필요한 여러 정보를 꺼내고, 파일을 저장하는 기능까지 사용할 수 있는 객체이다.


이 단계에서 정리할 핵심은 이것이다.
4번은 파일이 컨트롤러 매개변수에 어떻게 들어오는지 설명하는 부분이고, 5번은 그렇게 들어온 MultipartFile 객체로 무엇을 할 수 있는지 설명하는 부분이다.



6. MultipartFile에서 꼭 봐야 하는 메서드

getName과 getOriginalFilename

getName()은 파라미터 이름을 반환한다.
즉, form에서 name="file"처럼 지정한 이름을 확인할 때 쓰는 메서드이다.
반면 getOriginalFilename()은 사용자가 실제로 업로드한 파일 이름을 반환한다.
즉, 화면에서 선택한 파일명이 무엇인지 확인할 때 쓰는 메서드이다.


초보자가 여기서 자주 헷갈리는 것은 두 메서드가 둘 다 파일 이름처럼 보인다는 점이다.
하지만 둘은 다르다.
getName()은 입력 필드 이름이고, getOriginalFilename()은 실제 업로드 파일 이름이다.
즉, 하나는 폼의 이름, 다른 하나는 실제 파일의 이름이다.


isEmpty와 getSize

isEmpty()는 업로드된 파일이 실제로 비어 있는지 확인한다.
사용자가 파일을 선택하지 않았는데 요청만 보냈다면 이 값이 중요해진다.
getSize()는 파일 크기를 반환한다.
즉, 업로드된 파일이 얼마나 큰지 확인할 수 있다.


이 두 메서드는 파일 상태를 먼저 확인하는 데 자주 쓰인다.
파일 업로드에서는 바로 저장하는 것보다 먼저 “정말 파일이 들어왔는가”, “크기는 어느 정도인가”를 보는 흐름이 중요하다.
그래서 isEmpty()와 getSize()는 초반에 가장 자주 보게 되는 메서드이다.


getBytes, getInputStream, transferTo

getBytes()는 파일 데이터를 바이트 배열로 가져온다.
getInputStream()은 입력 스트림으로 가져온다.
이 두 메서드는 실제 파일 데이터를 읽어서 별도 처리할 때 쓰인다.
반면 transferTo()는 업로드된 파일 데이터를 지정한 파일로 저장할 때 쓴다.


처음 단계에서는 getBytes()와 getInputStream()를 아주 깊게 파지 않아도 된다.
지금은 “파일 내용을 직접 읽어야 할 때 쓰는 도구” 정도로 이해하면 된다.
반대로 transferTo()는 파일 업로드에서 실제 저장과 연결되기 때문에 초보자 기준에서도 꼭 봐야 할 메서드이다.
즉, 업로드 파일을 실제 파일로 옮겨 저장하는 핵심 메서드라고 이해하면 된다.


기본 개념예제

아래 예제는 MultipartFile에서 가장 자주 보는 메서드 몇 개를 확인하는 예제이다.

// exam05.java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class MultipartFileMethodController {
    @PostMapping("/upload-check")
    public String upload(MultipartFile file) {
        if (file.isEmpty()) { // 파일이 없으면 더 진행하지 않음
            return "파일이 없습니다"; // 파일 없음 응답
        }
        return "필드이름:" + file.getName() // 폼의 파라미터 이름
                + ", 실제파일명:" + file.getOriginalFilename() // 업로드한 실제 파일명
                + ", 파일크기:" + file.getSize(); // 업로드 파일 크기
    }
}
// 출력결과
// 필드이름:file, 실제파일명:test.png, 파일크기:2048

이 예제는 파일 상태 확인 흐름을 보여 준다.
먼저 isEmpty()로 파일이 있는지 확인하고, 그다음 getName(), getOriginalFilename(), getSize()로 파일 정보를 읽는다.
즉, 파일 업로드의 가장 기본 흐름은 파일 존재 확인 → 파일 정보 확인이라고 이해하면 된다.


그리고 여기서 transferTo()는 아직 넣지 않았다.
왜냐하면 지금 단계의 목적은 저장이 아니라, MultipartFile이 어떤 정보를 가지고 있는지 먼저 이해하는 것이기 때문이다.
실제 저장 흐름은 응용예제에서 다시 자연스럽게 연결하면 된다.



7. 파일 업로드는 폼에서 어떻게 전송되는가

파일 업로드는 컨트롤러 코드만 보면 반쪽 이해에 머물기 쉽다.
브라우저 쪽 폼이 어떻게 생겼는지도 같이 봐야 한다.
파일 업로드 폼에서는 보통 method="post", enctype="multipart/form-data", input type="file"가 함께 들어간다.
이 셋은 따로 노는 설정이 아니라, 파일 업로드를 위해 같이 움직이는 구조이다.


method="post"는 데이터를 서버로 보내겠다는 뜻이고, enctype="multipart/form-data"는 파일 업로드에 맞는 전송 형식을 지정하는 것이다.
그리고 input type="file"는 사용자가 실제 파일을 선택하게 해 주는 입력칸이다.
즉, 파일 업로드는 폼 단계부터 이미 일반 입력과 다른 구조를 가진다.


실제 업로드 화면은 파일만 보내는 경우보다, 파일과 일반 입력값을 함께 보내는 경우가 더 많다.
예를 들어 제목과 첨부파일을 같이 보내거나, 설명과 이미지를 같이 보내는 식이다.
이때 브라우저는 문자열 입력값과 파일 입력값을 함께 묶어서 전송한다.
서버는 그중 문자열은 문자열대로 받고, 파일은 MultipartFile로 받으면 된다.


이 흐름이 중요한 이유는 파일 업로드가 이전글의 요청값 처리와 완전히 분리된 것이 아니라는 점을 다시 보여 주기 때문이다.
즉, 문자열과 파일은 한 폼 안에서 같이 전송될 수 있고, 컨트롤러도 이를 함께 받을 수 있다.
이전글에서 DTO나 VO로 여러 값을 묶어서 받던 구조와 자연스럽게 연결되는 이유가 바로 여기에 있다.


기본 개념예제

아래 예제는 제목 입력값 하나와 파일 하나를 함께 보내는 가장 단순한 업로드 예제이다.

// exam06.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>upload with title</title>
</head>
<body>
<form action="/upload-with-title" method="post" enctype="multipart/form-data">
<input type="text" name="title" placeholder="제목 입력"><!-- 일반 문자열 입력 -->
<input type="file" name="file"><!-- 업로드 파일 선택 -->
<input type="submit" value="전송"><!-- 파일과 문자열 함께 전송 -->
</form>
</body>
</html>
// exam06.java
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class UploadWithTitleController {
    @PostMapping("/upload-with-title")
    public String upload(String title, MultipartFile file) {
        System.out.println(title); // 일반 문자열 요청값 확인
        System.out.println(file.getOriginalFilename()); // 업로드 파일명 확인
        return "result"; // 결과 화면 이름 반환
    }
}
// 출력결과
// 공지사항 첨부
// notice.pdf

이 예제는 파일 업로드가 이전글의 요청 처리 구조 위에 얹혀 있다는 점을 가장 분명하게 보여 준다.
title은 일반 문자열 요청값으로 받고, file은 MultipartFile로 받는다.
즉, 한 폼 안에 있는 값들이라고 해도 서버에서는 각 타입에 맞게 나누어 받을 수 있다.


그래서 파일 업로드를 볼 때는 “파일만 따로 다루는 기술”이라고 생각하기보다, 기존 요청값 구조에 파일이 추가된 형태라고 이해하는 편이 훨씬 자연스럽다.
이 흐름이 잡혀 있으면 이후 응용예제에서도 어떤 부분이 새로 나온 개념이고, 어떤 부분이 이전글에서 본 요청 처리 구조의 반복인지 구분하기 쉬워진다.


이제 기본 흐름은 잡혔다.
다음부터는 실제 업로드 예제를 보면서, 위에서 정리한 multipart/form-data, MultipartFile, 파일 바인딩 방식이 코드 안에서 어떻게 이어지는지 2차로 이해하면 된다.




응용예제로 2차 이해

앞에서는 multipart/form-data, MultipartFile, 파일 바인딩 방식을 먼저 정리했다.
이제부터는 그 개념들이 실제 코드 안에서 어떻게 이어지는지 응용예제로 확인한다.
응용예제에서는 파일 업로드 기능만 따로 떼어 보는 것이 아니라, 폼 전송 → 객체 바인딩 → 파일 저장 → 결과 화면 출력이 한 흐름으로 어떻게 이어지는지도 같이 본다.
즉, 여기서는 개념을 다시 외우는 것이 아니라, 실제 업로드 요청이 들어왔을 때 스프링이 파일을 어떻게 받고, 어떻게 저장하고, 결과를 어떤 화면으로 보여 주는지를 결과까지 연결해서 이해하는 것이 핵심이다.


1. 업로드용 VO로 파일 하나 저장하기 (UploadController1.java, FileVO1.java, uploadForm1.html)

이 예제는 파일 하나를 업로드하는 가장 기본적인 응용 흐름이다.
하지만 단순히 파일 하나를 받는 데서 끝나지 않는다.
실제로는 업로드 폼 요청, 업로드용 VO 바인딩, 실제 파일명 확인, 파일 내용 읽기, 실제 경로 저장, 중복 파일 검사, 같은 화면 재사용까지 한 번에 보여 준다.
특히 이 묶음은 파일을 MultipartFile 매개변수로 직접 받지 않고, FileVO1 안에 넣어서 받는다는 점이 중요하다.
즉, 이전글에서 보던 DTO, VO 바인딩 흐름이 파일 업로드에도 그대로 이어진다는 것을 보여 주는 예제이다.


이 예제가 같이 보여주는 개념

  • form에서 enctype="multipart/form-data"를 설정해야 파일 업로드 요청이 만들어진다.
  • input type="file"의 name 값과 VO의 필드 이름이 같아야 파일이 정상적으로 바인딩된다.
  • 컨트롤러는 FileVO1 vo처럼 업로드용 객체를 매개변수로 받아 파일을 처리할 수 있다.
  • getOriginalFilename()으로 실제 업로드 파일명을 확인할 수 있다.
  • getBytes()로 파일 내용을 바이트 배열로 읽을 수 있다.
  • File 객체와 FileOutputStream을 사용하면 업로드 파일을 실제 경로에 저장할 수 있다.
  • 같은 이름의 파일이 이미 있으면 저장하지 않고 메시지만 보여 줄 수 있다.
  • ModelAndView와 Thymeleaf 조건식을 이용하면 같은 화면을 재사용하면서 결과 메시지를 보여 줄 수 있다.

즉, 이 예제는 파일 업로드 기능만 따로 보여 주는 것이 아니라,
폼 전송 → 객체 바인딩 → 파일 확인 → 실제 저장 → 결과 화면 출력 전체 흐름을 한 번에 보여 주는 예제이다.


코드 설명

먼저 업로드 화면부터 보면, 사용자가 파일 하나를 선택해서 /upload1로 전송하는 구조이다.

// uploadForm1.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JSP Test Example</title>
</head>
<body>
<h2>SpringMVC FileUpload</h2>
<hr>
<form th:unless="${ msg }" action="/upload1" enctype="multipart/form-data" method="post">
<input type="file" name="uploadFile" /> <!-- 업로드할 파일 선택 -->
<input type="submit" value="업로드"/> <!-- 업로드 요청 전송 -->
</form>

<h2 th:if="${ msg }" th:text="${ msg }"></h2> <!-- 결과 메시지 출력 -->
</body>
</html>

이 코드에서 먼저 봐야 할 것은 enctype="multipart/form-data"이다.

앞에서 정리한 것처럼 파일 업로드 요청을 만들려면 이 설정이 꼭 필요하다.
그리고 파일 입력칸의 이름이 uploadFile인데, 이 이름은 나중에 VO 안의 필드 이름과 연결된다.
즉, 폼의 name 값과 업로드용 객체의 필드 이름이 맞아야 스프링이 파일을 자연스럽게 넣어 줄 수 있다.


또 th:unless="${ msg }"와 th:if="${ msg }"도 중요하다.
처음에는 msg가 없으므로 업로드 폼이 보인다.

반대로 업로드가 끝나서 메시지가 생기면 폼은 숨고, 그 자리에 결과 메시지가 출력된다.

즉, 이 화면은 입력 화면과 업로드 결과 화면을 같은 페이지에서 함께 처리하는 구조이다.
이제 업로드용 객체를 보면, 파일을 MultipartFile 필드 하나로 감싸고 있다.

// FileVO1.java
package com.example.springedu.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.web.multipart.MultipartFile;

@Getter
@AllArgsConstructor
public class FileVO1 {
    private MultipartFile uploadFile; // 업로드 파일이 바인딩될 필드
}

여기서 핵심은 uploadFile이라는 필드 이름이다.
이 이름이 폼의 name="uploadFile"과 맞기 때문에 스프링이 업로드 파일을 이 필드에 연결할 수 있다.
즉, 이 예제는 파일 업로드도 결국 이전글에서 본 객체 바인딩 구조 위에서 동작한다는 점을 보여 준다.

이제 실제 저장을 처리하는 컨트롤러를 보면 흐름이 더 분명해진다.

// UploadController1.java
package com.example.springedu.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import com.example.springedu.domain.FileVO1;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UploadController1 {
    @RequestMapping("/uploadForm1")
    public void formFile() {
        // 요청 경로 이름으로 화면을 찾는다
    }

    @RequestMapping("/upload1")
    public ModelAndView saveFile(FileVO1 vo) {
        String fileName = vo.getUploadFile().getOriginalFilename(); // 실제 업로드 파일명 확인

        byte[] content = null;
        ModelAndView mav = new ModelAndView();
        mav.setViewName("uploadForm1"); // 같은 화면 재사용
        try {
            content = vo.getUploadFile().getBytes(); // 업로드 파일 내용을 바이트 배열로 읽음
            File f = new File("/Users/leeunduck/uploadtest/" + fileName); // 저장할 실제 파일 경로 생성
            if (f.exists()) { // 같은 이름의 파일이 이미 있으면
                mav.addObject("msg", fileName + " : 파일이 이미 존재해요!!");
            } else {
                FileOutputStream fos = new FileOutputStream(f); // 파일 저장 스트림 생성
                fos.write(content); // 실제 파일 저장
                fos.close(); // 스트림 닫기
                mav.addObject("msg", fileName + ": 파일이 저장되었어요!!");
            }
        } catch (IOException e) {
            e.printStackTrace();
            mav.addObject("msg", "오류가 발생했어요!!");
        }
        return mav;
    }
}

이 코드에서 먼저 봐야 할 것은 saveFile(FileVO1 vo)이다.
컨트롤러가 MultipartFile을 직접 받는 것이 아니라 FileVO1 객체를 통째로 받고 있다.

즉, 업로드 파일이 VO 안으로 먼저 바인딩된 다음, 컨트롤러는 그 객체에서 다시 파일을 꺼내 쓰는 구조이다.


이 지점이 바로 이전글의 DTO, VO 바인딩 흐름과 연결되는 부분이다.
그리고 vo.getUploadFile().getOriginalFilename()으로 실제 파일명을 확인한다.
이건 앞에서 본 MultipartFile 메서드 중 getOriginalFilename()이 실제로 어디서 쓰이는지를 보여 준다.

그다음 getBytes()로 파일 내용을 바이트 배열로 읽는다.
즉, 이 예제는 transferTo() 대신 getBytes() + FileOutputStream 방식으로 저장하는 흐름을 보여 준다.


또 File f = new File("/Users/leeunduck/uploadtest/" + fileName); 부분도 중요하다.
이 줄은 업로드한 파일을 어디에 저장할지 실제 경로를 만드는 부분이다.
그리고 f.exists()로 같은 이름의 파일이 이미 있는지 먼저 확인한다.

즉, 이 예제는 업로드 파일을 무조건 저장하지 않고, 같은 이름의 파일이 이미 있으면 저장을 막는 흐름까지 함께 보여 준다.


그래서 결과가 이렇게 나온다

이 묶음의 실행 결과를 보면, 먼저 파일을 선택하고 업로드를 진행하는 흐름이 보인다.
파일 하나 업로드 실행 과정

그리고 업로드가 끝나면 같은 화면으로 돌아오면서 결과 메시지가 출력된다.
파일 하나 업로드 결과 메시지
이 장면에서 중요한 점은 화면이 다른 페이지로 바뀌지 않았다는 것이다.
같은 uploadForm1.html을 다시 사용하면서, msg 값이 생겼기 때문에 업로드 폼 대신 처리 결과가 출력된 것이다.

즉, Thymeleaf의 th:unless와 th:if가 실제로 어떻게 쓰이는지도 결과 화면에서 확인할 수 있다.


그래서 이 묶음의 전체 흐름은
업로드 폼 요청 → 업로드용 VO 바인딩 → 실제 파일명 확인 → 파일 내용 읽기 → 실제 경로 저장 → 같은 화면에 결과 메시지 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
파일 하나 업로드도 결국 폼 전송, 객체 바인딩, 파일 메서드 사용, 실제 저장, 결과 화면 출력이 모두 이어지는 흐름이라는 점이다.


2. 업로드용 VO 배열로 여러 파일 저장하기 (UploadController2.java, FileVO2.java, uploadForm2.html)

이 예제는 파일 하나가 아니라 여러 파일을 한 번에 업로드하는 흐름을 보여 준다.
하지만 단순히 파일 입력칸에 multiple만 붙였다고 끝나는 예제가 아니다.

실제로는 여러 파일 선택, 배열 바인딩, 반복문 처리, 중복 파일 검사, transferTo() 저장, 여러 결과 메시지 누적 출력까지 함께 보여 준다.

즉, 이 묶음은 앞의 1번 예제가 “파일 하나를 VO로 받는 흐름”이었다면, 이번에는 여러 파일을 MultipartFile[] 배열로 묶어 받는 흐름으로 확장된 예제이다.


이 예제가 같이 보여주는 개념

  • input type="file"에 multiple을 주면 여러 파일을 선택할 수 있다.
  • 파일 입력칸 이름과 VO 안의 배열 필드 이름이 같아야 여러 파일이 정상적으로 바인딩된다.
  • 업로드용 객체 안에 MultipartFile[] 배열을 두면 여러 파일을 한 번에 받을 수 있다.
  • for 반복문으로 업로드한 파일들을 하나씩 처리할 수 있다.
  • transferTo()를 사용하면 각 파일을 실제 저장 경로에 바로 저장할 수 있다.
  • 같은 이름의 파일이 이미 있으면 그 파일만 저장을 막고 메시지를 누적할 수 있다.
  • 여러 파일 처리 결과를 하나의 문자열로 누적해서 같은 화면에 보여 줄 수 있다.

즉, 이 예제는 여러 파일 업로드에서 가장 중요한
배열 바인딩 → 반복 처리 → 개별 저장 → 결과 누적 출력 흐름을 한 번에 보여 준다.


코드 설명

먼저 업로드 화면부터 보면 파일 선택칸에 multiple이 들어가 있다.

// uploadForm2.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JSP 테스트 예제</title>
</head>
<body>
<h2>SpringMVC FileUpload(Multiple)</h2>
<hr>
<form th:unless="${ msg }" action="/upload2" enctype="multipart/form-data" method="post">
<input type="file" name="uploadFiles" multiple/> <!-- 여러 파일 선택 -->
<input type="submit" value="파일올리기"/> <!-- 업로드 요청 전송 -->
</form>
<h2 th:if="${ msg }" th:utext="${ msg }"></h2> <!-- 여러 결과 메시지 출력 -->
</body>
</html>

여기서 가장 먼저 봐야 할 것은 multiple이다.
이 속성이 있기 때문에 사용자는 파일을 여러 개 선택할 수 있다.

그리고 파일 입력칸 이름이 uploadFiles인데, 이 이름은 나중에 VO 안의 배열 필드 이름과 연결된다.
즉, 여러 파일 업로드도 결국 입력칸 이름과 객체 필드 이름이 맞아야 바인딩이 된다는 점은 같다.
이제 업로드용 객체를 보면 MultipartFile[] 배열이 들어가 있다.

// FileVO2.java
package com.example.springedu.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.web.multipart.MultipartFile;

@Getter
@AllArgsConstructor
public class FileVO2 {
    private MultipartFile[] uploadFiles; // 여러 업로드 파일이 바인딩될 배열 필드
}

이 객체는 앞의 FileVO1과 비슷하지만, 파일 하나가 아니라 배열을 가진다는 점이 다르다.

즉, 이번에는 파일 여러 개가 한 번에 들어올 수 있기 때문에 MultipartFile 하나가 아니라 MultipartFile[] 배열로 받아야 한다.
이제 실제 저장을 처리하는 컨트롤러를 보면 반복문 흐름이 핵심이다.

// UploadController2.java
package com.example.springedu.controller;

import java.io.File;
import java.io.IOException;

import com.example.springedu.domain.FileVO2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UploadController2 {
    @RequestMapping("/uploadForm2")
    public void formFile() {
        // 요청 경로 이름으로 화면을 찾는다
    }

    @RequestMapping("/upload2")
    public ModelAndView saveFile(FileVO2 vo) {
        System.out.println(vo.getUploadFiles().length + "개가 업로드 됨"); // 업로드된 파일 개수 확인

        String resultStr = "";
        String path = "/Users/leeunduck/uploadtest/multi"; // 여러 파일 저장 경로

        File isDir = new File(path);
        if (!isDir.isDirectory()) {
            isDir.mkdirs(); // 저장 폴더가 없으면 생성
        }

        ModelAndView mav = new ModelAndView();
        mav.setViewName("uploadForm2"); // 같은 화면 재사용

        for (MultipartFile mfile : vo.getUploadFiles()) { // 여러 파일을 하나씩 꺼내 처리
            String fileName = mfile.getOriginalFilename(); // 실제 업로드 파일명 확인

            try {
                File f = new File(path + "/" + fileName); // 저장할 실제 파일 경로 생성

                if (f.exists()) { // 같은 이름의 파일이 이미 있으면
                    resultStr += fileName + " : 파일이 이미 존재해요!!<br>";
                } else {
                    mfile.transferTo(f); // 실제 파일 저장
                    resultStr += fileName + " : 파일이 저장되었어요!!<br>";
                }

            } catch (IOException e) {
                e.printStackTrace();
                resultStr += fileName + " : 오류가 발생했어요!!<br>";
            }
        }

        mav.addObject("msg", resultStr); // 여러 처리 결과를 한 번에 전달
        return mav;
    }
}

이 코드에서 핵심은 vo.getUploadFiles()이다.
이번에는 파일 하나가 아니라 여러 파일이 배열로 바인딩되어 들어온다.
그래서 컨트롤러는 그 배열을 반복문으로 돌면서 파일 하나씩 꺼내 처리해야 한다.

즉, 이 예제는 여러 파일 업로드를 결국 “파일 하나 처리 로직을 여러 번 반복하는 구조”로 해결한다는 점을 보여 준다.


그리고 저장 방식은 앞의 1번과 다르다.
1번은 getBytes()와 FileOutputStream을 사용했다.

이번에는 mfile.transferTo(f)를 사용한다.
즉, 이 예제는 여러 파일 업로드에서 transferTo()가 실제로 어떻게 쓰이는지를 보여 준다.


또 resultStr += ... 부분도 중요하다.
여러 파일을 처리하면 결과도 여러 줄이 될 수 있다.
그래서 결과 문자열을 하나씩 이어 붙인 뒤, 마지막에 한 번에 msg로 넘긴다.

즉, 이 예제는 여러 파일의 저장 결과를 누적해서 같은 화면에 출력하는 구조도 함께 보여 준다.


그래서 결과가 이렇게 나온다

이 묶음의 실행 결과를 보면, 여러 파일을 선택해서 업로드하는 흐름이 보인다.

여러 파일 업로드 실행 과정

그리고 업로드가 끝나면 여러 파일 처리 결과가 한 번에 메시지로 출력된다.

여러 파일 업로드 결과 메시지
이 장면에서 중요한 것은 결과가 한 줄이 아니라 여러 줄로 누적되어 보인다는 점이다.
파일을 여러 개 처리했기 때문에, 각 파일의 저장 성공 또는 중복 여부가 결과 문자열에 차례대로 붙는다.

즉, 이 예제는 여러 파일 업로드에서는 결과도 파일 개수만큼 누적해서 보여 줄 수 있다는 점을 보여 준다.


그래서 이 묶음의 전체 흐름은
여러 파일 선택 → VO 배열 바인딩 → 반복문으로 파일 하나씩 처리 → transferTo() 저장 → 결과 문자열 누적 → 같은 화면에 한 번에 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
여러 파일 업로드는 파일 하나 처리 로직을 배열 바인딩과 반복문으로 확장한 구조라는 점이다.


3. MultipartRequest로 여러 파일 저장하기 (UploadController3.java, uploadForm3.html)

이 예제도 여러 파일 업로드를 처리한다.
하지만 2번과는 받는 방식이 다르다.

2번은 업로드용 VO 안에 MultipartFile[] 배열을 넣어서 받았다.
이번에는 업로드 요청 자체를 MultipartRequest로 받고, 그 안에서 getFiles("mfile")로 파일 목록을 꺼내는 구조이다.

즉, 이 묶음은 여러 파일 업로드를 처리할 때 배열 바인딩 방식 말고 MultipartRequest 방식도 사용할 수 있다는 점을 보여 주는 예제이다.


이 예제가 같이 보여주는 개념

  • 여러 파일 업로드는 MultipartRequest로도 받을 수 있다.
  • getFiles("mfile")를 사용하면 같은 이름으로 전송된 파일 목록을 List<MultipartFile>로 꺼낼 수 있다.
  • 업로드 폼의 파일 입력칸 이름과 getFiles() 안의 이름이 같아야 정상적으로 파일 목록을 얻을 수 있다.
  • 저장 폴더가 없으면 mkdirs()로 폴더를 먼저 만들 수 있다.
  • 반복문으로 파일 목록을 돌면서 각 파일을 저장할 수 있다.
  • transferTo()로 여러 파일을 실제 경로에 저장할 수 있다.
  • 각 파일 결과를 문자열로 누적해 한 번에 화면에 보여 줄 수 있다.

즉, 이 예제는 여러 파일 업로드를
요청 객체에서 직접 목록을 꺼내 처리하는 방식으로 보여 준다.


코드 설명

먼저 업로드 화면을 보면 여러 파일 선택 구조는 2번과 비슷하다.

// uploadForm3.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>JSP 테스트 예제</title>
</head>
<body>
<h2>SpringMVC FileUpload(Multiple)-MultipartRequest</h2>
<hr>
<form th:unless="${ msg }" action="/upload3" enctype="multipart/form-data" method="post">
<input type="file" name="mfile" multiple/> <!-- 여러 파일 선택 -->
<input type="submit" value="파일올리기"/> <!-- 업로드 요청 전송 -->
</form>
<h2 th:if="${ msg }" th:utext="${ msg }"></h2> <!-- 여러 결과 메시지 출력 -->
</body>
</html>

여기서 중요한 것은 파일 입력칸 이름이 mfile이라는 점이다.
이 이름은 나중에 컨트롤러에서 mreq.getFiles("mfile")로 연결된다.
즉, 이번에도 여전히 폼의 이름과 서버에서 꺼내는 이름이 정확히 맞아야 한다.
이제 실제 저장을 처리하는 컨트롤러를 보면 MultipartRequest를 직접 받고 있다.

// UploadController3.java
package com.example.springedu.controller;

import java.io.File;
import java.io.IOException;
import java.util.List;

import jakarta.servlet.ServletContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class UploadController3 {
    @Autowired
    ServletContext context;

    @RequestMapping("/uploadForm3")
    public void formFile() {
        // 요청 경로 이름으로 화면을 찾는다
    }

    @RequestMapping("/upload3")
    public ModelAndView saveFile(MultipartRequest mreq) {
        ModelAndView mav = new ModelAndView();
        List<MultipartFile> list = mreq.getFiles("mfile"); // 같은 이름의 업로드 파일 목록 추출
        System.out.println(list.size() + "개가 업로드 됨"); // 업로드된 파일 개수 확인

        String resultStr = "";
        String path = "/Users/leeunduck/uploadtest//multi"; // 여러 파일 저장 경로
        File isDir = new File(path);
        if (!isDir.isDirectory()) {
            isDir.mkdirs(); // 저장 폴더가 없으면 생성
        }
        mav.setViewName("uploadForm3"); // 같은 화면 재사용

        for (MultipartFile mfile : list) { // 파일 목록을 하나씩 꺼내 처리
            String fileName = mfile.getOriginalFilename(); // 실제 업로드 파일명 확인
            try {
                File f = new File("/Users/leeunduck/uploadtest//multi/" + fileName); // 저장할 실제 파일 경로 생성
                if (f.exists()) { // 같은 이름의 파일이 이미 있으면
                    resultStr += fileName + " : 파일이 이미 존재해요!!<br>";
                } else {
                    mfile.transferTo(f); // 실제 파일 저장
                    resultStr += fileName + " : 파일이 저장되었어요!!<br>";
                }
            } catch (IOException e) {
                e.printStackTrace();
                resultStr += fileName + " : 오류가 발생했어요!!<br>";
            }
        }
        mav.addObject("msg", resultStr); // 여러 처리 결과를 한 번에 전달
        return mav;
    }
}

이 코드에서 가장 중요한 부분은 saveFile(MultipartRequest mreq)이다.
이번에는 업로드 파일을 VO 배열로 받는 것이 아니라, 업로드 요청 전체를 받을 수 있는 MultipartRequest를 받고 있다.

즉, 이 예제는 업로드 요청 객체에서 직접 파일 목록을 꺼내는 방식이다.


그리고 mreq.getFiles("mfile")를 보면, 같은 이름 mfile로 전달된 파일들을 List<MultipartFile> 형태로 꺼내고 있다.

즉, 여러 파일 업로드를 배열 대신 리스트로 처리하는 구조라고 이해하면 된다.


그 이후 흐름은 2번과 비슷하다.
반복문으로 파일을 하나씩 꺼내고, getOriginalFilename()으로 실제 파일명을 확인하고, transferTo()로 저장하고, 결과 문자열을 누적한다.

즉, 받는 방식은 달라도 이후 저장 흐름은 결국 파일 하나씩 반복 처리하는 구조는 같다.


또 ServletContext가 주입되어 있지만, 현재 코드는 실제 저장 경로를 직접 문자열로 지정해서 사용하고 있다.
즉, 이 예제에서는 ServletContext보다 MultipartRequest로 파일 목록을 꺼내는 방식이 핵심이다.


그래서 결과가 이렇게 나온다

이 묶음의 실행 결과를 보면, 여러 파일을 선택해서 업로드하는 흐름이 보인다.

MultipartRequest 여러 파일 업로드 실행 과정

그리고 업로드가 끝나면 여러 파일 처리 결과가 한 번에 메시지로 출력된다.

MultipartRequest 여러 파일 업로드 결과 메시지
이 장면에서 중요한 것은 2번과 결과 화면은 비슷해 보여도, 컨트롤러가 파일을 받는 방식이 다르다는 점이다.
2번은 VO 안의 MultipartFile[] 배열을 사용했다.
이번에는 MultipartRequest에서 getFiles("mfile")로 파일 목록을 직접 꺼냈다.

즉, 여러 파일 업로드는 결과는 비슷해도 컨트롤러가 파일을 받는 방식은 두 가지로 나뉠 수 있다는 점을 보여 준다.


그래서 이 묶음의 전체 흐름은
여러 파일 선택 → MultipartRequest로 업로드 요청 받기 → getFiles("mfile")로 파일 목록 추출 → 반복문으로 파일 하나씩 처리 → transferTo() 저장 → 결과 문자열 누적 출력
이라고 정리할 수 있다.
이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
여러 파일 업로드는 VO 배열 방식으로도 처리할 수 있고, MultipartRequest에서 직접 파일 목록을 꺼내는 방식으로도 처리할 수 있다는 점이다.


여기서 먼저 정리해야 할 핵심

  • 1번은 파일 하나를 업로드용 VO 안의 MultipartFile로 받아 저장하는 흐름이다.
  • 2번은 여러 파일을 업로드용 VO 안의 MultipartFile[] 배열로 받아 반복 처리하는 흐름이다.
  • 3번은 여러 파일을 MultipartRequest로 받고, getFiles()로 파일 목록을 꺼내 처리하는 흐름이다.
  • 1번은 저장할 때 getBytes()와 FileOutputStream을 사용했다.
  • 2번과 3번은 저장할 때 transferTo()를 사용했다.
  • 세 예제 모두 같은 이름의 파일이 이미 있으면 저장을 막고 메시지를 다르게 보여 준다.
  • 세 예제 모두 결과 화면을 새로 만들기보다 같은 화면을 다시 사용하면서 메시지를 출력하는 구조를 사용한다.

즉, 파일 업로드 응용예제는 겉으로 보면 비슷해 보여도,
파일 하나인지 여러 개인지,
VO로 받을지 MultipartRequest로 받을지,
어떤 메서드로 저장할지에 따라 코드 구조가 달라진다.
하지만 공통 흐름은 같다.
결국 파일 업로드는 폼 전송 → 파일 바인딩 → 실제 저장 → 결과 출력으로 이어지는 구조라고 이해하면 된다.


4. canvas에 그린 그림을 업로드하고 다시 불러오기 (UploadController4.java, canvas_memo.html, canvas_memo.js)

이 예제는 앞에서 본 일반 파일 업로드와 결이 조금 다르다.
앞의 예제들은 사용자가 input type="file"로 이미 존재하는 파일을 선택해서 서버로 보냈다.
그런데 이번 예제는 브라우저 안의 canvas에 직접 그린 그림을 서버로 업로드한다.
즉, 브라우저에서 만든 그림 데이터를 파일처럼 만들어 업로드하고, 다시 불러와서 복원하는 흐름을 보여 주는 예제이다.


그래서 이 묶음은 단순히 파일 업로드만 보여 주는 것이 아니라, canvas 그림 생성, Blob 만들기, FormData 전송, MultipartFile 수신, 서버 저장, 문자열 응답, 이미지 복원까지 한 번에 연결해서 보여 준다.


이 예제가 같이 보여주는 개념

  • canvas.toDataURL()을 사용하면 현재 canvas 그림을 문자열 데이터로 바꿀 수 있다.
  • Blob과 FormData를 사용하면 브라우저 안에서 만든 데이터를 파일처럼 전송할 수 있다.
  • data.append("mfile", ...)의 이름과 컨트롤러의 MultipartFile mfile 이름이 같아야 정상적으로 연결된다.
  • @RestController는 뷰를 찾지 않고 응답 본문 자체를 바로 반환한다.
  • 업로드 요청 결과를 "OK", "FAIL" 같은 짧은 문자열로 바로 응답할 수 있다.
  • getBytes()와 FileOutputStream으로 업로드 데이터를 실제 경로에 저장할 수 있다.
  • 이 예제의 다운로드는 일반 파일 다운로드가 아니라, 저장된 문자열 데이터를 다시 읽어 이미지로 복원하는 흐름이다.
  • localStorage에 저장하는 save(), restore()와 서버에 저장하는 upload(), downimage()는 서로 다른 기능이다.

즉, 이 예제는 파일 업로드를 이미 있는 파일을 보내는 기능으로만 보지 않고, 브라우저 안에서 만든 데이터도 파일처럼 보내고 다시 복원할 수 있는 기능으로 확장해서 보여 준다.


코드 설명

먼저 화면을 보면 버튼과 canvas가 같이 있다.

// canvas_memo.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8"/>
<title>Draw memo on canvas</title>
</head>
<body>
<button onclick="save();">Save</button>
<button onclick="restore();">Restore</button>
<button onclick="initialize();">Clear</button>
<button onclick="upload();">Upload</button>
<button onclick="downimage();">DownImage</button><br><br>
<button onclick="setColor('red');">RED</button>
<button onclick="setColor('blue');">BLUE</button>
<button onclick="setColor('green');">GREEN</button>
<button onclick="setColor('yellow');">YELLOW</button>
<button onclick="setColor('black');">BLACK</button><br><br>
<canvas id="myCanvas" width="580" height="450"></canvas>
<script src="canvas_memo.js"></script>
</body>
</html>

이 화면에서 중요한 것은 Upload와 DownImage 버튼이다.
Save, Restore는 브라우저의 localStorage를 사용하는 기능이고, Upload, DownImage는 서버와 통신하는 기능이다.
즉, 이 예제는 브라우저 내부 저장과 서버 저장을 같이 보여 주지만, 실제 파일 업로드와 직접 연결되는 것은 Upload, DownImage 쪽이다.


이제 JavaScript를 보면 업로드와 복원 흐름이 더 분명해진다.

// canvas_memo.js
function upload() {
    var data = new FormData(); // 업로드용 폼 데이터 객체 생성
    var myblob = new Blob([canvas.toDataURL()], {type : 'text/plain'}); // canvas 그림을 문자열로 만든 뒤 Blob으로 감쌈
    data.append('mfile', myblob, "test.png"); // mfile 이름으로 서버에 보낼 데이터 추가

    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        if (xhr.responseText == "OK")
            alert("UPLOAD SUCCESS"); // 업로드 성공
        else
            alert("UPLOAD FAIL"); // 업로드 실패
    };
    xhr.open("POST", "/canvasupload", true); // 업로드 요청
    xhr.send(data);
}

function downimage() {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        var img = new Image();
        img.onload = function() {
            context.drawImage(img, 0, 0); // 받은 이미지를 다시 canvas에 그림
        }
        img.src = xhr.responseText; // 서버가 돌려준 문자열을 이미지 소스로 사용
    }
    xhr.open("GET", "/canvasdownload", true); // 저장된 데이터 읽기 요청
    xhr.send();
}

여기서 가장 중요한 줄은 canvas.toDataURL()이다.
이 메서드는 현재 canvas에 그려진 그림을 문자열 형태로 바꾼다.
즉, 지금 이 예제는 진짜 이미지 파일 자체를 바로 보내는 것이 아니라, 그림을 문자열 데이터로 바꾼 뒤 그것을 업로드하는 구조이다.


그다음 new Blob([canvas.toDataURL()], {type : 'text/plain'})도 중요하다.
이 코드는 canvas 그림 문자열을 Blob으로 감싼다.
그리고 data.append('mfile', myblob, "test.png")를 보면, 이 Blob을 mfile이라는 이름으로 FormData에 넣고 있다.
즉, 브라우저 안에서 만든 데이터를 파일처럼 포장해서 서버로 보내는 구조이다.


또 여기서 초보자가 가장 헷갈리기 쉬운 부분이 있다.
파일 이름은 "test.png"이지만, 실제 내용은 일반 PNG 바이너리 파일이 아니다.
안에 들어 있는 내용은 canvas.toDataURL()이 만든 문자열 데이터이다.
즉, 확장자는 .png처럼 보이지만, 실제 저장 내용은 그림을 나타내는 문자열이라는 점이 이 예제의 핵심이다.


이제 서버 쪽 코드를 보면 왜 이런 흐름이 가능한지가 보인다.

// UploadController4.java
package com.example.springedu.controller;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class UploadController4 {
    @RequestMapping(value="/canvasupload", produces="text/plain; charset=utf-8")
    public String saveFile(MultipartFile mfile) {
        String fileName = mfile.getOriginalFilename(); // 업로드 파일명 확인
        byte[] content = null;
        String result = "OK";
        try {
            content = mfile.getBytes(); // 업로드 데이터 읽기
            File f = new File("/Users/leeunduck/uploadtest/" + fileName); // 저장 경로 생성
            FileOutputStream fos = new FileOutputStream(f);
            fos.write(content); // 실제 파일 저장
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
            result = "FAIL";
        }
        return result; // 결과 문자열 직접 응답
    }

    @RequestMapping(value="/canvasdownload", produces="text/plain; charset=utf-8")
    public String downloadFile() {
        String path = "/Users/leeunduck/uploadtest/";
        char[] buffer = null;
        try {
            FileReader reader = new FileReader(path + "test.png"); // 저장된 문자열 데이터 읽기
            buffer = new char[(int)(new File(path + "test.png").length())];
            reader.read(buffer);
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new String(buffer); // 읽은 문자열을 그대로 응답
    }
}

이 코드에서 먼저 @RestController를 봐야 한다.
이 예제는 결과 화면으로 이동하는 구조가 아니라, "OK", "FAIL" 같은 문자열이나 다운로드용 문자열 데이터를 응답 본문으로 바로 보내는 구조이다.
즉, 앞의 예제들처럼 같은 화면을 다시 보여 주는 방식이 아니라, 이번에는 AJAX 요청에 맞게 데이터 자체를 바로 응답하는 방식이다.


saveFile(MultipartFile mfile)를 보면, MultipartFile 이름이 mfile이다.
이 이름은 JavaScript의 data.append('mfile', ...)와 정확히 맞아야 한다.
즉, 이번에도 결국 브라우저에서 보내는 이름과 컨트롤러 매개변수 이름이 연결 기준이 된다.


그리고 mfile.getBytes()로 업로드된 내용을 읽고, FileOutputStream으로 /Users/leeunduck/uploadtest/ 아래에 저장한다.
이 부분은 1번 예제와 비슷하다.
다만 여기서는 사용자가 고른 일반 파일이 아니라, 브라우저가 만든 canvas 데이터 문자열을 저장하고 있다는 점이 다르다.
또 이 코드는 중복 검사 없이 같은 이름이면 그냥 다시 저장하기 때문에, 이전 예제들처럼 중복 메시지를 만드는 구조는 아니다.
즉, 이번 예제는 덮어쓰기 흐름에 더 가깝다.


downloadFile()도 중요하다.
여기서는 FileReader로 test.png를 문자처럼 읽고, 그 결과를 문자열로 반환한다.
일반적인 PNG 바이너리 파일이라면 이렇게 읽는 흐름이 자연스럽지 않다.
그런데 이번 예제는 저장된 내용 자체가 canvas.toDataURL() 문자열이기 때문에, 문자로 읽어서 다시 문자열로 돌려줘도 동작한다.
즉, 이 예제는 파일처럼 저장했지만, 실제로는 문자열 데이터를 저장하고 다시 돌려주는 구조이다.


그래서 결과가 이렇게 나온다

이 묶음의 실행 결과를 보면, 먼저 canvas에 그림을 그리고 Upload 버튼을 눌러 업로드하는 흐름이 보인다.




이 장면에서 중요한 것은 일반 파일 선택 창을 거치지 않았다는 점이다.
사용자가 직접 파일을 고른 것이 아니라, 브라우저 안의 canvas 그림이 Blob과 FormData를 통해 서버로 전송되었다.
즉, 이 예제는 브라우저에서 만든 데이터를 파일처럼 업로드할 수 있다는 점을 실제로 보여 준다.


그리고 업로드가 끝나면 uploadtest 폴더 아래에 test.png가 저장된 것을 확인할 수 있다.




이 장면에서 초보자가 꼭 알아야 할 점은, 이 파일이 이름은 test.png이지만 내부에는 canvas.toDataURL()에서 만든 문자열 데이터가 들어 있다는 점이다.
즉, 겉보기에는 이미지 파일처럼 저장되어 있어도, 이번 예제의 핵심은 그림 데이터를 문자열로 저장하고 다시 복원하는 흐름에 있다.


그래서 이 묶음의 전체 흐름은 canvas에 그림 그리기 → toDataURL()로 문자열 만들기 → Blob과 FormData로 업로드 → MultipartFile로 받기 → 실제 경로 저장 → 다시 문자열로 읽기 → img.src에 넣어 복원하기라고 정리할 수 있다.


이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
파일 업로드는 이미 있는 파일만 보내는 기능이 아니라, 브라우저 안에서 만든 데이터도 파일처럼 만들어 서버에 저장하고 다시 불러올 수 있는 기능이라는 점이다.


5. 저장된 Data URL 문자열을 다시 이미지로 출력하기 (DataURLController.java, imgtag.html)

이 예제는 바로 앞의 canvas 업로드 예제와 자연스럽게 이어진다.
앞 예제에서는 canvas.toDataURL()로 만든 문자열 데이터를 test.png라는 이름으로 저장했다.
이번 예제는 그 저장된 문자열을 다시 읽어서 브라우저로 보내고, 브라우저가 그 문자열을 img 태그의 src에 넣어 이미지를 다시 보여 주는 흐름을 확인하는 예제이다.


즉, 이 묶음은 일반적인 파일 다운로드 예제가 아니다.
서버가 이미지 파일 자체를 바이너리로 내려주는 것이 아니라, 그림을 표현하는 문자열 데이터를 다시 보내고, 브라우저가 그 문자열을 이미지 주소처럼 해석해서 화면에 그리는 구조를 보여 준다.


이 예제가 같이 보여주는 개념

  • canvas.toDataURL()로 만든 문자열은 그림을 나타내는 Data URL 데이터이다.
  • 서버는 저장된 Data URL 문자열을 다시 읽어서 그대로 응답할 수 있다.
  • @RestController는 뷰를 찾지 않고 응답 본문 자체를 바로 반환한다.
  • produces="text/plain; charset=US-ASCII"는 지금 반환값이 화면 이름이 아니라 일반 문자열이라는 뜻이다.
  • fetch("/dataurl")로 받은 응답은 문자열이므로 res.text()로 읽는다.
  • 브라우저는 img.src = 문자열처럼 Data URL을 이미지 주소처럼 사용할 수 있다.
  • 즉, 이 예제는 저장된 그림 문자열 → 서버 응답 → 브라우저 이미지 복원 흐름을 보여 준다.

즉, 이 예제의 핵심은 파일처럼 저장된 문자열 데이터를 다시 읽어 와서, 브라우저가 그것을 이미지로 복원할 수 있다는 점이다.


코드 설명

먼저 서버 쪽 컨트롤러를 보면 /dataurl 요청이 들어왔을 때 test.png 파일 내용을 문자 단위로 읽어서 그대로 문자열로 반환한다.

// DataURLController.java
package com.example.springedu.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.File;
import java.io.FileReader;

@RestController
public class DataURLController {
    @RequestMapping(value = "/dataurl", produces="text/plain; charset=US-ASCII")
    public String getDataURL() {
        char[] content = null;
        File f = new File("/Users/leeunduck/uploadtest/test.png"); // 저장된 test.png 확인
        if (f.exists() && f.length() > 0) {
            content = new char[(int)f.length()]; // 파일 길이만큼 문자 배열 준비
            try (FileReader reader = new FileReader("/Users/leeunduck/uploadtest/test.png")) {
                reader.read(content); // 파일 내용을 문자로 읽음
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return new String(content); // 읽은 문자열을 그대로 응답
    }
}

여기서 가장 중요한 점은 FileReader를 사용하고 있다는 것이다.
일반적인 진짜 PNG 바이너리 파일이라면 이렇게 문자로 읽는 흐름이 자연스럽지 않다.
그런데 지금 test.png 안에는 앞 예제에서 저장한 canvas.toDataURL() 문자열이 들어 있으므로, 문자로 읽어서 다시 문자열로 반환하는 흐름이 가능하다.


또 @RestController와 produces="text/plain; charset=US-ASCII"도 중요하다.
이 설정 때문에 이 메서드는 어떤 화면으로 이동하는 것이 아니라, 파일에서 읽은 문자열 자체를 그대로 응답 본문으로 보낸다.
즉, 이 예제는 뷰 반환이 아니라 문자열 데이터 반환 구조이다.


이제 브라우저 쪽 코드를 보면, /dataurl에서 받은 문자열을 어떻게 쓰는지가 더 분명해진다.

// imgtag.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Data URL 형식 테스트!!!</h1>
<img width="500" src="">
<script>
    window.addEventListener("load", () => {
        fetch("/dataurl") // 서버에 저장된 Data URL 문자열 요청
        .then((res) => {
           return res.text(); // 응답 본문을 문자열로 읽음
        }).then((content) => {
           alert(content); // 받은 문자열 확인
           document.querySelector("img").src = content; // 문자열을 이미지 주소로 사용
        });
    });
</script>
</body>
</html>

이 코드에서 핵심은 fetch("/dataurl")와 res.text()이다.
서버가 돌려주는 값은 JSON도 아니고 HTML도 아니라 일반 문자열이기 때문에 text()로 읽는다.
그리고 그 문자열을 alert(content)로 한 번 확인한 뒤, document.querySelector("img").src = content;로 이미지 태그의 src에 그대로 넣는다.


즉, 브라우저는 이 문자열을 그냥 글자로만 보는 것이 아니라, data:image/png;base64,... 같은 형식의 Data URL이면 이미지 주소처럼 해석할 수 있다.
그래서 사용자는 파일을 다시 선택하거나 업로드하지 않아도, 서버가 돌려준 문자열만으로 이미지를 다시 볼 수 있게 된다.


그래서 결과가 이렇게 나온다

먼저 /dataurl 주소를 직접 열어 보면, 화면에는 아주 긴 문자열이 보인다.
이 문자열은 일반 문장이 아니라 data:image/png;base64,... 형태의 Data URL 데이터이다.
즉, 서버가 실제로 보내고 있는 것은 그림을 표현하는 문자열이라는 점을 여기서 바로 확인할 수 있다.
![](


이 장면에서 중요한 것은 왜 이미지가 아니라 긴 글자가 보이느냐이다.
그 이유는 지금 이 주소가 이미지 페이지가 아니라, 이미지를 표현하는 문자열 자체를 그대로 응답하는 주소이기 때문이다.
즉, /dataurl은 그림을 직접 그려 주는 페이지가 아니라, 그림의 원본 문자열 데이터를 돌려주는 주소라고 이해하면 된다.


그리고 imgtag.html을 실행하면, 브라우저는 그 문자열을 받아 alert로 먼저 보여 주고, 이어서 img.src에 넣는다.
그 결과 문자열 자체가 화면에 그대로 보이는 것이 아니라, 이미지가 다시 렌더링된다.




이 흐름은 문자열을 받아서 이미지로 바꾸는 과정을 잘 보여 준다.
처음에는 서버 응답이 그냥 긴 텍스트처럼 보이지만, 브라우저가 그것을 img.src에 넣는 순간부터는 실제 그림처럼 해석해 버린다.
즉, 같은 데이터라도 어디에 넣느냐에 따라 글자가 될 수도 있고 이미지가 될 수도 있다는 뜻이다.


마지막으로 결과 화면을 보면, 저장된 그림이 이미지 형태로 정상적으로 복원된 것을 확인할 수 있다.




이 장면에서 초보자가 꼭 잡아야 하는 핵심은 이것이다.
이번 예제는 파일 다운로드 버튼을 눌러서 이미지를 받는 구조가 아니다.
대신 서버가 문자열 데이터를 돌려주고, 브라우저가 그 문자열을 이미지 주소처럼 해석해서 화면에 그리는 구조이다.
즉, 이 예제는 앞의 canvas 업로드 예제가 저장한 문자열 데이터를 어떻게 다시 시각적인 그림으로 복원할 수 있는지를 보여 주는 후속 예제이다.


그래서 이 묶음의 전체 흐름은 canvas 그림 문자열 저장 → 서버가 문자열 다시 읽기 → /dataurl로 문자열 응답 → fetch()로 문자열 수신 → img.src에 대입 → 이미지 복원이라고 정리할 수 있다.


이 묶음에서 꼭 잡아야 하는 핵심은 이것이다.
브라우저에서 만든 그림은 문자열 형태로 저장할 수도 있고, 서버가 그 문자열을 다시 돌려주면 브라우저는 그것을 이미지로 다시 복원할 수 있다.

0개의 댓글