Skip to content

인증샷 샘플링 로직 추가#120

Open
chanho0908 wants to merge 4 commits intodevelopfrom
feat/#119-photolog-upload-modifiy
Open

인증샷 샘플링 로직 추가#120
chanho0908 wants to merge 4 commits intodevelopfrom
feat/#119-photolog-upload-modifiy

Conversation

@chanho0908
Copy link
Member

@chanho0908 chanho0908 commented Mar 3, 2026

이슈 번호

#119

작업내용

  • 팀 컨벤션에 맞게 PhotologCaptureUiState의 상태 업데이트 로직을 viewModel로 이동
  • 기존에 인증샷 업로드시 너무 시간이 오래걸리는 문제를 해결 😵‍💫

🥊 문제 상황

갤럭시 S25 기준 촬영 이미지 해상도는 4080x3060 이며 이를 원본 그대로 presigned URL을 통해 업로드하면서 최대 2.6초까지 소요되는 현상을 확인했어

📝 원인 분석

PresignedUploader에 로깅을 추가해 계측한 결과 다음 결과가 나왔어

  • 평균 파일 크기: 약 2.8MB
  • 업로드 시간: 약 1.8 ~ 2.6초

이를 통해 고해상도 이미지를 리사이징 없이 그대로 업로드하면서 네트워크 전송 구간에서 병목이 발생하고 있음을 확인했어

🛎️ 개선 내용

  1. 이미지를 바로 Bitmap으로 디코딩하는 대신 BitmapFactory.Options(inJustDecodeBounds = true) 를 사용해
    실제 디코딩 없이 이미지 크기만 먼저 측정
  2. calculateInSampleSize() 로 목표 해상도(FHD 기준)에 맞는 샘플링 비율 계산
  3. 다운샘플링 후 JPEG 재압축

이번 작업을 진행하면서 공부한 내용을 블로그에 정리해봤어

결과물

PresignedUploader에 로그를 통해서 업로드에 걸리는 시간을 측정한 결과야 !

Before After
image image

리뷰어에게 추가로 요구하는 사항 (선택)

샘플링 기준을 FHD 기준으로 잡았는는데 혹시 의견 있다면 편하게 말해줘 !
image

@chanho0908 chanho0908 self-assigned this Mar 3, 2026
@chanho0908 chanho0908 added the Refactor Good for newcomers label Mar 3, 2026
@chanho0908 chanho0908 linked an issue Mar 3, 2026 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

이미지 디코딩/회전 로직이 개선되어 두 단계 디코딩(inJustDecodeBounds + 샘플링)과 public한 calculateInSampleSize가 추가되었고, EXIF 기반 회전 적용 후 원본 비트맵을 필요시 recycle하도록 변경되었습니다. 압축 실패 시 명시적 ImageProcessException이 던져집니다. 캡처 흐름은 Continuation에서 CancellableContinuation으로 변경되어 취소 안전성이 추가됐고, cameraProvider 캐시 재사용 및 언바인드 시 surface request 초기화가 추가되었습니다. UI 레이어에서는 CommentUiModel과 PhotologCaptureUiState의 공개 변이 헬퍼들이 제거되고 ViewModel은 상태 복사 방식으로 업데이트하도록 리팩터링되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

상세 리뷰

긍정적인 부분

  • 메모리 효율성: inJustDecodeBounds + 샘플링 도입은 표준적이며 메모리 사용을 줄입니다.
  • EXIF 회전 처리 및 원본 recycle: 회전 적용 후 불필요한 비트맵을 해제하는 점은 리소스 관리 측면에서 적절합니다.
  • 예외 분리: 압축 실패에 대한 명확한 예외 타입(ImageProcessException.CompressionFailedException) 추가는 호출부 처리 명확화를 돕습니다.
  • 코루틴 취소 안정성: CancellableContinuation 도입 및 isActive 검사로 취소 시 안전한 동작이 보장됩니다.
  • 상태 불변성 강화: ViewModel에서 상태 복사 패턴으로 변경하고 공개 변이 헬퍼를 제거한 것은 일관된 상태 관리로 이어집니다.

