개발자로 후회없는 삶 살기

spring PART.파일 업로드 본문

[백엔드]/[spring | 학습기록]

spring PART.파일 업로드

몽이장쥰 2023. 4. 28. 15:27

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

- 파일업로드 소개

html form을 통해서 파일 업로드를 이해하려면 폼을 전송하는 두가지 방식의 차이를 알아야합니다.

-> html 폼 전송 방식

1) application/x-www-form-urlencoded

일반적으로 form을 전송하는 방식으로 바디에 요청 바라미터 방식으로 name=value 형식으로 넘어가며 content-type이 application/x-www-form-urlencoded라는 형식으로 되고 content type은 바디에 있는게 어떤 형식(name=value)인지 설명합니다. form 태그에 enctype 옵션이 없으면 웹 브라우저는 요청 http 메시지의 헤더에 content-type이 application/x-www-form-urlencoded을 자동으로 추가합니다.

파일 업로드를 하려면 파일은 문자가 아니라 바이너리 데이터를 전송해서 위는 문자를 전송하는 방식이라서 안되고 보통 폼을 전송할 때 파일하나만 전송하는 것도 아닙니다. 이름, 나이, 첨부파일 등 이름과 나이도 전송해야하고 첨부파일도 함께 전송해야하는데

 

> 문제는 이름과 나이는 문자로 전송해야하고 파일은 바이너리로 전송해야합니다. 결과적으로 문자와 바이너리 데이터를 동시에 전송해야합니다.

 

2) multipart/form-data

이를 해결하기 위해 http에서 multipart/form-data라는 전송 방식을 제공합니다. 이 방식을 사용하려면 form 태그에 enctype을 multipart/form-data라고 지정을 해야합니다. 이러면 content type은 multipart/form-data로 넘어가는데 boundary가 있습니다. 이 경계를 기반으로 메시지에 파트가 나뉘어져서 값 3개가 구분이 되어있습니다. 구분된 값별로 content-disposition이라는 특별한 헤더가 들어가고 엔터하고 벨류값이 들어갑니다.

> 이렇게 여러가지 타입의 데이터를 한 번에 보낼 수 있습니다. 3번째 파일을 보면 name까지는 form에 name 속성한 것으로 문자 타입과 똑같은데 filename이 실제 업로드한 파일명이 들어가고 content type이 filename을 읽어서 image가 됩니다. 이것은 파일이 어떤 타입인지 브라우저가 자동으로 읽어서 넣어줍니다. 그리고 엔터하고 파일 바이너리 데이터를 넣어줍니다.

 

-> 정리

multipart/form-data는 다른 종류의 여러 파일을 폼의 내용과 함께 전송할 수 있습니다. 추가로 영상파일도 경계를 가지고 추가할 수 있습니다.

 

- 서블릿과 파일 업로드1

@GetMapping("/upload")
public String newFile() {
    return "upload-form";
}

먼저 서블릿으로 파일 업로드를 하는 방법을 알아보겠습니다. 컨트롤러 패키지와 업로드 컨트롤러를 하나 만듭니다. getmapping으로 업로드 폼을 보여주고 폼에서 제출할 postmapping을 만듭니다. 

 

@PostMapping("upload")
public String upload(HttpServletRequest request) throws ServletException, IOException {
    log.info("request={}", request);

    String username = request.getParameter("username");
    log.info("username={}", username);

    Collection<Part> parts = request.getParts();
    log.info("parts={}", parts);

    return "upload-form";
}

여기서 서블릿 버전이니 Http요청으로 받습니다. request.getParameter로 상품명은 문자로 받고 getParts를 받습니다. parts가 바로 위에서 설명한 경계를 기준으로 3개입니다. 이 3개의 파트를 각각 받아볼 수 있습니다.

 

-> 뷰

<form th:action method="post" enctype="multipart/form-data">
    <ul>
      <li>상품명 <input type="text" name="itemName"></li>
      <li>파일<input type="file" name="file" ></li>
    </ul>
    <input type="submit"/>
</form>

