[Android] AWS S3 파일 받아오기

알린·2025년 1월 7일
0

Android

목록 보기
16/21
  1. Manifest 파일에 네트워크 관련 권한S3 전송 서비스 등록
  • <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    : 네트워크 상태를 확인하여 전송 작업을 조정
  • <uses-permission android:name="android.permission.INTERNET" />
    : 인터넷 통신을 위한 필수 권한
  • <service android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService" android:enabled="true" />
    : AWS S3 전송을 관리하는 서비스

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher_pres"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_pres_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Pie"
        tools:targetApi="31">
        <service
            android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService"
            android:enabled="true" />

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".PresActivity" />
    </application>

</manifest>

  1. app 수준 gradle에서 AWS 자격 증명의존성 추가

💡 AWS 자격 증명 보안 관리 방법
1. AWS의 Access key와 Secret key를 local.properties에 저장해 자격 증명 로드
2. BuildConfig에 자격 증명 설정
3. 추후 키 사용 시 BuildConfig.설정한 Key명 형식으로 사용
📌 자격 증명 Key 숨기기 관련 포스팅

build.gradle.kt

import java.util.Properties

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

// 자격 증명 로드
val localProperties = Properties()
localProperties.load(project.rootProject.file("local.properties").inputStream())
val awsAccessKey = localProperties.getProperty("awsAccessKey")?:""
val awsSecretKey = localProperties.getProperty("awsSecretKey")?:""

