Code Review

sung eon·2022년 7월 19일
0

스터디

목록 보기
11/13

| AmazonS3Config.java

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmazonS3Config {
    //AmazonS3Client를 Build 해주는 cofig 생성
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

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

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

    @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();
    }
}

여기서 주의할 점:

@Value 어노테이션은 종류가 두가지다. 하나는 Lombok에서 제공하는 어노테이션, 다른 하나는 스프링 프레임워크이다. 여기서 우리는 무엇을 써야할까?

  • Lombok의 @Value: 한번 생성하면 변경할 수 없는 불변의 객체를 만들기 위한 클래스를 선언할 때 쓰임.

  • Spring Framework의 @Value: properties 파일에 접근해서 정의한 값을 읽어와 Spring 변수에 주입하는 역할을 한다.

| application.properties

cloud.aws.credentials.access-key= 어쩌구 저쩌구

라고 cloud.aws.credentials.access-key 값을 정의를 해뒀다면,

| AmazonS3Config.java

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

${cloud.aws.credentials.access-key} 자리에 눈에 보이지 않지만 어쩌구 저쩌구가 들어가게 된다.

@Value로 S3Service bean을 생성할 때 설정 파일의 S3관련 property 값을 매핑


@Bean: 개발자가 직접 제어가 불가능한 외부 라이브러리 등을 Bean으로 만들려할 때 사용된다.

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

여기서 AmazonS3Client 라이브러리를 Bean으로 등록하기 위해서는 별도로 해당 라이브러리 객체를 반환하는 Method를 만들고, @Bean 어노테이션을 붙여주면 된다.

참고 자료 : @Bean / @Configuration


File Upload 시나리오

  • 클라이언트에게 Multipart/form-data 형식으로 파일을 전송 받는다.
    • 이 때 파일의 data 타입은 MultipartFile
      (MemberUpdateRequestDto에 MutipartFile 선언)
  • S3Service를 통해 파일을 S3에 업로드하고, 파일이 저장된 URL을 DB에 저장한다.
  • 클라이언트가 파일을 요청시 파일이 아닌 파일이 저장된 경로를 반환한다.

따라서 클라이언트로부터 데이터를 받을 떄는 MultipartFile 데이터 타입으로 받지만, 반환할 땐 Sting 타입으로 반환


| S3Service.java

import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.igocst.coco.security.MemberDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
@Component
public class S3Service {
    static { System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true"); }

    private final AmazonS3Client amazonS3Client;

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

    public String upload(MultipartFile multipartFile, String dirName, MemberDetails memberDetails) throws IOException {
        File uploadFile = convert(multipartFile)  // 파일 변환할 수 없으면 에러
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File로 전환을 실패했습니다"));


        try {
            return upload(uploadFile, dirName, memberDetails);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
    private String upload(File uploadFile, String dirName, MemberDetails memberDetails) throws NoSuchAlgorithmException {
        //filename을 받고 -> uploadImageUrl을 반환 받음
        // 난수화를 위해 UUID 사용
//        String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName();
        String fileName = memberDetails.getNickname();
        String cryptogram = encrypt(fileName);// S3에 저장된 파일 이름
        String uploadImageUrl = putS3(uploadFile, dirName+"/"+cryptogram); // s3로 업로드
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    //파일명 암호화를 위한 세팅
    public String encrypt(String text) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(text.getBytes());

        return bytesToHex(md.digest());
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder builder = new StringBuilder();
        for (byte b : bytes) {
            builder.append(String.format("%02x", b));
        }
        return builder.toString();
    }

    // S3로 업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(
                new PutObjectRequest(bucket, fileName, uploadFile)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

//    private void delete(String fileKey) {
//        amazonS3Client.deleteObject(bucket, fileKey);
//    }
//   public String reupload(MultipartFile file, String currentFilePath, String imageKey) {
//        String fileName =
//    }

    // 로컬에 저장된 이미지 지우기
    // 임시로 생성된 new file을 삭제해준다!
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("파일이 삭제되었습니다");
            return;
        } else {
            log.info("파일 삭제에 실패했습니다");
        }
    }

    // 로컬에 파일 업로드 하기
    //multipartFile을 File타입으로 변환해줌 (변환된 파일을 가지고 put을 해주면 됨) -> ?왓..난 이미 put했는데..!
    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
        if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
            try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }
}
  • @Slf4j: 로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음이다.
    인터페이스를 사용하여 로깅을 구현하게 되면 좋은 점은 추후에 필요로 의해 로깅 라이브러리를 변경할 때 코드의 변경 없이 가능하다는 점이다.
    그래서 클래스를 생성할 때마다 항상 로그를 위해 Logger 변수를 선언해야 했는데 어노테이션으로 편하게 사용할 수 있다

  • IOException: RuntimeException을 상속받지 않은 Checked Exception(필수예외처리) 이라 반드시 try/catch or throw로 예외처리를 해야한다고 한다. -> 안하면 컴파일 조차 안됨.

  • NoSuchAlgorithmException: 어느 암호 알고리즘이 요구되었음에도 불구하고, 현재의 환경에서 사용하지 못하는 경우에 예외를 발생시킨다.


