Day_44 ( 스프링 - 11 )

HD.Y·2023년 12월 30일

한화시스템 BEYOND SW

목록 보기
39/58
post-thumbnail

MultipartFile 이란 ❓

  • 파일을 백엔드 서버에서 받기 위해서는 스프링에서 제공하는 MultipartFile 인터페이스를 사용한다.

    프론트 서버에서 파일을 보낼때는 enctype="multipart/form-data" 로 보내고 백엔드 서버에서 요청을 받으려면 DTO 객체의 파일을 저장할 변수를 String 이 아닌 MultipartFile로 해야되는 것이다.

    만약, String 으로 해서 요청을 받으면 해당 변수에는 단순히 파일의 이름만 입력받게 된다. 하지만 MultipartFile 로 요청을 받으면 binary 형태로 받게 되는데 이것은 파일을 청크 단위로 쪼개서 효율적으로 파일을 업로드 할 수 있도록 도와준다.


🐵 파일 업로드 및 DB 경로 저장 실습하기

  • 실습 전에 파일을 보낼때 아래와 같이 FileSizeLimitExceededException 에러가 뜬다면 application.yml 파일에서 별도 설정을 해줘야된다. 이것은 파일을 업로드 할 때 용량을 초과한다는 에러인데, 설정에서 용량사이즈를 직접 지정해 줄 수 있다.
    spring:
      servlet:
        multipart:
          max-file-size: 10MB
          max-request-size: 10MB

  • 실습은 간단하게 회원가입할때 회원 이미지를 같이 등록하도록 하는 상황으로 진행해보겠다.

    💻 pom.xml 파일 추가한 라이브러리

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-jpa</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    		</dependency>
    
    		<dependency>
    			<groupId>com.mysql</groupId>
    			<artifactId>mysql-connector-j</artifactId>
    		</dependency>

    💻 application.yml 파일 설정

    spring:
      datasource:
        url: jdbc:mysql://77.77.77.111/shop
        username: test01
        password: qwer1234
        driver-class-name: com.mysql.cj.jdbc.Driver
    
      servlet:
        multipart:
          max-file-size: 10MB
          max-request-size: 10MB
    
      jpa:
        hibernate:
          ddl-auto: update
          naming:
            implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
            physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
        properties:
          hibernate:
            format_sql: true
        show-sql: true
    
    logging:
      level:
        org.springframework.security: DEBUG
    
    project:
      upload:
        path: C:\test   // 파일을 저장할 폴더명 지정

    ➡ 여기다가 파일을 저장할 폴더명을 설정해준다. 그 이유는, 클래스에 변수로 생성하여
      설정하면 나중에 저장할 폴더명을 바꿀때 하나씩 다 바꿔줘야되기 때문에 설정
      파일에 설정하고 값을 불러와서 사용토록 할 예정이다.


    💻 User Entity 클래스

    @Entity
    @Builder
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
    
       @Id
       @GeneratedValue(strategy = GenerationType.IDENTITY)
       private Long id;
    
       @Column(nullable = false, length = 50, unique = true)
       private String email;
    
       @Column(nullable = false, length = 200)
       private String password;
    
       @Column(length = 30)
       private String name;
    
       // DB에 저장할때는 경로로 저장되기 때문에 String 타입으로 생성한다.
       @Column(length = 200, unique = true)
       private String image;
    }

    ➡ 여기서 image 변수의 타입을 String으로 한것에 의문을 가질 수 있다. 이것은 DB에
      저장될 속성의 타입으로 우리가 파일을 DB에 저장한다고 하면 파일이 저장된 경로를
      저장
    하기 때문에 String 으로 설정해줘야 된다.

    💻 UserRepository 인터페이스

    @Repository
    public interface UserRepositorty extends JpaRepository<User, Long> {
    
    }

    💻 PostCreateUserReq 클래스

    @Builder
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class PostCreateUserReq {
    
       private String email;
       private String password;
       private String name;
       private MultipartFile image;
    
    }

    💻 PostCreateUserRes 클래스

    @Builder
    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public class PostCreateUserRes {
    
       private Long id;
       private String email;
       private String name;
       private String image;
    
    }

    💻 UserService 클래스

    @Service
    public class UserService {
    
       // 파일을 저장할 경로를 설정한 값을 불러온다.
       @Value("${project.upload.path}")
       private String uploadPath;
       
       private final UserRepositorty userRepositorty;
    
       public UserService(UserRepositorty userRepositorty) {
           this.userRepositorty = userRepositorty;
       }
    
       // 폴더가 없다면, 폴더를 생성하기 위한 메서드이다.
       public String makeFolder(){
           // 현재 시간을 기준으로 2023/11/11 형식으로 설정해준다. 
           // 폴더를 생성할때 2023 폴더 안에 11 폴더 안에 11 폴더 안에 파일을 저장하기 위한 작업이다.
           // 포맷을 지정해주지 않으면, ms 단위 까지 나오기 때문에 폴더를 생성할때 엄청나게 많이 생긴다.
           String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
           
           // 폴더의 경로를 지정하는데, 이때 File.separator 를 사용하며 "/" 을 "\" 로 바꿔준다.
           // 그 이유는 윈도우 OS는 경로를 "\" 로 표현하기 때문이다.
           // 폴더 경로는 내가 저장하기로 한 폴더 밑에 날짜들을 분리하여 만들어 준다 ex) c:\test\2023\11\11
           String folderPath = str.replace("/", File.separator);
           
           //  new File(A, B) 메서드는 A라는 경로에 B라는 이름의 파일을 생성한다는 메서드이다.
           File uploadPathFolder = new File(uploadPath, folderPath);
    
           // 경로가 생성되어있는것이 없다면 폴더를 생성해주기 위한 작업이다.
           if(uploadPathFolder.exists() == false) {
               // 폴더를 만들어주는 메서드이다.
               uploadPathFolder.mkdirs();
           }
           // 폴더 경로를 반환한다.
           return folderPath;
       }
    
       // 파일을 저장하는 메서드이다.
       public String saveFile(MultipartFile file) {
           
           // 전달받은 MutipartFile에서 파일의 이름만 뽑아낸다.
           String originalName = file.getOriginalFilename();
    
           // 저장할 폴더 경로가 있는지 확인하여 없다면 경로에 폴더를 생성한다.
           String folderPath = makeFolder();
    
           // 파일의 이름이 중복되지 않도록 설정하는 것이다. UUID를 사용하면 중복되지 않는 문자열을 만들 수 있다.
           // 만약 이렇게 해주지 않으면, 동시에 똑같은 이름의 파일을 클라이언트가 등록하기 위해 요청하면
           // 파일이 저장될때 기존에 있던 것을 덮어쓰기 때문에, 기존의 파일은 사라지게 된다.
           String uuid = UUID.randomUUID().toString();
           
           // 생성한 폴더경로에 UUID와 파일의 이름을 더하여 최종 파일 이름을 설정한다.
           String saveFileName = folderPath+ File.separator + uuid + "_" + originalName;
           
           // 해당 경로에 파일을 생성한다.
           File saveFile = new File(uploadPath, saveFileName);
    
           try {
           
               // 파일을 전달한다. transferTo는 스트림의 일종이기 때문에 예외처리를 해줘야된다. 
               // 이때 폴더에 파일이 생성되어 저장된다.
               file.transferTo(saveFile);
    
           } catch (IOException e) {
               throw new RuntimeException(e);
           }
           
           // DB에 파일 이름을 저장하기 위해 반환
           return saveFileName;
           }
           
       // CREATE 메서드
       public PostCreateUserRes create(PostCreateUserReq postCreateUserReq) {
    
           String saveFileName = saveFile(postCreateUserReq.getImage());
    
           User user = User.builder()
                   .email(postCreateUserReq.getEmail())
                   .password(postCreateUserReq.getPassword())
                   .name(postCreateUserReq.getName())
                   
                   // 파일을 윈도우 컴퓨터에 저장하기 위해 "\" 로 바꿧지만, 
                   // 파일을 불러올때는 url로 요청이 들어오기때문에 DB에 저장할때는 다시 "/" 로 바꿔야 한다.
                   .image(saveFileName.replace(File.separator, "/"))
                   
                   .build();
    
           User result = userRepositorty.save(user);
    
           PostCreateUserRes response = PostCreateUserRes.builder()
                   .id(result.getId())
                   .email(result.getEmail())
                   .name(result.getName())
                   .image(result.getImage())
                   .build();
    
           return response;
       }
    }
    

    💻 UserController 클래스

    @RestController
    @RequestMapping("/user")
    public class UserController {
       private final UserService userService;
    
       public UserController(UserService userService) {
           this.userService = userService;
       }
    
       @RequestMapping(method = RequestMethod.POST, value = "/create")
       public ResponseEntity create(PostCreateUserReq postCreateUserReq) {
           PostCreateUserRes postCreateUserRes = userService.create(postCreateUserReq);
    
           return ResponseEntity.ok().body(postCreateUserRes);
       }
    }
  • 이렇게 하고 포스트맨을 이용하여 요청을 보내면 정상적으로 파일이 지정한 경로의 폴더에 생기는 것을 확인할 수 있을 것이다.


