인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지
유요한

작성한 질문수

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술

정리

업로드에대한 질문이 있습니다.

해결된 질문

작성

·

1K

·

수정됨

1

학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.

1. 강의 내용과 관련된 질문을 남겨주세요.
2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.
(자주 하는 질문 링크: https://bit.ly/3fX6ygx)
3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.
(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)

질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.
=========================================
[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]
1. 업로드 강의를 듣고 이해하지 못한 부분이 있는데 지금 강의에서 나온 부분이 메모리에 넣고 map에 넣는 방식이고 만약 서버에 넣고 db에 주소값만 넣어서 사용하려고 하면 AWS의 S3를 사용해서 외부 서버에 넣고 사용하는 건가요?

 

  1. 혹시 jpa에 적용하는 것은 실전 jpa때 한번더 나오나요? 업로드는 이대로 끝인가요?

 

  1. 여기서 db에 넣는 방식으로 바꾸려고 하면 repository 부분을 빼고 mybatis는 마이바티스 jpa는 jpa방식으로 하는 건가요? 2번 질문이 끝이면 어떤식으로 구조를 짜야하나요?

 

  1. Item과 ItemForm 클래스의 차이는 뭔가요?

답변 1

1

안녕하세요, 유요한 님. 공식 서포터즈 y2gcoder 입니다.

  1. 현재 예시에서 보여드린 방식은 우리 어플리케이션이 실행되고 있는 서버에 물리적인 파일을 저장하고, 해당 파일 경로 및 정보를 Map에 저장하는 방식입니다. AWS S3 에 저장하는 것도 어렵게 생각할 필요없이 물리 파일은 S3라는 다른 파일 서버에 저장하고, 해당 파일의 경로 및 정보는 DB에 저장한다고 보시면 됩니다.

  2. JPA 관련 강의에서는 아쉽게도 파일 업로드와 같이 연동하는 부분은 나오지 않습니다. 강의의 핵심 주제에 집중하기 위함으로 생각해주시면 감사하겠습니다. JPA 강의를 수강하신 후 이번 강의의 소스를 직접 바꿔보시는 것도 좋은 학습이 될 거라 생각합니다 :)

  3. 구조를 바꿀 일은 크게 없을 것 같습니다. DB 접근 기술들의 경우 해당 기술들에 맞는 설정방법을 따라주시고, 특히 JPA의 경우에는 Spring Data JPA의 도움을 받을 수 있어 엔티티 매핑을 위한 애노테이션을 몇 개 달고 Repository만 좀 수정해준다면 현재 구조에서도 이상없이 돌아갈 것으로 보입니다.

  4. Item은 도메인 클래스로 ItemForm은 Item와 관련된 Form처리를 할 때 사용하는 클래스라고 생각해주시면 감사하겠습니다. 도메인 클래스와 Form 처리를 위한 클래스는 역할이 다르기 때문에 분리해줬다고 생각해주세요!



감사합니다.

유요한님의 프로필 이미지
유요한
질문자

혹시 JPA 엔티티 매핑을 위한 애노테이션하고 Repository 코드로 예를들어주실 수 있으신가요? JPA로는 감이 안잡히네요 ㅠㅠ

처음에는 그냥 슈도 코드로만 작성해서 말씀을 드릴까 하다가 그래도 실제로 작동하는 것을 볼 수 있으면 좋겠다 싶어서 간단하게 예제 프로젝트로 만들어봤습니다.

https://github.com/y2gcoder/file-upload-jpa

원래 코드에서 최대한 많이 고치지 않고 간단하게 고치기 위해 노력했습니다.

도움이 되셨으면 좋겠습니다.

유요한님의 프로필 이미지
유요한
질문자

이거 그대로 해봤는데

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Thu Mar 30 09:46:14 KST 2023

There was an unexpected error (type=Internal Server Error, status=500).

An error happened during template parsing (template: "class path resource [templates/item-view.html]")