form 태그를 보면 enctype이 멀티 파트입니다. 입력으로 상품명은 type이 text이고 파일은 type이 file이고 name이 file입니다. 2가지 다른 것이 넘어오는 것입니다.

 

-> 실행

폼에서 파일을 image.png를 넣고 제출합니다. 개발자 도구로 보면 content type이 멀티 파트입니다.

logging.level.org.apache.coyote.http11=debug를 넣고 서버에서 http 메세지 로그를 보면 막 로그들이 나옵니다. 컨텐트 타입하고 바운더리를 뭐로 하겠다는 이전에 ----XXX가 나오고 이것을 경계로 파트가 2개 있습니다.

 

하나는 문자와 벨류인 itemV, 아래는 이미지 파일이 바이너리 파일이 다 깨져서 보입니다.

 

컨트롤러에서 로그 찍은 것을 보면 request도 이전까지 request의 기본 구현체가 requestFacade였는데 그게 아니라 표준 멀티 파트 http 라는게 들어왔습니다.

 

멀티 파트를 사용할 경우 스프링이 제공하는 멀티파트 리졸버가 request를 HttpRequest아 아니고 표준 멀티파트로 넣어줍니다. 이를 사용하면 멀티 파트에 관련된 여러가지 처리를 더 편리하게 할 수 있습니다. 스프링에서는 더 편리한 MultiPart를 직접 사용할 수 있습니다. 상품 명이 찍히고 parts가 2개가 있는데 이게 아까 form에서 2개를 보낸 것이 파트 2개로 들어온 것이고 이것을 열어서 파일을 꺼낼 수 있는 것입니다.

 

- 서블릿과 파일 업로드2

서블릿이 제공하는 part를 알아보고 실제 파일도 서버에 업로드 해보겠습니다. 먼저 파일을 업로드할 실제 파일 경로를 프로퍼티스에 작성해야합니다.

 

컨트롤러를 만들고 파일 업로드를 할 것 입니다. 등록한 경로를 @Value로 프로퍼티스의 속성을 그대로 가져올 수 있고 String fileDir이라고 하면 프로퍼티스의 벨류가 fileDir에 들어갑니다.

 

for (Part part : parts) {
    log.info("==== PART ====");
    log.info("name={}", part.getName());
    Collection<String> headerNames = part.getHeaderNames();
    for (String headerName : headerNames) {
        log.info("header {}: {}", headerName,
                part.getHeader(headerName));
    }

post에 업로드한 부분을 고쳐야합니다. parts를 열면 꺼낼 수 있다고 했습니다. for문으로 루프를 돌면서 파트를 하나씩 불러옵니다. 파츠도 헤더와 바디로 구분이 됩니다. 그래서 파츠 각각의 헤더와 바디를 출력해 볼 것입니다. getName으로 파트의 이름을 보면 이것은 form의 name 속성이고 getHeaderNames로 헤더를 출력해봅니다.

 

 

파트에 보면 content-dispostion이 있는 부분이 헤더이고 value가 있는 것이 바디입니다.

 

-> 편의 메서드

1) getSubmitiedFileName

//content-disposition; filename
log.info("submittedFileName={}", part.getSubmittedFileName());

그리고 편의 메서드도 제공합니다. content disposition을 보면 filename이 있었는데 앞에 form-data, name 등도 많습니다. 그래서 다 파싱해서 뽑기 힘든데 쉽게 뽑을 수 있는 편의 메서드가 있습니다.

2) getSize

log.info("size={}", part.getSize()); //part body size

파츠의 바디 사이즈를 볼 수 있습니다.

 

-> 데이터 읽기

InputStream inputStream = part.getInputStream();
String body = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
log.info("body={}", body);

바디에 있는 form에서 입력한 데이터를 읽을 수 있습니다. type이 text면 문자일 것이고 file이면 바이너리입니다. 인풋 스트림으로 part의 바디를 꺼내고 StreamUtils로 문자로 바이너리를 읽습니다. 항상 문자와 바이너리 형변환은 인코딩 타입을 적어줘야 합니다. body를 찍으면 밑에 깨진 로그가 나올 것입니다.서블릿으로 데이터를 읽으려면 이렇게 힘듭니다.

 