🐷 개인 프로젝트에 적용하기

  • 개인적으로 진행중인 쇼핑몰 프로젝트에 이미지 파일을 그동안 "img_url" 로 임시로 입력해놨었는데, 위에서 한 내용을 적용해봤다.

  • 파일을 업로드하는 코드는 위와 동일하기 때문에, 잘 동작하는지만 그림으로 표현하겠다.

    1) 서버로 브랜드 등록 요청 보내기


    2) DB에 정상적으로 저장됬는지 확인


    3) 지정한 폴더 경로에 파일이 생성됬는지 확인


  • 위와 같이 설정한대로 정상적으로 DB에도 저장되고 파일도 폴더에 생성된 것을 확인 할 수 있었다.

  • 하지만, 이렇게 하면 내 하드디스크에 파일들을 저장하기 때문에, 하드 디스크의 용량이 감당안될 수도 있다. 또한 가장 큰 문제는 클라이언트의 요청이 있을때마다 파일을 불러오기 위해서는 내 하드디스크 경로로 저장한 DB에 요청이 들어오기 때문에 서버가 요청이 많이 들어오면 서버가 정상적으로 동작하기가 힘들다.

  • 그렇기때문에, 파일을 클라우드 저장소에 저장해서, 파일을 조회할 때 클라우드 저장소에서 조회해서 요청을 처리하도록 한다. 클라우드는 ASW 클라우드를 사용했다.


