📝 TIL

[TIL] 6주차 미니 프로젝트ㅣAWS RDS, S3 다중 이미지 업로드

오늘 ONEUL 2022. 12. 19. 23:16

 

AWS RDS 클라우드 DB

AWS RDS(Relational Database Service)란?
애플리케이션에 필요한 빠른 성능, 고가용성, 보안 및 호환성을 제공하는 클라우드 환경 관계형 데이터베이스

기존에는 원활한 테스트를 위해 조원 각자의 로컬에 MySQL을 설치하고 사용했으나,
어느 정도 프로젝트가 완성되면서 데이터베이스를 공용으로 써야 할 필요성이 느껴졌다.
그래서 AWS에서 제공하는 RDS 서비스를 이용하기로 했다.

Spring boot 프로젝트에 RDS 적용하기

  • AWS에 로그인 후, RDS 구매(프리티어)
    - 퍼블릭 액세스 기능을 "예"로 설정해야 로컬과 RDS가 연결 가능
  • RDS 포트 열어주기
    - 보안 그룹 → 인바운드 규칙 편집 → 소스를 위치무관으로 설정
  • 인텔리제이(IntelliJ)에서 확인하기
    - 해당 RDS의 엔드포인트 복사
    - Database 탭에서 Data Source → MySQL 선택
    - Host에는 복사한 엔드포인트, User와 Password 채워서 Test Connection
  • 스프링 부트를 MySQL과 연결하기
    - application.properties에 다음과 같이 설정
spring.datasource.url=jdbc:mysql://나의엔드포인트:3306/초기데이터베이스이름
spring.datasource.username=나의USERNAME
spring.datasource.password=나의패스워드
spring.jpa.hibernate.ddl-auto=update

 

 

 

AWS S3를 이용한 다중 이미지 업로드

단일 이미지까지는 성공했지만,
우리의 서비스는 여러 장의 이미지가 필요하기 때문에 다중 이미지 업로드에 도전했다.
다중 이미지 업로드를 구현하면서 내가 신경 쓴  포인트는 다음과 같다.

  • 파일 이름은 UUID로 변경
    - 중복된 파일이름을 막기 위해
  • S3뿐만 아니라 DB에도 저장
    - 더 원활한 관리를 위해 파일 이름, 경로, 크기 등을 저장
  • 상품 정보와 이미지를 동시에 받아서 저장
    - 컨트롤러에서 ReqeustPart로 처리
  • 컨트롤러에서의 반환값은 ResponseEntity로 통일

 

AwsS3Config

@Configuration
public class AwsS3Config {

    @Value("${cloud.aws.credentials.accessKey}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secretKey}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}

 

AwsS3Service

@Slf4j
@Service
@RequiredArgsConstructor
public class AwsS3Service {

    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    private final AmazonS3 amazonS3;
    private final ImageFileRepository imageFileRepository;

    // 이미지 파일 s3에 저장 후 Dto 리스트에 담아서 반환
    public List<ImageFileRequestDto> uploadFile(List<MultipartFile> multipartFile, String dirName) {
        List<ImageFileRequestDto> reponseDto = new ArrayList<>();

        // forEach 구문을 통해 multipartFile로 넘어온 파일들 하나씩 reponseDto에 추가
        multipartFile.forEach(file -> {
            String fileName = createFileName(file.getOriginalFilename(), dirName);
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(file.getSize());
            objectMetadata.setContentType(file.getContentType());

            try(InputStream inputStream = file.getInputStream()) {
                amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
                log.info("a3 업로드 성공");
            } catch(IOException e) {
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
            }

            // file에 관한 내용을 Dto로 변환 후 list에 담아 return
            reponseDto.add(new ImageFileRequestDto(file.getOriginalFilename(), fileName, file.getSize()));



        });

        return reponseDto;
    }

    public void deleteFile(String fileName, String dirName) {
        amazonS3.deleteObject(new DeleteObjectRequest(bucket, dirName + "/" + fileName));
    }

    // 먼저 파일 업로드 시, 파일명을 난수화하기 위해 UUID를 붙여준다.
    private String createFileName(String fileName, String dirName) {
        return dirName + "/" + UUID.randomUUID().toString().concat(getFileExtension(fileName));
    }

    // file 형식이 잘못된 경우를 확인하기 위해 만들어진 로직이며, 파일 타입과 상관없이 업로드할 수 있게 하기 위해 .의 존재 유무만 판단하였다.
    private String getFileExtension(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
        }
    }
}

 

ProductService

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    private final ImageFileRepository imageFileRepository;


