정적 파일
사용자에 따라 변할 필요 없이 항상 똑같은 모습으로 보여야 할 파일들을 정적 파일(png, js 등)이라고 부른다.
multipart/form-data
enctype
속성HTML
에서 JS
없이 데이터를 전송(HTTP 요청)할때는 form
요소를 사용한다. form
요소는 자기 내부에 있는 input
요소(ex. 타입 file
) 들이 들고 있는 데이터를 수합해서 action
속성에 정의된 URL로 요청을 보내는데, 이 때 이 데이터를 어떻게 인코딩할 것인지를 결정하는 방법이 enctype
속성이다.enctype
종류application/x-www-form-urlencoded
(기본값)multipart/form-data
<form enctype="multipart/form-data">
<input type="text" name="name">
<input type="file" name="photo">
<input type="submit">
</form>
MultipartFile 받기 & 돌려주기
application.yaml 설정
static-path-pattern
static-path-pattern: /static/**
로 설정하면, 클라이언트는 /static/
경로를 통해 정적 파일에 접근할 수 있다.static-locations
static-locations: classpath:/static, file:media/
로 설정하면, 정적 파일을 classpath:/static
과 file:media/
디렉토리에서 검색한다.file:media/
: 현재 실행중인 경로의 media
라는 폴더classpath:/static
: 빌드된 어플리케이션의 클래스패스의 /static
경로 (즉, resources/static)spring:
mvc:
# 어떤 URL 요청을 받았을 때 정적 파일을 돌려줄 것인가
static-path-pattern: /static/**
web:
resources:
# 정적 파일을 검색할 위치 설정
# "/static/**"로 요청된 정적 파일은 "classpath:/static"과 "file:media/"에서 검색됩니다.
static-locations: classpath:/static, file:media/
# Multipart 요청 크기 제한
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
동작 과정
file:media/
디렉토리에 저장됨 http://localhost:8080/static/facebook.png
로 요청을 보낸다.static-path-pattern : /static/**
설정에 따라 /static/**
경로가 정적 파일 요청으로 매핑된다.static-locations
에 설정된 디렉토리에서 해당 파일을 검색한다.classPath:/static/facebook.png
에서 검색한다.file:media/facebook.png
에서 검색한다.코드
Controller
@PutMapping("/{userId}/avatar")
public UserDto avatar(
@PathVariable("userId")
Long userId,
@RequestParam("image")
MultipartFile imageFile
) {
return service.updateUserAvatar(userId, imageFile);
}
Service
RequestParam
으로 전달받은 MultipartFile 은 transferTo()
메소드를 이용해 저장할 수 있다.
public UserDto updateUserAvatar(
Long id,
MultipartFile image
) {
// 1. 유저의 존재 확인
Optional<User> optionalUser = repository.findById(id);
if (optionalUser.isEmpty())
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
// 2. 파일을 어디에 업로드할 것인지 결정
// media/{id}/profile.{확장자}
// 2-1. (없다면) 폴더를 만들어야 한다. (media/{id})
String profileDir = String.format("media/%d/", id);
try {
// 주어진 Path를 기준으로, 없는 모든 디렉토리를 생성하는 메서드
Files.createDirectories(Path.of(profileDir));
} catch (IOException e) {
// 폴더를 만드는데 실패하면 기록을 하고 사용자에게 전달
log.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
// 2-2. 실제 파일 이름을 경로와 확장자를 포함하여 만들기 (profile.png 만들기)
// 원본 이미지 가져오기
String origianlFilename = image.getOriginalFilename();
// "whale.png" -> {"whale", "png"}
// 확장자 분리하기 (. 을 기준으로 분리 <- 정규표현식)
String[] filenameSplit = originalFilename.split("\\.");
// "blue.whale.png" -> {"blue", "whale", "png"}
String extension = filenameSplit[filenameSplit.length - 1];
// 최종 파일명 만들기
String profileFilename = "profile" + extension;
String profilePath = profileDir + profileFilename;
// 3. 실제 해당 위치에 파일을 저장
try {
image.transferTo(Path.of(profilePath));
} catch (IOException e) {
log.error(e.getMessage());
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
// 4. User에 아바타 위치를 저장
// http://localhost:8080/static/{id}/profile.{확장자}
String requestPath = String.format("/static/%d/%s", id, profileFilename);
log.info(requestPath);
User target = optionalUser.get();
target.setAvatar(reqeustPath);
// 5. 응답하기
return UserDto.fromEntity(repository.save(target));
}
byte[]
로 형변환하여 추가 작업을 진행할 수도 있다.
@PutMapping(
value = "multipart",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public String multipart(
@RequestParam("name")
String name,
// 파일을 받아주는 자료형을 MutlipartFile
@RequestParam("file")
MultipartFile multipartFile
) throws IOException {
// 저장할 파일 이름
File file = new File("./media/" + multipartFile.getOriginalFilename());
// 파일에 저장하기 위한 OutputStream
try (OutputStream outputStream = new FileOutputStream(file)) {
// byte[] 데이터를 받는다.
byte[] fileBytes = multipartFile.getBytes();
// 추가작업
System.out.println(new String(fileBytes, StandardCharsets.UTF_8));
// OutputStream에 MultipartFile의 byte[]를 저장한다.
outputStream.write(fileBytes);
}
return "done";
}
etc
현 개발 단계에서는 지금과 같은 static
폴더에 넣지만 나중에 프로젝트를 서비스화할 때는 서버 용량 최소화를 위해 NGINX
또는 S3
와 같은 것들을 고려해야할 수도 있다.