티스토리 뷰

 

프로젝트를 진행하던 중 presigned url을 적용해서 이미지 저장을 하자고 결정하였다.

그 이유는 목차에 따라 설명하겠다.

 

📖 목차

1. presigned url이 무엇인가
2. presigned url 발급
3. presigned url 저장

 

 


1. presigned url이 무엇인가

 

 

사용자가 앱 사용 중 이미지를 저장하고 싶은 경우가 있을 것이다.

그럼 우린 그 이미지를 어디에 저장하는게 좋을까?

 

✏️ 어디에 이미지를 저장할 것인가

 

1️⃣ Server

앱이 동작하는 서버에 이미지를 저장하는 방법이다.

이미지를 서버에 저장하기 때문에, 다른 외부 스토리지와 통신할 필요가 없어서 네트워크 대역폭을 절약할 수 있다.

 

  • 서버에 큰 용량을 가지는 이미지를 저장하려고 하면, 사진을 저장하기 위한 대역폭 또한 제공해야하기 때문에 부하가 심해질 수 있다.
  • 큰 용량이 아닌 작은 용량의 이미지를 저장하려고 하면, 앱의 자원을 쉽게 관리할 수 있어 나쁘지 않을 수 있다.

 

2️⃣ Object Storage

앱이 동작하는 서버 외부의 독립적인 스토리지 시스템에 이미지를 저장하는 방법이다.

대표적인 예로 Amazon S3와 같은 클라우드 스토리지가 있으며,

이미지를 서버에 직접 저장하지 않고 스토리지 서비스에 저장하고 해당 URL을 사용하여 이미지를 로드한다.

 

 

  • 대용량 파일을 저장하거나, 저장 용량이 급격히 증가하더라도 쉽게 대응할 수 있다.
  • 클라우드 서비스 제공 업체는 보안과 안정성을 제공하기 때문에, 데이터 손실이나 서버 장애에 대한 우려를 줄일 수 있다.
  • 서버와 독립적으로 동작하기 때문에, 서버의 상태와 무관하게 언제든지 이미지에 접근할 수 있다.

 


 

서버에 이미지를 저장하는 방법과 Object Storage를 사용하는 방법은 각각의 장단점이 있지만,

앱의 규모, 예상 트래픽, 비용등을 고려하여 적절한 방법을 선택하는 것이 중요하다.

 

현재 진행중인 프로젝트에서는 서버의 부하를 줄이기 위해 Object Storage에 저장하는 방법을 선택하였다.

 


 

이제 presigned url이 무엇인지 알아보자.

 

📌 presigned url

 

 

presigned url은 간단하게 설명하면 Object Storage에 이미지를 저장할 수 있는 허용된 경로이다.

 

이게 무슨 말이냐면, Object Storage에 누구나 접근해서 수정하면 안되기 때문에 이미지를 저장하기 위해서는 권한 인증이 필요하다.

이를 위해 서버에서 Object Storage에 접근 권한을 얻은 후, Client에게 해당 경로를 반환하는데, 이 경로가 presigned url이다.

 

하지만 presigned url은 접근이 허용된 경로이기 때문에 무한정으로 열어두면 보안상 위험할 수 있다.

따라서, presigned url을 발급받을 때 적절한 권한 제어와 만료시간을 설정해야 한다.

 

 

일단 여기까지 "presigned url이 Object Storage에 저장하기 위한 경로구나" 까지는 이해할 수 있을 것이다.

 

 


2. presigned url 발급

 

 

presigned url의 발급 과정은 아래와 같다.

 

1. Server에 presigned url 요청

2. API에서 Object Storage에 presigned url 요청(PUT 권한)

3. Object Storage에서 Server에 presigned url 전달

4. Server에서 Client 측으로 presigned url 반환

5. 유효시간 내에 Client가 Object Storage으로 사진 업로드

 

이 과정을 통해 presigned url 발급은 후, Client는 Server를 거치지 않고 Object Storage에 직접 사진을 업로드 할 수 있다. 

 

📌 presigned url 발급

 

 

Image Picker에서 사진을 선택하고 난 후 presigned url 발급 API 요청을 보낸다고 가정해보자.

 

presigned url 발급 과정은 쉽다.

쿼리에 type(이미지 구분), ext(확장자)데이터만 넣고 GET 요청을 보낸 후 받은 presigned url을 확인하면 된다.

 

// MARK: - ImageType

enum ImageType: String {
    case profile = "PROFILE"
    case feed = "FEED"
    case chatroomProfile = "CHATROOM_PROFILE"
    case caht = "CHAT"
    case chatProfile = "CHAT_PROFILE"
}

// MARK: - Ext

enum Ext: String {
    case jpg = "jpg"
    case png = "png"
    case jpeg = "jpeg"
}

 

Image Picker에서 사진을 선택한 경우 presigned url 발급 요청을 보낸다.