검토 권장사항 (왜 문제가 되는가 → 어떻게 개선할지)

  1. 회전 후 recycle 타이밍 검증

    • 왜 문제가 되는가: 호출자가 원본 비트맵을 여전히 참조하는 상황에서 즉시 recycle하면 NPE나 UI 문제를 유발할 수 있습니다.
    • 어떻게 개선할지: 함수 KDoc에 소유권 규약(누가 recycle 책임을 가지는지)을 명시하거나, 원본과 회전본이 동일이면 recycle하지 않도록 하고, 회전본을 반환할 때 호출자에게 명확히 문서화하세요. 회전이 불필요(orientation == 0)한 경우에는 recycle을 피하세요.
    • 질문: 현재 이 API의 호출자들이 반환된 비트맵을 즉시 사용·파기한다는 전제가 확실한가요?
  2. EXIF 부재/읽기 실패 처리 명확화

    • 왜 문제가 되는가: PNG 등 EXIF가 없는 포맷에서 orientation 판독 실패 시 예외 또는 불필요한 회전이 발생할 수 있습니다.
    • 어떻게 개선할지: EXIF 읽기 실패 시 안전한 fallback(orientation = 0) 처리와 KDoc에 그 동작을 명시하세요. 예외 케이스에서 로그를 일관되게 남기면 디버깅에 도움이 됩니다.
  3. calculateInSampleSize 입력 검증 추가

    • 왜 문제가 되는가: reqWidth/reqHeight가 0 또는 음수이면 잘못된 결과(잠재적 0/나눗셈 문제)가 발생할 수 있습니다.
    • 어떻게 개선할지: 함수 시작부에서 reqWidth, reqHeight > 0 검증을 하고, 유효하지 않다면 1을 반환하거나 IllegalArgumentException을 던져 경계 조건을 명확히 하세요. KDoc에 경계 조건을 설명하세요.
  4. bitmapToByteArray 및 관련 메서드 예외 문서화

    • 왜 문제가 되는가: 새로 던지는 ImageProcessException 계열을 호출자가 모르고 있으면 런타임 예외가 예상치 못한 곳에서 발생할 수 있습니다.
    • 어떻게 개선할지: 각 메서드 KDoc에 던지는 예외 타입과 발생 조건(디코드 실패, 압축 실패 등)을 구체적으로 기술하고, 권장 처리 방법(재시도, 사용자 메시지 등)을 제안하세요.
  5. CaptureCamera CancellableContinuation 경로 일관성 검증

    • 왜 문제가 되는가: isActive 검사를 일부 경로에서만 확인하면 race 조건으로 중복 resume/예외가 발생할 수 있습니다. 또한 cameraProvider 캐시가 Context/라이프사이클 변화에서 안전한지 확인 필요합니다.
    • 어떻게 개선할지: 모든 resume/throw 경로에서 continuation.isActive 체크를 일관되게 적용하고, provider 캐시가 Context를 직접 참조하는 경우 메모리 누수를 피하도록 약한 참조 또는 명확한 해제 로직을 고려하세요. surfaceRequests 초기화가 예상 타이밍에 호출되는지 단위 테스트로 검증하세요.
    • 질문: cameraProvider 캐시가 여러 lifecycle 전환에서 안전하게 동작하나요?
  6. 상태 변경 리팩터링 검증

    • 왜 문제가 되는가: 공개 변이 헬퍼 제거 후, 외부 코드가 이전 방식에 의존하고 있을 수 있습니다. 또한 상태 복사 시 일부 필드를 누락해 불변성 규약이 깨질 가능성이 있습니다.
    • 어떻게 개선할지: 변경된 API에 맞춘 호출부 리팩터링 여부를 점검하고, 상태 복사시 모든 필드가 일관되게 유지되는지 단위 테스트를 추가하세요. 공개 헬퍼 제거에 대한 마이그레이션 가이드를 문서화하면 좋습니다.
    • 질문: 기존 호출부(타 모듈/화면)가 이 변경에 따라 모두 업데이트되었나요?
  7. 단위 및 통합 테스트 보강 권장

    • 왜 필요한가: 회전(0/90/180/270), EXIF 없음, 다양한 이미지 크기 샘플링, 압축 실패, 캡처 취소 등 경계 케이스에서 버그가 노출되기 쉽습니다.
    • 어떻게 개선할지: 각 케이스에 대한 단위·통합 테스트를 추가해 회전·샘플링·예외 경로·코루틴 취소 동작을 검증하세요.

전반적으로 구현 방향은 적절하며 메모리·취소 안전성·상태 일관성 측면에서 향상된 부분이 보입니다. 위의 검증 포인트를 보완하면 안정성과 문서화가 더욱 좋아질 것입니다.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목 '인증샷 샘플링 로직 추가'는 PR의 핵심 변경사항(고해상도 이미지 샘플링을 통한 업로드 최적화)을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed 설명은 문제 상황, 원인 분석, 개선 내용, 결과물을 체계적으로 제시하며 PR의 모든 주요 변경사항과 직접 관련이 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#119-photolog-upload-modifiy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt (1)

66-69: 타겟 해상도(1920x1080) 하드코딩에 대한 제안

