나는 동영상 업로드기능을 구현하면서, 백그라운드 스레드 관리의 중요성에 대해서 알게 되었다.
왜 백그라운드 스레드를 관리해야 할까?
메인 스레드로 하면, 개발자 입장에서 코드 짜기도 쉬운데...
바로 '사용자 경험' 때문이다.
예를들어, http 레트로핏 요청으로 서버에 동영상 파일을 업로드 하면 짧게는 20초~1분 가량 시간이 걸렸다.
이런 경우에 http요청은 응답이 올때까지 기다리는 특성이 있어서, 액티비티에서 http 요청을 보낸 경우엔 계속 대기를 해야 한다.
사용자 입장에서는 언제까지 기다려야 하는지 알길이 없기 때문에앱을 끄고 싶어진다... 따라서, 멀끔한 앱이 될 수 있도록
(1)동영상 업로드 요청을 비동기적으로 처리(요청을 보낸 후 응답과 관계없이 다음 동작을 실행)하고,
(2)동영상이 잘 업로드 되었는지를 추적하는프로그레스 알림을 만들기로 했다.
1. 액티비티에서 바로 요청보내지 않고, workManager 사용함
=> 요청 응답과 관계 없이 사용자가 다른 화면으로 이동할 수 있음
이것이 가능한 이유는 workManager API 가 내부적으로 여러 백그라운드 스레드를 관리해주기 때문임
(메인 스레드를 막지 않는 상태에서)
// WorkRequest (and its subclasses) define how and when it should be run.
WorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(UploadVideoWorker.class)
.setInputData(inputData)
.build();
WorkManager.getInstance(this).enqueue(uploadWorkRequest);
2. PHP 서버에서 s3 서버로 동영상 파일을 업로드 하는 과정을 추적해서 progress 알림 으로 나타냄
(1) progress 라는 aws 내장 이벤트 함수를 쓰면, 전체 업로드 용량 대비 얼만큼 업로드 했는지 나타낼 수 있다.
단, 멀티 파트 업로드가 아닌 싱글 객체 업로드를 사용해야 한다.
멀티 파트 업로드는 조각으로 나누어서 각 조각을 병렬적으로 업로드 하기 때문에, 전체 용량 대비 얼만큼 업로드 했는지를 통합적으로 (하나의 progress 바)로 나타내기 어렵다.
멀티파트 업로드 진행률을 추적하면, 이런식으로 원만한 오름차순이 아니라. 들쑥날쑥 한데
그 이유는 각 조각에 대한 진행률을 보여주기 때문이다.다른 시도도 해보았지만 안됐음(각 조각의 용량을 내가 설정해서, 각 조각을 합쳐서 진행률을 낸다든지... 근데 안됬음... aws 공식문서에 나와있는거만 하자 ^^)
멀티 파트 업로드는 100MB 이상부터 권장된다고 하니, 짧은 영상(1분 이내)의 경우에는 나처럼 싱글(단일) 객체 업로드를 쓰시는 것 좋을 듯하다.
용량이 크지 않은 경우에는 굳이 멀티파트 업로드가 효율적이지 않을 수도 있기 때문이다.
싱글 객체 업로드의 경우에는 이렇게 오름차순으로 로그가 잘찍힌다.
나는 10% 마다 잘라서 결과를 내보냈다.
public function uploadSingleWtihTrack($key, $filePath, $contentType){
try {
// 파일을 읽어서 Body에 넣어 업로드
$body = file_get_contents($filePath);
// S3 버킷에 데이터 업로드
$result = $this->s3Client->putObject([
'Bucket' => $this->bucket,
'Key' => $key,
'Body' => $body, // 직접 데이터를 Body에 넣어 업로드
'ContentType' => $contentType, // 데이터 타입 지정
'@http' => [
'progress' => function ($downloadTotalSize, $downloadSizeSoFar, $uploadTotalSize, $uploadSizeSoFar) use ($key) {
static $lastReportedProgress = -10; // Initialize to -10 so it starts reporting at the first 10% increment
if ($uploadTotalSize > 0) { // To avoid division by zero
$percentComplete = floor($uploadSizeSoFar / $uploadTotalSize * 100);
// Check if the new percentComplete is at least 10% greater than the last reported progress
if ($percentComplete >= $lastReportedProgress + 10) {
$this->updateUploadStatus($key, $percentComplete);
error_log("percentComplete". $percentComplete);
$lastReportedProgress = $percentComplete; // Update the last reported progress
}
}
}
]
]);
// 업로드 성공 시 파일의 URL 반환
return ['success' => true, 'url' => $result['ObjectURL']];
} catch (S3Exception $e) {
error_log($e->getMessage());
return ['success' => false, 'message' => $e->getMessage()];
}
하지만, 이 데이터를 저장할 db가 필요하다.
기존에 사용하던 mysql로 저장해보았지만, 덮어쓰기만 될뿐 실시간으로 db가 업데이트 되지 않았다.
찾아보니, 실시간 업데이트에 적합한 noSQL 데이터 베이스가 있었다.
(2) Firebase Realtime Database 연결 및 데이터 쓰기
https://firebase-php.readthedocs.io/en/7.10.0/
다음 API를 다운받아서 db 연결 스크립트 만들고, require 해서 써먹으면 된다.
(그리고, 사전에 이미 firebase 프로젝트 계정이 있어야 한다)
이때, firebase키에 허용되지 않는 문자는 _로 대체 해줘야 한다.
왜냐하면 이 데이터 베이스는 고유한 경로에 칼럼(key), 값(value)를 저장하는 트리구조 방식이기 때문이다.
public function updateUploadStatus($key, $progress){
$timestamp = time(); // 현재 시간을 Unix 타임스탬프로 가져옵니다.
// Firebase 키에 허용되지 않는 문자를 언더스코어로 대체
$safe_key = strtr($key, [
'.' => '_',
'$' => '_',
'#' => '_',
'[' => '_',
']' => '_'
]);
//지정된 경로에 있는 데이터를 업데이트하거나, 해당 경로가 존재하지 않으면 새로운 데이터를 추가
$this -> database
->getReference($safe_key)
->update([
'progress' => $progress,
'updated_at' => $timestamp
]);
}
(3) 안드로이드에서 백그라운드 스레드 사용해서 db 데이터 가져오고, 알림 업데이트 하기
이때, 같은 날짜에 같은 이름의 파일을 2번 이상 업로드하는 경우에는 progress 칼럼이 이미 100이 된 상태라 문제가 되었다.
같은 이름의 파일을 여러 번 업로드하는 경우에도 각 업로드의 상태를 확인할 수 있어야 한다.
따라서, updated_at 칼럼을 넣어서 파일 업로드 시, 현재시간과의 차이가 10초이상 나면(지금 업데이트 된 정보가 아니라 과거의 데이터인것)은 "업로드 준비중"이라고 알림을 띄우고, 10초이상 나지 않는 경우(즉, 현재 실시간으로 db가 업데이트 되고 있는 상태)라면 "진행률 (ex, 50%, 60%,70%..)알림을 띄웠다.
public void checkUploadStatus(){
// Realtime Database에서 해당 key에 대한 데이터 가져오기
databaseReference.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
// 여기서 progress 값 가져오기
Long updatedAt = dataSnapshot.child("updated_at").getValue(Long.class);
updatedAt *= 1000; // 초 단위를 밀리초 단위로 변환
Long currentTime = System.currentTimeMillis();
long timeDifference = currentTime - updatedAt;
// Log.d("updatedAt", String.valueOf(updatedAt));
// Log.d("currentTime", String.valueOf(currentTime));
// Log.d("timeDifference", String.valueOf(timeDifference));
if(timeDifference > 10000){
// 특히 같은 이름의 파일을 여러 번 업로드하는 경우에도 각 업로드의 상태를 개별적으로 확인할 수 있게 한다
NotificationUtils.initProgressNotification(context, NOTIFICATION_ID);
}else {
Integer progress = dataSnapshot.child("progress").getValue(Integer.class);
if (progress == 100) {
// 알림은 UploadVideoWorker 클래스에서
}else {
// 진행률을 UI에 업데이트 (UI 스레드에서 실행해야 함)
NotificationUtils.updateProgressNotification(context, progress, NOTIFICATION_ID);
}
}
}else {
NotificationUtils.initProgressNotification(context, NOTIFICATION_ID);
}
}
시연 영상
백그라운드 스레드를 관리하는 여러가지 방법들
1. 핸들러와 러너블을 이용하는 클래스 : 주로 짧은 시간(몇 초내에 끝나는 계산 등)에 해당하는 작업수행
2. 서비스: 백그라운드에서 오랫동안 실행될 수 있는 구성요소 (음악 재생, 파일 다운로드, 위치 추적 등), 사용자가 직접 종료하지 않는한 계속 실행, 단, 배터리 소모 주의
3. WorkManager: 여러개의 백그라운드 스레드를 관리하고, 예약할 수 있는 API
1번 요청, 혹은 주기적 요청(최소 15분 간격)이 모두 가능함, 배터리 수명과 같은 리소스 효율적 관리
단, 실시간 작업등에는 적합X
'Android' 카테고리의 다른 글
fcm 콘솔에서 서버키 못찾는 경우 해결방법 (0) | 2024.04.09 |
---|---|
(java) fcm 알림이 한번씩 누락될때 (2) | 2024.04.07 |
FirebaseMessaging.getInstance().getToken() 메서드가 호출안됨 (0) | 2024.03.30 |
안드로이드 스튜디오 layout editor 개념정리 (0) | 2023.09.12 |
안드로이드 매니패스트(manifest) 파일 (0) | 2023.09.09 |