org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "class path resource [templates/item-view.html]") at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:241) at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parseStandalone(AbstractMarkupTemplateParser.java:100) at org.thymeleaf.engine.TemplateManager.parseAndProcess(TemplateManager.java:666) at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1098) at org.thymeleaf.TemplateEngine.process(TemplateEngine.java:1072) at org.thymeleaf.spring5.view.ThymeleafView.renderFragment(ThymeleafView.java:366) at org.thymeleaf.spring5.view.ThymeleafView.render(ThymeleafView.java:190) at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1405) at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1149) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1088) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) at javax.servlet.http.HttpServlet.service(HttpServlet.java:670) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) at javax.servlet.http.HttpServlet.service(HttpServlet.java:779) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.base/java.lang.Thread.run(Thread.java:829) Caused by: org.attoparser.ParseException: failed to lazily initialize a collection of role: com.example.fileupload.domain.Item.imageFiles, could not initialize proxy - no Session at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393) at org.attoparser.MarkupParser.parse(MarkupParser.java:257) at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:230) ... 48 more Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.fileupload.domain.Item.imageFiles, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:614) at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:591) at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149) at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:387) at org.thymeleaf.engine.IteratedGatheringModelProcessable.computeIteratedObjectIterator(IteratedGatheringModelProcessable.java:442) at org.thymeleaf.engine.IteratedGatheringModelProcessable.<init>(IteratedGatheringModelProcessable.java:82) at org.thymeleaf.engine.TemplateModelController.startGatheringIteratedModel(TemplateModelController.java:231) at org.thymeleaf.engine.ProcessorTemplateHandler.handleStandaloneElement(ProcessorTemplateHandler.java:930) at org.thymeleaf.engine.TemplateHandlerAdapterMarkupHandler.handleStandaloneElementEnd(TemplateHandlerAdapterMarkupHandler.java:260) at org.thymeleaf.templateparser.markup.InlinedOutputExpressionMarkupHandler$InlineMarkupAdapterPreProcessorHandler.handleStandaloneElementEnd(InlinedOutputExpressionMarkupHandler.java:256) at org.thymeleaf.standard.inline.OutputExpressionInlinePreProcessorHandler.handleStandaloneElementEnd(OutputExpressionInlinePreProcessorHandler.java:169) at org.thymeleaf.templateparser.markup.InlinedOutputExpressionMarkupHandler.handleStandaloneElementEnd(InlinedOutputExpressionMarkupHandler.java:104) at org.attoparser.HtmlElement.handleStandaloneElementEnd(HtmlElement.java:79) at org.attoparser.HtmlMarkupHandler.handleStandaloneElementEnd(HtmlMarkupHandler.java:241) at org.attoparser.MarkupEventProcessorHandler.handleStandaloneElementEnd(MarkupEventProcessorHandler.java:327) at org.attoparser.ParsingElementMarkupUtil.parseStandaloneElement(ParsingElementMarkupUtil.java:96) at org.attoparser.MarkupParser.parseBuffer(MarkupParser.java:706) at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:301) ... 50 more

이런 에러가 발생합니다. 분명히 view는 다 있는데

properties

spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=true

# MySQL 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# DB Source URL
spring.datasource.url=jdbc:mysql://localhost:3306/study01
# DB username
spring.datasource.username=root
# DB userpassword
spring.datasource.password=1234

spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.open-in-view=false
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.format_sql=true


# thymeleaf
spring.thymeleaf.prefix=classpath:templates/
## 템플릿 위치 존재 확인 - templates 디렉토리에 파일이 있는지 없는지 체크, 없으면 에러를 발생
spring.thymeleaf.check-template-location=true
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
## true로 하면 개발된 화면을 수정했을 때 매번 프로젝트를 다시 시작해야 한다.
## thymeleaf 템플릿에 대한 캐시를 남기지 않음
## cache=false 설정하고 개발하다 운영시에는 true로 변경
spring.thymeleaf.cache=false

file.dir=c:/upload/file/

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=30MB

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
package com.example.fileupload3.controller;

import com.example.fileupload3.domain.Item;
import com.example.fileupload3.domain.ItemForm;
import com.example.fileupload3.domain.ItemRepository;
import com.example.fileupload3.domain.UploadFile;
import com.example.fileupload3.file.FileStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.util.UriUtils;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Slf4j
@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemRepository itemRepository;
    private final FileStore fileStore;

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

    @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(form.getItemName(), attachFile, storeImageFiles);
        itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", item.getId());

        return "redirect:/items/{itemId}";
    }

    @GetMapping("items/{id}")
    public String items(@PathVariable Long id, Model model) {
        Item item = itemRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("NOT FOUND ITEM :" + id));
        model.addAttribute("item", item);
        return "item-view";
    }

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

    @GetMapping("attach/{itemId}")
    public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
        Item item = itemRepository.findById(itemId)
                .orElseThrow(() -> new IllegalArgumentException("NOT FOUND ITEM :" + itemId));
        String storeFileName = item.getAttachFile().getStoreFileName();
        String uploadFileName = item.getAttachFile().getUploadFileName();

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

        log.info("uploadFileName={}", uploadFileName);
        String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
        String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition).body(resource);
    }
}
package com.example.fileupload3.domain;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
@Table(name = "Item_file")
public class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String  itemName;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name ="uploadFileName", column = @Column(name = "attach_upload_file_name")),
            @AttributeOverride(name = "storeFileName", column = @Column(name = "attach_store_file_name"))
    })
    private UploadFile attachFile;

    @ElementCollection
    @CollectionTable(name = "item_image", joinColumns = @JoinColumn(name = "item_id"))
    private List<UploadFile> imageFiles = new ArrayList<>();

    public Item(String itemName, UploadFile attachFile, List<UploadFile> imageFiles) {
        this.itemName = itemName;
        this.attachFile = attachFile;
        this.imageFiles = imageFiles;
    }
}
package com.example.fileupload3.domain;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Data
public class ItemForm {
    private Long itemId;
    private String itemName;
    private List<MultipartFile> imageFiles;
    private MultipartFile attachFile;
}
package com.example.fileupload3.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.Repository;

