반응형

Spring Boot 파일 업로드 예제

  • [Spring Boot] CRUD 게시판 (DB 사용 X, 회원 기능 X) 이 예제에 파일 업로드, 다운로드 + 이미지 업로드, 미리보기 기능을 추가해보며 파일업로드 기능에 대해 알아보자
  • Thymeleaf Form을 통해 홈페이지에서 파일과 이미지를 업로드
  • 이 파일과 이미지를 로컬 디렉토리에 저장
  • 다시 이 파일을 다운로드 받거나 이미지를 미리보기 할 수 있게하는 예제
  • 파일은 하나씩만 가능하고 이미지는 여러개 가능

구현

UploadFile 객체 생성

  • 사용자가 이미지나 파일을 업로드하면 이 파일이 로컬 디렉토리에 저장됨
  • 만약 저장할 때 파일 이름을 사용자가 업로드한 그대로 저장한다면 이름의 중복이 생겨 덮어쓰기가 될 수도 있음
  • 이를 해결하기 위해 UploadFile이라는 객체를 만들고 업로드한 파일명과 서버에서 저장한 파일명을 묶어서 저장해 놓음
@Data
@AllArgsConstructor
public class UploadFile {
    private String uploadFilename;  // 작성자가 업로드한 파일명
    private String storeFilename;   // 서버 내부에서 관리하는 파일명
}

Content 객체에 변수 추가

  • 전에는 Content 객체에 id, title, texts, writer, password, updateDate 만 있었음
  • 이제 첨부파일과 이미지 파일들을 담아 둘 변수 2개를 추가해야 함
    • 여기에 파일을 담아두는 것이 아닌 위에서 묶은 파일명을 담는 것임
private UploadFile attachFile;          // 첨부 파일
private List<UploadFile> imageFiles;    // 첨부 이미지

ContentForm 추가

  • 전에는 Form 입력을 Content 객체로 받았음
  • 이제는 ContentForm이라는 DTO를 생성하고 이 객체로 입력을 받은 후 Content 객체로 변환시켜 줘야함
    • 입력 attachFile과 imageFiles의 데이터 타입이 달라졌기 때문
    • 이와 같이 다양한 상황이나 변경 사항이 생길 수 있으므로 다음부터는 귀찮더라도 DTO를 활용하자
@Data
public class ContentForm {

    private int id;
    private String title;
    private String texts;

    private String writer;
    private String password;

    private String updateDate;

    private MultipartFile attachFile;          // 첨부 파일
    private List<MultipartFile> imageFiles;    // 첨부 이미지
}

FileStore 클래스

  • 이 클래스(컴포넌트)가 파일을 저장하고 UploadFile 타입으로 변환해줌
@Component
public class FileStore {

    // 루트 경로 불러오기
    private final String rootPath = System.getProperty("user.dir");
    // 프로젝트 루트 경로에 있는 files 디렉토리
    private final String fileDir = rootPath + "/files/";

    public String getFullPath(String filename) { return fileDir + filename; }

    public UploadFile storeFile(MultipartFile multipartFile) throws IOException {

        if(multipartFile.isEmpty()) {
            return null;
        }

        String originalFilename = multipartFile.getOriginalFilename();
        // 작성자가 업로드한 파일명 -> 서버 내부에서 관리하는 파일명
        // 파일명을 중복되지 않게끔 UUID로 정하고 ".확장자"는 그대로
        String storeFilename = UUID.randomUUID() + "." + extractExt(originalFilename);

        // 파일을 저장하는 부분 -> 파일경로 + storeFilename 에 저장
        multipartFile.transferTo(new File(getFullPath(storeFilename)));

        return new UploadFile(originalFilename, storeFilename);
    }

    // 파일이 여러개 들어왔을 때 처리해주는 부분
    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;
    }

    // 확장자 추출
    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(".");
        return originalFilename.substring(pos + 1);
    }
}

ContentController 수정 사항

  • 위에서 언급했듯 전에는 따로 DTO를 활용하지 않았기 때문에 입력이 간편하긴 했었음
  • 하지만 이제는 DTO를 활용해 입력받는 것을 습관화하자
  • ContentForm으로 입력받아 Content로 변환해주는 부분
  • 이 때, form으로 받은 attachFile, imageFiles는 MultipartFile 타입이지만 Content에 들어갈 attachFile, imageFiles는 UploadFile 타입이므로 fileStore 클래스의 storeFile 메소드를 사용해 타입 변환 + 로컬 디렉토리에 파일 저장
 @PostMapping("/content/write")