-> 실행

파일을 선택하고 업로드해보면 바이너리를 문자로 콘솔에 찍어줍니다. 로그를 보면 파트를 2개로 name은 form에 name 속성에 적은 값이고 헤더는 content-disposition이 뜨고 submittedFilename은 없으면 null입니다. 사이즈와 바디도 읽어줍니다.

두번째 파트는 헤더가 2개로 disposition와 content-type이 들어아고 제출 파일명이 사용자가 전송한 파일명이 나오고 사이즈와 바디 데이터가 바이너리를 문자로 변환해서 나옵니다.

 

- 파일 저장하기

if(StringUtils.hasText(part.getSubmittedFileName())) {
    String fullPath = fileDir + part.getSubmittedFileName();
    log.info("파일 저장 fullPath {}", fullPath);

    part.write(fullPath);
}

먼저 part.submittedFilename 제출한 파일이 있는가를 확인하고 경로에 파일 명을 더해서 fullpath로 잡고 part는 write를 제공해서 바로 저장이 됩니다.

 

파일을 선택하고 제출하면 파일 저장이 이 경로에 되어있다고 나오니 가보면 잘 저장이 되어있습니다.

 

-> txt 파일 적용

txt 파일을 업로드 해보면 content-type이 text이고 body 내용물이 문자라 잘 나옵니다.

 

- 스프링 파일 업로드

@PostMapping("upload")
public String upload(@RequestParam String itemName,
                     @RequestParam MultipartFile file) throws ServletException, IOException {

    log.info("itemName = {}", itemName);
    log.info("file = {}", file);

    if(!file.isEmpty()) {
        String fullPath = fileDir + file.getOriginalFilename();
        file.transferTo(new File(fullPath));
    }

    return "upload-form";
}

스프링은 MultipartFile이라는 인터페이스로 편리하게 파일 업로드를 지원합니다. itemName은 요청 파라미터 형식으로도 부를 수 있으니 requestparam으로 부르고 또 reqeustParam으로 멀티파트파일 타입으로 file 변수를 쓰면 예전에 requestParam으로 받을 때 input 태그의 name 속성이 itemName과 age라@requestParam String itemName @requestParam int age했었는데 이게 폼도 요청 파라미터 방식이라서 그랬습니다. 멀티 파트도 requestParam하고 name 속성에 쓴 file을 변수로 가져옵니다.

 

로그로 file을 찍고 파일이 비어있지 않으면 fileDIr + file.getOriginalFilename()해서 fullPath를 잡고 file.transferTo(new File(fullPath))를 넣으면 저장과 읽는 게 끝입니다. 코드가 엄청 깔끔해집니다.

 

하는중

- 예제로 구현하는 파일 업로드, 다운로드

실제 파일을 업로드할 때 고려해야할 것이 있는데 예제를 통해서 알아보겠습니다.

 

-> 요구사항

1. 상품은 상품 이름, 첨부파일 하나, 이미지 파일 여러개를 업로드 할 수 있어야합니다.
2. 첨부파일을 업로드 다운로드 할 수 있어야합니다.
3. 업로드한 이미지를 웹 브라우저에서 확인할 수 있어야합니다.

 

- 상품 도메인

1) 상품 클래스

상품 클래스는 id, 상품명, 하나의 첨부파일, 이미지 파일들을 필드로 가지고 있고

 

2) 첨부 파일

업로드 파일이름과 저장 이름이 있는데 업로드 파일명이 고객에게 보여질 값이고 저장파일 명이 서버에서 관리할 파일명입니다. 업로드 파일이 상품 클래스의 필드로 들어갈 것인데 타입이 업로드 파일 타입이고 이렇게 하는 이유는 고객과 서버에 다른 이름으로 보여질 것이라 그렇습니다.

 

- 구현

1. 도메인

1) item 클래스

public class Item {
    private Long id;
    private String itemName;
    private UploadFile uploadFile;
    private List<UploadFile> imageFiles;
}

