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