[Spring Boot] Multipart

익선·2024년 7월 29일
0

스프링부트

목록 보기
7/8

정적 파일

사용자에 따라 변할 필요 없이 항상 똑같은 모습으로 보여야 할 파일들을 정적 파일(png, js 등)이라고 부른다.

multipart/form-data

  • enctype 속성
    • HTML 에서 JS 없이 데이터를 전송(HTTP 요청)할때는 form 요소를 사용한다.
    • 이 때 form 요소는 자기 내부에 있는 input 요소(ex. 타입 file) 들이 들고 있는 데이터를 수합해서 action 속성에 정의된 URL로 요청을 보내는데, 이 때 이 데이터를 어떻게 인코딩할 것인지를 결정하는 방법이 enctype 속성이다.
  • enctype 종류
    • enctype 으로 설정할 수 있는 값은 세 종류인데 실질적으로 하나는 사용하지 안힉 떄문에 두 종류를 사용한다.
    • application/x-www-form-urlencoded (기본값)
      • input 데이터를 모아 하나의 문자열로 표현해 전송한다.
    • multipart/form-data
      • 각각의 input 데이터를 개별적으로 인코딩해, 여러 부분(multipart)로 나눠서 전송한다.
    • 파일 같이 별도의 인코딩이 필요한 경우 활용될 수 있다.
<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:/staticfile: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/** 경로가 정적 파일 요청으로 매핑된다.
  • 파일 검색 및 응답
    • Spring Boot 는 static-locations 에 설정된 디렉토리에서 해당 파일을 검색한다.
      • 먼저 classPath:/static/facebook.png 에서 검색한다.
      • 없으면 file:media/facebook.png 에서 검색한다.
      • 파일을 찾으면 클라이언트에게 해당 파일을 반환한다. 없으면 404 응답을 보낸다.

코드

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 와 같은 것들을 고려해야할 수도 있다.

profile
꾸준히 기록하는 사람

0개의 댓글