Download / Upload to AWS S3 with Amazon Cognito in Android

windsekirun·2022년 4월 26일
0
post-thumbnail

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2018-06-07

도입

이번 프로젝트를 진행하면서 파일 서버를 AWS S3(Simple Storage Service) 로 사용하게 되었는데, 사실 AWS 로 진행하는 것이 처음이라 조금 많이 애 먹었지만 생각보다 쉽게 적용이 가능했다.

이번 글에서는 AWS Cognito 를 이용하여 S3에 연결하고, 파일을 다운받고 업로드 하는 AWSConnector 클래스를 작성해보려 한다.

S3 버킷 만들기

S3에서 버킷이란 개념은 객체를 저장하는 저장소라 생각하면 된다. 무한대로 객체를 저장할 수 있으므로 스토리지의 요구를 신경 쓸 필요 없기도 하다.

S3 Console (https://s3.console.aws.amazon.com/s3/home?region=us-east-2#) 에 들어가서 새 버킷을 만들어주자.

AWS Cognito 설정하기

원래 AWS 를 연동하려면 AccessKey, SecretKey 로 접근해야 하지만 어디선가 보기에는 인증서가 앱에 탑재되므로 보안성이 낮아진다고 들어서, AWS 자체에서 제공하는 Cognito 서비스를 이용하여 인증 권한자를 추가하고, 이 정보를 앱에 탑재하는 방법을 사용한다.

맨 먼저, Cognito 콘솔 (https://us-east-2.console.aws.amazon.com/cognito/home?region=us-east-2) 에 들어간다.

이 중 두번째 Manage Identify Pools 를 누른다.

그러면 아래 화면이 뜨는데, 이름을 넣고 Unauthenticated identities 의 체크박스에 체크를 표시한다.

그리고 Create Pool 를 누르면 정보를 확인하는 창이 나오는데, Allow를 눌러준다.

그러면 이제 Getting Started with Amazon Cognito 하면서 밑 부분 Get AWS Credentials 에 코드가 있을텐데, 이 부분을 메모장에 잘 옮겨놓으면 된다.

IAM 설정하기

Cognito 를 만들었다고 끝나는 것이 아니라, 해당 Cognito 에 S3 접속 권한을 부여해야 한다.

IAM 콘솔 (https://console.aws.amazon.com/iam/home?region=us-east-2) 로 들어가보자.

옆에 roles 를 누르면 아까 생성한 Cognito Role 들이 보일텐데, 이 중 Unauth 가 붙은 Role를 들어간다.

그 다음, Add inline policy 를 눌러준다.

이제 Policy 를 생성하는 창이 나오는데, Service 에는 S3 를, 두 번째 Action 은 PutObject, GetObject 를 넣어준다. 여기서는 최소한의 역할만 부여하도록 한다.

그 다음, object resource type 를 설정하라면서 ARN 을 추가하라는 곳이 있는데, 해당 부분을 클릭하면 팝업창이 뜬다.

bucket name 에는 방금 생성한 S3의 버킷 이름을, object 에는 Any 를 체크한다.

Add 를 클릭한 다음 Review policy 를 클릭하면 생성한 policy 를 검토하는 창이 나온다. 그대로 Create Policy 를 누른다.

이제 해당 Cognito Role 에 S3 에 대한 접속 권한이 생겼다. 남은 작업은 앱에 연동하는 것 뿐이다.

Gradle 불러오기

// s3
implementation ('com.amazonaws:aws-android-sdk-mobile-client:2.6.+@aar') { transitive = true }
implementation 'com.amazonaws:aws-android-sdk-s3:2.6.+'
implementation 'com.amazonaws:aws-android-sdk-cognito:2.6.+'

앱의 build.gradle 에 세 개의 의존성을 추가해준다.

AndroidManifest.xml 추가

AWS Moblie SDK 내부에서는 서비스로 업로드 / 다운로드 과정을 진행하기 때문에, 해당 서비스 객체를 AndroidManifest.xml 에 직접 적어야 한다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<service
        android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService"
        android:enabled="true" />

권한으로는 INTERNET, WRITE_EXTERNAL_STORAGE 를 적어주고 서비스에는 TransferService 를 적어준다.

awsconfiguration.json 추가

해당 파일은 AWS Mobile SDK 초기화 때 기본값을 설정할 수 있게 하는 파일이다.

{
  "Version": "1.0",
  "CredentialsProvider": {
    "CognitoIdentity": {
      "Default": {
        "PoolId": "POOL ID",
        "Region": "us-east-2"
      }
    }
  },
  "IdentityManager": {
    "Default": {
      
    }
  },
  "S3TransferUtility": {
    "Default": {
      "Bucket": "S3 bucket",
      "Region": "S3 Region"
    }
  }
}

POOL ID 에는 처음 Cognito 때 나왔던 코드 중 us-east-2: 로 시작하는 String 문자열을 넣으면 되고, S3 Bucket 는 생성한 S3의 이름, S3 Region 에는 생성한 S3의 리전을 넣는다.

위에서 생성한 S3의 이름은 windsekirunbucket 이고,  리전은 아시아/태평양 서울이므로 ap-northeast-2 를 넣으면 된다.

이렇게 생성한 파일은 res/raw 폴더에 잘 넣어주면 된다.

AWSMobileClient 초기화

파일을 업로드 / 다운로드 하기 전 AWSMobileClient 를 초기화 하고, 해당 정보로 TransferUtility 를 만들어야 한다. 여기서는 코드의 효율성을 위해 파라미터로 고차함수를 받아 업로드 / 다운로드 메서드 실행 시 초기화 작업을 하고 진행할 수 있도록 구현하면 된다.

fun initializeAWSMoblieClient(job: (TransferUtility) -> Unit) {
        AWSMobileClient.getInstance().initialize(activity) {
            val transferUtility = TransferUtility.builder()
                    .context(activity)
                    .awsConfiguration(AWSMobileClient.getInstance().configuration)
                    .s3Client(AmazonS3Client(AWSMobileClient.getInstance().credentialsProvider))
                    .build()

            job(transferUtility)
        }.execute()
    }

주의할 점은 context 를 Activity Context 만 받는다는 점이다.

TransferListener - 결과 콜백 받기

업로드 / 다운로드 둘 다 TransferListener 를 설정할 수 있는데, 이 TransferListener 로 전송 상태, 오류, Progress 등을 알 수 있다.

private val listener = object: TransferListener {
    override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
        val percentDoneFloat = bytesCurrent.toFloat() / bytesTotal.toFloat() * 100
        val percentDone = percentDoneFloat.toInt()

    }

    override fun onStateChanged(id: Int, state: TransferState?) {
        if (TransferState.COMPLETED == state) {
              
        }
    }

    override fun onError(id: Int, ex: java.lang.Exception?) {
            
    }
}

다운로드 / 업로드 메서드 구현

fun downloadFile(key: String, file: File, ...) {
        initializeAWSMoblieClient {
            val observer = it.download(key, file)
            observer.setTransferListener(listener)
            ...
        }
    }

fun uploadFile(key: String, file: File, ...) {
        initializeAWSMoblieClient {
            val observer = it.upload(key, file)
            observer.setTransferListener(Listener(callback))
            ...
        }
    }

두 코드의 차이점은 it, 즉 TransferUtility 의 메서드 호출이 다를 뿐이지 거의 비슷한 코드이다. 단, download 일 때는 key 는 S3 에 업로드 된 객체의 키 (경로), file 는 다운받을 파일의 File 객체이고, upload 일 때는 key 가 S3 에 업로드 될 객체의 키 이고, file는 업로드될 파일의 File 객체란 점이다.

전체 AWSConnector 코드

주석까지 포함한 코드는 다음과 같다.

class AWSConnector constructor(val activity: Activity) {

    private class Listener(val callback: F3<Mode, Int, Exception?>) : TransferListener {
        override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
            val percentDoneFloat = bytesCurrent.toFloat() / bytesTotal.toFloat() * 100
            val percentDone = percentDoneFloat.toInt()

            callback.invoke(Mode.Progress, percentDone, null)
        }

        override fun onStateChanged(id: Int, state: TransferState?) {
            if (TransferState.COMPLETED == state) {
                callback.invoke(Mode.Done, 100, null)
            }
        }

        override fun onError(id: Int, ex: java.lang.Exception?) {
            callback.invoke(Mode.Error, 0, ex)
        }
    }

    /**
     * 주어진 키(S3의 상대경로) 에 위치한 파일을 다운로드 합니다.
     * [File] 파라미터는 유효해야 하며, 폴더는 지원하지 않습니다.
     * 해당 파일이 이미 있을 경우에는 덮어쓰기 됩니다.
     *
     * @param key S3의 상대경로
     * @param file 다운로드 될 경로
     * @param callback mode: [AWSConnector.Mode] 참고
     * int: Done Progress Percent
     * Exception: not-null if mode = ERROR
     */
    fun downloadFile(key: String, file: File, callback: F3<Mode, Int, Exception?>) {
        initializeAWSMoblieClient {
            val observer = it.download(key, file)
            observer.setTransferListener(Listener(callback))
        }
    }

    /**
     * 주어진 키(S3의 상대경로) 에 주어진 파일을 업로드 합니다.
     * [File] 파라미터는 유효해야 하며, 폴더는 지원하지 않습니다.
     * 해당 파일이 이미 있을 경우에는 덮어쓰기 됩니다.
     *
     * @param key S3의 상대경로
     * @param file 업로드 할 경로
     * @param callback mode: [AWSConnector.Mode] 참고
     * int: Done Progress Percent
     * Exception: not-null if mode = ERROR
     */
    fun uploadFile(key: String, file: File, callback: F3<Mode, Int, Exception?>) {
        initializeAWSMoblieClient {
            val observer = it.upload(key, file)
            observer.setTransferListener(Listener(callback))
        }
    }

    private fun initializeAWSMoblieClient(job: (TransferUtility) -> Unit) {
        AWSMobileClient.getInstance().initialize(activity) {
            val transferUtility = TransferUtility.builder()
                    .context(activity)
                    .awsConfiguration(AWSMobileClient.getInstance().configuration)
                    .s3Client(AmazonS3Client(AWSMobileClient.getInstance().credentialsProvider))
                    .build()

            job(transferUtility)
        }.execute()
    }

    enum class Mode {
        Progress, Error, Done
    }
}

마무리

처음 시도했을 때는 어떤게 어떤건지 잘 몰랐지만, 막상 해보니 그렇게 어려운 것도 아니었던 것 같다. 차후에도 꽤나 많이 이용할 것 같다.

참고로, 이 글을 작성하는 시점 (2018-06-08) 때는 업로드 메서드를 테스트 하지 못했지만, 거의 똑같으니 아마 될 것 같다.

profile
Android Developer @kakaobank

1개의 댓글

comment-user-thumbnail
2022년 6월 21일

고맙습니다.

답글 달기