현재 FHD(1920x1080)가 하드코딩되어 있습니다. PR 설명에서 리뷰어에게 이 기준에 대한 의견을 요청하셨는데, FHD는 대부분의 모바일 화면에서 충분한 해상도이므로 합리적인 선택으로 보입니다.

다만, 향후 유지보수성을 위해 상수로 추출하는 것은 어떨까요?

♻️ 상수 추출 제안
 companion object {
     private const val IMAGE_COMPRESSION_ERROR_MESSAGE = "Config: %s, Size: %dx%d"
+    private const val TARGET_WIDTH = 1920
+    private const val TARGET_HEIGHT = 1080
 }

그리고 사용처에서:

-            inSampleSize = calculateInSampleSize(bounds, 1920, 1080)
+            inSampleSize = calculateInSampleSize(bounds, TARGET_WIDTH, TARGET_HEIGHT)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt` around lines 66 -
69, The code in ImageGenerator.kt hardcodes the 1920x1080 target resolution when
creating BitmapFactory.Options (inSampleSize = calculateInSampleSize(bounds,
1920, 1080)); extract these magic numbers into named constants (e.g.,
TARGET_WIDTH and TARGET_HEIGHT) either as top-level constants or in a companion
object of ImageGenerator, replace the literals in the calculateInSampleSize call
with those constants, and update any related usages/comments to reference the
new constants so the target resolution is configurable and easier to maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt`:
- Around line 60-74: In uriToBitmap, validate the first-pass bounds before
calling calculateInSampleSize: after the inJustDecodeBounds decode, check
bounds.outWidth and bounds.outHeight for <= 0 (or -1) and if invalid throw
ImageProcessException.DecodeFailedException(imageUri) so you don't proceed with
a bogus inSampleSize; then continue to create the real BitmapFactory.Options and
call calculateInSampleSize only when bounds are valid (reference symbols:
uriToBitmap, bounds.outWidth, bounds.outHeight, calculateInSampleSize,
ImageProcessException.DecodeFailedException).

---

Nitpick comments:
In `@core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt`:
- Around line 66-69: The code in ImageGenerator.kt hardcodes the 1920x1080
target resolution when creating BitmapFactory.Options (inSampleSize =
calculateInSampleSize(bounds, 1920, 1080)); extract these magic numbers into
named constants (e.g., TARGET_WIDTH and TARGET_HEIGHT) either as top-level
constants or in a companion object of ImageGenerator, replace the literals in
the calculateInSampleSize call with those constants, and update any related
usages/comments to reference the new constants so the target resolution is
configurable and easier to maintain.

ℹ️ Review info

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db90af9 and 0b1107d.

📒 Files selected for processing (1)
  • core/ui/src/main/java/com/twix/ui/image/ImageGenerator.kt

@chanho0908 chanho0908 requested a review from dogmania March 3, 2026 09:33
- `capture` 시 `CancellableContinuation`의 활성 상태(`isActive`)를 확인하여 불필요한 resume 방지
- `unbind` 호출 시 `surfaceRequests`를 초기화하도록 수정
- `bindToLifecycle` 시 기존에 생성된 `cameraProvider`가 있다면 재사용하도록 개선
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
feature/photolog/capture/src/main/java/com/twix/photolog/capture/model/camera/CaptureCamera.kt (1)

73-83: 코루틴 취소 시 리소스 정리를 위한 invokeOnCancellation 핸들러 추가를 고려해보세요.

현재 구현에서 isActive 체크는 취소된 continuation에 resume하는 것을 방지해주지만, 코루틴이 취소되더라도 CameraX의 takePicture 작업은 백그라운드에서 계속 진행됩니다. 이로 인해 사용자가 화면을 떠난 후에도 이미지가 MediaStore에 저장될 수 있습니다.

CameraX의 takePicture는 자체적인 취소 API를 제공하지 않지만, invokeOnCancellation을 통해 최소한 로깅이나 상태 추적은 가능합니다:

suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
        // 취소 시 로깅 또는 필요한 정리 작업
        Log.d("CaptureCamera", "Image capture was cancelled")
    }
    // ... 기존 코드
}