궁금해서 찾아보고 적어둔 DTO 에 대하여


참고 자료 : 장호튜터님
수정이 가능하면 뮤터블하다. setter를 쓰면 객체안의 필드값이 변경이 된다. 그렇다면 디버깅할 때 굉장히 힘들어질 수 있다.
생성되어 있을 때랑, 메서드 호출해서 파라미터로 객체를 전달할때 내가 생성했을 때는 null이 안들어가는데 전달할 때는 내가 생성한 객체에 null을 세팅해놔. 그럼 어딘가에서 그걸 참고하다간 exception이 발생할 수 있음. 그럼 Null이 어디에 들어가있는지 찾다보면 디버깅이 상당히 어렵다는 걸 알 수 있다. 그래서 불변으로 해놓으면 생성한 곳만 보면 된다.

디티오까지는 세터로 쓸 수 있지만, 엔티티단에서는 세터를 쓰지 않음. 디티오까지는 뭔가 받아서 엔티티를 만들거나 업데이트할때 파라미터로 사용하는데, 엄격하게하자면 세터를 안쓰면 좋지만 엔티티에는 절대 세터를 넣지 않음.
만약에 여기서 뭔가 comment 엔티티를 보면 콘텐트를 수정하는 경우가 생김. 그럼 똑같은 기능이긴 한데 setcontent라기보단 updateContent (업데이트메소드)를 만들어서 사용하는게 좋다.

빌터패턴은
커멘트.빌더
.id
.email

id값 집어넣고 email 집어넣고... 객체를 생성하는게 빌더패턴을 사용할 수도 있고 스테틱 생성자를 사용하거나 생성자로 생성을 할 수도 있는데, 빌더 패턴경우 코멘트라는 객체에 필드가 20개가 있다, 그럼 이거를 전제 아규먼트를 가지는 생성자를 사용해서 객체를 사용할 수도 있는데 그렇게 되면 필드가 너무 많으니까 첫번째 있는 파라미터가 어느 필드인지 두번째 파라미터는 어느 필드인기 알아보기가 힘들 수 있다.
그래서 빌더하면서 필요한것만 빌더 패턴으로 간단하게 사용할 수 있으므로 우리가 잘하고있다고 하신듯.

왜 그걸 주장하는지는 이유가 있다.
빌터패턴 노! 입장에서는
예를들어서 생성할 때 규칙이 적용이 될 수도 있다. 예를들어 코멘트라는 엔티티를 생성하기위해서 콘텐트라는 필드는 무조건 존재해야한다 라는 규칙이있다면
빌더 패턴 응용이 조금 어렵다.

장호튜터님은 빌더패턴을 주로 사용하심 하지만 취향차이

profile
코베베

0개의 댓글