item 클래스에 필드는 id, 상품명, 업로드 파일, 이미지 파일들입니다.

 

2) 업로드 파일 클래스

@Data
public class UploadFile {
    private String uploadFileName;
    private String storeFileName;

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

 

업로드한 파일명과 저장한 파일명을 넣습니다. 왜 이렇게 하냐면 2명의 사용자가 같은 파일명으로 올리면 겹칠 수 있으니 시스템에 저장되는 이름은 다르게 해야합니다.

 

3) 아이템 레포

@Repository
public class ItemRepository {
    private final Map<Long, Item> store = new HashMap<>();
    private long sequence = 0L;
    public Item save(Item item) {
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }
    public Item findById(Long id) {
        return store.get(id);
    }
}

아이템 등록, id로 아이템 찾는 것 2개를 할 것입니다.

 

4) 파일 스토어

① 파일 하나 업로드 

파일 저장과 관련된 별도의 객체를 만들어서 아이템 클래스의 업로드 파일 필드에 들어갈 업로드 파일 객체를 만들 것입니다.

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

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

저장할 파일 경로를 @value로 가져오고 FullPath를 만드는 메서드를 만듭니다. 그리고 멀티파트 파일을 받아서 실제로 파일을 서버에 저장한 후에 업로드 파일 클래스로 바꿔주는 메서드를 만듭니다.

 

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)));
    return new UploadFile(originalFilename, storeFileName);
}

private String createStoreFileName(String originalFilename) {
    String ext = extractExt(originalFilename);
    String uuid = UUID.randomUUID().toString();
    //123-123-123.png
    return uuid + "." + ext;
}

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

오리지널 파일명을 꺼냅니다. image.png라면 서버에 저장하는 파일명을 만들어야하는데 uuid를 써서 123-13-123.png로 uuid.확장자로 저장을 하고 싶습니다.

> 오리지널 파일명에서 확장자를 꺼내려면 lastIndexOf하면 위치를 가져올 수 있고 substring으로 위치 + 1하면 png를 뽑을 수 있습니다. 이제 uuid와 뽑은 확장자를 더해서 storeFileName을 만듭니다. 이렇게 하면 오리지널 파일명과 store 파일명을 구했습니다. > 구한 파일명으로 transferTo하면 파일이 서버에 storeFileName으로 저장될 것입니다.

> 그리고 이것을 업로드 파일로 바꿔줄 것이라고 했으니 new UpLoadFile에 생성자로 2 파일명을 넣어주면 됩니다. 이것을 왜 FileStore 객체를 만들어서 하는 것인지 생각을 해보면 UploadFile 객체나 Item 객체는 도메인 클래스로 메서드가 있으면 안됩니다. 그렇다고 Item레포지토리에 하기는 좀 애매합니다.

 

② 이미지 여러개 업로드

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;
}

지금 만든 것은 파일 하나만 업로드 하는 것이고 이미지 같은 경우는 여러 개를 업로드할 수 있습니다. 얘도 최종 목적은 여러개를 업로드하고 서버에 여러개를 저장하고 item의 필드에 연결하는 것입니다. 그냥 여러개를 각각으로 생각해서 위에서 한 것처럼 각각 저장하고 각각 uploadFile 만들면 됩니다.

> 파일 하나를 서버에 저장하고 item과 연결하는 메서드는 storeFile이었고 이번에는 같은 목적으로 이미지를 저장하는 storeFiles 메서드를 만듭니다. 얘는 그냥 멀티 파트 파일이 리스트로 들어와서 루프로 돌리면서 서버에 저장하고 uploadFile 타입으로 바꿔서 List로 만들면 됩니다. 이미지도 밑에 storeFile을 이용해서 UploadFile 객체를 계속 만들 것이라서 만들어진 UploadFile들을 저장할 ArrayList를 선언합니다.