android {
    namespace = "com.example.pie"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.pie"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        // 자격 증명 설정
        buildConfigField("String", "AWS_ACCESS_KEY", awsAccessKey)
        buildConfigField("String", "AWS_SECRET_KEY", awsSecretKey)
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    buildFeatures {
        dataBinding = true
        viewBinding = true
        buildConfig = true
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.13.1")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    
    // AWS S3 의존성
    implementation("com.amazonaws:aws-android-sdk-s3:2.57.0")
    implementation("com.amazonaws:aws-android-sdk-core:2.57.0")
    
    // coroutines 의존성
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")

    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.2.1")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}

  1. AWS S3와 상호작용을 위한 유틸리티 객체 생성

💡 S3 다운로드 로직 최적화

  • withTimeout: 작업 타임아웃을 설정하여 무한 대기 방지
  • TransferListener: 다운로드 상태 세밀하게 관리

🚨 파일을 동기로 받을 때 이슈
사진 파일을 받아올 때 시간이 60,0000ms 이상 걸리게 됨
CoroutinesuspendCancellableCoroutine을 조합하여 비동기 작업의 중단 가능성을 보장
800ms로 실행 시간 개선

💡 suspendCancellableCoroutine
kotlin.coroutines.suspendCancellableCoroutine은 코루틴에서 실행되는 작업이 중단 가능(cancellable)하도록 만들어줌

핵심 기능

  • 코루틴이 실행되는 동안 작업 취소를 요청받으면 즉시 중단 가능
  • Continuation 객체를 사용해 작업이 완료되거나 실패했을 시 코루틴을 재개(resume())하거나, 작업 취소 요청 시 중단(cancel()) 가능

S3Utils.kt

package com.example.pie

import android.content.Context
import android.util.Log
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtilityOptions
import com.amazonaws.services.s3.AmazonS3Client
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
import java.io.File
import java.util.Properties
import kotlin.coroutines.resume

object S3Utils {
    private const val TAG = "S3Utils"

    // S3 클라이언트 초기화
    fun initializeS3Client(context: Context): TransferUtility {
        val accessKey = BuildConfig.AWS_ACCESS_KEY
        val secretKey = BuildConfig.AWS_SECRET_KEY
        val bucketRegion = "ap-northeast-2"

        // AmazonS3Client 생성
        val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
        val s3Client = AmazonS3Client(awsCredentials)
        s3Client.setRegion(com.amazonaws.regions.Region.getRegion(bucketRegion))

        // 전송 설정(스레드 풀 크기) 정의
        val options = TransferUtilityOptions()
        options.transferThreadPoolSize = 5

        return TransferUtility.builder()
            .context(context)
            .s3Client(s3Client)
            .transferUtilityOptions(options)
            .build()
    }

    // 파일 비동기 다운로드
    suspend fun downloadFileFromS3Async(
        transferUtility: TransferUtility,
        bucketName: String,
        fileKey: String,
        destinationFile: File
    ): Boolean = withTimeout(30_000L) { // 30초 타임아웃
        suspendCancellableCoroutine { continuation ->
            val transferObserver = transferUtility.download(bucketName, fileKey, destinationFile)
            transferObserver.setTransferListener(object : TransferListener {
                override fun onStateChanged(id: Int, state: TransferState?) {
                    when (state) {
                        TransferState.COMPLETED -> {
                            Log.d(TAG, "Download completed for $fileKey")
                            continuation.resume(true)
                        }
                        TransferState.FAILED -> {
                            Log.e(TAG, "Download failed for $fileKey")
                            continuation.resume(false)
                        }
                        else -> Log.d(TAG, "Download state changed for $fileKey: $state")
                    }
                }

                override fun onError(id: Int, ex: Exception?) {
                    Log.e(TAG, "Error during download for $fileKey", ex)
                    continuation.resume(false)
                }

                // 다운로드 진행률 업데이트
                override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
                    if (bytesTotal > 0) {
                        val progress = (bytesCurrent * 100 / bytesTotal).toInt()
                        Log.d(TAG, "Download progress for $fileKey: $progress%")
                    }
                }
            })
        }
    }
}

  1. Mainctivity에서 S3에서 파일을 비동기로 다운로드하고 PresActivity로 받은 파일 전송

MainActiviy.kt

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // TransferNetworkLossHandler 초기화
        TransferNetworkLossHandler.getInstance(applicationContext)
        
		// S3 클라이언트를 생성 (파일 전송에 필요한 TransferUtility 객체)
        val transferUtility = S3Utils.initializeS3Client(this)

        val bucketName = "버켓 이름"
        val fileKey = "파일루트/파일명"
        val destinationFile = File(getExternalFilesDir(null), "downloaded_file.jpeg") // 로컬 저장 경로

        binding.btnPrescription.setOnClickListener {
            lifecycleScope.launch {
                Log.d(TAG, "Download started...")

                // S3 다운로드
                val downloadTime = measureTimeMillis {
                    val isSuccess = withContext(Dispatchers.IO) {
                        S3Utils.downloadFileFromS3Async(
                            transferUtility, bucketName, fileKey, destinationFile
                        )
                    }
                    if (isSuccess) {
                        Log.d(TAG, "Download completed successfully.")
                        val intent = Intent(this@MainActivity, PresActivity::class.java)
                        intent.putExtra("filePath", destinationFile.absolutePath)
                        startActivity(intent)
                    } else {
                        Log.e(TAG, "File download failed!")
                    }
                }
                Log.d(TAG, "Download time: $downloadTime ms")
            }
        }
    }
}

  1. MainActivity에서 받은 이미지 파일 표시

💡 비트맵(Bitmap)

  • 이미지를 픽셀 단위로 표현하는 방식
  • 각 픽셀의 색상 정보를 저장하여 그래픽을 렌더링하는 데 사용

PresActivity

class PresActivity : AppCompatActivity() {

    private lateinit var binding: ActivityPresBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPresBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnBack.setOnClickListener {
            finish()
        }

        val filePath = intent.getStringExtra("filePath")
        if (filePath != null) {
            val file = File(filePath)
            if (file.exists()) {
                // 이미지 파일을 Bitmap으로 변환하여 ImageView에 표시
                val bitmap = BitmapFactory.decodeFile(file.absolutePath)
                binding.presImageView.setImageBitmap(bitmap)
            }
        }
    }
}
profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글