File Handling (정적파일과 Multipart/form-data)

2024. 1. 22. 17:34
728x90

1. 정적 파일

: 사용자에게 변환 없이 전달되는 파일. (사용자에 따라 변할 필요가 없는 것들)

- CSS, 이미지, 영상파일, 몇몇 HTML

- resources/static에 추가 

- 정적 파일은 Spring Boot 기본 설정으로 정적 파일을 전달할 수 있다. 

 http://localhost:8080/image.jpg

 http://localhost:8080/script.js

 http://localhost:8080/styles.css

 

- 설정을 바꾸면 요청 경로도 변경 가능하다. >> application.yaml 파일에 설정

 http://localhost:8080/static/image.jpg

 http://localhost:8080/static/script.js

 http://localhost:8080/static/styles.css

// application.yaml 
spring: 
  mvc:
    static-path-pattern: /static/**
    // 여기서 static은 요청!! (url)

 

 

2. Multipart/form-data

1) form

: HTML에서 JS 없이 데이터를 보낼 때 form을 사용한다. 

이 때 내부에 input 요소를 이용해 전달할 데이터를 정의한다. & input type="submit"을 이용해 form 요소 내부의 데이터를 수합하여 넘긴다. 

이 때 >> 데이터를 어떻게 인코딩 할 것인지를 결정하는 방법이 enctype 속성이다.

  • application/x-www-form-urlencoded (기본값) : input 데이터를 모아 하나의 문자열로 표현해 전송
  • multipart/form-data : 각각의 input 데이터를 개별적으로 인코딩해 여러 부분(multipart)로 나눠서 전송
  • ** type="image" : 이미지 업로드가 아닌 이미지를 이용해 제출 버튼을 표현하고 싶을 때 사용하는 형식.
<form enctype="multipart/form-data">
    <input type="file" name="file">
// input type="image"는 input type="submit"을 이미지 파일로 설정하고 싶을 때 사용.
    <input type="image"> 
    <input type="submit">
</form>

 

2) Multipart 파일 받기

- 사용자가 multipart/form-data에 파일을 포함해서 보낸다면 >> @RequestParam 어노테이션을 이용해 데이터를 받을 수 있다. 

  • @RequestMapping 시 consumes = MediaType.MULTIPART_FORM_DATA_VALUE 설정 필요.
  • @RequestParam으로 MultipartFile 인자를 받는다. 
  • 사용자가 올린 파일의 본래 파일명을 알 수 있다 :: file.getOriginalFilename();
  • 파일 저장 경로를 생성할 수도 있다 :: Files.createDirectories(Path.of("xx"));
  • 파일을 저장할 경로를 포함해 파일명을 지정 :: Path path = Path.of("저장경로/"+파일명);
  • getBytes() 메서드로 byte[] 데이터로 사전 확인 가능하다.
@PostMapping(value = "/multipart", 
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseDto multipart(
        @RequestParam("name") String name,
        @RequestParam("photo") MultipartFile file
) throws IOException {
		// file의 이름 그대로 저장하고 싶다면 getOriginalFileName() 활용
        String fileName = file.getOriginalFilename();
        // 파일을 받아서 저장할 폴더 생성.
        Files.createDirectories(Path.of("media"));
        // 파일을 저장할 경로 + 파일명 지정
        Path downloadPath = Path.of("media/"+fileName);
        file.transferTo(downloadPath);
        return "done";
    }
    
    
    @PostMapping(value = "/multipart",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String multipart(
            @RequestParam("name") String name,
            @RequestParam("file") MultipartFile multipartFile
    ) throws IOException {
        // 저장할 파일 이름
        File file = new File("./media/" + multipartFile.getOriginalFilename());
        try (OutputStream outputStream = new FileOutputStream(file)){
            // byte[] 형태로 파일을 받는다.
            byte[] fileBytes = multipartFile.getBytes();
            // TODO 이미지 파일을 구성하는 byte[]가 맞는지 확인
            System.out.println(new String(fileBytes, StandardCharsets.UTF_8));
            outputStream.write(fileBytes);
        }
        return "done";
    }

 

3) 업로드된 데이터 돌려주기

: 사용자가 업로드한 파일은 또다른 정적 파일이다. > 한 폴더에 사용자가 업로드한 파일을 다 저장하고, 해당 경로의 파일을 정적 파일의 형태로 전달하자. 

  •  어떤 폴더의 파일을 정적 응답으로 전달할지를 설정 가능 (application.yaml)
  • file:media/ ::  현재 실행중인 경로의 media라는 폴더
  • classpath:/static :: 빌드된 어플리케이션의 클래스패스의 /static 경로!! 
// application.yaml
spring:
  web:
    resources:
      static-locations: file:media/,classpath:/static
      // 쉼표로 여러 경로 구분 가능. (여기서 media, static은 디렉토리이다.)

 

🌟🌟🌟

** 정리하자면........... yaml 파일에서 설정한 것 두가지가 중요하다 !! 헷갈리지 말것. (둘다 static이라 이름을 일부러 바꿔서 헷갈리지 않게 해보았다...!!)

spring:
  web:
    resources:
      static-locations: file:media/,classpath:/static
  mvc:
    static-path-pattern: /hello/**

1) static-path-pattern : 사용자가 /hello/<정적 파일 경로>로 요청을 보내면 ~

2) static-locations 설정에 따라 파일은 media라는 정적파일 폴더 내부, static 폴더 내부에서 파일을 응답받을 수 있다.

폴더 구조를 보면

1) http://localhost:8080/hello/filename.png  >>> hello/filename.png 라는 요청에서 media 폴더의 filename.png를 찾아 반환해주었다. 

2) http://localhost:8080/hello/assets/fish.png >>> hello/assets/fish.png 라는 요청에서 resources/static 내부에서 assets/fish.png를 찾아 반환해주었다.

헷갈리지 말기~~~

🌟🌟🌟

 

4) 사용자 프로필 이미지 (실습)

: 사용자가 프로필 사진을 업로드하면, 컴퓨터의 media 경로에 저장한 뒤, 해당 파일에 접근할 수 있는 url을 User의 profile에 업데이트 한다. 

 

<유저는 :: id, username, email, phone, bio, profilePhoto의 속성으로 이루어져 있다.>

더보기

- UserEntity

// UserEntity
@Entity
@Getter
@NoArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String username;
    private String email;
    @Setter
    private String phone;
    @Setter
    private String bio;
    @Setter
    private String profile;

    public User(String username, String email, String phone, String bio) {
        this.username = username;
        this.email = email;
        this.phone = phone;
        this.bio = bio;
    }
}

 

- UserDto

@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    private Long id;
    private String username;
    private String email;
    @Setter
    private String phone;
    @Setter
    private String bio;
    @Setter
    private String profile;


    public static UserDto fromEntity(User user) {
        return new UserDto(user.getId(),
                user.getUsername(),
                user.getEmail(),
                user.getPhone(),
                user.getBio(),
                user.getProfile());
    }
}

1) User 정보 생성하기 

더보기

- User Controller

    @PostMapping("/create")
    public UserDto createUser(@RequestBody UserDto dto) {
        return service.create(dto);
    }

 

- UserService

    public UserDto create(UserDto dto) {
    // Dto에서 정보 받아와서 User 객체 생성 후 저장
        User user = new User(dto.getUsername(), dto.getEmail(), dto.getPhone(),
                dto.getBio());
        return UserDto.fromEntity(repository.save(user));
    }

2) username으로 유저 찾기

더보기

- UserController

    @GetMapping("/{username}")
    public UserDto read(@PathVariable("username") String username) {
        return service.readUserByUsername(username);
    }

 

- UserService

    public UserDto readUserByUsername(String username) {
    // Optional로 받아서 예외처리
        Optional<User> optionalUser = repository.findAllByUsername(username);
        if (optionalUser.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        return UserDto.fromEntity(optionalUser.get());
    }
    
    
    // 얘를 한문장으로도 표현 가능하다.
    public UserDto readUserByUsername(String username) {
        return repository.findAllByUsername(username)
                .map(UserDto::fromEntity)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

- UserRepository 

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findAllByUsername(String username);
}

3) 프로필 사진 업데이트 (MultipartFile 활용)

더보기

- UserController

// url의 userId 가져오고, 업로드된 파일 인자로 받아오기
	@PutMapping("/{userId}/profile")
    public UserDto profile(@PathVariable("userId") Long id,
            @RequestParam("profile") MultipartFile file) {
        return service.updateUserProfile(id, file);
    }

- UserService

    // 프로필사진 업데이트
    public UserDto updateUserProfile(Long id, MultipartFile image) throws IOException {
    	// userId로 찾기 & 없을 경우 예외처리
        Optional<User> optionalUser = repository.findById(id);
        if (optionalUser.isEmpty()) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);
        }
        
        // 파일을 저장할 경로에 디렉터리 생성 (newMedia)
        try {
            Files.createDirectories(Path.of("newMedia/"+id));
        } catch (IOException e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        
        // 파일명이 중복되지 않도록 UUID 활용해서 지정해주기.
        // 1. 원래 파일명 추출
        String originalName = image.getOriginalFilename();
        // 2. 파일 이름으로 쓸 uuid 생성
        String uuid = UUID.randomUUID().toString();
        // 3. 확장자 추출
        String extension = originalName.substring(originalName.lastIndexOf("."));
        // 확장자 형식이 맞지 않으면 예외 처리
        if (!(extension.toLowerCase().equals(".jpg") || 
                extension.toLowerCase().equals(".png") || 
                extension.toLowerCase().equals(".jpeg"))) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
        }
        // 4. uuid와 확장자 결합 ==> 이게 최종 저장될 파일명.
        String fileName = uuid + extension;

        // 그 폴더 내에 업로드된 프로필 사진을 집어넣을 것.
        Path path = Path.of("newMedia/"+id+"/"+fileName);
        try { //받은 파일을 이 경로로 변환해줌.
            image.transferTo(path);
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        } 

        // 유저 엔티티의 profile로 저장해주기 + Dto로 변환
        User updateUser = optionalUser.get();
        updateUser.setProfile(String.format("/hello/%d/%s", id, fileName));
        //>> application.yaml에 설정을 hello로 요청이 와서 file이면 newMedia폴더 내에서 찾기때문에
        // newMedia폴더 내부의 해당 유저의 아이디 폴더를 찾고, 그 안의 파일이름을 찾아서 저장해주면된다.
        return UserDto.fromEntity(repository.save(updateUser));
    }

> 확장자가 맞지 않으면 Bad Request

 

++ Multipart 요청 크기 제한을 설정하였다.

// application.yaml 
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB

 

** 여기서 헷갈릴 수 있는 부분이 바로 유저의 profile에 저장을 어떻게 할 것이냐이다!!!!! **

// application.yaml
spring:
  web:
    resources:
      static-locations: file:newMedia/,classpath:/static
  mvc:
    static-path-pattern: /hello/**

>> 다시한번 yaml파일 설정을 들여다보면 !! 

내가 저장한 사용자가 업로드한 정적파일에 접근하는 방법을 생각하면 된다. 

hello라는 요청을 받고 newMedia폴더 혹은 static폴더로 들어가서 그 파일을 찾을 것이다. 

그러니까 profile에 url을 넣으려면 이 요청을 포함시켜주어야 한다! 그래서 나는 hello/요청에서/ 해당 사용자의 id의/그 파일이름으로 요청할거야~~~~라고 저장을 해두는 것이다.

updateUser.setProfile(String.format("/hello/%d/%s", id, fileName));

 

그렇게 저장해둔 url을 갖고와서 요청을 하면? 바로 이미지로 접근이 가능한 것을 볼 수 있다!!! 예~

 

 

 

 

 

파일 삽입 과정과 꺼내오는 과정을 무한반복해서 익숙해지기로 해~🌟🌟

 

 

728x90

'Programming > Spring, SpringBoot' 카테고리의 다른 글

Testing  (1) 2024.01.24
Exception & Validation  (0) 2024.01.23
Query Parameter  (0) 2024.01.22
Spring Boot & REST  (0) 2024.01.18
연습문제(로깅)  (0) 2024.01.15

BELATED ARTICLES

more