🐻 AWS 클라우드 파일 업로드 실습하기

  • 먼저 AWS 로그인을 하고, S3 (클라우드 저장소) 를 검색하여 버킷을 생성해준다.
    생성하는 방법은 구글 검색해보면 자세히 나와있다.

  • 그다음 내 계정의 개인 보안에서 루트계정 키를 발급한다. 여기서 주의할점은 루트계정 키가 만약에 공개되면, 해커들이 그것을 통해 악의적 목적으로 사용할 수 있다고 하니 주의해야 한다. 나도 실습을 진행하고 나서 바로 삭제했다.

  • 이렇게 해주면, 이제 스프링 부트의 코드를 일부 수정해주면 된다.

    💻 pom.xml 파일 aws 라이브러리 추가

    		   <dependency>
    			<groupId>org.springframework.cloud</groupId>
    			<artifactId>spring-cloud-starter-aws</artifactId>
    			<version>2.2.6.RELEASE</version>
    		</dependency>

    💻 application.yml 파일 aws 클라우드 연결을 위한 설정 추가

    cloud:
      aws:
        s3:
          bucket: ${bucket}  // 생성한 버킷 이름
        credentials:
           access-key: ${access-key}   // 발급받은 액세스 키
           secret-key: ${secret-key}   // 발급받은 시크릿 키
        region:
          static: ${region}  // 생성한 지역 보통 서울은 ap-northeast-2 이다.
          auto: false
        stack:
          auto: false

    ➡ 위에서 말한 것과 같이 키에 대한 정보가 실수로라도 깃허브 업로드 과정에서
       공개되면 문제가 발생할 수도 있기 때문에 환경변수로 처리해주는 것이 좋다.


    💻 UserService 클래스 수정

    @Service
    public class UserService {
    
     @Value("${project.upload.path}")
     private String uploadPath;
     private final UserRepositorty userRepositorty;
    
     // ✅ application.yml 파일에서 설정한 bucket 값을 사용
     @Value("${cloud.aws.s3.bucket}")
     private String bucket;
     
     // ✅ AmazonS3 인터페이스 의존성 주입
     private AmazonS3 s3;
    
     public UserService(UserRepositorty userRepositorty, AmazonS3 s3) {
         this.userRepositorty = userRepositorty;
         this.s3 = s3;
     }
    
     public String makeFolder(){
         String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
         String folderPath = str.replace("/", File.separator);
         File uploadPathFolder = new File(uploadPath, folderPath);
    
         if(uploadPathFolder.exists() == false) {
             uploadPathFolder.mkdirs();
         }
         return folderPath;
     }
    
     public String saveFile(MultipartFile file) {
     
         String originalName = file.getOriginalFilename();
         String folderPath = makeFolder();
    
         String uuid = UUID.randomUUID().toString();
    
         String saveFileName = folderPath+ File.separator + uuid + "_" + originalName;
         File saveFile = new File(uploadPath, saveFileName);
    
         try {
             
             // 기존 하드디스크 저장 시 사용했던 파일 전송 코드
             // file.transferTo(saveFile);
    
             // ✅ AWS 클라우드에 파일을 전송하기 위한 코드
             ObjectMetadata metadata = new ObjectMetadata();
             metadata.setContentLength(file.getSize());
             metadata.setContentType(file.getContentType());
    
             s3.putObject(bucket, saveFileName.replace(File.separator, "/"), file.getInputStream(), metadata);
    
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
         // 기존 하드디스크 저장 시 사용했던 코드
         //return saveFileName;
         
         // ✅ AWS에 저장한 파일을 불러오기 위한 작업(클라우드 url 자체를 DB에 저장토록 하는 설정)
         return s3.getUrl(bucket, saveFileName.replace(File.separator, "/")).toString();
    
     }
     
     // CREATE
     public PostCreateUserRes create(PostCreateUserReq postCreateUserReq) {
    
         String saveFileName = saveFile(postCreateUserReq.getImage());
    
         User user = User.builder()
                 .email(postCreateUserReq.getEmail())
                 .password(postCreateUserReq.getPassword())
                 .name(postCreateUserReq.getName())
                 .image(saveFileName.replace(File.separator, "/"))
                 .build();
    
         User result = userRepositorty.save(user);
    
         PostCreateUserRes response = PostCreateUserRes.builder()
                 .id(result.getId())
                 .email(result.getEmail())
                 .name(result.getName())
                 .image(result.getImage())
                 .build();
    
         return response;
     }
    }
    

    ➡ 수정한 내용은 체크 표시로 표현했다. 보시다시피 기존 코드와 크게 달라진 것은 없다.

    ➡ AWS 클라우드 저장소에 저장하기 위한 정보들과 파일 전송 코드부분만 변경됬다.


  • AWS 클라우드에 파일 업로드하는 것을 개인적으로 진행중인 쇼핑몰 프로젝트에 적용해봤다.

    1) 서버로 브랜드 등록 요청 보내기


    2) DB에 정상적으로 저장됬는지 확인


    3) AWS 클라우드의 버킷에 파일이 생성됬는지 확인


    ✅ 파일을 불러오기 위한 작업

    1) 파일을 불러올때는 속성의 객체 URL 을 통해 불러온다.


    2) 하지만 눌러보면 접근이 거부됬다고 뜰 것이다.


    ✅ 이를 해결하기 위해서는 접근 권한을 설정해줘야된다.

    1) 버킷정보에서 권한 클릭 ( 속성은 잘못 표시하였다. )

    2) 버킷 정책에서 편집 클릭 후 정책생성기 클릭


    3) 아래 그림처럼 설정 / 이때 Resources 는 arn:aws:s3:::[버킷이름]/* 로 설정


    4) 정책 생성 후 나오는 코드 복사


    5) 복사한 코드를 입력 후 저장한 뒤 다시 객체 URL을 클릭하면 정상적으로 파일이
      불러와지는것을 확인 할 수 있다.


오늘의 느낀점 👀

  • 오늘은 드디어 이미지 파일 업로드하는 것을 실습해봤다. 그동안 사실 매번 실습하면서 이미지에 img_url 이라고만 적어놔서 게속 궁금했었는데 오늘 궁금증이 완벽히 해결됬다.

  • 이제 나는 개인 프로젝트에 브랜드 이미지와 상품 이미지를 등록하는 과정을 끝냈다. 이제 상품 여러개 생성해서 주문하는 기능을 구현해볼 예정이다.

  • 다음주에는 그럼 결제 기능을 적용시켜볼 수 있을 것 같다.

  • 착착 진행되어 가는 수업 커리큘럼과 나의 프로젝트에 뿌듯함을 느끼며 열정을 불태워본다. 🔥🔥

profile
Backend Developer

0개의 댓글