이미지 업로드 기능 구현

bethe·2022년 10월 10일
0
post-custom-banner

1. Gradle 설정

파일 업로드를 위해서 사용하는 라이브러리는 COS.jar, Apache Commons FileUpload, Servlet(3.0이후 version)의 Part(API) 등이 있으며 본 프로젝트에서는 Apache Commons FileUpload를 사용하였다.

프로젝트 빌드 툴을 Gradle로 사용하므로 build.gradle의 dependencies에 라이브러리를 추가해주었다.

https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload

// https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
	implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'

구글링시 라이브러리 설정을 pom.xml에 하라는 글을 볼 수 있다. pom.xml은 의존성 관리도구 중 Maven을 사용했을 때 생성되는 라이브러리 관리 파일이므로 사용된 라이브러리를 찾아 Gradle 형식에 맞춰 설정해주면 된다.


2. DB 생성(MariaDB)사용

create table image(
	 image_id int primary KEY AUTO_INCREMENT,
	 origin_image_name VARCHAR(256) NOT null,
	 new_image_name VARCHAR(256) NOT null,
	 image_path VARCHAR(256) NOT null
);

이미지 파일은 이미지 정보이미지 자체의 정보 두 가지가 저장되어야 한다.

  • 이미지 자체 파일은 프로젝트의 내부(리소스 폴더)에 저장하고
  • 이미지 정보는 DB에 저장한다.

DB에는 PK값(image_id), 사용자가 업로드하는 파일의 원본이름(origin_image_name), 이미지 중복방지를 위해 붙여질 고유한 새 이름(new_image_name), 이미지 경로(image_path) 값이 삽입된다.

👉 new_image_name이 있는 이유
: 이미지 파일 업로드시, 기존에 있는 파일과 동일한 파일명으로 업로드하면 기존 파일이 새로 업로드한 파일로 덮어씌워진다. 따라서 파일명이 곂치지 않도록 고유한 파일명으로 저장해주어야 한다.


3. 기본세팅

3-1. Entity, DTO 생성

[Image.java]

package site.metacoding.frontproject.domain.Image;

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

@Getter
@Setter
@NoArgsConstructor
public class Image {
    private Integer id;
    private String originImageName;
    private String newImageName;
    private String imagePath;
}

[ImageDto]

package site.metacoding.frontproject.domain.Image;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor
@Getter
@Setter
public class ImageDto {
    private String originImageName;
    private String newImageName;
    private String imagePath;

    @Builder
    public ImageDto(String originImageName, String newImageName, String imagePath) {
        this.originImageName = originImageName;
        this.newImageName = newImageName;
        this.imagePath = imagePath;
    }

}

DTO를 사용하지 않고 Image로 바로 받아도 될 것 같지만

  • 정석적으로는 DB 트랜잭션시 필요한 값을 DTO에 받는 것이 맞고
  • 무엇보다 차후 사용자(employee, company)의 id(PK)나 해당 사용자의 resume id(PK)와 FK 관계를 가진 Entity로 재구성 해야 할 것 같기 때문에

깔끔히 DTO 테이블을 만들어 값을 받는 방향으로 했다.

@builder 어노테이션에 대해 쓰기
https://atoz-developer.tistory.com/120
static을 가지고 있ㄷ음


3-2. Mapper와 DAO 생성

