Blov-v3(리팩토링), 이미지 업로드하기

이미지 업로드
홍윤's avatar
Sep 13, 2024
Blov-v3(리팩토링), 이미지 업로드하기

1. nobody.png

notion image

2. 사진 업로드 그림 그리기

user/profile-form.mustache
notion image
{{> layout/header}} <div class="container p-5"> <!-- 요청을 하면 localhost:8080/login POST로 요청됨 username=사용자입력값&password=사용자값 --> <div class="card"> <div class="card-header"><b>프로필 사진을 등록해주세요</b></div> <div class="card-body d-flex justify-content-center"> <img src="/nobody.png" width="200px" height="200px"> </div> <div class="card-body"> <form> <div class="mb-3"> <input type="file" class="form-control" name="profile"> </div> <button type="submit" class="btn btn-primary form-control">사진변경</button> </form> </div> </div> </div> {{> layout/footer}}
 

3. UserController 코드 수정

@GetMapping("/api/user/profile-form") public String profileForm(){ return "user/profile-form"; }
 

4. header.mustache 수정

<li class="nav-item"> <a class="nav-link" href="/api/user/profile-form">프로필</a> </li>
notion image
 

5. 화면 실행

notion image
 

6. 파일 처리 프로세스

 

6.1 WebConfig.java 수정하기

// 웹서버 폴더 추가 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { WebMvcConfigurer.super.addResourceHandlers(registry); // 1. 절대경로 file:///c:/upload/ // 2. 상대경로 file:./upload/ registry .addResourceHandler("/images/**") .addResourceLocations("file:" + "./images/") .setCachePeriod(60 * 60) // 초 단위 => 한시간 .resourceChain(true) .addResolver(new PathResourceResolver()); }
  • 코드설명
💡
이 코드는 Spring Boot에서 정적 리소스(이미지, CSS, JavaScript 등)를 특정 경로로 서빙하기 위한 설정입니다. 특히, 서버의 파일 시스템에 저장된 파일(예: 이미지)을 클라이언트가 요청할 때, 해당 파일을 제공할 수 있도록 경로를 설정하는 역할을 합니다.