    //게시글 생성하기
    public ResponseDto addProduct(ProductRequestDto productRequestDto, User user, List<ImageFileRequestDto> imageRequestDtoList){
        Product product = productRepository.save(new Product(productRequestDto,user));         // 저장소에 입력 받은 데이터 저장 // save()때문에 @Transactional 을 사용하지 않아도 됨
        
        // 전달받은 이미지파일 Dto 리스트를 DB에 저장
        for (ImageFileRequestDto imageFileRequestDto : imageRequestDtoList) {
            ImageFile imageFile = imageFileRepository.save(new ImageFile(imageFileRequestDto, product));
        }
        return new ResponseDto(HttpStatus.OK.value(), "게시글 업로드 성공");
    }

 

ProductController

@Controller
@RequiredArgsConstructor
@RequestMapping("/api/products")
public class ProductController {
    private final ProductService productService;
    private final AwsS3Service awsS3Service;
    private static String dirName = "product-img";

    //게시글 작성 + S3 이미지 업로드(+ DB저장)
    @PostMapping
    public ResponseEntity<ResponseDto> addProduct(
            @RequestPart(value = "key") ProductRequestDto productRequestDto,
            @RequestPart(value = "multipartFile") List<MultipartFile> multipartFile,
            @AuthenticationPrincipal UserDetailsImpl userDetails){
        
        // AwsS3Service의 uploadFile 메소드를 호출하여 S3에 저장하고,
        // 그 반환값인 이미지파일 Dto 리스트를 ProductService의 addProduct 메소드의 매개변수로 전달하여
        // 유저가 작성한 내용과 함께 이미지 파일 DB에 저장
        return ResponseEntity.ok(productService.addProduct(productRequestDto,userDetails.getUser(), awsS3Service.uploadFile(multipartFile, dirName)));
    }

 

📚 참고자료

 

[SpringBoot] SpringBoot를 이용한 AWS S3에 여러 파일 업로드 및 삭제 구현하기

들어가기 전에 원래는 AWS API Gateway + AWS lambda + AWS S3 방식으로 이미지 업로드 및 삭제를 구현하고자 했습니다. 이때, 일반적으로 javascript나 python을 사용하는 것으로 보았는데 해당 언어로 구현하

earth-95.tistory.com

 

 

 

yml 파일 분리

application 설정 파일들이 많아지면서 key-value 형식을 사용하는 properties 형식보다
계층적 구조를 가진 yml 형식을 사용하는 것이 더 효율적이라는 판단이 들었다.

설정 파일을 분리해서 민감한 key가 들어있는 파일은 gitignore 설정 후 조원들과 따로 공유하여
aws 과금사태가 일어나지 않도록(...) 대비하였다.

 

application.yml

spring:
  profiles:
    active: rds
    include:
      - aws
      - credentials

  servlet:
    multipart:
      enabled: true
      max-file-size: 20MB
      max-request-size: 20MB

 

application-aws.yml

cloud:
  aws:
    s3:
      bucket: S3 버킷이름
    region:
      static: S3 지역
    stack:
      auto: false

 

application-credentials.yml

cloud:
  aws:
    credentials:
      accessKey: S3 엑세스 키
      secretKey: S3 시크릿 키

 

application-rds.yml

server:
  port: 8080

spring:
  config:
    activate:
      on-profile: rds
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://나의엔드포인트:3306/초기데이터베이스이름?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
    username: 나의 USERNAME
    password: 나의 패스워드
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true

 

 

 

멘토링 질문사항

프로젝트를 진행하면서 궁금했던 부분들을 기술 매니저님께 여쭤보기 위해
조원들과 미리 질문을 작성하여 중간 멘토링 시간을 가졌다.