[Image.xml]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="site.metacoding.frontproject.domain.Image.ImageDao">

    <insert id="save">
        INSERT INTO image(origin_image_name, new_image_name, image_path)
		VALUES(#{originImageName}, #{newImageName}, #{imagePath})
    </insert>
</mapper>

[ImageDao.java]

package site.metacoding.frontproject.domain.Image;

public interface ImageDao {
    public void save(ImageDto imageDto);
}

insert할 때 필요한 값만 받는 ImageDTO를 파라매터로 설정했다.


4. form태그를 이용한 파일 전송 (jsp생성)

(Controller에서 받을) 파일 데이터를 전송할 페이지를 만든다.


[upload.jsp]

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
    <!DOCTYPE html>
    <html>

    <head>
        <meta charset="UTF-8">
        <title>사진</title>
    </head>

    <body>

        <h1>사진업로드테스트</h1>

        <form method="post" action="/upload" enctype="multipart/form-data">
            <ul>
                <li>이미지 파일
                    <input type="file" multiple="multiple" name="image">
                    <input type="submit" id="submit" value="전송" />
                </li>
            </ul>
        </form>
    </body>
  • form 태그에 enctype 속성을 "multipart/form-data"로 설정해야 한다.

    • enctype : 폼 데이터(form data)가 서버로 제출될 때
      해당 데이터가 인코딩되는 방법을 명시한다. (enctype을 지정해주지 않을 경우 기본적으로 x-www-form-urlencoded으로 설정되어 있다.)
    • multipart/form-data : 모든 문자를 인코딩하지 않음을 명시한다.
      form 요소로 파일이나 이미지를 서버로 전송할 때 주로 사용한다.
  • input 태그의 경우 type="file" multiple="multiple" 으로 설정해야 한다.

    multipart

multiple이 없을 경우 오류가 난다 < 왜인지 적기 (두가지 정보를 전달해야 해서)

  • form이 submit이 되면 form안에 있는 컨트롤들의 데이터가 서버로 전송된다.

5. Controller

[UploadController.java]

package site.metacoding.frontproject.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.service.ImageService;

@Controller
@RequiredArgsConstructor
public class UploadController {
    private final ImageService imageService;

	//upload 페이지
    @GetMapping("/upload")
    public String upload() {
        return "upload";
    }
	
    //upload 페이지에서 전송버튼을 누르면 해당 메서드 실행
    @PostMapping("/upload")
    public String uploadImage(@RequestParam MultipartFile image){
        imageService.insertImage(image);
        return "upload";
    }
}

5-1. MultipartFile

MultipartFile이란 사용자가 업로드한 File을 Handler에서 손쉽게 다룰 수 있도록 도와주는 Handler의 매개변수 중 하나이다.

<input type="file" multiple="multiple" name="image"> 에서 file 데이터를 전송했고, Controller와 Service에서 코드를 용이하게 사용하기 위해 MultipartFile 타입으로 받았다.

이거 수정해야됨 왜 MultipartFile로 받았는가

5-2. @RequestParam

Controllor에서 패러미터에 값을 할당하는 대표적인 방법으로는 @RequestBody 와 @RequestParam 이 있다.

🔎 @RequestBody 어노테이션

  • HTTP request의 body에 담긴 데이터를 받는다. HttpMessageReader가 body를 읽어 Java Object로 역직렬화하여 주는 것.
  • 각 변수별로 데이터를 저장할 순 없다.

💡 @RequestParam 어노테이션

  • HTTP 단일 요청 패러미터의 값을 메서드 패러미터에 넣어주는 어노테이션. (1:1로 값 바인딩)
  • 요청 패러미터의 값은 메서드 패러미터의 타입에 따라 적절히 변환 된다.
  • @RequestParam으로 데이터를 받을 때에는 데이터를 저장하는 이름으로 메서드의 변수명을 설정해 주어야 한다.

(@RequestPart를 사용해도 무관하다)
<설명 적기
https://velog.io/@yu-jin-song/SpringBoot-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EA%B5%AC%ED%98%84-4-MultipartFile-%EB%8B%A4%EC%A4%91-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C


6. ImageService

6-1. 파일 업로드 기본 코드

package site.metacoding.frontproject.service;

import java.io.File;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.domain.Image.ImageDao;
import site.metacoding.frontproject.domain.Image.ImageDto;

@Service
@RequiredArgsConstructor
public class ImageService {

    private final ImageDao imageDao;

    public void insertImage(MultipartFile image) {

        // save할 DTO 띄우기
        ImageDto imageDto = new ImageDto();

		//저장할 파일경로 지정
        String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();

        // 확장자 추출
        if (!image.isEmpty()) {
            String contentType = image.getContentType();
            String originalImageExtension;
            if (contentType.contains("image/jpeg")) {
                originalImageExtension = ".jpg";
            } else if (contentType.contains("image/png")) {
                originalImageExtension = ".png";
            } else if (contentType.contains("image/gif")) {
                    originalImageExtension = ".gif";
       		}
       	}
            
		//UUID로 랜덤으로 이름 생성
        String newImageName = UUID.randomUUID().toString() + originalImageExtension;

        // DTO에 담기
       	imageDto = ImageDto.builder()
                    .originImageName(image.getOriginalFilename())
                    .newImageName(newImageName)
                    .imagePath(absolutePath)
                    .build();

		//DAO 실행
        imageDao.save(imageDto);
    }
}

1) imageDao.save에 필요한 DTO 띄우기

해당 메서드의 역할은 request로 받은 image 파일 정보를 DB에 insert하는 것이므로 우선 imageDao.save()에 필요한 패러미터인 DTO 객체를 생성한다. DTO에는 originImageName, newImageName, imagePath가 필요하다.


