[스프링부트] 파일 데이터 전송 - form 태그 활용

이나겸's avatar
Dec 06, 2024
[스프링부트] 파일 데이터 전송 - form 태그 활용
💡

multipart/form-data

HTTP 프로토콜에서 사용하는 콘텐츠 타입(Content-Type) 중 하나로 폼 데이터가 여러 부분으로 나뉘어져 전송되는 인코딩 타입
파일 업로드나 여러 종류의 데이터를 함께 전송하는 것과 같이 큰 데이터를 포함한 폼을 서버로 전송할 때 사용

특징

  • 복합 데이터 전송
    • 폼 데이터는 여러 부분으로 나뉘고 각 부분은 텍스트 필드 값이나 파일 등 다양한 데이터 포함 가능
    • 단순 텍스트 데이터와 파일 동시 전송 가능
    • 각 부분은 경계(boundary) 문자열로 구분됨
  • 폼 데이터 처리
    • HTML 폼에서 파일 업로드를 포함한 데이터를 서버로 보낼 때 주로 사용
  • 파일 업로드 지원
    • 파일과 같은 바이너리 데이터 전송할 때 적합
    • 텍스트 필드와 파일을 동일한 폼으로 함께 전송 가능
  • 헤더 정보
    • 각 부분은 고유의 헤더를 가지며 컨텐츠 타입, 이름 등의 정보 포함됨
    • ex) Content-Disposition: form-data; name=”filename”; filename=”filename.jpg”

동작 방식

  • 클라이언트가 Content-Typemultipart/form-data로 설정
  • 데이터를 각 부분으로 나누어 경계(boundary)로 구분
  • 각 데이터 부분은 헤더와 실제 데이터로 구성됨

서버 처리

  • 서버는 수신한 multipart/form-data를 파싱하여 각 부분 데이터를 처리
  • 파일은 저장하거나 처리하고, 텍스트 데이터는 파라미터로 활용
 

multipart/form-data를 사용하여 텍스트 필드와 파일 전송 활용

[file1.mustache 파일 - 파일 전송 폼]

  • 폼 태그 <form action="/v1/upload" method="post" enctype="multipart/form-data">
    • action="/v1/upload"
      • 폼이 제출될 때 데이터를 전송할 URL (/v1/upload)
    • method="post"
      • 데이터를 전송할 HTTP 메서드를 지정
      • POST는 데이터를 서버로 전송하는 데 사용
    • enctype="multipart/form-data"
      • 파일과 같은 바이너리 데이터를 전송할 때 필요한 인코딩 타입
  • 사용자 이름 입력 필드 <input type="text" name="username">
    • type="text"
      • 텍스트 입력 필드 생성
    • name="username"
      • 입력 필드의 이름 지정
      • 서버로 전송될 때 이 이름으로 값 참조
  • 파일 업로드 입력 필드 <input type="file" name="img">
    • type="file"
      • 파일 선택 입력 필드 생성
    • name="img"
      • 입력 필드의 이름 지정
      • 사용자가 선택한 파일을 서버로 전송할 때 이 이름으로 파일 참조
  • 제출 버튼 <button type="submit">
    • type="submit"
      • 폼 제출 버튼
      • 사용자가 버튼을 클릭하면 폼 데이터가 지정된 action URL로 전송됨
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Document</title> </head> <body> <h1>사진 파일 전송</h1> <hr> <!-- enctype="multipart/form-data" : 인코딩 타입을 multipart/form-data로 지정하면 여러가지 타입의 모든 데이터 동시 전송 가능 / 여러가지 데이터 타입 섞어놓은걸 multipart --> <form action="/v1/upload" method="post" enctype="multipart/form-data"> <input type="text" name="username"> <br> <input type="file" name="img"> <br> <button type="submit">전송</button> </form> </body> </html>