public String writeContent(ContentForm form) throws IOException {
    Content content = new Content();
    content.setTitle(form.getTitle());
    content.setWriter(form.getWriter());
    content.setTexts(form.getTexts());

    LocalDateTime NowTime = LocalDateTime.now();
    String formatDate = NowTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    content.setUpdateDate(formatDate);

    // 첨부파일, 이미지들 처리하는 부분
    UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
    List<UploadFile> imageFiles = fileStore.storeFiles(form.getImageFiles());
    content.setAttachFile(attachFile);
    content.setImageFiles(imageFiles);

    contentService.writeContent(content);

    return "redirect:/basic-board";
}

write-page.html 추가 코드

  • 이제는 입력받을 때 첨부파일, 이미지파일 같이 입력받아야 함
  • input 태그 타입을 file로 해주면 파일 입력을 받을 수 있음
  • 이미지들은 multiple을 사용해 여러개 받을 수 있게 함
<label for="attachFile">첨부파일 : </label>
<input type="file" id="attachFile" name="attachFile"/>
<br/><br/>
<label for="imageFiles">이미지 파일들 : </label>
<input type="file" id="imageFiles" name="imageFiles" multiple="multiple"/>
<br/><br/>

+ 감싸고 있는 form에 enctype="multipart/form-data" 추가해야함

content-page.html 추가 코드

  • 첨부파일
    • th:text를 통해 uploadFilename을 불러옴으로써 사용자가 업로드한 파일명을 그대로 가져와야 함
    • th:href를 통해 클릭하면 "/basic-board/attach/{contentId}" URL로 이동해 다운받게 해줌
    • 컨트롤러에서 이것을 추가로 처리해야 함
  • 이미지
    • img 태그를 통해 이미지가 바로 뜰 수 있게끔 해줌
    • th:src를 통해 "/basic-board/images/저장한파일명" URL에서 이미지를 불러오고 컨트롤러에서 이것도 처리해야 함
<text>첨부파일(클릭시 다운) : </text>
<a th:if="${content.attachFile}" th:href="|/basic-board/attach/${content.id}|" th:text="${content.attachFile.uploadFilename}" />
<br/><br/>
<text>첨부된 이미지 파일들</text>
<br/>
<img th:each="imageFile : ${content.imageFiles}" th:src="|/basic-board/images/${imageFile.storeFilename}|" width="100px" height="100px" style="border-color: black; border-style: solid; border-width: thin;"/>
<br/><br/>

ContentController 추가 코드 - 이미지 출력

  • 바로 위에서 언급한 이미지 출력 부분을 처리해 주는 부분
  • Resource 타입으로 return 해줌으로써 파일을 불러와 전송해줌
  • UrlResoure("file:저장경로+파일명")으로 로컬에 저장된 파일을 불러옴
@ResponseBody
@GetMapping("/images/{filename}")
public Resource showImage(@PathVariable String filename) throws MalformedURLException {
    return new UrlResource("file:" + fileStore.getFullPath(filename));
}

ContentController 추가 코드 - 첨부파일 다운로드

  • PathVariable로 입력된 id를 통해 content를 찾고 그에 맞는 첨부파일명과 변경된 첨부파일명을 불러옴
  • 사용자들에게는 첨부파일명을, 파일을 찾아올때는 변경된 첨부파일명이 필요하기 때문
  • ResponseEntity를 통해 파일을 return함
  • 이 때, header에 CONTENT_DISPOSITION을 아래와 같이 지정해줘야 클릭 시 다운로드가 진행됨
  • header을 따로 지정하지 않으면 페이지에서 파일이 바로 열림
@GetMapping("/attach/{id}")
public ResponseEntity<Resource> downloadAttach(@PathVariable int id) throws MalformedURLException {
    Content content = contentService.getContent(id);

    System.out.println(content.getAttachFile());
    String storeFilename = content.getAttachFile().getStoreFilename();
    String uploadFilename = content.getAttachFile().getUploadFilename();
    System.out.println(fileStore.getFullPath(storeFilename));

    UrlResource urlResource = new UrlResource("file:" + fileStore.getFullPath(storeFilename));

    // 업로드 한 파일명이 한글인 경우 아래 작업을 안해주면 한글이 깨질 수 있음
    String encodedUploadFileName = UriUtils.encode(uploadFilename, StandardCharsets.UTF_8);
    String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";

    // header에 CONTENT_DISPOSITION 설정을 통해 클릭 시 다운로드 진행
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(urlResource);
}

결과

  • hello.txt 파일 첨부, 이미지 파일 3개 첨부
  • 지정한 경로(프로젝트 루트경로의 files 디렉토리)에 첨부한 파일들이 UUID 명으로 변경되어 저장됨

  • 글 보기 화면으로 가면 이미지들이 모두 잘 출력되는 것을 확인할 수 있고, 첨부파일 클릭시 다운로드도 잘 되는것을 확인할 수 있음

반응형

↓ 클릭시 이동

복사했습니다!