Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(S3PreSignedUrl): S3 파일 업로드 방식을 PreSigned URL로 구현 #159

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from

Conversation

qogustj
Copy link
Contributor

@qogustj qogustj commented Nov 28, 2024

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📝 작업 내용

이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능)

  • S3 파일 업로드 방식을 PreSigned URL 방식으로 전환하여 서버 부하 감소
  • PreSigned URL 생성 및 폴더 경로별 URL 생성 기능 구현
  • 파일 업로드 프로세스를 2단계로 분리하여 업로드 완료 확인 절차 추가
  • S3PreSignedUrlUtils 클래스 및 관련 DTO 클래스 신규 개발

기존 프로세스:

  1. 클라이언트가 파일과 함께 서버에 업로드 요청
  2. 서버가 파일을 임시 저장
  3. 서버가 임시 저장된 파일을 S3에 업로드
  4. S3 업로드 완료 후 URL 반환
  5. 서버가 임시 파일 삭제

새로운 PreSigned URL 프로세스:

  1. 클라이언트가 서버에 PreSigned URL 요청
  2. 서버가 S3 PreSigned URL 생성하여 반환
  3. 클라이언트가 PreSigned URL을 통해 S3에 직접 파일 업로드
  4. 클라이언트가 업로드 완료 후 서버에 확인 요청
  5. 서버가 S3 객체 존재 여부 확인 후 완료 처리

✏️ 관련 이슈(선택 사항)


코드 리뷰 받고 싶은 부분

  • PreSigned URL 생성 로직의 보안성 검토
  • 파일 업로드 완료 확인 프로세스의 신뢰성
  • 새로운 DTO 클래스들의 구조 적합성

📝 작업 내용 (추가)

  • 비동기 처리를 통한 파일 업로드 성능 개선

    • CompletableFuture를 활용하여 다수 파일 동시 검증 구현
    • 10개 파일 기준 검증 시간 약 80% 단축 (1000ms → 100-200ms)
  • 상세한 예외 처리 체계 구현

    • S3 관련 예외를 체계적으로 분류하는 S3ErrorCode enum 클래스 개발
    • 파일 not found, 유효하지 않은 URL, 삭제 실패 등 세분화된 예외 처리
  • 파일 컨텐츠 타입 검증 기능 추가

    • ContentTypeValidator를 통한 업로드 파일 타입 사전 검증
    • S3 업로드 시 Content-Type 파라미터 처리

🔍 기술적 구현 상세

  1. S3PreSignedUrlUtils 클래스 주요 기능:

    • Pre-signed URL 생성 (만료 시간 설정 가능)
    • 파일 존재 여부 비동기 확인
    • S3 버킷 URL 생성
  2. 파일 업로드 확인 프로세스 개선:

    • 비동기 병렬 처리를 통한 다수 파일 동시 검증
    • CompletableFuture.allOf를 활용한 검증 완료 대기
    • 검증 실패 시 상세한 예외 메시지 제공

🎯 기대 효과

  1. 시스템 성능 개선:

    • 서버 리소스 사용 효율화
    • 대용량/다수 파일 처리 시 성능 향상
    • 병렬 처리를 통한 응답 시간 단축
  2. 시스템 안정성 강화:

    • 체계적인 예외 처리
    • 파일 업로드 완료 검증 절차 추가
    • 컨텐츠 타입 사전 검증

💡 후속 계획

  • 레이트 리미팅 도입 검토
  • 파일 업로드 실패 시 재시도 메커니즘 구현
  • 업로드 진행 상태 모니터링 기능 추가

코드 리뷰 받고 싶은 부분 (추가)

  • 비동기 처리 방식의 적절성 검토
  • 예외 처리 체계의 충분성
  • 파일 검증 timeout 설정의 적절성
  • CompletableFuture 활용 패턴의 개선 여지

S3 직접 업로드 방식을 PreSigned URL 방식으로 변경하여 서버 부하 감소
- PreSigned URL 생성 기능 구현
- 폴더 경로별 URL 생성 기능 구현
- 파일 업로드 완료 확인 프로세스 추가

변경사항:
- S3PreSignedUrlUtils 유틸리티 클래스 생성
- PreSigned URL 생성 로직 구현
- PostFileService 로직 변경
- 파일 업로드 프로세스 2단계로 분리
- Request/Response DTO 추가

관련 컴포넌트:
- S3PreSignedUrlUtils
- PostFileService
- FileUploadRequest
- FileRequest
- PreSignedUrlResponse
- FileUploadConfirmRequest

