File Handling (정적파일과 Multipart/form-data)
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));
}
![](https://blog.kakaocdn.net/dn/dnaOrv/btsDJg5W4GP/CLMnIPnVYWKqfEUxFuONl1/img.png)
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);
}
![](https://blog.kakaocdn.net/dn/bGFW66/btsDQ2dG1Bc/BCGd8RyjCXDiPGC1OXPvWk/img.png)
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));
}
![](https://blog.kakaocdn.net/dn/lAQxr/btsDKJfheXS/kSubQBy2xlUQkB8R2Ts5Ak/img.png)
![](https://blog.kakaocdn.net/dn/mJs15/btsDKOU59RX/2ZsMCASZdkMXKhOYxb2bJ1/img.png)
> 확장자가 맞지 않으면 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을 갖고와서 요청을 하면? 바로 이미지로 접근이 가능한 것을 볼 수 있다!!! 예~
파일 삽입 과정과 꺼내오는 과정을 무한반복해서 익숙해지기로 해~🌟🌟
'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 |