hades

[Spring] ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณธ๋ฌธ

๐Ÿƒ๐Ÿป‍โ™‚๏ธ ๊ธฐ๋ณธํ›ˆ๋ จ/Spring

[Spring] ํŒŒ์ผ ์—…๋กœ๋“œ

hades1 2024. 8. 13. 15:38

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋˜ ๋„์ค‘, ํŒŒ์ผ ์—…๋กœ๋“œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜์—ฌ ํ•™์Šต ํ›„, ํ”„๋กœ์ ํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •๋ฆฌํ•œ ๊ธ€์ด๋‹ค. ๋ณธ์ธ์€ ๊ฒŒ์‹œ๊ธ€์— ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜์˜€๊ณ , ๊ทธ๋กœ ์ธํ•ด ๊ฒŒ์‹œ๊ธ€๊ณผ ํŒŒ์ผ ๊ฐ„์˜ ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์žˆ๋‹ค.

 

Multipart

๋ฌธ์ž, ์ˆซ์ž ๋ฐ์ดํ„ฐ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ application/x-www-form-urlencoded  ๋ฐฉ์‹์„ ์ด์šฉํ•œ๋‹ค. ํ•˜์ง€๋งŒ, ํŒŒ์ผ์€ ์ด๋“ค๊ณผ ๋‹ค๋ฅด๊ฒŒ ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๋กœ ๋ณด๋‚ด์•ผ ํ•œ๋‹ค. ๋ฌธ์ž, ์ˆซ์ž, ํŒŒ์ผ์„ ๋™์‹œ์— ๋ณด๋‚ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์ด Form ํƒœ๊ทธ์— enctype="multipart/form-data" ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. 

Content-Disposition์— ์˜ํ•ด ๊ตฌ๋ถ„๋˜๊ณ , ํŒŒ์ผ์€ ์ถ”๊ฐ€์ ์œผ๋กœ filename๊ณผ Content-Type์ด ์ถ”๊ฐ€๋˜๊ณ  ๋ฐ”์ด๋„ˆ๋ฆฌ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „์†ก๋œ๋‹ค.

 

์Šคํ”„๋ง์—์„œ๋Š” MultipartFile์„ ์ด์šฉํ•˜์—ฌ Request๋กœ ์ „์†ก๋œ ํŒŒ์ผ์„ ์ „๋‹ฌ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. MultipartFile์€ ์•„๋ž˜ ๋‘ ๊ฐ€์ง€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

  • file.getOriginalFileName() : ์—…๋กœ๋“œ ํŒŒ์ผ๋ช…
  • file.transferTo(PATH) : PATH์— ํŒŒ์ผ ์ €์žฅ

 

์—…๋กœ๋“œ ํŒŒ์ผ ์ •๋ณด

UploadFile

@Entity
@Getter
@Setter
public class UploadFile {
    @Id
    @GeneratedValue
    @Column(name = "FILE_ID")
    private Long id;

    private String uploadFileName;

    private String storeFileName;

    protected UploadFile() {
    }

    public UploadFile(String uploadFileName, String storeFileName) {
        this.uploadFileName = uploadFileName;
        this.storeFileName = storeFileName;
    }

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "POST_ID")
    private Post post;

}

uploadFileName์€ ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ–ˆ์„ ๋•Œ, ์‹ค์ œ ํŒŒ์ผ๋ช…์ด๋‹ค.

storeFileName์€ ์„œ๋ฒ„ ์ปดํ“จํ„ฐ์— ์ €์žฅ๋˜๋Š” ํŒŒ์ผ๋ช…์ด๋‹ค. ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ์„œ๋กœ ๋‹ค๋ฅธ ํŒŒ์ผ์„ ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ ์ €์žฅํ•  ๊ฒฝ์šฐ, ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

ํŒŒ์ผ ์ €์žฅ ๊ด€๋ จ

UploadFileService

@Service
@RequiredArgsConstructor
@Transactional
public class UploadFileService {
    private final UploadFileRepository uploadFileRepository;

    @Value("${file.dir}")
    private String fileDir;

    public String getFullPath(String filename){
        return fileDir + filename;
    }
    
    public UploadFile storeFile(MultipartFile multipartFile) throws IOException {
        if (multipartFile.isEmpty()){
            return null;
        }

        String originalFilename = multipartFile.getOriginalFilename();

        String storeFilename = createStoreFilename(originalFilename);

        multipartFile.transferTo(new File(getFullPath(storeFilename)));

        UploadFile uploadFile = new UploadFile(originalFilename, storeFilename);

        uploadFileRepository.save(uploadFile);

        return uploadFile;
    }

    public List<UploadFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException {
        List<UploadFile> storeFileResult = new ArrayList<>();
        for (MultipartFile multipartFile: multipartFiles){
            if (!multipartFile.isEmpty()){
                storeFileResult.add(storeFile(multipartFile));
            }
        }
        return storeFileResult;
    }

    public void removeFile(Long id){
        UploadFile uploadFile = uploadFileRepository.findOne(id);
        uploadFileRepository.remove(uploadFile);
    }

