이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2018-06-07
이번 프로젝트를 진행하면서 파일 서버를 AWS S3(Simple Storage Service) 로 사용하게 되었는데, 사실 AWS 로 진행하는 것이 처음이라 조금 많이 애 먹었지만 생각보다 쉽게 적용이 가능했다.
이번 글에서는 AWS Cognito 를 이용하여 S3에 연결하고, 파일을 다운받고 업로드 하는 AWSConnector 클래스를 작성해보려 한다.
S3에서 버킷이란 개념은 객체를 저장하는 저장소라 생각하면 된다. 무한대로 객체를 저장할 수 있으므로 스토리지의 요구를 신경 쓸 필요 없기도 하다.
S3 Console (https://s3.console.aws.amazon.com/s3/home?region=us-east-2#) 에 들어가서 새 버킷을 만들어주자.
원래 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 에 코드가 있을텐데, 이 부분을 메모장에 잘 옮겨놓으면 된다.
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 에 대한 접속 권한이 생겼다. 남은 작업은 앱에 연동하는 것 뿐이다.
// 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 에 세 개의 의존성을 추가해준다.
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 를 적어준다.
해당 파일은 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 를 초기화 하고, 해당 정보로 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 로 전송 상태, 오류, 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 객체란 점이다.
주석까지 포함한 코드는 다음과 같다.
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) 때는 업로드 메서드를 테스트 하지 못했지만, 거의 똑같으니 아마 될 것 같다.
고맙습니다.