import java.util.Optional;

public interface ItemRepository extends JpaRepository<Item, Long> {
    Optional<Item> findById(Long id);
}

여기서는 Repository로 주셨는데 이걸로 바꿔봤습니다. 근데 기존의 Repository에서도 안됐습니다.

 

package com.example.fileupload3.domain;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.Embeddable;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UploadFile {
    private String uploadFileName;
    private String storeFileName;

    public UploadFile(String uploadFileName, String storeFileName) {
        this.uploadFileName = uploadFileName;
        this.storeFileName = storeFileName;
    }
}
package com.example.fileupload3.file;

import com.example.fileupload3.domain.UploadFile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

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

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

    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 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();
        return uuid + "." + ext;
    }
    private String extractExt(String originalFileName){
        int pos = originalFileName.lastIndexOf(".");
        return originalFileName.substring(pos +1);
    }
}

 

 

이거 그대로 해봤는데

예제 코드를 README.md 부분만 읽고 하셨을 때도 작동하지 않으셨나요?

작동하는 것을 확인하고 올렸는데 이상합니다. 예제 코드를 수정하지 않은 상태로 구동했을 때의 예외를 말씀해주시겠습니까?

그리고 예외는 예제코드로 드렸던 application.yml에서 application.properties로 바꾸시면서 추가한 값들 중에서 예외가 있는 것으로 보입니다. 제 예상으로는 spring.jpa.open-in-view 값을 false로 주시면서 해당 문제가 생긴 것 같습니다. 해당 문제를 방지하기 위해서는 애플리케이션 구조의 수정이 필요합니다.

예시로 드렸던 예제 코드는 강의에서 보여드렸던 코드를 최대한 수정하지 않고 기존 요구사항과 유요한님이 말씀해주셨던 요구사항을 구현하기 위해서만 작성한 참고용 코드이기 때문에 수정하시면 예외가 발생할 수 있습니다:) spring.jpa.open-in-view 를 false 로 주는 이유에 대해서 알고 계시다면 혼자서 수정하실 수 있는 예외라고 생각합니다.

유요한님의 프로필 이미지
유요한
질문자

말씀해주신 것처럼 Open-In-View이거를 빼고 말해주시는 설정으로만 하니까 되네요.

근데, 궁금한 점이 있습니다.

 

  1. 올려주신것처럼 yml에 file: dir: ${file-dir}(여기서는 그냥 한줄로 표현) 하고 FileForm에서 @Value("${file.dir}")하니까 에러가 발생했습니다. bean을 생성할 수 없다고 나오더군요. 그래서 혹시나해서 yml에만 file: dir: c:/upload/file/로 바꾸니까 제대로 돌아갔습니다. file: dir: ${file-dir}이거는 무슨 의미인가요?

  2. 제가 알고있기론 JPA에서 Repository는 JpaRepository<>로 extends 하는걸로 알고 있는데 Repository하고 무슨 차이가 있나요??

  3. 현재 뷰단의 item-form에서 form을 보면 th:action이 안정해줬는데 ItemController에서 어떻게 @PostMapping("/items/new") 이런 URL로 받아올 수 있나요? 제가 알고 있는 방법은 action에서 url보내주면 받아오는 방식으로 알고있어서요 ㅠㅠ

  4. 제대로 이해하고 있나 질문드립니다.

    image여기서는 DB에서 올릴 때마다 Long id에서 숫자가 증가하는 것을 {id}로 하나만 보여주는 형식이고 findById(id)로 JPA로 DB에서 찾아서 보여준다.

    Item 엔티티를 만들어준다.

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "uploadFileName", column = @Column(name = "attach_upload_file_name")),
            @AttributeOverride(name = "storeFileName", column = @Column(name = "attach_store_file_name"))
    })
    private UploadFile attachFile;

    여기서는 잘 이해가 안가더군요 ㅠㅠ

    상품명: <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"/>
    @ResponseBody
    @GetMapping("/images/{filename}")
    public Resource downloadImage(@PathVariable String filename) throws MalformedURLException {
        return new UrlResource("file:" + fileStore.getFullPath(filename));
    }
    
    @GetMapping("/attach/{itemId}")
    public ResponseEntity<Resource> downloadAttach(@PathVariable Long itemId) throws MalformedURLException {
        Item item = itemRepository.findById(itemId)
                .orElseThrow(() -> new IllegalArgumentException("NOT FOUND ITEM :" + itemId));
        String storeFileName = item.getAttachFile().getStoreFileName();
        String uploadFileName = item.getAttachFile().getUploadFileName();
    
        UrlResource resource = new UrlResource("file:" + fileStore.getFullPath(storeFileName));
    
        log.info("uploadFileName={}", uploadFileName);
        String encodedUploadFileName = UriUtils.encode(uploadFileName, StandardCharsets.UTF_8);
        String contentDisposition = "attachment; filename=\"" + encodedUploadFileName + "\"";
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition).body(resource);
    }

