1. 멀티파트(Multipart)에 이해
멀티파트(Multipart)는 HTTP 프로토콜을 사용하여 웹 서버로 파일이나 데이터를 업로드할 때 사용되는 데이터 전송 방식 중 하나입니다. "멀티파트"라는 용어는 말 그대로 메시지가 여러 부분으로 구성되어 있음을 의미하며, 이러한 각각의 부분은 다른 유형의 데이터를 담을 수 있습니다.
HTTP 메시지에는 클라이언트가 전송하는 HTTP 요청, 그리고 서버가 반환하는 HTTP 응답이 있습니다.
텍스트 기반 HTTP 메세지
POST /example HTTP/1.1
Host: example.com
Content-Type: text/plain
Content-Length: 13
---- CLRF 빈줄 공백 --------
Hello, World!
바이너리 기반 16진수 HTTP 메세지
POST /example HTTP/1.1
Host: example.com
Content-Type: application/octet-stream
Content-Length: 5
\x48\x65\x6C\x6C\x6F
HTTP 메시지는 크게 시작 라인(start line), 헤더(headers), 그리고 바디(body) 세 부분으로 구성됩니다. 여기서 바디 부분은 실제 전송하려는 데이터를 담고 있고, 멀티파트 요청에서는 이 바디 부분에 텍스트 기반 데이터와 바이너리 데이터가 함께 포함될 수 있습니다.
이해를 돕기 위한 HTTP 예시
POST /submitForm HTTP/1.1
Host: tenco.com
Content-Type: multipart/form-data; boundary="boundary123"
Content-Length: [계산된 총 길이]
--boundary123
Content-Disposition: form-data; name="username"
길동
--boundary123
Content-Disposition: form-data; name="password"
1234
--boundary123
Content-Disposition: form-data; name="binaryData"; filename="data.bin"
Content-Type: application/octet-stream
01010101 01101100 01101100 01101111
--boundary123--
boundary="boundary123” 은 멀티파트 메시지의 각 부분을 구분하기 위한 문자열입니다 그리고 2진수로 표현되어 있는 부분은 보통 16진수 인코딩 되어 전송이 됩니다(이해를 돕기 위해 01010로 2진수로 표현)
여기서 핵심은 ‘바디 부분에 텍스트 기반 데이터와 바이너리 데이터가 함께 포함 될 수 있다’ 입니다.
2. 스프링 프로젝트에서의 멀티파트 처리
스프링 프레임워크에서는 멀티파트 요청을 처리하기 위한 기능을 제공합니다. 스프링의MultipartResolver 인터페이스는 멀티파트 요청을 파싱하고, 업로드된 파일과 폼 데이터에 접근할 수 있는 API를 제공합니다. 즉, 스프링 부트(Spring Boot) 프로젝트에서는 추가적인 설정 없이도 멀티파트 지원이 자동으로 활성화되며, 필요한 경우 application.properties 또는 application.yml 파일을 통해 멀티파트 관련 설정을 커스터마이즈할 수 있습니다.
멀티 파트 요청 처리 예시 코드
@PostMapping("/upload")
public String handleFileUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
// 파일 처리 로직
}
// 추가 작업
return "redirect:/success";
}
@RequestParam("file") MultipartFile file
@RequestParam 어노테이션을 사용하여 멀티파트 폼 데이터에서 업로드된 파일을 받아옵니다. MultipartFile 인터페이스는 업로드된 파일에 대한 정보와 데이터에 접근할 수 있는 메서드를 제공합니다. 우리 프로젝트에서는 WebDataBinder 를 활용한 파싱 방식을 사용할 예정 입니다.(DTO)
3. 회원 가입시 파일 업로드 기능 구현
spring:
mvc:
view:
prefix: /WEB-INF/view/ #JSP파일이 위치한 디렉토리 접두사를 설정합니다.
suffix: .jsp #뷰 이름에 자동으로 추가될 파일 확장자를 설정합니다.
servlet:
multipart:
max-file-size: 20MB #파일 최대 크기 20MB
max-request-size: 20MB #멀티파트 전체 요청 크기 20MB제한
- max-file-size: 기본값은 1MB입니다. 이는 단일 파일 업로드 시 최대 파일 크기를 의미합니다.
- max-request-size: 기본값은 10MB입니다. 이는 멀티파트 요청의 전체 크기에 대한 최대값을 의미합니다.
회원 가입 화면에서 파일 업로드 폼 만들어 주기
https://www.w3schools.com/bootstrap4/bootstrap_forms_custom.asp
signUp.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- header.jsp -->
<%@ include file="/WEB-INF/view/layout/header.jsp"%>
<!-- start of content.jsp(xxx.jsp) -->
<div class="col-sm-8">
<h2>회원 가입</h2>
<h5>Bank App에 오신걸 환영합니다</h5>
<form action="/user/sign-up" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="username">username:</label> <input type="text" class="form-control" placeholder="Enter username" id="username" name="username" value="야스오1">
</div>
<div class="form-group">
<label for="pwd">Password:</label> <input type="password" class="form-control" placeholder="Enter password" id="pwd" name="password" value="asd123">
</div>
<div class="form-group">
<label for="fullname">fullname:</label> <input type="text" class="form-control" placeholder="Enter fullname" id="fullname" name="fullname" value="바람검객">
</div>
<div class="custom-file">
<input type="file" class="custom-file-input" id="customFile" name="mFile">
<label class="custom-file-label" for="customFile">Choose file</label>
</div>
<br>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary mt-md-4">회원가입</button>
</div>
</form>
</div>
<!-- end of col-sm-8 -->
</div>
</div>
<!-- end of content.jsp(xxx.jsp) -->
<script>
// Add the following code if you want the name of the file appear on select
$(".custom-file-input").on("change", function() {
console.log($(this).val());
let fileName = $(this).val().split("\\").pop();
$(this).siblings(".custom-file-label").addClass("selected").html(fileName);
});
</script>
<!-- footer.jsp -->
<%@ include file="/WEB-INF/view/layout/footer.jsp"%>
User
package com.tenco.bank.repository.model;
import java.sql.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class User {
private Integer id;
private String username;
private String password;
private String fullname;
private String orginFileName;
private String uploadFileName;
private Timestamp createdAt;
}
SignUpDTO
package com.tenco.bank.dto;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.tenco.bank.repository.model.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class SignUpDTO {
private String username;
private String password;
private String fullname;
private MultipartFile mFile;
private String originFileName;
private String uploadFileName;
// 2단계 로직 - User Object 반환
public User toUser() {
return User.builder()
.username(this.username)
.password(this.password)
.fullname(this.fullname)
.orginFileName(this.originFileName)
.uploadFileName(this.uploadFileName)
.build();
}
}
user.xml
<insert id="insert">
insert into user_tb(username, password, fullname, origin_file_name, upload_file_name )
values( #{username}, #{password}, #{fullname}, #{orginFileName}, #{uploadFileName})
</insert>
UserService 코드 수정 및 파일 추가
package com.tenco.bank.service;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.tenco.bank.dto.SignInDTO;
import com.tenco.bank.dto.SignUpDTO;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.repository.interfaces.UserRepository;
import com.tenco.bank.repository.model.User;
import com.tenco.bank.utils.Define;
import lombok.RequiredArgsConstructor;
@Service // IoC 대상( 싱글톤으로 관리)
@RequiredArgsConstructor
public class UserService {
@Autowired
private final UserRepository userRepository;
@Autowired
private final PasswordEncoder passwordEncoder;
/**
* 회원 등록 서비스 기능
* 트랜잭션 처리
* @param dto
*/
@Transactional // 트랜잭션 처리는 반드시 습관화
public void createUser(SignUpDTO dto) {
int result = 0;
System.out.println("-----------------------");
System.out.println(dto.getMFile().getOriginalFilename());
System.out.println("-----------------------");
if(!dto.getMFile().isEmpty()) {
// 파일 업로드 로직 구현
String[] fileNames = uploadFile(dto.getMFile());
dto.setOriginFileName(fileNames[0]);
dto.setUploadFileName(fileNames[1]);
}
try {
// 코드 추가 부분
// 회원 가입 요청시 사용자가 던진 비밀번호 값을 암호화 처리 해야 함
String hashPwd = passwordEncoder.encode(dto.getPassword());
System.out.println("hashPwd : " + hashPwd);
dto.setPassword(hashPwd);
result = userRepository.insert(dto.toUser());
} catch (DataAccessException e) {
throw new DataDeliveryException("중복 이름을 사용할 수 없습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (Exception e) {
throw new RedirectException("알 수 없는 오류", HttpStatus.SERVICE_UNAVAILABLE);
}
if(result != 1) {
throw new DataDeliveryException("회원가입 실패", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public User readUser(SignInDTO dto) {
User userEntity = null; // 지역 변수 선언
try {
userEntity = userRepository.findByUsername(dto.getUsername());
} catch (DataAccessException e) {
throw new DataDeliveryException("잘못된 처리 입니다.", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (Exception e) {
throw new RedirectException("알수 없는 오류", HttpStatus.SERVICE_UNAVAILABLE);
}
if(userEntity == null) {
throw new DataDeliveryException("존재하지 않는 아이디 입니다.", HttpStatus.BAD_REQUEST);
}
boolean isPwdMatched = passwordEncoder.matches(dto.getPassword(), userEntity.getPassword());
if(isPwdMatched == false) {
throw new DataDeliveryException("비밀번호가 잘못되었습니다", HttpStatus.BAD_REQUEST);
}
return userEntity;
}
/**
* 서버 운영체제에 파일 업로드 기능
* MultipartFile getOriginalFilename : 사용자가 작성한 파일 명
* uploadFileName : 서버 컴퓨터에 저장 될 파일 명
* @return
*/
private String[] uploadFile(MultipartFile mFile) {
if(mFile.getSize() > Define.MAX_FILE_SIZE) {
throw new DataDeliveryException("파일 크기는 20MB 이상 클 수 없습니다.", HttpStatus.BAD_REQUEST);
}
// 서버 컴퓨터에 파일을 넣을 디렉토리가 있는지 검사
String saveDirectory = Define.UPLOAD_FILE_DERECTORY;
File directory = new File(saveDirectory);
if(!directory.exists()) {
directory.mkdirs();
}
// 파일 이름 생성(중복 이름 예방)
String uploadFileName = UUID.randomUUID() + "_" + mFile.getOriginalFilename();
// 파일 전체경로 + 새로생성한 파일명
String uploadPath = saveDirectory + File.separator + uploadFileName;
System.out.println("----------------------------");
System.out.println(uploadPath);
System.out.println("----------------------------");
File destination = new File(uploadPath);
// 반드시 수행
try {
mFile.transferTo(destination);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
throw new DataDeliveryException("파일 업로드중에 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
}
return new String[] {mFile.getOriginalFilename(), uploadFileName};
}
}
package com.tenco.bank.repository.model;
import java.sql.Timestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class User {
private Integer id;
private String username;
private String password;
private String fullname;
private String orginFileName;
private String uploadFileName;
private Timestamp createdAt;
}
'Spring boot > Bank App 만들기' 카테고리의 다른 글
30.존재하지 않는 경로에 대한 요청 처리(404페이지 연결) (0) | 2024.09.25 |
---|---|
29. 파일 업로드 - 2단계(ResourceHandler 사용,초기파라미터 사용) (1) | 2024.09.25 |
27.사용자 비밀번호 암호화 처리 (0) | 2024.09.25 |
26.intercepter 활용(인증검사 공통 처리) (0) | 2024.09.25 |
25. 계좌 상세보기 페이징 처리 (0) | 2024.09.25 |