관심쟁이 영호

[Spring Boot] 파일 업로드 본문

Bank-End/Spring Boot

[Spring Boot] 파일 업로드

관심쟁이 영호 2021. 8. 3. 15:41
반응형

오늘은 Spring Boot를 이용하여 파일 업로드에 대해서 공부할 예정이다.

 

목차

  • HTML Form 전송 방식
  • File 도메인 생성
  • 게시글 쓰기 Post Mapping 수정
  • 파일 올리기 Service 작성
  • FileHanler 작성

 


HTML 폼 전송 방식

 

파일을 서버와 주고받기 위해서는 가장 먼저, HTML Form 전송 방식을 알아야 한다.

두 가지의 전송 방식이 있다.

  1. application/x-www-form-urlencoded
  2. multipart/form-data

 

application/x-www-form-urlencoded

- 해당 방식은 HTML 폼 데이터를 서버로 전송하는 가장 기본적인 방법이다. form 태그에 enctype 옵션을 넣어주지 않으면 브라우저는 헤더에 다음 내용을 추가한다.

Content-Type: application/x-www-form-urlencoded

또한, 폼 내용을 HTTP Body에 A=10&B=13과 같이 "&"를 구분자로 이용하여 전송한다.

 

※ 파일을 업로드를 하기위해서는 문자가 아닌 바이너리 데이터를 전송해야 한다. 문자로는 파일을 전송하기가 어렵다. 근데 폼을 이용하여 파일을 전송해야 할 경우에는 파일뿐만 아니라 문자도 포함되어 있는 폼일 확률이 높다.

 

위와 같은 이유때문에 단순히 구분자 "&"를 이용하는 application/x-www-form-urlencoded를 이용하기는 한계가 있다.

 

multipart/form-data

- 해당 방식은 문자와 바이너리 데이터를 같이 보낼 수 있도록 하는 방식이다. 이것을 이용하기 위해서는 form에 enctype="multipart/form-data"라는 속성을 추가해야 한다.

 

- 이 방식으로 생성한 HTTP 메시지는 각각 전송 항목이 구분되어있다. 위의 예에서는 A, B, File로 각각 분리되어서 전송된다. 문자는 문자 그대로 전송, 파일은 파일 이름과 Content-Type이 추가되고 바이너리 데이터가 전송된다.

 

 

더 정확한 이해를 위해 실제 코드를 살펴보자.

 


실제 코드

 

html form

<form id="board" action="/board_write" method="post" th:object="${board}" enctype="multipart/form-data">

        <div class="form-group">
            <label for="title" class="h4">제목</label>
            <input type="text" class="form-control" id="title" name="title" placeholder="제목을 작성해주세요.">
        </div>

        <hr width = "100%" color = "blue" size = "3"><br>

        <input type="file" id="imageFiles" name="imageFiles" multiple="multiple" accept="image/*">

        <hr width = "100%" color = "blue" size = "3">

        <div class="form-group">
            <label for="content" class="h4">내용</label><br>
            <textarea class="form-control" id="content" name="content" rows="10"></textarea>
        </div>

        <hr width = "100%" color = "blue" size = "3"> <br>

        <div class="text-center">

            <button type="submit" class="btn btn-success" style="color:white;">등록하기</button>

            <button type="button" class="btn btn-danger" style="color:white;" onclick="location.href='/board'">목록으로</button>
        </div>
    </form>

그리고 다음과 같이 내용을 작성했다.

 

그럼 HTTP 내용을 살펴보자!

※ HTTP 요청 내용을 보기 위해서 application yml에 다음과 같은 로깅 코드를 추가했다.

logging:
  level:
    org:
      apache:
        coyote:
          http11: debug

 

그럼 HTTP 내용을 보자!

 

------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="title"

this is title!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="image"; filename="사진.jpg"
Content-Type: image/jpeg

... ÿØÿà·'j©?AGÙ'ìÿÙ ...
------WebKitFormBoundaryMVA4MPoFDDjKPJl2
Content-Disposition: form-data; name="content"

this is content!
------WebKitFormBoundaryMVA4MPoFDDjKPJl2--

바이너리 코드가 너무 길어 생략했다.

 

살펴보면 다음과 같은 구조를 볼 수 있다.

 

  • "---xxx"로 영역 구분
  • "Content-Dispositon: form-data; ~~" : 영역의 시작(해당 영역에 대한 정보)
  • "---xxx--"로 끝 명시

File 도메인 생성

File 도메인을 새롭게 생성하는 이유는 다음과 같다.

  1. User 정보에서 올린 파일들을 조회할 수 있기 위해
  2. 파일 또는 게시글 정보로만 양방향으로 접근할 수 있게 하기 위해
  3. 사용자가 정해진 여러 개의 파일을 올릴 수 있게 하기 위해

 

File이라는 객체를 따로 도메인으로 생성해놓으면, 여러 가지 방면에서 좋은 점이 있을 것이라고 판단이 된다.

Board 내에 리스트로 만들어놓는 것보다 유연하다고 판단된다. 그리고 User 정보만으로도 File로 접근할 수 있고(리스트로 하면 Board를 거쳐야 한다.) File만으로도 User 및 Board에 접근할 수 있다.

 

※ 따로 관리하는 만큼 조금 더 복잡하겠지..

 

다음은 도메인을 생성한 코드이다.