controller의 이부분은 대략적인 느낌은 알겠지만 저기 뷰하고 연관되서는 구체적으로 모르겠네요 ㅠㅠ

  1. 여기서 REST 방식으로 하려면 @RestController를 달고 return하고 String 방식으로 되어있는 것을 JSON형태로 데이터를 보내주면 되나요?

예를 들어,

@GetMapping("/items/new")
public @ResponseBody ItemForm newItem(ItemForm form) {
    return form;
}
@PostMapping("/items/new")
public void saveItem(@RequestBody ItemForm form ) throws IOException {
    UploadFile attachFile = fileStore.storeFile(form.getAttachFile());
    List<UploadFile> storeImageFiles = fileStore.storeFiles(form.getImageFiles());

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

이런식으로 하면되나요? 질문이 많아서 죄송합니다ㅠㅠ 이왕 막힌김에 제대로 알고 가고싶어서 자세히 질문드립니다!

1) README.md에 적어드린 것처럼 실제 파일 저장 위치를 넣어주시지 않으면 애플리케이션이 구동하고 @Value에서 해당 값을 넣을 때 "${file-dir}" 이라고 들어가게 됩니다. 해당 부분은 유요한님께서 해주셨던 것처럼 직접 해당 값을 설정 파일에서 넣어주시거나,

image이렇게 Enviroment variables 값으로 넣어주실 수 있습니다. 편하신 방법으로 하시면 될 것 같습니다 :)

2) 해당 프로젝트에서는 JpaRepository에서 기본적으로 만들어주는 다양한 기능이 필요없기 때문에 Repository를 상속하도록 했습니다. 이렇게 하는 이유는 링크(클릭) 을 참고해주십시오! 선택의 영역이기 때문에 JpaRepository로 하셔도 상관없을 것 같습니다.

3) 해당 부분은 thymeleaf의 영역이라기 보단 html form 태그의 action 속성의 기본 작동 방식입니다. 링크(클릭) 의 2번 답변을 참고해주세요!

4) 전제가 약간 다르지 않나 생각합니다.
요청과 응답의 관점에서 생각해보면 items/2 라는 요청으로 들어오면
- @GetMapping("/items/{id}")의 패턴에 해당하기 때문에 해당 메서드가 처리하게 됩니다.
- @PathVariable로 2를 id 에 넣어줍니다.
- 저장해놓은 item들 중 id가 2인 Item을 찾습니다.
- 해당 Item을 model에 담아 뷰와 함께 응답합니다.
이렇게 이해해주셔야 합니다. 앞서 @PostMapping("/items/new")에서 저장한 후 받은 id를 이용해서 다시 @GetMapping("/items/{id}")에 요청을 보낸다고 생각하셔야 합니다. id를 올린다고 생각하시는 것은 기존에 메모리로 저장하던 메모리 저장소를 보고 그대로 생각하신 것 같습니다. id를 올리는 것이 아니라 저장할 때는 해당 Item을 식별할 수 있는 유일한 값과 함께 저장한다고 생각하셔야 합니다.

그리고 다음의 @Embedded 어노테이션의 JPA의 기술적인 부분이기 때문에 학습을 하셔야 아실 수 있는 부분입니다.

또한 밑의 첨부파일과 이미지 파일을 조회하는 컨트롤러 내역은 제 git에서 커밋 내역을 보시면 아시겠지만 강의 내용과 로직적으로 달라진 부분이 없습니다. 해당 영상 부분을 다시 학습해주시길 권해드립니다.

5) 해당 부분은 4)에 대한 이해가 선행되어야 합니다. 또한 REST API 방식은 생각하시는 것보다 많은 변경이 필요합니다. 또한 지금 예를 들어주신 코드를 봤을 때 개인적으로 영한님의 MVC 1편 강의에 대해서 복습해보시면 어떨까 제안 드립니다.

유요한님의 프로필 이미지
유요한

작성한 질문수

질문하기