Project/ClassFlix

[ClassFlix] EP 13. 리팩토링과 성능최적화 - 2 (사진업로드, 출력)

목차 (클릭시 해당 목차로 이동)


     

     

     

    1. 리뷰수정기능개발 
    2. 리뷰삭제기능개발 
    3. 회원가입창에서 footer 크기조정 
    4. 리뷰등록 디자인 (입력부분가로로나열, 등록버튼 모양) 
    5. 사진등록기능
    6. 리팩토링 (쿼리나가는 개수 계산해서 성능 최적화하기)
    7. 디자인다듬기
    8. 느낀점, 발전할점, 추가할점 정리하기

     

     

     

    사진등록기능

    기존에는 데이터베이스에 이미지파일을 직접 넣는 식으로 구현했습니다.

    그러나 이 방식은 비효율적이고 병목현상을 일으킵니다.

    그래서 이미지는 서버의 특정 위치에 저장하고 그 이미지에 대한 정보를 DB에 저장하는 식으로 구현하려 합니다.

     

    파일업로드는 스프링에서 제공하는 MultipartFile 이라는 인터페이스를 이용해서, HTTP multipart 요청을 처리합니다.
    MultipartFile는 큰 파일을 청크 단위로 쪼개서 효율적으로 파일 업로드 할 수 있게 해줍니다.

     

    구현계획

    1. lecture Form 에서 이미지를 파일을 넘깁니다.
    2. 컨트롤러에서 이미지파일을 multipartfile로 받고 fileParser에 넘깁니다.
    3. fileParser에서는 받은 파일을 server에 저장하고 이미지의 이름, 사이즈, path를 생성해서 반환합니다.
    4. 다시 컨트롤러에서는 fileParser에서 반환된 값들과 lecture form에서 받은 정보들을 lecture에 등록합니다.
    5. lecture image가 필요한 페이지에서는 th:src 에 lecture의 path를 넣어주어 이미지를 읽어오게 합니다.

     

     

     

     

    구현

    lecture Form

     

     

    multipartfile로 받기 위해서는 multiple="true"를 꼭 해주어야 합니다.

    그렇지 않으면 파일업로드 폼이 동작하지 않습니다.

     

            <div class="form-group">
                <label th:for="representImage">강의 대표사진 업로드</label>
                <input type="file" multiple="true" th:field="*{image}" accept="image/*" class="form-control" placeholder="강의 대표사진을 업로드해주세요">
            </div>

     

     

    Controller

     

    Controller에서는 multipartfile(form.getImage())을 fileParser에 넘겨주고 fileInfo를 받습니다.

    그리고 form과 fileInfo의 데이터를 lecture에 넘겨주어 lecture를 생성합니다.

     

     

    (fileInfo)

    package dongho.classflix.service;
    
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.stereotype.Component;
    
    @Component
    @Getter
    @Setter
    public class FileInfo {
        private String filePath;
        private String fileName;
        private long fileSize;
    }
    

    (controller)

        @PostMapping("/lectures/new")
        public String create(@Valid LectureForm form, BindingResult result) throws IOException {
            if (result.hasErrors()) {
                return "lectures/lectureForm";
            }
            FileInfo fileInfo = lectureService.fileParser(form.getImage());
    
            log.info("path : {}, size : {}, name : {}", fileInfo.getFilePath(), fileInfo.getFileSize(), fileInfo.getFileName());
    
            Lecture lecture = new Lecture(form.getLectureName(), form.getTeacherName(),
                    form.getContent(), fileInfo.getFilePath(), fileInfo.getFileSize(), fileInfo.getFileName(), form.getSiteName(), form.getUri(), LocalDateTime.now());
    
            Long testId = lectureService.join(lecture);
            return "redirect:/";
        }

     

     

     

     

    Service (fileParser)

     

    multipartFile을 받습니다.

    만약 multipartFile이 비어있으면 fileInfo를 "", "", 0으로 세팅 후 반환합니다.

     

    fileName은 겹치는 이름을 없애기 위해 현재날짜+nanoTime으로 설정합니다.

    path는 static의 images/represent/에 저장합니다. ( "."+"png" 수정 필요)

    absolutPath는 이미지를 서버의 로컬디스크에 실제로 저장하기 위해 필요합니다.

     

    absolutePath와 fileName을 이용해 file 객체를 생성합니다.

    해당 디렉토리가 없을 경우를 위해 mkdir을 추가합니다.

    multipartfile을 file객체로 변환하고 실제 로컬에 저장합니다. (transfer)

     

    fileInfo에 name, size, path를 저장한 후 반환합니다.

     

        // 파일파싱
        public FileInfo fileParser(MultipartFile multipartFile) throws IOException {
            FileInfo fileInfo = new FileInfo();
    
            if (multipartFile.isEmpty()) {
                fileInfo.setFileName("");
                fileInfo.setFilePath("");
                fileInfo.setFileSize(0);
                return fileInfo;
            }
    
            String fileName = "" + LocalDate.now() + System.nanoTime();
    
            String absolutePath = new File("").getAbsolutePath() + "/src/main/resources/static/images/represent/";
            String path = "/images/represent/" + fileName + "." + "png";
    
            log.info("type : {}, name : {}, path : {}", multipartFile.getContentType(), fileName, path);
    
            File file = new File(absolutePath + fileName + ".png");
    
            if (!file.exists()) {
                file.mkdirs();
            }
    
            multipartFile.transferTo(file);
    
            fileInfo.setFileName(fileName);
            fileInfo.setFileSize(multipartFile.getSize());
            fileInfo.setFilePath(path);
            return fileInfo;
    
        }

     

     

     

     

     

    View (home.html)

     

    먼저 home에서 각 강의의 이미지를 읽어오도록 할 것입니다.

     

     

    <src = "/images/springInstroduction.png"> 를

    <th:src = "${lecture.getRepresentImagePath()}"> 치환하도록 합니다.

    (실제 값은 th:src = "/images/represent/2021-05-20241424898036005.png")

     

    <img class="lecture-images" th:src="${lecture.getRepresentImagePath()}" src="/images/springInstroduction.png" alt="스프링입문">

     

     

     

     

    로직 테스트

     

    1. 실제 저장되어있는 이미지를 가져와서 multipartFile로 변환합니다. (mFile)
    2. mFile을 컨트롤러에서 하는것처럼 fileParser에 넣고 lecture를 생성합니다.
    3. fileParser에서 저장한 서버의 로컬디스크에 있는 이미지를 가져와서 multipartFile로 변환합니다. (mFile1)
    4. mFile, mFile1 을 비교합니다. (파일 자체의 바이트, 크기, 타입)

     

    @Test
        public void 강의사진저장() throws Exception {
            //given
            MultipartFile mFile = getMultipartFile();
    
            //when
            FileInfo fileInfo = lectureService.fileParser(mFile);
            Lecture lecture = getLecture(fileInfo);
    
            MultipartFile mFile1 = getMultipartFile(lecture);
    
            //then
            // 1 : 인자로 받은 파일 2 : 저장한 후 다시 읽은 파일
            //파일자체, 크기, 타입 비교
            assertThat(mFile.getBytes()).isEqualTo(mFile1.getBytes());
            assertThat(mFile.getSize()).isEqualTo(mFile1.getSize());
            assertThat(mFile.getContentType()).isEqualTo(mFile1.getContentType());
    
        }
    
        private Lecture getLecture(FileInfo fileInfo) throws URISyntaxException {
            URI uri = new URI("https://www.inflearn.com/");
            Lecture lecture = new Lecture("테스트", "테스트", "좋아요", fileInfo.getFilePath(), fileInfo.getFileSize(), fileInfo.getFileName(), "인프런", uri, LocalDateTime.now());
            lectureService.join(lecture);
            return lecture;
        }
    
        private MultipartFile getMultipartFile(Lecture lecture) throws IOException {
            File file;
            FileItem fileItem;
            file = new File(new File("").getAbsolutePath() + "/src/main/resources/static"+ lecture.getRepresentImagePath());
            fileItem = new DiskFileItem("newFile", Files.probeContentType(file.toPath()), false, file.getName(), (int) file.length(), file.getParentFile());
    
            try {
                InputStream input = new FileInputStream(file);
                OutputStream os = fileItem.getOutputStream();
                IOUtils.copy(input, os);
                // Or faster..
                // IOUtils.copy(new FileInputStream(file), fileItem.getOutputStream());
            } catch (IOException ex) {
                // do something.
            }
    
            //저장한 파일 다시 multipartfile로 가져오기
            MultipartFile mFile1 = new CommonsMultipartFile(fileItem);
            return mFile1;
        }
    
        private MultipartFile getMultipartFile() throws IOException {
            File file = new File(new File("").getAbsolutePath() + "/src/main/resources/static/images/jpa.png");
            FileItem fileItem = new DiskFileItem("originFile", Files.probeContentType(file.toPath()), false, file.getName(), (int) file.length(), file.getParentFile());
    
            try {
                InputStream input = new FileInputStream(file);
                OutputStream os = fileItem.getOutputStream();
                IOUtils.copy(input, os);
                // Or faster..
                // IOUtils.copy(new FileInputStream(file), fileItem.getOutputStream());
            } catch (IOException ex) {
                // do something.
            }
    
            //jpa.png -> multipart 변환
            MultipartFile mFile = new CommonsMultipartFile(fileItem);
            return mFile;
        }

     

     

     

    file -> multipartfile 변환하는 법은 아래 글에서 설명합니다.

    2021.05.20 - [Web/팁] - java File to MultipartFile (import 포함)

     

    java File to MultipartFile (import 포함)

    목차 (클릭시 해당 목차로 이동) 스프링에서 파일업로드를  구현할때 테스트 코드를 작성하려면 File을 읽어와서 MultiPartFile로 변환해주어야 합니다. 변환하는 코드는 인터넷에 많지만 어떤것을

    ksabs.tistory.com

     

     

     

     

    클라이언트 테스트

     

    사진을 가져오지 못합니다.

     

     

    th:src의 문제인지 판단하기위해 path를 그대로 넣어봤습니다.

        @RequestMapping("/")
        public String home(Model model) {
            List<Lecture> lectures = lectureService.findAll();
    
            List<HomeLectureDto> HomeLectureDtos = new ArrayList<>();
    
            for (int i = 0; i < lectures.size(); i++) {
                HomeLectureDto lectureDto = new HomeLectureDto();
                lectureDto.setId(lectures.get(i).getId());
    //            lectureDto.setRepresentImagePath(lectures.get(i).getRepresentImagePath());
                lectureDto.setRepresentImagePath("/images/represent/2021-05-20250364915623752");
                lectureDto.setLectureName(lectures.get(i).getLectureName());
                lectureDto.setAverageRating(lectures.get(i).getAverageRating());
    
                HomeLectureDtos.add(lectureDto);
            }
    
            model.addAttribute("lectures", HomeLectureDtos);
            return "home";
        }

     

     

    그래도 가져오지 못합니다.

     

     

     

     

    그래서 url 형식으로 src를 바꿔보았습니다.

    package dongho.classflix.controller.dto;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @Setter
    public class HomeLectureDto {
        private Long id;
    //    private String representImagePath;
        private String representImageName;
        private String lectureName;
        private double averageRating;
    }
        @RequestMapping("/")
        public String home(Model model) {
            List<Lecture> lectures = lectureService.findAll();
    
            List<HomeLectureDto> HomeLectureDtos = new ArrayList<>();
    
            for (int i = 0; i < lectures.size(); i++) {
                HomeLectureDto lectureDto = new HomeLectureDto();
                lectureDto.setId(lectures.get(i).getId());
                
    //            lectureDto.setRepresentImagePath(lectures.get(i).getRepresentImagePath());
    //            lectureDto.setRepresentImagePath("/images/represent/2021-05-20250364915623752");
                lectureDto.setRepresentImageName(lectures.get(i).getRepresentImageName());
                
                lectureDto.setLectureName(lectures.get(i).getLectureName());
                lectureDto.setAverageRating(lectures.get(i).getAverageRating());
    
                HomeLectureDtos.add(lectureDto);
            }
    
            model.addAttribute("lectures", HomeLectureDtos);
            return "home";
        }
    <img class="lecture-images" th:src="@{/images/represent/{imageName}(imageName = ${lecture.getRepresentImageName()})}" src="/images/springInstroduction.png" alt="스프링입문">

     

     

    그래도 안됨.

     

     

     

    삽질끝에 알아낸 해결방법

    • 서버 재시작없이 변경된 정적리소스를 가져올 수 있어야 한다.

     

    안될때의 공통점을 발견했는데, 항상 방금 업로드한 이미지만 로딩이 안되었습니다.

    그래서 정적리소스가 바로 반영이 안되는 경우를 검색했는데 많은 사람들이 문제점에 직면했고 해결했습니다.

     

    아래 정리한 글을 참고해 프로젝트를 다시 세팅해준 후 진행을 해보았습니다.

     

    2021.05.20 - [Web/팁] - [Spring] [IntelliJ] 정적리소스 서버 재시작없이 바로 반영하기

     

    [Spring] [IntelliJ] 정적리소스 서버 재시작없이 바로 반영하기

    목차 (클릭시 해당 목차로 이동) 스프링으로 이미지 업로드와 출력 기능을 구현하면서 업로드 한 파일을 바로 출력할때 반영이 안되는 상황이 있었습니다. 이미지를 업로드하자마자 바로 반영

    ksabs.tistory.com

     

     

     

     

    클라이언트 다시 테스트

     

    강의 Form에서 정보를 입력하고 등록하기를 누릅니다.

     

     

    업로드 즉시 반영은 되지 않았지만

     

     

    인텔리제이화면으로 돌아가서 잠시만 기다리면 원하는 경로에 이미지가 추가됩니다.

     

     

    그리고 브라우저를 새로고침하면 이미지가 반영되어 있습니다.

     

     

     

     

    ".png" 수정하기

    실제로 이미지파일은 png, jpg 등 다양한 확장자로 업로드 됩니다.

    fileParser 서비스 로직에서 multipartfile의 getContentType을 해보면 image/png로 나옵니다.

    아마 jpg면 image/jpg로 나올텐데 여기서 뒤에있는 png, jpg만 떼와서 fileName 옆에 붙여야합니다.

    (현재는 ".png" 로 되어있습니다.)

     

     

    구현계획

    1. multipartfile의 original name을 가져와서 String을 "."으로 구분합니다.
    2. 그러면 2개로 구분되는데, 뒤에것이 확장자입니다.
    3. ".png"로 하드코딩 되어있는것을 이 확장자로 바꿉니다.

     

    여기서 중요한 점은 String의 split을 사용할때 "."으로 구분하기 위해서는

     

    split(".")이 아닌 split("\\.")을 사용해야 합니다.

     

    split은 정규표현식으로 문자를 분리하는데, 정규표현식의 점(.)은 하나의 문자와 대응하는 일종의 메타문자이기 때문에 점(.)을 사용하기 위해서는 이스케이프 문자인 "\\"를 앞에 붙여줘야 합니다.

        // 파일파싱
        public FileInfo fileParser(MultipartFile multipartFile) throws IOException {
            FileInfo fileInfo = new FileInfo();
    
            if (multipartFile.isEmpty()) {
                fileInfo.setFileName("");
                fileInfo.setFilePath("");
                fileInfo.setFileSize(0);
                return fileInfo;
            }
    
            String[] strArray = multipartFile.getOriginalFilename().split("\\.");
            String fileName = "" + LocalDate.now() + System.nanoTime() + "." + strArray[strArray.length-1];
    
            String absolutePath = new File("").getAbsolutePath() + "/src/main/resources/static/images/represent/";
            String path = "/images/represent/" + fileName;
    
            File file = new File(absolutePath + fileName);
    
            if (!file.exists()) {
                file.mkdirs();
            }
    
            multipartFile.transferTo(file);
    
            fileInfo.setFileName(fileName);
            fileInfo.setFileSize(multipartFile.getSize());
            fileInfo.setFilePath(path);
            return fileInfo;
    
        }

     

     

    jpeg 이미지를 업로드해도

     

     

    이미지가 잘 반영되어 있는 것을 볼 수 있습니다.