1. addResourceHandlers(ResourceHandlerRegistry registry)

  • 설명: Spring에서 정적 리소스를 처리하기 위한 경로를 설정하는 메서드입니다. 여기서는 /images/**로 시작하는 URL 요청을 처리하는 설정을 추가하고 있습니다.

2. WebMvcConfigurer.super.addResourceHandlers(registry)

  • 설명: WebMvcConfigurer의 기본 동작을 유지하면서, 추가적인 리소스 경로 설정을 하기 위해 호출되는 메서드입니다. 생략해도 무방하지만, 기본 설정을 변경하고 싶지 않을 때 호출할 수 있습니다.

3. registry.addResourceHandler("/images/**")

  • 설명: /images/**로 시작하는 URL 요청에 대한 매핑 경로를 정의합니다.
    • 예를 들어, 클라이언트가 localhost:8080/images/example.png를 요청하면, 이 설정에 따라 서버는 해당 파일을 찾기 위한 로직을 실행합니다.

4. addResourceLocations("file:" + "./images/")

  • 설명: 실제 파일이 저장된 서버의 경로를 지정합니다.
    • file: 접두사는 파일 시스템 경로를 의미하며, **./images/*는 프로젝트의 상대 경로에서 images 디렉토리를 지정합니다.
    • 결과적으로, 서버는 ./images/ 디렉토리에서 파일을 찾게 됩니다.

5. setCachePeriod(60 * 60)

  • 설명: 리소스를 캐시할 시간을 설정합니다.
    • 60 * 60초 단위로 1시간(3600초)을 의미합니다.
    • 클라이언트는 한 번 다운로드한 리소스를 1시간 동안 캐시할 수 있으며, 같은 리소스를 요청할 때 서버에 다시 요청하지 않고 캐시된 데이터를 사용하게 됩니다.

6. resourceChain(true)

  • 설명: 리소스 처리 체인을 활성화합니다.
    • 이 설정은 정적 리소스를 처리할 때 성능을 최적화하거나 추가적인 리소스 처리를 적용할 수 있게 합니다.

7. addResolver(new PathResourceResolver())

  • 설명: 리소스 경로를 해석하는 **PathResourceResolver*를 추가합니다.
    • *PathResourceResolver*는 요청된 리소스 경로를 기반으로 실제 파일을 찾는 역할을 합니다.
    • 서버가 /images/**로 들어오는 요청을 처리할 때, 이 경로에 해당하는 파일을 지정된 ./images/ 폴더에서 찾도록 도와줍니다.

6.2 images 폴더 만들기 (사진도 추가해두기 nobody.png)

notion image
 
 

6.3 파일 저장 프로세스 만들기

 
User
package org.example.springv3.user; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; import java.sql.Timestamp; @Builder @Setter @Getter @Table(name = "user_tb") @NoArgsConstructor @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String username; // 아이디 @Column(nullable = false) private String password; @Column(nullable = false) private String email; private String profile; @CreationTimestamp private Timestamp createdAt; @Builder public User(Integer id, String username, String password, String email, String profile, Timestamp createdAt) { this.id = id; this.username = username; this.password = password; this.email = email; this.profile = profile; this.createdAt = createdAt; } }
 
MyFile
package org.example.springv3.core.util; import org.example.springv3.core.error.ex.Exception500; import org.springframework.web.multipart.MultipartFile; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.UUID; public class MyFile { public static String 파일저장(MultipartFile file){ UUID uuid = UUID.randomUUID(); // uuid String fileName = uuid+"_"+file.getOriginalFilename(); // 롤링 Path imageFilePath = Paths.get("./images/"+fileName); try { Files.write(imageFilePath, file.getBytes()); } catch (Exception e) { throw new Exception500("파일 저장 오류"); } return fileName; } }
  • 코드설명
💡
이 코드는 파일을 서버에 저장하는 기능을 담당하는 유틸리티 클래스입니다. 주로 사용자가 업로드한 파일(예: 이미지 파일)을 서버의 특정 경로에 저장하고, 해당 파일의 이름을 반환하는 역할을 합니다. 이 코드에서는 UUID를 이용해 파일 이름을 고유하게 만든 후, 파일을 저장하는 로직을 구현했습니다.

각 부분에 대한 설명:

1. UUID uuid = UUID.randomUUID();

  • 설명: UUID(Universally Unique Identifier)를 생성합니다. UUID는 고유한 식별자로, 파일 이름의 충돌을 방지하기 위해 사용됩니다.
  • 목적: 파일 이름이 중복되지 않도록 고유한 식별자를 생성하는데, 이 식별자는 파일 이름의 앞에 붙게 됩니다.

2. String fileName = uuid + "_" + file.getOriginalFilename();

  • 설명: 파일의 최종 이름을 구성합니다. 원본 파일 이름에 고유한 UUID를 붙여 파일 이름을 중복되지 않게 만듭니다.
    • 예를 들어, 사용자가 example.png라는 파일을 업로드하면, 이 코드는 UUID_example.png와 같은 형식의 이름을 생성합니다.

3. Path imageFilePath = Paths.get("./images/" + fileName);

  • 설명: 파일을 저장할 경로를 설정합니다.
    • *Paths.get()*은 파일 시스템의 경로를 나타내며, 여기에 파일 이름을 포함시킵니다.
    • ./images/ 경로는 현재 작업 디렉토리 내의 images 폴더를 의미합니다. 즉, 파일이 images 폴더에 저장됩니다.

4. Files.write(imageFilePath, file.getBytes());

  • 설명: 실제로 파일을 저장하는 부분입니다.
    • file.getBytes(): 업로드된 파일의 바이트 데이터를 가져옵니다.
    • Files.write(): 지정한 경로(imageFilePath)에 파일 데이터를 씁니다. 즉, 이 메서드가 호출되면 실제로 서버 디스크에 파일이 저장됩니다.

5. return fileName;

  • 설명: 저장된 파일의 이름을 반환합니다. 고유한 UUID와 원본 파일 이름이 결합된 새로운 파일 이름을 반환하여, 서버나 클라이언트가 이 이름을 통해 파일을 다시 찾을 수 있도록 합니다.

6. catch (Exception e)

  • 설명: 파일 저장 과정에서 오류가 발생했을 경우 예외를 처리합니다. 예를 들어 디스크에 쓰는 도중 오류가 발생할 경우, 사용자 정의 예외(Exception500)를 발생시켜 처리합니다.

개선할 수 있는 부분:

  1. 파일 크기 제한: MultipartFile을 처리할 때 파일 크기를 제한하는 로직을 추가할 수 있습니다.
  1. 파일 형식 검증: 파일 확장자를 체크하여 허용된 형식(ex. 이미지 파일만 허용)만 저장되도록 할 수 있습니다.
 
UserService
@Transactional public void 프로필업로드(MultipartFile profile, User sessionUser){ String imageFileName = MyFile.파일저장(profile); // DB에 저장 User userPS = userRepository.findById(sessionUser.getId()) .orElseThrow(() -> new Exception404("유저를 찾을 수 없어요")); userPS.setProfile(imageFileName); } // 더티체킹 update됨
 
UserController
@PostMapping("/api/user/profile") public String profile(@RequestParam("profile") MultipartFile profile){ User sessionUser = (User) session.getAttribute("sessionUser"); userService.프로필업로드(profile, sessionUser); return "redirect:/api/user/profile-form"; } @GetMapping("/api/user/profile-form") public String profileForm(HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); String profile = userService.프로필사진가져오기(sessionUser); request.setAttribute("profile", profile); return "user/profile-form"; }
 
profile-form 그림 파일 수정 (머스테치)
{{> layout/header}} <div class="container p-5"> <!-- 요청을 하면 localhost:8080/login POST로 요청됨 username=사용자입력값&password=사용자값 --> <div class="card"> <div class="card-header"><b>프로필 사진을 등록해주세요</b></div> <div class="card-body d-flex justify-content-center"> <img src="/images/{{profile}}" width="200px" height="200px"> </div> <div class="card-body"> <form action="/api/user/profile" method="post" enctype="multipart/form-data"> <div class="mb-3"> <input type="file" class="form-control" name="profile"> </div> <button type="submit" class="btn btn-primary form-control">사진변경</button> </form> </div> </div> </div> {{> layout/footer}}
  • 코드설명
💡
HTML에서 파일 업로드를 처리할 때는 반드시 <form> 태그에서 enctype="multipart/form-data" 속성을 사용해야 합니다. 이 속성은 폼 데이터가 인코딩되는 방식을 지정하며, 파일을 포함한 데이터는 일반적인 폼 인코딩 방식으로 처리할 수 없기 때문에 이 속성이 필수입니다.

enctype="multipart/form-data"란?

  • *enctype*은 "encryption type"의 줄임말로, 폼 데이터를 서버로 전송할 때 인코딩되는 방식을 지정합니다. HTML의 <form> 태그에서 사용되며, 기본적으로 다음과 같은 세 가지 방식이 있습니다:
  1. application/x-www-form-urlencoded (기본 값)
  1. multipart/form-data
  1. text/plain
이 중 파일 업로드를 처리하려면 반드시 **multipart/form-data**를 사용해야 합니다.

multipart/form-data의 필요성

1. 기본 인코딩 방식: application/x-www-form-urlencoded

기본 인코딩 방식은 텍스트 데이터를 URL 인코딩하여 서버로 전송하는 방식입니다. 예를 들어, 일반 텍스트 필드나 체크박스, 라디오 버튼 등의 데이터를 처리할 때는 이 방식이 사용됩니다. 그러나 이 방식은 파일과 같은 바이너리 데이터를 처리할 수 없습니다.
  • 텍스트 필드만 처리할 경우:
    • 예: name=John&age=30

2. multipart/form-data를 사용해야 하는 이유

파일은 텍스트가 아닌 바이너리 데이터이므로, 이를 URL 인코딩 방식으로는 처리할 수 없습니다. 파일을 업로드할 때는 폼 데이터를 여러 부분(part)로 나누어 각각의 필드를 따로 인코딩하는 방식인 **multipart/form-data**를 사용해야 합니다. 이 방식에서는 각 폼 필드(파일, 텍스트 등)가 각각 독립된 파트로 나뉘고, 파일 데이터는 바이너리 형태로 전송됩니다.
  • 파일과 텍스트 데이터를 처리할 경우:
    • 각각의 데이터가 경계를 나누어 전송됩니다.
    • 파일은 바이너리로 전송되고, 텍스트는 각 부분에 인코딩되어 전송됩니다.

multipart/form-data가 어떻게 동작하는지:

  • 텍스트 필드는 일반 텍스트로 전송되지만, 파일은 바이너리로 전송됩니다.
  • 서버에서 각 데이터 파트를 구분할 수 있도록 경계를 설정하고, 각각의 필드를 독립된 데이터 블록으로 처리합니다.
Share article

Uni