  1. 주석을 달 때 한 줄 한 줄 기본적인 코드 모두 주석을 달아야 하는지 아니면 이해하기 어려운 부분만 주석을 달아도 되는지?
    1. 논란의 여지가 있음 코드는 수정했는데 주석은 수정하지 않았다면?ㄷㄷ 유지보수 측면에서 권장하지 않음 (클린코드 책 추천)
    2. 그러나 현재는 공부하는 단계. 한 줄 한 줄 정리하는 것도 해볼 만함
    3. 이후에는 다른 사람의 이해를 위한 주석
    4. 사실 주석 없이도 이해가 가는 코드가 좋은 코드!
  2. 댓글과 대댓글 entity를 하나로 통합해서 사용하는 게 효율적 일지 따로 분리해서 사용하는 게 효율적 일지?
    1. entity는 설계하기 나름 답은 없다
    2. 굳이 분리할 필요성을 못 느낀다면 entity 수를 줄이는 게 낫다
  3. success code도 error code처럼 enum으로 분리해서 반환하는 게 좋은 지?
    1. 에러 코드를 enum으로 나눈 이유는? 에러코드가 다양하기 때문에 한 번에 관리하기 위해서
    2. HttpStatus가 분리돼서 나갈 경우가 많은가? 그렇지 않다
    3. msg 자체도 보낼 필요성이 있는지 생각해 볼 것
  4. dept라는 건? 가령 대댓글이라면 products/3/comments/2/reply/1 이런 식으로 가야 한다는 의미인지? RESTful API에 따르면 Document는 단수, Collection은 복수로 표현해야 한다고 하는데 어떤 부분이 Document이고, Collection인지 어떻게 적용시켜야 할지 궁금하다.
    1. 게시글은 여러 개가 있을 수 있고, 댓글도 여러 개가 있을 수 있고, 그렇다면 대댓글은 여러 개?
    2. 같은 계층에서 여러 개라면 replies가 맞을지도! 정답은 없다! 구현하기 나름~
  5. 이미지 업로드 방식에서 흐름을 어떻게 가져가는 게 좋을지?
    1. s3에 먼저 이미지를 업로드하고(+DB저장) 받아온 파일 경로를 product 엔티티에 저장
      이미지를 DB에 저장할 때 product는? product create를 하지 않는다면?
    2. product create를 요청할 때 파일도 같이 받아서 한 번에 저장
      form-data형식으로 json과 file을 같이 받는 게 옳은 방향일지? 그렇다면 product의 service단에서 한 번에 처리해주는 게 맞는 건지?
      1. reuqest param으로 list로 묶어서? json과 동시에? 각각 request param으로 받는 방법도 있음
      2. 서비스에서 서비스를 불러도 괜찮다!
  6. 게시글 작성이나, 수정 요청에도 성공 여부와 메시지뿐만 아니라 데이터를 함께 반환하는 이유가 있는지?
    1. 반드시 반환할 필요는 없음
    2. 프론트 상황에 따라 다르게 적용

 

너무 유용했던 시간!
따로 프로젝트 진행 관련 조언도 해주셨다.

  • api 수정사항은 바로바로 수정하여 프론트와 공유!
  • service단이 커지므로 메소드 분리를 고려할 것 → 코드 분리도 많이 해보기! (네이밍, 접근제어자)
  • 조원들과 코드 리뷰 해보기!

 

 

 

오늘의 나는

오늘 프론트 조원 한 분이 항해99를 하차하셨다.
프로젝트 도중에 하차해버린 조원 분이 야속하다가도,
이 압박감이 얼마나 힘들었을지 알기에 이해할 수밖에 없었다.

어떻든 간에 프로젝트는 진행되어야 한다.
멘탈 다 잡고, 고군분투하고 있는 조원들의 노력이 헛되지 않게
내가 할 수 있는 일들을 기꺼이 해내자.