본문 바로가기
BackEnd/Spring-boot

[SpringBoot-JPA] 게시판 CRUD 이후 파일 첨부 - AWS S3

by Chaedie 2023. 7. 14.
728x90

게시판 CRUD

  • TEXT 만으로 간단하게 게시판을 구현해보았다.
  • Data JPA를 사용했기에 List Page에서의 Pagination까지 간단하게 완료할 수 있었다.
  • 여기에 AWS S3 이용해서 File Upload를 구현하고자 했다.
  • 한 파일첨부는 쉽다. post 테이블에 url만 넣어주면되니까.
  • 근데 여러 파일 첨부는 진짜 어려웠다. 데이터 중심적 설계로 접근하면 어렵지 않다.
    • 단순히 upload_files 테이블을 만들고 FK관계로 여러 file들을 insert하는데 이 때 post_id만 잘 넣어주면된다.
    • 근데 이걸 JPA로 == 객체 지향 중심적 설계 관점으로 접근하려다 보니 너무 어려웠다.
    • 결국 김영한님의 `<자바 ORM 표준 JPA 프로그래밍>` 책을 절반 가량 읽어서 연관관계에 대한 개념을 잡은 뒤, 다른 사람들의 코드를 참고해가며 겨우겨우 성공했다...
  • 이번 파일 첨부 개발 건을 통해 겸손에 대해 제대로 배운 것 같다. 대단한 실력이 없다는건 알고 있었지만, 이력서에 적을 만한 내용이 없는거지 개발 자체가 안 맞거나 못하는 사람은 아니라고 생각했는데, 간단해 보이는 이 작업을 이틀이나 붙잡고 있었다는게 조금 민망하기도 하고.. 그렇더라.. 겸손을 제대로 배웠고, 진짜 실력을 많이 키우고 싶다는 생각을 하는 중이다...!

AWS S3 파일 업로드

  • pom.xml 세팅
<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-context</artifactId>
    <version>2.3.5</version>
    </dependency>
  • application.properties 세팅
# AWS Account Credentials
cloud.aws.credentials.accessKey=[액세스키]
cloud.aws.credentials.secretKey=[시크릿키]

# AWS S3 Service bucket
cloud.aws.s3.bucket=[버킷이름]
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false

# file upload max size
spring.servlet.multipart.max-file-size=20MB
spring.servlet.multipart.max-request-size=20MB

# AWS S3 Bucket URL
cloud.aws.s3.bucket.url=https://s3.ap-northeast-2.amazonaws.com/[버킷이름]
  • config

@Configuration
public class AWSConfig {

    @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 BasicAWSCredentials basicAWSCredentials() {
        return new BasicAWSCredentials(accessKey, secretKey);
    }

    @Bean
    public AmazonS3Client amazonS3Client(AWSCredentials awsCredentials) {
        AmazonS3Client amazonS3Client = new AmazonS3Client(awsCredentials);
        amazonS3Client.setRegion(Region.getRegion(Regions.fromName(region)));
        return amazonS3Client;
    }
}
  • Service Code - (다 적고 보니 S3 업로드 로직은 Service 가 아니라 Utils로 가도 될것같다..?)

@Service
public class AwsS3ServiceImpl {
    private final AmazonS3Client amazonS3Client;

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

    @Autowired
    public AwsS3ServiceImpl(AmazonS3Client amazonS3Client) {
        this.amazonS3Client = amazonS3Client;
    }

    private String upload(InputStream inputStream, String uploadKey) {
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, uploadKey, inputStream, new ObjectMetadata());
        PutObjectResult putObjectResult = amazonS3Client.putObject(putObjectRequest);
        IOUtils.closeQuietly(inputStream, null);
        return amazonS3Client.getUrl(bucket, uploadKey).toString();
    }

    public List<String> upload(MultipartFile[] multipartFiles) {
        List<String> storeUrlList = new ArrayList<>();
        Arrays.stream(multipartFiles)
                .filter(multipartFile -> !StringUtils.isEmpty(multipartFile.getOriginalFilename()))
                .forEach(multipartFile -> {
                    try {
                        storeUrlList.add(upload(multipartFile.getInputStream(), createStoreFileName(multipartFile.getOriginalFilename())));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
        return storeUrlList;
    }

    public ResponseEntity<byte[]> download(String key) throws IOException {
        GetObjectRequest getObjectRequest = new GetObjectRequest(bucket, key);
        S3Object s3Object = amazonS3Client.getObject(getObjectRequest);
        S3ObjectInputStream objectInputStream = s3Object.getObjectContent();
        byte[] bytes = IOUtils.toByteArray(objectInputStream);
        String fileName = URLEncoder.encode(key, "UTF-8").replaceAll("\\+", "%20");
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        httpHeaders.setContentLength(bytes.length);
        httpHeaders.setContentDispositionFormData("attachment", fileName);
        return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
    }

    public List<S3ObjectSummary> list() {
        ObjectListing objectListing = amazonS3Client.listObjects(new ListObjectsRequest().withBucketName(bucket));
        List<S3ObjectSummary> s3ObjectSummaries = objectListing.getObjectSummaries();
        return s3ObjectSummaries;
    }

    private String createStoreFileName(String originalFileName) {
        String ext = extractExt(originalFileName);
        String uuid = UUID.randomUUID().toString();
        return uuid + "." + ext;
    }

    private String extractExt(String originalFileName) {
        int pos = originalFileName.lastIndexOf(".");
        return originalFileName.substring(pos + 1);
    }
}
  • PostService Code
@Override
    public PostResponseDTO insertPost(PostRequestDTO postRequestDTO, MultipartFile[] multipartFiles) {
        /**
         * 1. Post 엔티티 생성
         */
        Post post = Post.builder()
                .postTitle(postRequestDTO.getPostTitle())
                .postContent(postRequestDTO.getPostContent())
                .userId(postRequestDTO.getUserId())
                .uploadFiles(new ArrayList<>())
                .build();

        /**
         * 2. S3 Upload
         * 2.1. UploadFile 엔티티 생성
         * 2.2. post <-> uploadFiles 양방향 연관관계 매핑
         */
        awsS3Service.upload(multipartFiles).stream()
                .forEach(url -> {
                    UploadFile uploadFile = UploadFile.builder()
                            .fileUrl(url)
                            .build();
                    post.getUploadFiles().add(uploadFile);
                    uploadFile.setPost(post);
                });

        return new PostResponseDTO(postRepository.save(post));
    }

진짜 이 부분이 잘 안되서 고생을 엄청했다. 이 부분을 잘 이해하려면 일단 김영한님의 JPA 책을 읽고 시작하는게 맞는 순서라고 본다. 그 책을 읽고 나름대로 연관관계에 대해 이해도가 생겼다고 생각했음에도 이 부분을 너무 해멨다...

댓글