그리고 응답이 돌아온 경우 그 url을 presignedUrl 변수에 저장한다.

 

/// presigned url 발급
func generatePresignedUrlApi(completion: @escaping (Bool) -> Void) {
    let generatePresigendUrlRequestDto = GeneratePresigendUrlRequestDto(type: ImageType.profile.rawValue, ext: Ext.jpeg
        .rawValue)

    ObjectStorageAlamofire.shared.generatePresignedUrl(generatePresigendUrlRequestDto) { result in
        switch result {
        case let .success(data):
            if let responseData = data {
                do {
                    let response = try JSONDecoder().decode(GeneratePresignedUrlResponseDto.self, from: responseData)
                    self.presignedUrl = response.presignedUrl//받은 url 저장
                    Log.debug("presigned_url 발급 성공 \(self.presignedUrl)")
                    completion(true)
                } catch {
                    Log.fault("Error parsing response JSON: \(error)")
                    completion(false)
                }
            }
        case let .failure(error):
            if let StatusSpecificError = error as? StatusSpecificError {
                Log.info("StatusSpecificError occurred: \(StatusSpecificError)")
            } else {
                Log.error("Network request failed: \(error)")
            }
            completion(false)
        }
    }
}

 

 

🔎 presigned url 구조 분석

 

 

presigned url을 분석해보자.

 

presigned url: https://s3.ap-northeast-2.amazonaws.com/S3 버킷과 파일 경로?X-Amz-Algorithm=&X-Amz-Date=&X-Amz-SignedHeaders=&X-Amz-Credential=&X-Amz-Expires=X-Amz-Signature=

 

🟡 기본 url

https://s3.ap-northeast-2.amazonaws.com/S3 버킷과 파일 경로

  • https://s3.ap-northeast-2.amazonaws.com: S3의 기본 URL


🟡 쿼리 파라미터

X-Amz-Algorithm=&X-Amz-Date=&X-Amz-SignedHeaders=&X-Amz-Credential=&X-Amz-Expires=&X-Amz-Signature=

 

 

  • X-Amz-Algorithm=AWS4-HMAC-SHA256:
    • AWS의 서명 알고리즘을 지정
  • X-Amz-Date=20240808T085931Z:
    • 이 파라미터는 요청이 생성된 시각을 지정, UTC 표준시를 따르며, 20240808T085931Z는 2024년 8월 8일 08시 59분 31초를 의미한다.
  • X-Amz-SignedHeaders=host:
    • 서명에 포함된 헤더 목록을 지정, 여기서는 host 헤더가 서명에 포함되었다.
  • X-Amz-Credential=사용자 액세스 키/20240808/ap-northeast-2/s3/aws4_request:
    • 이 파라미터는 AWS 자격 증명을 포함, /20240808/ap-northeast-2/s3/aws4_request 부분은 요청이 만들어진 날짜와 AWS 서비스 및 리전을 나타낸다.
  • X-Amz-Expires=600:
    • presigned URL의 유효 기간(초 단위)을 지정, 여기서는 600초(10분) 동안 URL이 유효하다.
  • X-Amz-Signature=HMAC-SHA256 서명:
    • 요청을 인증하기 위한 HMAC-SHA256 서명, 사용자의 비밀 키와 위의 정보들을 기반으로 생성되며, URL이 위변조되지 않았음을 보장한다.

 

 

 


 

3. presigned url 저장

 

이제 위에서 발급받은 presigned url에 이미지를 저장해보자.

 

presigned url을 저장하기 위해서는 받은 presigned url과 같은 url로 요청을 보내면서 body에 이미지를 넣어 보내야 한다.

 

🔴 발급받은 presigned url과 저장하기 위해 보내는 PUT 요청 url이 같아야 한다. 🔴

 

똑같은 url에 다시 재요청하기 위해서는 발급받은 presigned url의 스펙대로 잘라서 구현해야 한다.

presigned url의 ? 앞까지는 payload에 저장하고 나머지 쿼리 파라미터를 Dto에 저장하려고 한다.

 

 

📌 presigned url 저장 API 요청

 

 

아래는 presigned url 저장 API 요청하는 로직이다. 선택한 이미지가 있는 경우에만 실행되도록 하였다.

 /// presigned url 저장
func storePresignedUrlApi(completion: @escaping (Bool) -> Void) {
    if let range = presignedUrl.range(of: "?") {
        payload = String(presignedUrl[..<range.lowerBound])
    } else {
        payload = presignedUrl
    }

    let storePresignedUrlRequestDto = createStorePresignedUrlRequestDto(from: presignedUrl)

    if image != nil {
        ObjectStorageAlamofire.shared.storePresignedUrl(payload, image!, storePresignedUrlRequestDto) { result in
            switch result {
            case .success:
                Log.debug("presigned_url 저장 성공")
                completion(true)
            case let .failure(error):
                if let StatusSpecificError = error as? StatusSpecificError {
                    Log.info("StatusSpecificError occurred: \(StatusSpecificError)")
                } else {
                    Log.error("Network request failed: \(error)")
                }
                completion(false)
            }
        }
    } else {
        Log.error("선택한 image가 없음")
    }
}

 

 