    private String createStoreFilename(String originalFilename){
        String ext = extractExt(originalFilename);
        String uuid = UUID.randomUUID().toString();
        return uuid + "." + ext;
    }

    private String extractExt(String originalFilename){
        int pos = originalFilename.lastIndexOf(".");
        return originalFilename.substring(pos+1);
    }
}

 

@Value("${file.dir}")๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜จ๋‹ค. file.dir์—๋Š” ํŒŒ์ผ๋ช…์„ ์ œ์™ธํ•œ ์„œ๋ฒ„ ์ปดํ“จํ„ฐ์— ์ €์žฅ๋  ์œ„์น˜๊ฐ€ ์ €์žฅ๋˜์–ด ์žˆ๋‹ค.

 

getFullPath๋Š” ํŒŒ์ผ์ด ์„œ๋ฒ„ ์ปดํ“จํ„ฐ์— ์ €์žฅ๋  ์ตœ์ข… ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

storeFile์—์„œ๋Š” multipartFile์„ ์ „๋‹ฌ๋ฐ›์•„ ์„œ๋ฒ„ ์ปดํ“จํ„ฐ์— ์ €์žฅํ•˜๊ณ , ์›๋ž˜ ํŒŒ์ผ๋ช…๊ณผ ์„œ๋ฒ„์— ์ €์žฅํ•  ๊ฒฝ๋กœ๋ฅผ UploadFile์— ์ €์žฅํ•˜์—ฌ DB์— ์ €์žฅํ•˜๊ณ , ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

storeFiles์—์„œ๋Š” multipartFiles๋ฅผ ๋ฐ›์•„ storeFileํ•œ๋‹ค.

 

removeFile์—์„œ๋Š” ์ „๋‹ฌ๋ฐ›์€ id๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ œ๊ฑฐํ•œ๋‹ค.

 

createStoreFilename์€ ํŒŒ์ผ์ด ์„œ๋ฒ„ ์ปดํ“จํ„ฐ์—์„œ ์ €์žฅ๋  ๋•Œ, ์ด๋ฆ„์ด ์ค‘๋ณต๋˜์ง€ ์•Š๋„๋ก UUID๋ฅผ ์ด์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ํŒŒ์ผ๋ช…์„ ์ƒ์„ฑํ•œ๋‹ค.

 

extractExt๋Š” ํŒŒ์ผ ํ™•์žฅ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

createPost.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>์š”๋ฆฌ ์ธ์ฆ ๊ธ€ ์ž‘์„ฑ</title>
</head>
<body>
<h1>์š”๋ฆฌ ์ธ์ฆ ๊ธ€ ์ž‘์„ฑ</h1>

<h2>๋‚ด๊ฐ€ ๋งŒ๋“  ์š”๋ฆฌ ์ž๋ž‘ํ•˜๊ธฐ</h2>
<form th:action="|/post/create/${foodId}|" method="post" enctype="multipart/form-data" th:object="${postDto}">
  <ul>
    <li>์ด๋ฏธ์ง€ <input type="file" th:field="*{imageFiles}" multiple="multiple" required></li>
    <li>ํ•œ๋งˆ๋”” <input type="text" th:field="*{comment}" required></li>
  </ul>
  <button type="submit">๋“ฑ๋ก</button>
</form>
</body>
</html>

 

PostController

@PostMapping("/post/create/{foodId}")
public String writePost(@PathVariable Long foodId, @ModelAttribute PostDto postDto, HttpServletRequest request) throws IOException {
    List<UploadFile> imageFiles = uploadFileService.storeFiles(postDto.getImageFiles());
    String comment = postDto.getComment();

    HttpSession session = request.getSession();
    Member loginMember = (Member)session.getAttribute("LOGIN_MEMBER");

    Food food = foodRepository.findOne(foodId);

    Post post = Post.createPost(loginMember, food, imageFiles, comment);

    postService.write(post);

    return "redirect:/food/foodInfo/" + foodId;
}

@ResponseBody
@GetMapping("/post/images/{filename}")
public UrlResource downloadImage(@PathVariable String filename) throws MalformedURLException {
    return new UrlResource("file:" + uploadFileService.getFullPath(filename));
}

writePost๋Š” ์ „๋‹ฌ๋ฐ›์€ imageFiles๋ฅผ ์ €์žฅํ•˜๊ณ , UploadFile ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜๋ฐ›์•„, Post์— ์—ฐ๊ฒฐ์‹œํ‚จ๋‹ค.

 

downloadImage๋Š” img ํƒœ๊ทธ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•œ UrlResource๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. UrlResource์—์„œ "file:"์„ ๊ผญ ๋ถ™์—ฌ์•ผ๋˜๋‚˜ ํ–ˆ๋Š”๋ฐ, ๊ณต์‹๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•ด๋ณด๋‹ˆ ๋ถ™์—ฌ์•ผ ํ•œ๋‹ค.

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/UrlResource.html

 

UrlResource (Spring Framework 6.1.11 API)

This delegate creates a java.net.URL, applying the given path relative to the path of the underlying URL of this resource descriptor.

docs.spring.io