링큐 프로젝트의 안드로이드 리포지토리입니다.
📌 진행 사항 확인
| 유지민 | 채윤지 | 홍지현 | 문현우 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| @ugmin1030 | @KateteDeveloper | @Hongji03 | @Bidoof |
다음은 프로젝트의 구현을 위해 사용하는 기술 스택을 정리한 표입니다.
| 이름 | 설명 |
|---|---|
| Kotlin | 프로그래밍 언어 |
| Jetpack Compose | 인-코드 선언형 앱 설계 |
| Git | 체계적인 코드 관리 및 협업 |
다음과 같은 라이브러리 의존성을 가지고 있습니다.
| 이름 | 버전 | 설명 |
|---|---|---|
| Jetpack Navigation | 2.0.21 | 화면 전환 관리를 위한 라이브러리 |
| Hilt | 2.51.1 | 의존성 주입을 위한 라이브러리 |
| Retrofit2 | 2.11.0 | HTTP 통신을 위한 라이브러리 |
| SharedPreference | 1.2.1 | 로컬 데이터 저장 라이브러리 |
| Room | 2.6.1 | 로컬 데이터베이스 라이브러리 |
| Android JUnit | 1.2.1 | 단위 테스트를 위한 라이브러리 |
본 프로젝트는 멀티 모듈 아키텍쳐를 기반으로, 뷰모델을 사용한 MVVM 디자인 패턴으로 구성합니다.
다음은 본 프로젝트에 기여하는 개발자가 지켜야 할 컨벤션입니다.
본 프로젝트는 Gitflow 브랜치 전략을 따릅니다.
master: 배포 가능한 단위의 브랜치release: 배포 전 테스트가 가능한 단위의 브랜치develop: 개발 중인 브랜치feature/#issue_number: 개발 단위별 브랜치hotfix:master브랜치의 긴급 버그 수정 브랜치
모든 기능 개발은 다음 흐름을 따릅니다.
- 개발하고자 하는 기능에 대한 이슈를 등록하여 번호를 발급합니다.
develop브랜치로부터 분기하여 이슈 번호를 사용해 이름을 붙인feature브랜치를 만든 후 작업합니다.- 작업이 완료되면
develop브랜치에 풀 요청을 작성하고, 팀원의 동의를 얻으면 병합합니다.
커밋은 Gitmoji를 사용해 시각적으로 작성합니다. 다음은 본 프로젝트의 커밋 형식입니다. 각 줄 사이에는 빈 줄이 추가로 있음에 주의해주세요.
[깃모지] [제목]
[본문]
[이슈 번호 참조(선택)]
예시)
:bug: 버튼 버그 수정
키보드 콜백이 불러지지 않는 버그를 수정
관련 이슈 번호: #123, #234
각 깃모지의 의미는 이 블로그를 참고합니다. Android Studio 제공 플러그인을 사용하여 깃모지를 편리하게 이용할 수 있습니다.
이슈는 본 리포지토리에 등록된 목적에 맞는 이슈 템플릿을 사용하여 작성합니다.
Feature Template: 기능 추가를 위한 이슈에 사용Bug Template: 버그 수정을 위한 이슈에 사용
풀 요청은 본 리포지토리에 등록된 템플릿을 사용하여 작성합니다.
코드의 스타일은 Android 공식문서의 Kotlin 스타일 가이드를 최대한 따릅니다. 다음은 주요 네이밍 규칙입니다.
- 작성되는 모든 소스 파일은 UTF-8로 인코딩되어야 합니다.
- 코틀린 파일의 제목은 되도록이면
PascalCase를 사용하여야 합니다. - 컴포저블 함수의 이름은
PascalCase, 그 외 함수의 이름은 동사로 시작하는camelCase를 사용하며, 변수명은camelCase를 사용합니다. (람다식을 저장하는 변수도camelCase를 사용합니다.) - 콜백 함수를 전달하는 변수일 경우
on으로 시작합니다. ex)onButtonClicked,onDataLoaded - 안드로이드 스튜디오 상의 IDE의 노란 줄에 주의합니다.
다음은 본 프로젝트의 안드로이드 개발 환경입니다.
targetSDK: 36,minSDK: 26- Android 16(API 36) 대상 동작은 API 36 기기 또는 에뮬레이터에서 별도 런타임 회귀 검증이 필요합니다.
- 안드로이드 스튜디오 버전: Meerkat | 2024.3.2 또는 그 이상
- 테스트 환경: 안드로이드 스튜디오 제공 에뮬레이터(AVD)
-
- 기기명: Pixel 8
- API 35 (Android 15.0, x86_64)
- 1080 x 2400 px (412 x 915 dp)
다음 앱 설정은 키마다 local.properties의 non-blank 값, LinkU 전용 환경 변수 순서로 읽습니다.
따라서 Android Studio가 sdk.dir만 포함한 local.properties를 생성해도 환경 변수 fallback이 동작합니다.
local.properties 키 |
환경 변수 |
|---|---|
KAKAO_NATIVE_APP_KEY |
LINKU_KAKAO_NATIVE_APP_KEY |
GOOGLE_WEB_CLIENT_ID |
LINKU_GOOGLE_WEB_CLIENT_ID |
SERVER_DOMAIN |
LINKU_SERVER_DOMAIN |
SERVER_HOST |
LINKU_SERVER_HOST |
API_VERSION |
LINKU_API_VERSION |
우선순위는 파일 전체가 아니라 각 키를 기준으로 합니다. local.properties에 특정 앱 설정이 없거나
공백이면 표에 매핑된 LINKU_ 환경 변수를 사용합니다. sdk.dir은 Android Studio가 관리하는 파일에
그대로 둘 수 있으며, 파일이 없는 환경에서는 Android SDK 경로를 표준 ANDROID_HOME 환경 변수로
제공해야 합니다.
이 환경 변수들은 로컬 파일 복사 없이 빌드 설정을 전달하기 위한 수단입니다. 값은 BuildConfig 또는
Android Manifest 등에 포함될 수 있으므로 서버 비밀이나 관리자 토큰을 저장하는 용도로 사용하지 않습니다.
Windows 사용자 환경 변수와 외부 백업 파일도 암호화된 비밀 저장소가 아니며, 동일 사용자 권한의
프로세스가 읽을 수 있습니다.
환경 변수 등록 후에는 Android Studio와 JetBrains Toolbox를 모두 완전히 종료한 뒤 다시 실행해야 새 프로세스가 Windows User 환경 변수를 상속합니다.
Android Release AAB workflow는 GitHub Actions에서 수동으로만 실행합니다. 입력한
version_code를 release variant에 전달해 AAB와 R8 mapping을 생성하고, 기존 upload key로 AAB를
서명한 다음 jarsigner -verify -strict 검증을 통과한 signed AAB만 업로드합니다.
임의의 새 keystore나 key를 생성하지 않습니다. Play App Signing을 사용하는 경우 upload key는 개발자가 Google Play에 AAB를 제출할 때 사용하는 키이며, Google이 최종 APK를 서명하는 app signing key와는 다른 키입니다.
- 앞뒤 공백과 불필요한 선행 0 없이, ASCII 10진수로만 구성된
1..2,100,000,000범위의 양의 정수를 입력합니다(예:28). - 현재 Play Console에 등록된 최대 versionCode보다 크고, 이전에 사용하지 않은 값이어야 합니다.
- workflow는 Play Console을 조회하지 않으므로 값의 고유성과 배포 가능성은 실행자가 확인합니다.
- Gradle은 앞뒤 공백을 제거한 뒤 값을 검증합니다. 누락, 공백만 있는 값, 0, 음수, 소수, 내부 공백, 숫자가 아닌 값 또는 상한 초과 값은 release 빌드 전에 거부됩니다.
- artifact 이름에는 정리 전 workflow 입력이 사용되므로
28이나0028처럼 입력하면 AAB 내부의 정수 versionCode와 artifact 이름 표기가 달라질 수 있습니다. 따라서 허용되더라도 이런 표기는 사용하지 않습니다.
workflow는 다음 11개 Secret이 모두 non-blank인지 빌드 전에 검사합니다. 이름은
.github/workflows/android-release.yml의 계약과 정확히 일치해야 합니다.
| 구분 | GitHub Actions Secret | 용도와 취급 원칙 |
|---|---|---|
| 앱 구성 | LINKU_KAKAO_NATIVE_APP_KEY |
Kakao SDK 초기화와 Manifest/BuildConfig에 전달하는 Android client key |
| 앱 구성 | LINKU_GOOGLE_WEB_CLIENT_ID |
Google 로그인 요청에 사용하는 OAuth web client ID |
| 앱 구성 | LINKU_SERVER_DOMAIN |
scheme과 host를 포함하는 HTTP(S) 서버 기준 URL |
| 앱 구성 | LINKU_SERVER_HOST |
scheme, port, path를 제외한 서버 host 이름 |
| 앱 구성 | LINKU_API_VERSION |
서버 기준 URL 뒤에 결합하는 API path version |
| Firebase 구성 | LINKU_GOOGLE_SERVICES_JSON_BASE64 |
app/google-services.json 원본 파일의 Base64 표현 |
| AAB 서명 | LINKU_UPLOAD_KEYSTORE_BASE64 |
기존 upload keystore 파일의 Base64 표현 |
| AAB 서명 | LINKU_UPLOAD_KEYSTORE_TYPE |
기존 keystore 형식(예: JKS 또는 PKCS12) |
| AAB 서명 | LINKU_UPLOAD_KEYSTORE_PASSWORD |
기존 upload keystore 비밀번호 |
| AAB 서명 | LINKU_UPLOAD_KEY_ALIAS |
기존 upload key alias |
| AAB 서명 | LINKU_UPLOAD_KEY_PASSWORD |
기존 upload key 비밀번호 |
앞의 앱 설정 5개는 로컬 빌드에서 사용하는 LINKU_* 환경 변수와 같은 이름입니다.
LINKU_GOOGLE_SERVICES_JSON_BASE64는 workflow가 임시 runner에서 app/google-services.json으로
복원하기 위한 CI 전용 전달 값입니다.
앱 구성값은 BuildConfig, Manifest 또는 Firebase resource로 최종 AAB에 포함될 수 있으므로 서버 비밀, 관리자 token 또는 backend 전용 credential을 넣지 않습니다. 반면 upload keystore와 비밀번호는 외부에 공개해서는 안 되는 배포 credential입니다. Base64는 파일 형식을 문자열로 바꾸는 encoding일 뿐 암호화가 아니므로, Base64 결과도 원본 keystore와 같은 수준으로 보호합니다.
- 11개 Secret이 non-blank인지 검증합니다.
LINKU_GOOGLE_SERVICES_JSON_BASE64를 임시 runner의app/google-services.json으로 복원합니다.- Gradle이
version_code입력 계약을 검증한 뒤:app:bundleRelease -PlinkuVersionCode=<입력값>으로 R8이 적용된 unsigned AAB와 mapping을 생성합니다. - 기존 upload keystore를 runner의 임시 경로에 제한된 권한으로 복원합니다.
- unsigned AAB와 다른 경로에 signed AAB를 생성합니다.
- 기존 keystore의 지정 alias를 기준으로
jarsigner -verify -strict검증을 수행합니다. - 성공·실패 여부와 관계없이 일반적인 step 종료 경로에서는 임시 keystore를 삭제합니다.
- 앞 단계가 모두 성공한 경우에만 signed AAB와 R8 mapping을 각각 artifact로 업로드합니다.
누락된 Secret, Base64 decode 오류, release 빌드 실패, keystore type/비밀번호/alias 불일치, 서명 실패 또는 strict 검증 실패가 발생하면 workflow는 실패하며 artifact upload 단계로 진행하지 않습니다. 다만 runner 강제 종료처럼 cleanup step 자체가 실행될 수 없는 상황까지 로컬 삭제를 보장하지는 않습니다. GitHub-hosted runner는 작업 후 폐기되지만, Secret 원본의 별도 보관·회수·회전 정책은 저장소 밖에서 관리해야 합니다.
현재 workflow는 복원한 app/google-services.json을 별도로 삭제하지 않지만, 이 파일을 artifact로
업로드하지 않으며 GitHub-hosted runner의 작업 공간이 폐기될 때 함께 사라집니다. 향후
self-hosted runner로 변경한다면 workflow에 이 파일의 명시적인 cleanup 단계를 추가해야 합니다.
성공한 run은 입력한 versionCode를 이름에 포함한 다음 두 artifact를 생성합니다.
| Artifact 이름 | 업로드 소스 경로 | 용도 |
|---|---|---|
linku-signed-release-vc-<version_code> |
app/build/outputs/bundle/release/app-release-signed.aab |
Google Play에 제출할 upload key 서명 AAB |
linku-mapping-vc-<version_code> |
app/build/outputs/mapping/release/mapping.txt |
해당 release의 R8 난독화 stack trace 복원 |
app-release.aab은 서명 전 중간 산출물이며 artifact로 업로드하지 않습니다. signed AAB와 mapping은
서로 다른 artifact이므로 mapping 업로드에서만 실패하면 앞서 업로드된 AAB가 실패한 run에 남을 수
있습니다. 일부 step이 성공했더라도 전체 workflow 결론이 실패 또는 취소라면 그 run의 artifact를
배포에 사용하지 않습니다.
- GitHub Actions Secrets 11개가 정확한 이름으로 등록되어 있고 공백이 아닌지 확인합니다.
- keystore type, alias와 두 비밀번호가 기존 앱의 upload key 계약과 일치하는지 확인합니다.
version_code가 앞뒤 공백이나 불필요한 선행 0 없이 입력되었고, 현재 Play Console 최대값보다 크며 아직 사용되지 않은 값인지 확인합니다.- workflow 전체 결론이 성공인지 확인합니다.
- signed AAB와 mapping artifact 이름의 versionCode가 입력값과 같은지 확인합니다.
- Play 업로드 후 같은 release의 mapping을 보존하고 crash/ANR 분석에 연결합니다.
Secret 값, Base64 결과, keystore 원본과 복원 파일은 저장소·issue·PR·workflow 로그에 추가하지 않습니다. Secret 이름, 주입 위치 또는 artifact 계약을 변경할 때는 workflow와 이 문서를 같은 변경에서 함께 갱신합니다.