루프로 받은 멀티 파트 파일 리스트를 봐서 비어있지 않으면 밑에서 만든 storeFile을 호출하면서 루프로 하나씩 이미지 파일(multipartFile)을 넣습니다. 이렇게 하면 하나의 파일마다 이미지가 서버에 저장되고 UploadFile 객체가 생길 것입니다. 이것을 위에서 만든 UploadFile들을 저장할 ArrayList에 넣어줍니다. 결과적으로 UpLoadFile을 루프를 돌면서 만들고 LIst에 담습니다. List을 반환하면 Item의 private List<UploadFile> imageFiles;와 연결이 됩니다.

 

2. 컨트롤러와 뷰

1) 상품 저장용 폼

@Data
public class ItemForm {
    private Long itemId;
    private String itemName;
    private List<MultipartFile> imageFiles;
    private MultipartFile attachFile;
}

이제 데이터가 왔다 갔다할 폼을 만듭니다. 상품 저장용 폼 객체로 id, 이름, 멀티파트 list, 멀티파트를 가집니다. 이미지는 다중 업로드 하기 위해 멀티 파트를 리스트로 쓰고 첨부파일 하나는 멀티파트만 쓰게 했습니다. input 태그에 multiple 속성을 넣고 form에 List<MultipartFile>하면 여러개의 파일이 ModelAttribute로 List에 자동으로 저장됩니다.

 

2) 컨트롤러

레포와 FIleStore를 주입받습니다.

 

① getMapping

@Controller
@RequiredArgsConstructor
public class ItemController {
    @GetMapping("/items/new")
    public String newItem(@ModelAttribute ItemForm itemForm) {
        return "item-form";
    }
}

특히 폼을 보여주는 get에서는 빈 객체를 모델에 담아 보내는 게 좋으니 ModelAttribute로 넣고 form 뷰를 보여줍니다.

 

② postMapping

@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException {
    UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
    List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

ModelAttribute로 뷰의 form 태그에서 넘어오는 것을 받는데 ItemForm으로 받습니다. 이것으로 뷰에 form 태그에서 input 태그에서 파일 하나를 올리면 ModelAttribute에서 input태그의 name 속성과 같은 이름의 필드 속성에 바로 MultipartFile을 넣을 수 있다는 것을 알 수 있습니다. 지금까지 Str, int를 받기 위해 form 필드에 타입이 str, int였고 enum을 받기 위해 필드 타입이 ItemType이었는데 그것과 마친가지로 필트 타입을 MultipartFile로 하면 뷰에서 input 태그에서 전송을 하면 ModelAttribute Itemform form하면 form에 한 번에 초기화가 되는 것입니다.

 

> 뷰에서 첨부를 여러개 하면 필드에 List<MultipartFile>로 한 번에 여러개의 파일을 초기화할 수 있을 것입니다. 그리고 이렇게 받은 form의 필드를 Filestore에 넣으면 UploadFile로 만들어서 item에 연결할 수 있을 것입니다.

> 이전에 체크박스할 때도 Item 필드에 List와 enum 타입을 둔 적이 있습니다. 그때는 input의 field에 그 필드명을 썼고 form 객체에도 item 필드와 똑같은 타입으로 갯수만 id를 빼서 fit하게 맞췄습니다. 이때는 제출하는 타입 그대로 form에 초기화가 됐고 멀티 체크 박스의 경우 name이 같은 input 태그의 값들이 str로 컨트롤러로 날라와서 form 객체에 List<string>하거나 name이 같은 input 태그의 값들이 itemType으로 컨트롤러로 날라와서 form 객체에 ItemType[]으로 하면 됐습니다.

 

+ 그 다음 form 객체에 뷰에서 날라온 데이터를 List, Enum 타입으로 초기화한 후 item에 set하여 넣고 바로 레포에 넣어버립니다. 이것은 그 날라온 데이터 자체를 item 필드에 초기화하고 레포에 저장한 것입니다.

 

@PostMapping("/items/new")
public String saveItem(@ModelAttribute ItemForm form, RedirectAttributes redirectAttributes) throws IOException {
    UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
    List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

    //데이터베이스에 저장
    Item item = new Item();
    item.setItemName(form.getItemName());
    item.setAttachFile(attachFile);
    item.setImageFiles(storeImageFiles);
    itemRepository.save(item);

    redirectAttributes.addAttribute("itemId", item.getId());
    return "redirect:/items/{itemId}";
}

다시 강의로 돌아와서 ModelAttribute로 itemForm을 받고 RA를 하나 넣습니다. 먼저 form에서 첨부파일 하나만 저장을 하겠습니다. form에 필드에 attachFile이 있고 @Data를 붙였으니 get할 수 있습니다. 그리고 주입받은 filestore의 storeFile에 attachFile을 넣으면 uploadFile이 반환이 됩니다.

> 그리고 이번엔 여러개 넣어온 이미지 파일들을 가져와서 fileStore의 storefiles 메서드에 넘겨주면 uploadFiles list가 반환이 됩니다. 이렇게 하면 서버에 파일이 실제로 저장이 됩니다.

+ 이제 이것을 item 객체를 만들고 set하고 레포에 (DB) 넣으면 됩니다. ★ 여기서 진짜 중요한 것이 파일은 보통 DB에 저장하는 것이 아닙니다. 파일은 스토리지에 저장하고 DB에는 파일이 저장된 경로만 저장합니다. 그래서 FileStore에서 파일을 transferTo로 스토리지에 저장하고 업로드 파일 객체를 만들어서 원본 파일명, 저장 파일명만 저장하는 것입니다. 이제 리다이렉트로 등록한 상품 상세 뷰로 이동하게 합니다.

 

-> 실행

실행해보면 상세 페이지로 리다이렉트했고

 

저장할 스토리지 경로에 가보면 잘 저장이 됐습니다. 

 

3) 뷰