@Data
@NoArgsConstructor(access = AccessLevel.PUBLIC)
@Entity
@Table(name = "user_file")
public class UserFile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "file_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "boardId")
    private Board board;

    @Column(nullable = false)
    private String origFileName;  // 파일 원본명

    @Column(nullable = false)
    private String filePath;  // 파일 저장 경로

    @Column(nullable = false)
    private String savedName;

    private Long fileSize;

    @ManyToOne
    @JoinColumn(name = "userId")
    private User user;

    @CreationTimestamp
    private Timestamp createDate;

}

여러 가지의 내용이 있다.

처음에는 관련 자료들을 찾아보다가 이해가 안 되었다. 그래서 다음 그림을 보면서 이해해보자.

 

 

실제 서버는 각종 파일들을 어떻게 관리할까?

 

- 실제 서버가 가지고 있는 DB에는 파일이 저장되어있는 디렉터리 정보만 저장되어 있다.

- 이후에 해당 파일을 접근해야 될 일이 있으면 DB의 디렉터리를 이용하여 접근한다.

 

이러한 이유로 파일 도메인은

directory, original name 등등 여러 가지의 필드가 존재한다.

 

그 외 나머지는 @ManyToOne 관계의 객체, 저장 시간, 시퀀스 등등이다.

 


게시글 쓰기 Post Mapping 수정

이전에 사용한 post에서 파일을 받을 수 있게 수정해주어야 한다.

 

스프링에서는 파일을 주고받을 때, MultipartFile을 이용한다.

코드를 살펴보자.

 

 @PostMapping("/board_write")
    public String BoardWriter(@ModelAttribute Board board, @RequestParam(value="imageFiles", required=false) List<MultipartFile> files, @SessionAttribute(name = SessionConst.LOGIN_USER, required = false)
            User user) throws IOException {


        boardWriteService.write(board, user);
        boardUserFileService.userFilelRepository(fileHandler.UserFileUpload(files, user.getUserId()), board);
        return "redirect:/board";

    }

 

@RequestParam을 이용하여 파라미터를 받는 것을 볼 수 있다.

List인 이유는 사용자가 한 번의 요청으로 여러 개의 파일을 보낼 수가 있기 때문이다.

 


파일 올리기 서비스 작성

 

코드를 바로 살펴보자!

@Service
@RequiredArgsConstructor
public class BoardUserFileService {
    private final UserFileRepository userFileRepository;

    public void userFilelRepository(List<UserFile> userFileList, Board board){

        if(userFileList.isEmpty()){
            return;
        }
        for(UserFile userfile : userFileList){
            userfile.setBoard(board);
            userFileRepository.save(userfile);
        }
    }

};

 

유저가 파일을 안 올렸을 경우에는 바로 리턴,

파일을 올려주었을 경우에는 File과 해당 Board를 연결해주어야 한다. 그래서 File의 내용에 board객체를 set 해주는 것을 볼 수 있다.

 

그리고 jpa를 이용하여 저장했다.

 


FileHandler 작성

실제로 요청으로 오는 파일은 바이너리 값과 이름이다. 그래서 해당 바이너리를 디렉터리, 저장 이름 등을 설정해주어야 한다.

 

※ 여기서 디렉터리를 새롭게 생성해주거나 저장할 파일 이름(사용자가 입력한 거랑 다름!!) 그리고 MultipartFile -> File 객체로 변경해준다.

 

다음 코드를 보자

@Component
@RequiredArgsConstructor
@Slf4j
public class FileHandler {

    private final BoardUserFileService boardUserFileService;
    private final UserRepository userRepository;

    public List<UserFile> UserFileUpload(List<MultipartFile> files, String userId) throws IOException {

        // 반환할 파일 리스트
        List<UserFile> fileList = new ArrayList<>();
        User user = userRepository.findByUserId(userId);

        // 전달되어 온 파일이 존재할 경우
        if(!CollectionUtils.isEmpty(files)) {
            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter dateTimeFormatter =
                    DateTimeFormatter.ofPattern("yyyyMMdd");
            String current_date = now.format(dateTimeFormatter);

        for(MultipartFile multipartFile : files) {
            String path = "C:/test"+"/"+userId+"/";

            String extension;
            String contentType = multipartFile.getContentType();

            if(ObjectUtils.isEmpty(contentType)) {
                break;
            }

            extension = FilenameUtils.getExtension(multipartFile.getOriginalFilename());

            String new_file_name = current_date + "_"+System.nanoTime() + "." + extension;

            File file = new File(path + new_file_name); //파일 저장 경로 확인, 없으면 만든다.
            if (!file.exists()) {
                file.mkdirs();
            }
            UserFile userFile = new UserFile();


            userFile.setUser(user);
            userFile.setFilePath(path + new_file_name);
            userFile.setFileSize(multipartFile.getSize());
            userFile.setOrigFileName(multipartFile.getOriginalFilename());
            userFile.setSavedName(new_file_name);

            fileList.add(userFile);

            multipartFile.transferTo(file);

            file.setWritable(true);
            file.setReadable(true);
        }
            return fileList;
    }

        return null;

    }



};

 

  • 유저 id로 디렉터리 생성하였다.
  • 날짜 + 나노 초를 통해서 파일 이름의 중복 문제를 해결했다.
  • file을 저장해주고 File객체로 변경한 다음 List로 리턴해주었다.

List를 리턴해주면 Service에서 board와 연결할 것이다!

300x250
Comments