서버로 전송되는 데이터

  • boundary
    • 각 부분을 구분하는 문자열
    • 각 부분은 Content-Disposition 헤더로 이름과 파일 이름 지정
    • 파일의 경우 Content-Type 헤더로 파일의 MIME 타입을 나타냄
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="username" JohnDoe ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="photo.jpg" Content-Type: image/jpeg (binary data) ------WebKitFormBoundary7MA4YWxkTrZu0gW--
 

[Upload 클래스 - 엔티티]

  • 업로드한 파일을 Upload 객체에 넣음
@NoArgsConstructor @Getter @Table(name = "upload_tb") @Entity public class Upload { @Id // PK @GeneratedValue(strategy = GenerationType.IDENTITY) // AI private Integer id; private String username; // 업로드한 사람의 아이디 private String profileUrl; // 파일의 URL @Builder public Upload(Integer id, String username, String profileUrl) { this.id = id; this.username = username; this.profileUrl = profileUrl; } }
 

[UploadRepository 클래스]

  • id로 검색 + 파일 저장
@RequiredArgsConstructor @Repository public class UploadRepository { private final EntityManager em; // 업로드된 이미지 보기 public Upload findById(Integer id) { return em.find(Upload.class, id); } }
 

[UploadService]

  • UUID.randomUUID()
    • 파일 이름 앞에 추가해서 같은 이름을 가진 파일과 충돌 방지
@Transactional(readOnly = true) // 읽기 전용 트랜잭션 @RequiredArgsConstructor @Service public class UploadService { private final UploadRepository uploadRepository; @Transactional public void v1사진저장(UploadRequest.V1DTO v1DTO) { // 1. DTO에 사진파일명을 롤링 시킴 String imgName = UUID.randomUUID() + "_" + v1DTO.getImg().getOriginalFilename(); String profileUrl = "images/" + imgName; // DB에 저장할 URL String dbUrl = "/upload/" + imgName; // 2. DTO에 사진을 파일로 저장 (images 폴더) // 3. username + 사진의 경로를 DB에 저장 // 3.1 DB에 username과 파일 경로 저장 (images/uuid_파일명) // images/profile변수명 try { Path path = Paths.get(profileUrl); // 파일을 프로젝트 폴더(upload)안의 images 폴더에 저장 Files.write(path, v1DTO.getImg().getBytes()); // 외부에서 접근 가능한 경로로 db에는 다르게 저장 uploadRepository.save(v1DTO.toEntity(dbUrl)); } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } public Upload v1사진보기() { return uploadRepository.findById(1); } }
 

[WebConfig 클래스 - 사용자가 접근 가능한 url 설정]

  • .addResourceHandler("/upload/**")
    • 사용자는 http://localhost:8080/upload/파일이름.확장자명 으로 DB에 저장된 이미지에 접근 가능
    • 그 외의 경로는 막혀있음
  • .addResourceLocations("file:" + "./images/")
    • 웹 서버에 저장된 이미지 경로 설정
  • .setCachePeriod(60 * 60)
    • 정적 리소스의 캐시 유효 기간 설정
    • 단위는 second ⇒ 60* 60은 1시간
// 설정 파일 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { WebMvcConfigurer.super.addResourceHandlers(registry); // 1. 절대경로 file:///c:/upload/ => /// 슬래쉬 3개는 localhost가 생략돼있는것 // 2. 상대경로 file:./upload/ registry .addResourceHandler("/upload/**") // html에서 경로를 적으면 .addResourceLocations("file:" + "./images/") // 웹서버의 /images/ 폴더 경로를 찾음 .setCachePeriod(60 * 60); // 초 단위 => 한시간 } }
 

[UploadRequest 클래스 - 업로드에 사용할 V1DTO]

public class UploadRequest { @Data public static class V1DTO { private String username; private MultipartFile img; public Upload toEntity(String profileUrl) { return Upload.builder() .username(username) .profileUrl(profileUrl).build(); } } }
 

[UploadController 클래스]

@RequiredArgsConstructor @Controller public class UploadController { private final UploadService uploadService; // form 태그 이동 @GetMapping("/file1") public String file1(Model model) { return "file1"; } @PostMapping("/v1/upload") public String v1Upload(UploadRequest.V1DTO v1DTO) { // 요청, 응답 처리 완료(Controller의 책임을 완수) -> 다른 로직은 서비스로 uploadService.v1사진저장(v1DTO); return "index"; } // form 태그 이용 @GetMapping("/file1-check") public String file1Check(Model model) { Upload upload = uploadService.v1사진보기(); model.addAttribute("model", upload); return "file1-check"; } }
 

데이터 전달 받아서 DB에 저장하기

[UploadRepository 클래스]

@RequiredArgsConstructor @Repository public class UploadRepository { private final EntityManager em; // 이미지 파일 경로 DB 저장 public void save(Upload upload) { em.persist(upload); } }
 

[UploadController 클래스]

@RequiredArgsConstructor @Controller public class UploadController { private final UploadService uploadService; @PostMapping("/v1/upload") public String v1Upload(UploadRequest.V1DTO v1DTO) { // 요청, 응답 처리 완료(Controller의 책임을 완수) -> 다른 로직은 서비스로 uploadService.v1사진저장(v1DTO); return "index"; } }
 

[UploadService 클래스]

