내일배움캠프 프로젝트

BuySell - S3 이미지 업로드

공부처음하는사람 2024. 3. 28. 12:18

S3를 사용해 이미지 업로드 기능을 구현해보려고 한다.

S3 버킷을 채택한 이유!

1. 서비스의 가용성

  • 이 거의 100%이다. (서비스를 제공할 수 있는 상태)

2. 저렴한 비용

  • 사용한 만큼 비용을 낸다.
  • 장기간 보관을 하지만 빈도수가 낮은 파일들 (ex: 법적 보관기간 5년 파일)은 타입 저장방식을 달리해 비용 절감이 가능
    (파일 access 빈도수에 따라 보호수준을 차등할 수 있고 차별화 된 비용을 지불할 수 있다.)
  1. 확장성
  • AWS가 망하지 않는 한 원하는 만큼 서비스를 사용할 수 있다...
  1. 고성능
  • AWS Region을 선택해 가까운 Region에 데이터를 관리함으로써 네트워크 지연시간을 최소화 할 수 있다.
  1. 관련 자료의 양
  • AWS cloud 서비스에 대한 자료의 양이 많기에 문제 해결이 보다 쉬울 수 있다.

서버리스 기반 S3 Presigned URL 적용하기

보통의 파일 업로드 방식은 권한 설정이 필요하기에 서버를 경유해 업로드하게 된다. 저용량의 파일이라면 크게 상관없겠지만,
영상파일같은 대용량의 파일이 업로드 될 경우에 서버를 통해 스토리지에 저장되기 때문에 이중작업이 발생하기에 비효율적일 수 있다.
서버 CPU의 사용량이 커져 서버의 성능저하를 유발할 수도 있다. 또 서버에서 multipart file을 받아 s3 버킷에 업로드하면
서버쪽에도 파일을 갖고 있어야 하므로 리소스 낭비가 발생할 수 있다.

그럼 서버를 거쳐서 업로드를 하는 이유는 무엇이냐..
보안 때문에 그렇다.

S3 버킷을 생성할 때 여러 규칙과 권한이 설정되어있는데, 그 해당 권한을 가진 사용자만 S3에 접근해야한다.
이를 서버가 수행해준다고 보면 될 것 같다.

위의 단점을 개선해서 서버의 리소스를 사용하지 않고 클라이언트가 S3에 바로 접근해 업로드 할 수 있고, 보안까지 보장되는
Presigned Url 방식을 사용하려고 한다.

사용했을 때와 사용하지 않았을 때의 장단점은 마지막에 정리하려고 한다..

Controller

@RestController
@RequestMapping("/images")
class ImageController(
    private val awsS3Service: AwsS3Service
) {

    @GetMapping
    @Throws(IOException::class)
    fun getFile(
        @RequestParam fileName: String
    ): ResponseEntity<String> {
        val url = awsS3Service.getPresignURl(fileName)
        return ResponseEntity(url, HttpStatus.OK)
    }
}

get 요청으로 Presigned Url을 받아오는 메서드

Service

@Service
class AwsS3Service(
    private val s3Client: S3Client,
    private val presigner: S3Presigner
){
    @Value("\${cloud.aws.s3.bucket}")
    lateinit var bucketName: String

    fun getPresignURl(fileName: String?): String? {
        if (fileName.isNullOrBlank()) {
            return null
        }

        val getObjectRequest = GetObjectRequest.builder()
            .bucket(bucketName)
            .key(fileName)
            .build()

        val getObjectPresignRequest = GetObjectPresignRequest.builder()
            .signatureDuration(Duration.ofMinutes(5)) // 5분간 허용
            .getObjectRequest(getObjectRequest)
            .build()

        val presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest)
        val url = presignedGetObjectRequest.url().toString()

        return url
    }
}

fileName 기반으로 S3 버킷에서 프리사인된 URL을 생성하는 코드

Front 이미지 업로드 부분 코드

const handleFileUpload = async (event) => {
  const file = event.target.files[0];
  try {
    const presignedUrlResponse = await axios.get('/images', {
      params: {
        fileName: file.name,
      },
    });
    const presignedUrl = presignedUrlResponse.data;
    await uploadImage(presignedUrl, file);
    console.log('이미지 업로드 완료');
  } catch (error) {
    console.error('이미지 업로드 실패:', error);
  }
};

const uploadImage = async (presignedUrl, file) => {
  try {
    const response = await axios.put(presignedUrl, file, {
      headers: {
        'Content-Type': 'multipart/form-data'
      }
    });
    console.log('이미지 업로드 성공:', response);
  } catch (error) {
    console.error('이미지 업로드 실패:', error);
    throw error;
  }
};

 

 

이렇게 작성 후 테스트를 해본 결과..

 

 

 

GET 요청에 실패한다.

이유를 찾아본 결과, 스프링 시큐리티 config의 설정에서 "images/**"을 추가해줬다.

 

 

그 이후에 또 업로드를 시도해봤다.

 

 

업로드 된 파일을 저장하는 과정에서 문제가 생긴다.

 

PUT요청에 실패했다. s3 버킷에 파일이 저장이 안된다.

 

S3에 저장하는 과정에서 요청에 문제가 있는것으로 보이는데..

 

s3 버킷에 퍼블릭으로 설정, cors도 편집했는데 권한 문제는 아닌 것 같고..

 

Vue의 코드 문제이거나, 백의 코드문제이거나 둘 중 하나 인 것 같다.

 

자바스크립트 코드를 잘 모르니 일단 서버로 넘어가서 코드를 확인했다.

 

@Service
class AwsS3Service(
    private val s3Client: S3Client,
    private val presigner: S3Presigner,
    @Value("\${cloud.aws.s3.bucket}")
    private var bucketName: String
) {
    fun putPreSignUrl(fileName: String?): String? {
        if (fileName.isNullOrBlank()) {
            return null
        }

        val putObjectRequest = PutObjectRequest.builder()
            .bucket(bucketName)
            .key(fileName)
            .build()

        val putObjectPresignRequest = PutObjectPresignRequest.builder()
            .signatureDuration(Duration.ofMinutes(5))
            .putObjectRequest(putObjectRequest)
            .build()

        val presignedPutObjectRequest = presigner.presignPutObject(putObjectPresignRequest)
        val url = presignedPutObjectRequest.url().toString()

        return url
    }
}

 

원인은 getObjectRequest 였다.

 

친절하게 에러코드에 PUT 오류가 있었는데, 정작 내 코드엔 PUT에 대한 코드가 없었다.

 

HTTP에 대한 무지함이 드러나는 부분이다...ㅠㅠ PUT Request로 수정을 해줬다.

 

다시 이미지 업로드를 시도해봤다.

ㅠㅠ

 

드디어 성공했다. 이 업로드 하나때문에 거의 이틀 가까이 시간을 썼다.

 

getObject는 파일을 다운로드 할 때 필요한 메서드이고, 이미지 업로드랑은 아무 상관이 없는것이었다..

 

이미지 업로드는 정상적으로 작동이 되니, 이제 배포 서버에서 작동하게끔 하려면 권한설정을 새로 해야할 것 같다.

게시글 조회시 이미지를 불러오는 방법은 또 뭘까..