2) 파일경로 지정

String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();

✍️ File 타입
: File 객체는 하드디스크에 존재하는 실제 파일이나 디렉토리가 아니다. 그것에 대한 경로(Pathname) 또는 참조(reference)를 추상화한 객체이다. 즉, File 객체는 새 파일에 대한 경로나 만들고자 하는 디렉토리를 캡슐화한 것이다.

  • File 클래스의 생성자는 인자로 전달된 경로를 확인하지 않는다. => 추후 해당 경로를 만들어주어야 한다.
  • File 클래스의 생성자로 어떤 문자열을 넘겨도 된다.
  • File 객체는 불변적으로 객체를 생성하고 나면 그것이 가진 경로를 바꿀 수 없다. ⇒ 따라서 실제 저장 경로는 맨 마지막에 지정해줄 것이다. & 지금은 File 객체로 받는 것이 아닌, 절대경로를 추출하여 String으로 받는 것이기 때문에 상관 없다.

getPath() vs getAbsolutePath()

  • getPath()
    : File에 입력한 경로를 return한다. 인자로 전달한 경로를 그대로 리턴하기에 상대경로 가 된다.
    File file = new File("path/to/file");
    System.out.println(file.getPath());
    // console : path/to/file2
  • getAbsolutePath()
    : 현재 실행 중인 Working directory + File 인자 경로 => 절대 경로 가 된다.
    File file = new File("path/to/file");
    System.out.println(file.getPath());
    // console : C:/Users/Desktop/workspace/first_project/frontproject/path/to/file2

따라서 String absolutePath는 image를 저장할 정확한 파일 위치의 절대경로 값이다.


3) 확장자 추출

String contentType = image.getContentType();
String originalImageExtension;
if (contentType.contains("image/jpeg")) {
originalImageExtension = ".jpg";
}

👉 .getContentType()
: file의 확장자를 확인하여 MIME 타입을 확인한다.

👉 .contains()
: 대상 문자열에 특정 문자열이 포함되어 있는지 확인하는 함수이다. 대/소문자를 구분한다.


4) UUID로 랜덤으로 이름 생성 (newImageName 값 얻기)

String newImageName = UUID.randomUUID().toString() + originalImageExtension;

UUID(Universally Unique IDentifier) 범용 고유 식별자

  • 각 개체를 고유하게 식별 가능하게 해주는 값을 의미한다.
  • .randomUUID를 통해 생성 가능 ⇒ 생성시 UUID 형태이므로 String 형태로 바꿔줘야 한다. (.toString())

5) Builder를 이용해 DTO에 담기

		//imageDto는 맨 처음 객체 생성을 해놓았음
       	imageDto = ImageDto.builder()
                    .originImageName(image.getOriginalFilename())
                    .newImageName(newImageName)
                    .imagePath(absolutePath)
                    .build();

💡 Builder 패턴
객체를 생성할 때 사용하는 패턴 중 하나. 생성자에 들어갈 패러미터를 하나씩 받아들인 뒤 (패러미터 순서는 상관없다) 모든 패러미터를 받은 뒤에 이 변수들을 통합해 한번에 객체를 생성하는 방식이다.

< 문법 >

변수타입 변수명 = 생성자.builder()
	.파라매터(인자)
    .파라매터(인자)
    .build();

imagePath 값의 경우 일단은 세분화된 하위 폴더를 만들어 저장하지 않고 images 폴더에 모두 저장하기로 했다. 그래서 절대경로만 할당하였다.

6) Builder 패턴으로 생성한 imageDto를 DAO에 적용

imageDao.save(imageDto);


6-2. 이미지 파일 저장 폴더에 전송하기

위의 코드는 DB에 파일 정보값을 insert하는 코드일 뿐, 전송한 파일을 저장하는 코드는 아니다.

파일 경로에 실제로 파일을 저장하는 코드를 맨 밑에 추가해준다.

File file = new File(absolutePath + "/" + newImageName);
if (!file.exists()) {
	file.mkdirs();
}
image.transferTo(file);

👉.transferTo(경로)

  • 데이터를 전송하는 File 함수. 지정한 경로에 파일 데이터를 저장한다.
  • 내부적으로 FileCopyUtils.copy를 사용하며, 해당 메서드에서 파일을 전송하고 받는 inputstream과 outputstream이 있다. 본래 FileInputStream과 FileOutputStream을 사용할 경우 Stream이 흐르고 있기 때문에 File을 닫아주어야 하나( .close( ) ), 해당 메서드에서 inputstream과 outputstream을 close하고 있기 때문에 별도의 처리는 필요 없다.
    default void transferTo(Path dest) throws IOException, IllegalStateException {
    	FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));}