<form th:action method="post" enctype="multipart/form-data">
<ul>
  <li>상품명 <input type="text" name="itemName"></li>
  <li>첨부파일<input type="file" name="attachFile" ></li>
  <li>이미지 파일들<input type="file" multiple="multiple" name="imageFiles" ></li>
</ul>
<input type="submit"/>
</form>

컨트롤어와 뷰는 번갈아서 만든다고 했습니다. getMapping에서 보여줄 뷰 페이지를 하나 만듭니다. item-form.html을 만듭니다. 뷰를 보면 첨부파일은 한가지 선택할 수 있고 이미지 파일은 여러개 선택할 수 있습니다. input 타입에 file에 옵션이 multiple이라고 하면 여러개를 선택할 수 있습니다. 이게 post로 넘어갈 것입니다.

 

제출을 하고 로그를 찍어보면 각 part들과 업로드한 이미지가 보입니다.

 

4) 저장된 것을 보여주는 컨트롤러와 뷰

@GetMapping("/items/{itemId}")
public String items(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);

    return "item-view";
}

상세 뷰를 위한 getMapping을 만듭니다. pv로 id를 받고 model을 만들어서 레포에서 item을 찾고 모델에 담고 뷰에 보냅니다.

 

5) 상세 뷰

<div class="container">
  <div class="py-5 text-center">
    <h2>상품 조회</h2>
  </div>
  상품명: <span th:text="${item.itemName}">상품명</span><br/>
  첨부파일: <a th:if="${item.attachFile}" th:href="|/attach/${item.id}|" th:text="${item.getAttachFile().getUploadFileName()}" /><br/>
  <img th:each="imageFile : ${item.imageFiles}" th:src="|/images/${imageFile.getStoreFileName()}|" width="300" height="300"/>
</div> <!-- /container -->

아이템을 단순하게 보여줄 뷰입니다. 상품명은 텍스트로 보여주는데 첨부파일은 파일을 다운로드 받을 수 있는 컨트롤러 링크를 넣습니다. 이미지는 이미지 파일들을 보여줄 컨트롤러 링크를 넘깁니다. 모두 다 위에 상세 뷰를 보여주는 컨트롤러에서 모델에 담은 item 객체에 담긴 것입니다. > 이미지는 item에 담긴 List는 each로 돌려서 저장된 경로를 src에 넣습니다.

 

-> 실행