  • MyFileUtil 클래스의 fileSave 메서드 호출해서 사용
@Transactional(readOnly = true) // 읽기 전용 트랜잭션 @RequiredArgsConstructor @Service public class UploadService { private final UploadRepository uploadRepository; @Transactional public void v1사진저장(UploadRequest.V1DTO v1DTO) { // static 메서드로 프로젝트에 이미지를 저장한 뒤 db에 저장할 dbUrl 문자열을 반환 String dbUrl = MyFileUtil.fileSave(v1DTO.getImg()); // db에 저장 uploadRepository.save(v1DTO.toEntity(dbUrl)); } }
 

[MyFileUtil 클래스 - fileSave 메서드]

  • 원래 UploadService에 있던 로직 일부를 가져와서 fileSave 메서드로 만들어놓고 활용
    • UploadService에서 사용
  • UUID.randomUUID()
    • 파일 이름 앞에 추가해서 같은 이름을 가진 파일과 충돌 방지
  • 프로젝트에 저장하는 경로 String profileUrl = "images/"+imgName
    • 실제 파일이 저장되는 서버의 디렉토리
    • 서버 내부의 디렉토리 경로로, 파일 시스템에서 직접 접근할 수 있는 경로
    • ex) images/unique-identifier_filename.jpg
  • 데이터베이스에 저장하는 경로 String dbUrl = "/upload/"+imgName
    • 웹에서 접근 가능한 URL
    • 클라이언트가 파일에 접근할 수 있도록 서버에서 제공하는 URL
    • ex) /upload/unique-identifier_filename.jpg
public class MyFileUtil { public static String fileSave(MultipartFile file) { // 1. DTO에 사진파일명을 롤링 시킨다. String imgName = UUID.randomUUID()+"_"+file.getOriginalFilename(); String profileUrl = "images/"+imgName; String dbUrl = "/upload/"+imgName; // 2. DTO에 사진을 파일로 저장 (images 폴더) try { Path path = Paths.get(profileUrl); Files.write(path, file.getBytes()); return dbUrl; } catch (IOException e) { throw new RuntimeException(e.getMessage()); } } }

프로젝트 저장 경로와 DB 저장 경로가 서로 다른 이유

  • 보안상의 이유
    • 실제 파일이 저장된 경로와 클라이언트가 접근하는 경로를 분리하는 것이 일반적
  • 파일 보호
    • 서버의 내부 디렉토리 구조를 외부에 노출하지 않음으로써 파일 시스템을 보호
  • URL 관리
    • 웹 서버가 제공하는 URL을 통해 파일에 접근 권한을 제어 가능
    • 특정 사용자만 파일에 접근할 수 있도록 설정 가능
  • 유연성
    • 파일 경로를 변경하더라도 클라이언트 측의 URL은 유지될 수 있음
 
Share article

Nakyeom's Study