6-3. 조건 설정 추가 코드

package site.metacoding.frontproject.service;

import java.io.File;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import site.metacoding.frontproject.domain.Image.ImageDao;
import site.metacoding.frontproject.domain.Image.ImageDto;

@Service
@RequiredArgsConstructor
public class ImageService {

    private final ImageDao imageDao;

    public void insertImage(MultipartFile image) throws Exception {
        // 파일이 빈 것이 들어오면 메서드 종료
        if (image.isEmpty()) {
            return;
        }

        // save할 DTO 띄우기
        ImageDto imageDto = new ImageDto();

        // 절대경로 추출
        String absolutePath = new File("src/main/resources/static/images/").getAbsolutePath();

        // jpeg, png, gif 파일들만 받아서 처리
        if (!image.isEmpty()) {
            String contentType = image.getContentType();
            String originalImageExtension;
            // 확장자 명이 없으면 종료
            if (!StringUtils.hasText(contentType)) {
                return;
            } else {
                if (contentType.contains("image/jpeg")) {
                    originalImageExtension = ".jpg";
                } else if (contentType.contains("image/png")) {
                    originalImageExtension = ".png";
                } else if (contentType.contains("image/gif")) {
                    originalImageExtension = ".gif";
                }
                // 기타 확장자명일 경우 메서드 종료
                else {
                    return;
                }
            }
            String newImageName = UUID.randomUUID().toString() + originalImageExtension;

            // DTO에 담기
            imageDto = ImageDto.builder()
                    .originImageName(image.getOriginalFilename())
                    .newImageName(newImageName)
                    .imagePath(absolutePath)
                    .build();

            imageDao.save(imageDto);

            // 파일을 전송하기
            File file = new File(absolutePath + "/" + newImageName);
            if (!file.exists()) {
                file.mkdirs();
            }
            image.transferTo(file);
        }
    }
}

✅ return (함수의 반환 값)

  • 현재 실행하고 있는 함수를 종료하고, 함수를 호출한 곳으로 돌아가는 제어문
  • 함수가 반환할 값이 없는 경우 = 반환 형식이 void인 경우, return 문만 사용 가능하며(return;) 이 경우 값 반환 없이 메서드만 종료된다.

✅ StringUtils.hasText(@Nullable String str)

  • null 체크 후 String 클래스의 imEmpty를 호출하여 길이를 체크, 공백(" ")이 아닌 문자가 포함되어 있는지까지 체크하여 판별한다.

  • ❗️주의점 :
    문자열을 하나씩 순회하여 공백이 아닌 character가 1개 이상 포함되면 true를 리턴한다. 즉, isEmpty와 다르게 null 체크시 결과가 반대로 전달된다. (StringUtils.hasText(null); ==> false)

if (!StringUtils.hasText(contentType)) {
	return;}

👉


throw Exception 공부

보완해야 할 점

  1. 이미지가 한글명이어도 전달되게 하기
  2. 로그인한 세션사용자의 해당 이력서/공고에 해당 이미지가 들어가도록 DB 수정하기
  3. form으로 이미지를 전송하지 않고 ajax로 전송하기
  4. pic.path 프로퍼티스에서 설정하기

모델어트리뷰트
참조 :https://parkadd.tistory.com/70

모델로 바로 뿌릴때참고해야겠음
:https://galid1.tistory.com/564#:~:text=MultipartFile%20%EC%9D%B4%EB%9E%80%20%EC%82%AC%EC%9A%A9%EC%9E%90%EA%B0%80%20%EC%97%85%EB%A1%9C%EB%93%9C,%EC%9D%B4%20%EB%93%B1%EB%A1%9D%EB%90%98%EC%96%B4%EC%9E%88%EC%96%B4%EC%95%BC%20%ED%95%A9%EB%8B%88%EB%8B%A4.

https://simple-ing.tistory.com/47
왜 Param에 file을 붙이지 않아도 되는지 공부하기

나중에 resume 한번에 저장할 때 참고할 사이트
https://velog.io/@yu-jin-song/SpringBoot-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EA%B5%AC%ED%98%84-4-MultipartFile-%EB%8B%A4%EC%A4%91-%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C

profile
코딩을 배우고 기록합니다. 읽는 사람이 이해하기 쉽게 쓰려고 합니다.
post-custom-banner

0개의 댓글