실행해보면 첨부파일은 고객이 저장한 이름이니 da.txt가 보입니다. 고객에게 보이는 이름은 item의 uploadFile 필드의 uploadFileName이고 서버에 저장은 item의 uploadFile 필드의 storeFileName입니다. 그래서 item.getAttachFile().getUploadFileName() 이렇게 합니다. 역시 고객에게 보여줄 것과 시스템에 저장할 것이 다릅니다.

 

이미지는 그냥 src에 저장 경로를 넣어서 보여줄 것입니다. 근데 엑박이 듭니다. 파일 같은 경우는 파일을 다운로드 받는 컨트롤러를 만들어야해서 누르면 다운이 안 됩니다. 이미지를 보면 서버에 저장된 이미지 명으로 이 /images/95d67362-7d2c-4704-af4e-be0d83247023.png 컨트롤러도 만들어야 합니다.

 

6) 이미지 보여주는 컨트롤러

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

responsebody를 넣고 /images/{filename}을 넣습니다. 반환을 Resource로 할 건데 메서드 명은 downloadImage로 반환을 URL리소스에  path는 file: + fullpath를 넣어서 아까 url에 uuid로된 파일 명이 들어오면 서버에도 실제 uuid로 저장이 되어있으니깐 서버에 저장된 경로에서 넘겨주면 file:C:/users/123-123-123.png를 넘겨주게되고 이걸 urlresource가 진짜로 찾아와서 리턴하면 이 경로에 파일을 접근해서 그 파일을 스트림으로 반환합니다.

 

※ 구글 드라이브에서 파일 다운로드가 이런 방식으로 구글 클라우드에 저장된 사용자가 업로드한 파일이 클라우드에 uuid로 저장이 되고 다운로드하면 urlresource로 서버에서 접근해서 리턴해주는 것이라고 예상해볼 수 있습니다.

실행해보면 뷰에서 th:src="|/images/${imageFile.getStoreFileName()}|"으로 경로를 주어 컨트롤러에 접근하여 UrlResource로 실제 서버에 저장된 파일을 스트림으로 반환합니다.

 

- 첨부파일 다운로드

@GetMapping("/attach/{itemId}")
public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
    Item item = itemRepository.findById(itemId);
    String storeFileName = item.getAttachFile().getStoreFileName();
    String uploadFileName = item.getAttachFile().getUploadFileName();

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

    String contentDisposition = "attachment; filename=\"" + uploadFileName + "\"";

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(urlResource);
}

getmapping으로 /attach/{itemId}를 하고 반환을 헤더에 더 추가를 해야할게 있어서 ResponseEntity를 씁니다. itemId를 가지고 레포에서 item을 찾아서 첨부파일 하나 저장한 것을 다루고 있으니 attachFile에서 저장된 파일명과 업로드 파일명을 가지고 옵니다.

 

업로드 파일명은 사용자가 다운로드 받을 때 실제 업로드한 파일명이 나와야해서 그렇고 저장된 파일명은 실제 다운로드를 받을 때 사용합니다. > 이제 urlresource를 만들고 이미지에서 쓸 때도 실제 저장된 파일 명을 받기에 storeFIleName을 넣습니다.

resonponseEntitey를 ok로 반환을 해야하는데 헤더를 추가로 넣어야합니다. 헤더를 안 넣으면 다운로드가 안되고 저장된 파일명으로 서버에서 열어서 브라우저에 표현이 됩니다.

 

String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);

String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";

content Disposition에 attachment;filename=업로드 파일명을 넣어줘야합니다. 이게 표준 규약이라고 합니다. 브라우저가 response 헤더를 봤는데 content Disposition에 attachment;filename=이 있으면 "이거 첨부파일이구나"하고 다운로드 받습니다. 한글이 깨지는 경우 encode도 해줍니다.

'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글

[문법] JDBC 추상화를 한 이유  (0) 2023.05.01
spring PART.중간점검 2  (0) 2023.04.30
spring PART.API 예외처리  (0) 2023.04.27
spring PART.예외 처리와 오류 페이지  (0) 2023.04.26
spring PART.필터  (0) 2023.04.24
Comments