이 부분은 현재 동작에 문제를 일으키지는 않으므로, 향후 개선 사항으로 검토해주시면 됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/photolog/capture/src/main/java/com/twix/photolog/capture/model/camera/CaptureCamera.kt`
around lines 73 - 83, Add an invokeOnCancellation handler to the
suspendCancellableCoroutine in CaptureCamera.takePicture() so that when the
coroutine is cancelled you can perform cleanup/logging; inside the
suspendCancellableCoroutine block (where outputOptions is created and
imageCapture.takePicture(...) is called with capture(continuation)), call
continuation.invokeOnCancellation { /* log cancellation and/or update state to
avoid leftover background work */ } to track or log cancellations and prevent
silent background saves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@feature/photolog/capture/src/main/java/com/twix/photolog/capture/model/camera/CaptureCamera.kt`:
- Around line 73-83: Add an invokeOnCancellation handler to the
suspendCancellableCoroutine in CaptureCamera.takePicture() so that when the
coroutine is cancelled you can perform cleanup/logging; inside the
suspendCancellableCoroutine block (where outputOptions is created and
imageCapture.takePicture(...) is called with capture(continuation)), call
continuation.invokeOnCancellation { /* log cancellation and/or update state to
avoid leftover background work */ } to track or log cancellations and prevent
silent background saves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: e8ff4419-a7cf-4de0-9043-b4cfe746557d

📥 Commits

Reviewing files that changed from the base of the PR and between 0b1107d and a5820af.

📒 Files selected for processing (1)
  • feature/photolog/capture/src/main/java/com/twix/photolog/capture/model/camera/CaptureCamera.kt

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
feature/photolog/capture/src/main/java/com/twix/photolog/capture/PhotologCaptureViewModel.kt (1)

87-96: 상태 읽기와 업데이트 사이의 일관성 개선 제안

현재 currentState.lensreduce 블록 외부에서 읽고 있습니다. 이론적으로 외부 읽기와 reduce 호출 사이에 다른 상태 변경이 발생하면 의도하지 않은 동작이 발생할 수 있습니다.

reduce 블록 내부에서는 this가 현재 상태를 참조하므로, 상태 기반 계산을 블록 내부로 이동하면 더 안전합니다. 현재 코드가 실질적으로 문제를 일으킬 가능성은 낮지만, MVI 패턴의 일관성을 위해 고려해볼 수 있습니다.

♻️ reduce 블록 내부로 로직 이동 제안
     private fun reduceLens() {
-        val newLens =
-            if (currentState.lens == CameraSelector.DEFAULT_BACK_CAMERA) {
-                CameraSelector.DEFAULT_FRONT_CAMERA
-            } else {
-                CameraSelector.DEFAULT_BACK_CAMERA
-            }
-
-        reduce { copy(lens = newLens, torch = TorchStatus.Off) }
+        reduce {
+            val newLens =
+                if (lens == CameraSelector.DEFAULT_BACK_CAMERA) {
+                    CameraSelector.DEFAULT_FRONT_CAMERA
+                } else {
+                    CameraSelector.DEFAULT_BACK_CAMERA
+                }
+            copy(lens = newLens, torch = TorchStatus.Off)
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/photolog/capture/src/main/java/com/twix/photolog/capture/PhotologCaptureViewModel.kt`
around lines 87 - 96, reduceLens에서 외부의 currentState.lens를 읽지 말고 상태 일관성을 위해 상태
계산을 reduce 블록 내부로 이동하세요: inside reduceLens's reduce { ... } use this.lens
(current state) to choose between CameraSelector.DEFAULT_FRONT_CAMERA and
CameraSelector.DEFAULT_BACK_CAMERA and return copy(lens = chosenLens, torch =
TorchStatus.Off). This keeps the logic inside reduce (referencing reduceLens,
reduce, currentState.lens, TorchStatus.Off) and prevents races between read and
update.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@feature/photolog/capture/src/main/java/com/twix/photolog/capture/PhotologCaptureViewModel.kt`:
- Around line 87-96: reduceLens에서 외부의 currentState.lens를 읽지 말고 상태 일관성을 위해 상태 계산을
reduce 블록 내부로 이동하세요: inside reduceLens's reduce { ... } use this.lens (current
state) to choose between CameraSelector.DEFAULT_FRONT_CAMERA and
CameraSelector.DEFAULT_BACK_CAMERA and return copy(lens = chosenLens, torch =
TorchStatus.Off). This keeps the logic inside reduce (referencing reduceLens,
reduce, currentState.lens, TorchStatus.Off) and prevents races between read and
update.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ebc4ae90-52c7-4ab4-968e-1a3c489f99f8

📥 Commits

Reviewing files that changed from the base of the PR and between a5820af and 662f4cf.

📒 Files selected for processing (3)
  • core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt
  • feature/photolog/capture/src/main/java/com/twix/photolog/capture/PhotologCaptureViewModel.kt
  • feature/photolog/capture/src/main/java/com/twix/photolog/capture/contract/PhotologCaptureUiState.kt
💤 Files with no reviewable changes (2)
  • feature/photolog/capture/src/main/java/com/twix/photolog/capture/contract/PhotologCaptureUiState.kt
  • core/design-system/src/main/java/com/twix/designsystem/components/comment/model/CommentUiModel.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Refactor Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

인증샷 업로드 지연 문제 개선

1 participant