📌 presigned url에서 쿼리 추출

 

1️⃣ URLComponents로 쿼리 추출

 

func createStorePresignedUrlRequestDto(from presignedUrl: String) -> StorePresignedUrlRequestDto {
        if let payloadURL = URL(string: presignedUrl),
           let components = URLComponents(url: payloadURL, resolvingAgainstBaseURL: false),
           let queryItems = components.queryItems
        {
            // 필요한 값을 추출, 값이 없을 경우 빈 문자열을 기본값으로 설정
            let algorithm = queryItems[0].value ?? ""
            let date = queryItems[1].value ?? ""
            let signedHeaders = queryItems[2].value ?? ""
            let credential = queryItems[3].value ?? ""
            let expires = queryItems[4].value ?? ""
            let signature = queryItems[5].value ?? ""
            
            return StorePresignedUrlRequestDto(
                algorithm: algorithm,
                date: date,
                signedHeaders: signedHeaders,
                credential: credential,
                expires: expires,
                signature: signature
            )
        }
        return StorePresignedUrlRequestDto(
            algorithm: "",
            date: "",
            signedHeaders: "",
            credential: "",
            expires: "",
            signature: ""
        )
    }

 

URLComponents와 queryItems를 사용하여 URL에서 쿼리 매개변수를 추출해서 Dto에 저장하도록 하였다.

하지만 이 방식에 문제가 있었다.

 

쿼리 매개변수를 쉽게 저장할 수 있다는 점은 좋았지만, %2F문자가 자동으로 /문자로 변환되는 문제였다.

발급받은 presigned url 그대로 다시 전달해야 하는데 /문자로 변환되어 전송되니 계속 오류가 발생했다,,,,

 

그래서 이 방식이 아닌 url 문자열을 수동으로 추출하는 방식을 사용하기로 했다.

 

2️⃣ url 문자열에서 수동으로 쿼리 추출

 

/// 발급받은 presigned url 쿼리별로 자르기
func createStorePresignedUrlRequestDto(from presignedUrl: String) -> StorePresignedUrlRequestDto {
    var algorithm = ""
    var date = ""
    var signedHeaders = ""
    var credential = ""
    var expires = ""
    var signature = ""

    if let range = presignedUrl.range(of: "?") {
        let query = String(presignedUrl[range.upperBound...])
        let queryItems = query.split(separator: "&")

        for item in queryItems {
            let pair = item.split(separator: "=")
            if pair.count == 2 {
                let key = pair[0]
                let value = pair[1]

                switch key {
                case "X-Amz-Algorithm":
                    algorithm = String(value)
                case "X-Amz-Date":
                    date = String(value)
                case "X-Amz-SignedHeaders":
                    signedHeaders = String(value)
                case "X-Amz-Credential":
                    credential = String(value)
                case "X-Amz-Expires":
                    expires = String(value)
                case "X-Amz-Signature":
                    signature = String(value)
                default:
                    break
                }
            }
        }
    }

    return StorePresignedUrlRequestDto(
        algorithm: algorithm,
        date: date,
        signedHeaders: signedHeaders,
        credential: credential,
        expires: expires,
        signature: signature
    )
}

 

 

이렇게 변경하니 %2F 문자가 자동으로 인코딩되어 /문자로 변경되는 문제가 발생하지 않았다.

하지만 또 다른 문제를 발견할 수 있었다.

 

Dto에는 제대로 쿼리들이 저장한 걸 확인했는데 요청된 url을 보면 %2F 문자가 아니라 %252F 문자로 변환되어 요청이 간 것이다.

ㅎㅎㅎㅎ

 

왜 이런지 이유를 파악했는데 

if let queryParameters = queryParameters {
    var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
    components?.queryItems = queryParameters

    if let urlWithQuery = components?.url {
        request.url = urlWithQuery
    }
}

 

quertItems가 문제였다,, 

찾아보니 quertItems는 인코딩을 자동으로 처리한다고 한다. 그래서 %2F에서 %를 인코딩하여 %의 아스키코드 25로 변환하여 %252F가 된 것이다~~

 

그래서 quertItems를 percentEncodedQueryItems로 변경하니 제대로 요청이 간 걸 확인할 수 있었다!

 


- 구현 코드

 

 

📮 presigned url 발급 및 저장 by heejinnn · Pull Request #162 · CollaBu/pennyway-client-ios

작업 이유 presigned url 발급 presigned url 저장 쿼리 데이터 전달 로직 수정 작업 사항 PresignedUrlViewModel에서 관련 로직을 처리한다. generatePresignedUrlApi() => presigned url 발급 storePresignedUrlApi() => presigned url

github.com

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함