BREAKING CHANGES:
- 파일 업로드 API 인터페이스 변경
- S3 직접 업로드 로직 제거
- 클라이언트 측 파일 업로드 로직 수정 필요
@qogustj qogustj added the feature New feature or request label Nov 28, 2024
@qogustj qogustj requested a review from chahyunsoo November 28, 2024 02:28
@qogustj qogustj self-assigned this Nov 28, 2024
List<Map<String, String>> imageUrls = s3PreSignedUrlUtils.generatePreSignedUrlWithPath(
userId,
boardCode,
images.stream().map(FileRequest::fileName).collect(Collectors.toList()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 어차피 우리 자바17이어서 바로 toList() 어때여

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 stream.toList()로 하면 수정 불가능한 리스트를 반환하고
Collectors.toList()로 하면 수정 가능한 리스트를 반환한다고 하네요!

수정이 필요 없다면 toList()를 사용하는게 좋을 것 같습니다!

private String bucket;

public Map<String, String> generatePreSignedUrl(String fileName, String contentType) {
// URL 만료 시간 설정 (예: 5분)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html?utm_source=chatgpt.com

여기보면 최대 7일로 두긴 하는데 5분은 일단 정해둔건가욥???

.map(PostFile::getUrl)
.orElse(null);

AtomicInteger index = new AtomicInteger(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thread safe!!👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicInteger는 멀티스레드 환경을 위한 것인데, 여기서는 불필요한 오버헤드일 수 있다는 생각이 들어서,
IntStream으로 처리했습니다.

"0부터 리스트 크기만큼 순회하면서 매핑한다"는 의도로 작성했습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicInteger가 멀티스레드 환경에서 유용하다는 점에는 동의합니다. 하지만, 현재 코드에서는 단일 스레드 환경에서만 동작하므로 오버헤드가 발생할 수 있는 점을 고려해, 말씀하신 IntStream 방식으로 처리하는 것이 저도 더 적합하다고 생각이 드네요..

// URL 만료 시간 설정 (예: 5분)
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 5;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 만료시간을 상수나 application.yml 로 관리하는건 어떻게 생각해?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만료시간 5분은 딱 적당하다고 생각!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yml로 관리하는게 좋아보이네요!

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

public Map<String, String> generatePreSignedUrl(String fileName, String contentType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 클라이언트로 부터 content-type을 받는데, 이걸

// Content-Type 검증
List<String> allowedContentTypes = List.of("image/png", "image/jpeg", "application/pdf");
  if (!allowedContentTypes.contains(contentType)) {
        throw new IllegalArgumentException("Unsupported Content-Type");
  }
}

뭐 대충 이런식으로 contentType을 조금 지정하는건 어떻게 생각해, 뭐 저것도 상수로 빼거나 해서
의도치 않은 Content-Type이 전달될 가능성을 방지하기 위해서??

Copy link
Contributor Author

@qogustj qogustj Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentType를 지정하는 방법 차용해서 enum과 Validator를 조합하는 방식으로 수정했습니다!

아래와 같은 장점이 있을 것 같아요.

  1. 타입 안전성(enum)과 유연한 검증 로직(Validator) 모두 확보
  2. 커스텀 예외를 통한 더 명확한 에러 처리
  3. 검증 로직 재사용 가능
  4. 새로운 타입 추가가 용이하면서도 안전
  5. 테스트 작성이 쉬움

import java.util.Map;

public record PreSignedUrlResponse(
List<Map<String, String>> preSignedUrls,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 Map<String,String> 대신 명확한 의미를 가진 record 사용하면 타입 안정성과 가독성을 향상시킬 수 있을 것 같은데 어떻게 생각해???

public record PreSignedUrlDetail(String preSignedUrl, String fileUrl) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

명시적 도메인 객체 생성해서 관리하도록 수정하는게 좋을 것 같네요!!


@Transactional
public PostFileListResponse confirmUpload(Long userId, List<FileUploadConfirmRequest> confirmRequests) {
List<PostFile> postFiles = confirmRequests.stream()
Copy link
Contributor

@chahyunsoo chahyunsoo Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S3PreSignedUrlUtils.java 클래스에

// S3 객체 존재 여부 확인 메서드
    public boolean doesObjectExist(String fileUrl) {
        String fileKey = extractKeyFromUrl(fileUrl);
        return amazonS3.doesObjectExist(bucket, fileKey);
    }

    // S3 URL에서 객체 키를 추출하는 유틸리티 메서드
    private String extractKeyFromUrl(String fileUrl) {
        // fileUrl에서 S3 객체 키 추출 (버킷 이름 이후의 경로)
        return fileUrl.substring(fileUrl.indexOf(bucket) + bucket.length() + 1);
    }

이 메소드를 추가해서 ImageService의 confirmUpload 메소드에서 builder 들어가기전에, 한번 S3 객체 존재 여부를 확인하는건 어떻게 생각하시나여?

  • S3 객체 존재 여부 확인 메서드 추가

    • doesObjectExist 메서드를 통해 S3 객체가 실제로 존재하는지 확인하고
    • fileUrl에서 S3 키를 추출하는 extractKeyFromUrl 메서드로 URL 기반 키 처리를 안정적으로 수행하는 거!
  • ImageService 클래스에서 검증 적용

    • confirmUpload 메서드에서 S3 객체가 존재하지 않을 경우 예외 발생
    • 클라이언트가 잘못된 URL을 전달하거나 파일이 삭제된 경우를 방지

Copy link
Contributor Author

@qogustj qogustj Nov 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public boolean doesObjectExist(String fileUrl) {
    String fileKey = extractKeyFromUrl(fileUrl);
    return amazonS3.doesObjectExist(bucket, fileKey);
}

이 방식에서 더 나아가서

존재 여부 확인 메서드 같은 경우에는 네크워크 요청이기에 다수의 파일일때 블로킹으로 인한 자원 낭비가 있기 때문에,
현재 시스템이 작더라도 확장성을 고려했을때 비동기처리로 구현해놓으면 훨씬 효율적이라고 생각합니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그렇네요! doesObjectExist 메서드는 S3 파일 존재 여부를 확인하는 간단한 방식으로 구현되어 있지만, 코멘트 주신 대로 다수의 파일을 처리할 때는 네트워크 요청이 블로킹 방식으로 동작하여 비효율적일 수 있을 것 같네여!!;;

말씀하신 시스템이 작더라도 확장성과 효율성을 고려해 비동기 방식(CompletableFuture)으로 리팩토링하여 네트워크 요청의 병렬 처리를 지원하는 것은 좋은 방식인 것 같습니다!!

Java 17 스타일 및 함수형 프로그래밍 적용으로 코드 품질 향상
- Collectors.toList()를 Stream.toList()로 변경
- null & empty 체크를 CollectionUtils로 개선
- AtomicInteger를 IntStream으로 대체하여 함수형 스타일 적용

변경사항:
- Stream 종단 연산자 toList() 적용
- Spring CollectionUtils 도입
- IntStream.range() 활용한 인덱스 처리

관련 컴포넌트:
- ImageService
- PostFileResponse
- FileRequest

BREAKING CHANGES:
- 반환되는 List가 불변 객체로 변경됨
PreSigned URL 만료시간을 설정 파일로 분리하고 Content-Type 검증 로직 추가
- 만료 시간 설정 외부화로 환경별 유연한 설정 가능
- Content-Type enum 기반 검증으로 타입 안전성 확보

변경사항:
- cloud.aws.s3.presigned-url.expiration 설정 추가
- AllowedContentType enum 추가
- ContentTypeValidator 컴포넌트 구현
- UnsupportedContentTypeException 추가
- S3PreSignedUrlUtils에 ContentTypeValidator 주입

관련 컴포넌트:
- S3PreSignedUrlUtils
- ContentTypeValidator
- AllowedContentType
- UnsupportedContentTypeException
- application.yml

BREAKING CHANGES:
- application.yml에 presigned-url.expiration 설정 필요
- Content-Type이 enum에 정의된 타입과 일치하지 않을 경우 예외 발생
Map 대신 명시적 도메인 객체를 사용하여 타입 안전성과 가독성 향상
- PreSignedUrlInfo 도메인 객체 기반으로 로직 재구성
- URL 만료 시간 정보 추가
- 함수형 스타일 적용

변경사항:
- Map<String, String> → PreSignedUrlInfo 변경
- for 루프를 IntStream으로 개선
- URL 만료 시간 관리 추가
- originalFileName 처리 로직 통합

관련 컴포넌트:
- ImageService
- S3PreSignedUrlUtils
- PreSignedUrlResponse
- PreSignedUrlInfo

BREAKING CHANGES:
- PreSignedUrlResponse 구조 변경
- S3PreSignedUrlUtils 반환 타입 변경
S3 파일 검증 성능 개선을 위한 비동기 처리 구현
- CompletableFuture를 활용한 비동기 파일 검증
- S3 관련 예외 처리 코드 추가

변경사항:
- doesObjectExistAsync 메서드 추가
- S3 에러코드 추가 (S3_004, S3_005)
- confirmUpload 메서드에 비동기 검증 로직 적용

관련 컴포넌트:
- S3PreSignedUrlUtils
- ImageService
- ErrorCode (S3 관련 에러 코드 추가)

성능 개선:
- 다수 파일 검증 시 병렬 처리로 성능 향상
- S3 API 호출 시 블로킹 최소화

BREAKING CHANGES:
- 파일 존재하지 않을 경우 FILE_NOT_FOUND 예외 발생
- 파일 검증 실패 시 S3_VALIDATION_ERROR 예외 발생
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] : 임시url을 이용한 이미지 업로드 구현
2 participants