소개

안녕하세요. 신사업부문 만화경셀 안드로이드 개발자 오재환입니다.

만화경은 우아한형제들의 웹툰 플랫폼으로서 2020년 5월 구글피처드, 2020올해를 빛낸 인기 앱 우수상, 2020 올해를 빛낸 엔터테인먼트 앱 우수상을 받았습니다!

bestof2020

자랑 좀 하고 싶었어요

저는 2019년 3월에 입사하여 지금까지 만화경 안드로이드를 혼자 개발하고 있는데요

이 글은 저처럼 혼자 혹은 앱을 처음부터 개발하시는 분들을 위해 제가 어떤 부분을 고민하고 신경 써서 앱을 개발했는지를 공유하고자 합니다.

기술 블로그이긴 하나 심도 있는 기술보다는 제가 처음부터 안드로이드 앱을 만들면서 생각했던 것들을 경험 위주로 글을 풀어나가겠습니다.

모든 것의 시작

모든 것은 입사와 함께 시작합니다.

welcome

입사 후에는 바로 우아한형제들의 오리엔테이션 과정인 컬쳐캠프를 다녀왔고, 신규입사자 가이드에 있는 것들을 이것저것 세팅하고 나니 첫 주가 벌써 끝나버렸습니다. 그래서 제대로 된 개발은 2주차부터 시작하게 되었습니다.

이때 제게 주어진 상황은 5개월 이내에 앱을 완성해야 했고, 기획과 디자인이 미리 나와 있는 것이 아닌 같이 만들어나가야 하는 상황이었습니다. 그래서 아직 기획과 디자인이 픽스되지 않은 첫 주차 때 최대한 할 수 있는 환경설정을 다 해보려고 했습니다.

week_commits

개발 첫 주차에는 대체로 환경설정에 몰두했습니다.

위 커밋 메시지들을 보면

  • 컨벤션
  • 아키텍쳐
  • 코드 템플릿
  • 버전관리
  • CI/CD

이 항목들로 이루어져 있는데 이를 기반으로 어떤 식으로 만화경 안드로이드 앱을 만들었는지를 풀어나가 보겠습니다~

Day1 - 컨벤션의 날

day1

일단 코드를 작성하기 전에 규칙부터 세워야 한다고 생각합니다. 비록 혼자서 개발할지라도 언젠가는 팀원이 들어올 테고 통일된 규칙을 기반으로 코드를 작성하는 것이 좋다고 생각해서 일단 규칙부터 세우기로 했습니다.

rule

제가 적용한 규칙들은 총 4가지가 있습니다.

  • Code Style
  • Android Lint
  • Ktlint
  • Detekt
  • Git Hooks

Code Style

일반적으로 회사 혹은 오픈소스들에는 사용하는 스타일 가이드가 존재하는 경우가 많습니다. 저는 아래 스타일 가이드들을 참고하여 만화경 스타일 가이드를 만들었습니다.

image-20201122183131799

저는 안드로이드 스튜디오에서 일반적으로 사용하는 코틀린 스타일 가이드 및 xml 가이드를 적용하였습니다. 이 부분은 팀의 취향에 따라 갈릴 수 있습니다.

square_codestyle

예를 들어 Square의 안드로이드 코드스타일은 위와 같이 Indent가 2인 형태를 띠고 있습니다. 이 부분은 팀원의 취향껏 서로 합의하고 맞춰주시면 됩니다.

Android Lint

코드스타일을 설정해 뒀다면 그 이후에는 정말로 그 코드가 내가 원하는 규칙들에 따라서 작성됐는지 정적분석을 해주는 Lint를 돌리는 단계가 필요합니다.

일단 안드로이드의 기본적인 린트인 Android Lint부터 설정해줍니다.

lint_android

Android Lint는 앱 소스 파일과 lint.xml를 통해서 앱의 문제점들을 해결해 줍니다.

Ktlint, Detekt

Android Lint가 안드로이드의 관련된 디펜던시나 리소스 관련 부분들에 대해 정적분석을 해준다면 ktlint와 detekt는 kotlin 소스에 대한 정적분석을 실행합니다.

lint_ktlint

detekt

ktlint와 detekt는 다음과 같이 Kotlin 상에서 실수할 수 있는 부분을 고쳐주며 다양한 rule들을 통해 코드를 분석하여 html이나 xml과 같은 형식으로 받아서 볼 수 있습니다.

Git Hooks

이제 적용된 린트들을 깃에서 commit 혹은 push를 될 때마다 시켜주기 위해서 깃훅을 사용했습니다.

#!/bin/sh
#mac의 경우 위처럼 사용하면 되고 window일 경우는 !/bin/bash 사용

echo "정적 분석 중"

./gradlew ktlint detekt

status=$?

if [ "$status" = 0 ] ; then
    echo "이슈 없음"
    exit 0
else
    echo 1>&2 "이슈 있음"
    exit 1
fi

위와 같이 저 파일을 .git/hooks에 commit의 경우 pre-commit, push의 경우 pre-push라는 파일로 저장하면 깃훅이 잘 작동하게 됩니다. 하지만 이런 깃훅의 복사과정이 귀찮으면 다음과 같은 스크립트를 사용하면 새로운 개발자가 오면 좀 더 편하게 사용할 수 있습니다.

task copyGitHooks(type: Copy) {
    from("${rootDir}/teamConfig/git/git-hooks/") {
        include '**/*'
        rename '(.*)', '$1'
    }
    into "${rootDir}/.git/hooks"
}

task installGitHooks(type: Exec) {
    group 'git hooks'
    workingDir rootDir
    commandLine 'chmod'
    args '-R', '+x', '.git/hooks/'
    dependsOn copyGitHooks
}

만화경은 팀 설정에 관련된 파일은 모두 teamConfig 폴더에 모아두었고 ./gradlew installGitHooks 이나 ./gradlew copyGitHooks 명령어를 통해 해당 폴더에서 깃훅을 복사해서 사용하도록 스크립트를 짜놨습니다. 이 과정을 끝내면 이제 원하는 커밋 혹은 푸시 시 정적분석을 거쳐서 코드의 컨벤션을 지킬 수 있겠죠?

Day2 - 아키텍처의 날

day2

첫날에 규칙들을 설정해 뒀으니 두 번째 날은 앱의 아키텍처를 구성했습니다.

architecture

만화경은 Naivagation을 이용한 Single Activity를 가지고 ViewModel에는 여러 개의 비즈니스 로직에 해당하는 UseCase를 가지는 Testable한 구조로 설계했습니다.

사실 아키텍처의 경우 유행이 항상 바뀌고 언제든 새로운 개념이 등장할 수도 있기에 유연하게 바뀔 수 있어야 한다고 생각합니다. 그렇기에 만화경에서는 위와 같이 큰 개념만 정리하고 내부구현은 스스로 판단해서 개발하였습니다.

그렇기에 이번 포스팅에서는 아키텍처에 관한 부분은 다루지 않겠습니다. 아키텍처 및 사용한 라이브러리에 대해 세세한 설명까지 하려면 포스팅을 몇 번을 더해야 할지 모르니까요! ㅠㅠ

다만 참고하기에 괜찮은 Google의 샘플 코드를 첨부하겠습니다.

Day3 - 템플릿과 버저닝의 날

day3

두 번째 날에는 아키텍처를 짰습니다. 자 이제 전반적인 코드 베이스는 완성된 셈이죠. 그렇다면 더 해볼 것이 뭐가 있을까요?

바로 자동화를 위한 템플릿과 버저닝 전략을 구축하는 일입니다!

Template

아키텍처에 맞춰서 코드를 작성하다 보면 수많은 보일러 플레이트들이 발생하게 됩니다.

boilerplate

만화경의 경우에는 하나의 화면을 구성하게 될 때 위 그림과 같이 6~7개 이상의 파일이 발생하게 됩니다.

그림에서는 편의를 위해 하나의 패키지에 모든 코드를 몰아두었지만, 만화경의 경우 멀티모듈을 사용하기에 해당하는 모듈의 패키지를 찾아서 파일을 만들고 하는 더욱더 번거로운 작업들이 많았습니다.

이럴 때 보일러 플레이트들을 한 번에 생성하는 방법이 없을까? 라고 생각해본 적 있을 겁니다.

이럴 때 사용할 수 있는 방법이 바로 템플릿입니다.

템플릿을 생성하는 여러 방법이 있겠지만, 만화경은 Android Studio 4.1 이전에는 FreeMarker라는 템플릿 엔진을 사용했습니다.

  • FreeMarker (Android Studiod 4.1부터 지원되지 않음)

하지만 Android Studio 4.1부터 FreeMarker를 이용한 템플릿이 지원이 되지 않아서 intelij-platform-plugin-template을 사용해서 만화경 전용 템플릿 플러그인을 만들어서 사용했습니다.

plugin1

플러그인을 만들어서 설치해주고

plugin2

New에서 해당하는 템플릿을 클릭하게 되면

plugin3

위와 같이 각 모듈에 맞게 클래스 및 레이아웃들이 생성됩니다.

만화경의 경우 처음부터 앱을 만들었기 때문에 이런 보일러 플레이트를 줄이는 방법이 필요하였고 과거에는 FreeMarker, 지금은 직접 템플릿 플러그인을 만들어서 위와 같이 많은 부분을 자동화할 수 있었습니다.

플러그인을 만드는 방법을 설명하고 싶지만… 그러기엔 너무 길어지기에 따로 포스팅하는 것이 낫다고 생각하여 나중에 한 번 다시 템플릿 플러그인을 만드는 법을 가지고 포스팅해보겠습니다. 일단 아래 링크를 참조해주세요!

버저닝

앱을 만들다 보면 우리 모두 버저닝에 대한 생각을 하게 됩니다. 1.16.2에서 1, 16, 2 들이 해당하는 의미가 무엇인가.? 이런 고민을 한 번쯤은 해봤을 거로 생각합니다.

가장 확실한 방법은 이미 잘 사용하고 있는 누군가의 것을 가져다가 쓰는 것이 아닐까요?

sematic_versioning

그래서 만화경은 GitHub 의 공동창업자인 톰 프레스턴 베르나가 만든 Semantic Versioning을 기반으로 버전 관리를 하고 있습니다. 구글도 Semantic Versioning을 사용합니다.

그렇다면 실제로 만화경에서는 어떻게 버전 관리를 할까요?

Build Environment의 경우는 만화경에서는 debug, beta, state, release 총 4가지의 빌드타입을 가지고 있고 각 빌드타입별 네이밍을 가지고 갑니다.

  • debug : snapshot
  • beta : beta{1~99} (beta의 경우는 QA의 편의성을 위해 베타 버전에 따라 뒤에 추가적인 넘버링을 해줍니다)
  • stage : stage
  • release : 없음

전체적인 조합의 예를 들어보자면

  • 1.0.0.100-snapshot : debug 빌드타입의 1.0.0 버전
  • 1.0.0.101-beta1 : beta 빌드타입의 1번째 1.0.0 버전
  • 1.0.0.102-beta2 : beta 빌드타입의 2번째 1.0.0 버전
  • 1.0.0.103-stage : stage 빌드타입의 1.0.0 버전
  • 1.0.0.104 : release 빌드타입의 1.0.0 버전

버전에서 1.0.0 뒤에 붙는 100~104는 CI에서 붙여주는 빌드넘버이기에 기호에 따라서 빼거나 무시해도 상관없습니다만 같거나 낮은 버전코드를 사용하는 apk의 경우 설치가 되지 않는 문제가 있으므로 1씩 증가하는 빌드넘버를 뒤에 붙여주고 있습니다.

그렇다면 이런 네이밍룰을 정했으니 이걸 앱에 적용해 봐야겠죠

versioning1

안드로이드에서는 위 그림과 같이 gradle을 통해 versionCode와 versionName을 정해줘야 합니다.

만화경에서는 kotlin dsl을 통해

  • buildVersionCode()
  • buildeVersionName()

이 함수들을 통해서 조금 더 편하게 버저닝을 하고 있습니다.

versioning2

일단 이런 버전에 대한 정보를 가지고 있는 version.properties를 만들어준 후

versioning3

위와 같은 extensions를 사용하여 버전 명을 versions.properties만 고치면 각 타입에 맞는 버전 명을 만들어주고 있습니다.

다시 버전 코드를 만들어주는 코드만 다시 보면

val versionCode = (major * 100_000_000) + (minor * 1_000_000) + (patch * 10_000) + buildNumber()

이런식으로 되어있는데 주의할 점으로는 안드로이드의 경우 버전코드의 최대 값은 INT_MAX인 2,147,483,647이므로 버전코드가 이 값을 넘어가면 안됩니다.

만화경의 버저닝 방식대로라면 major의 최대는 21입니다. 21번째의 major 업데이트를 하게 된다면 버저닝 방식을 조금 수정해야겠지만 그런 일은 일어나지 않을 거라 생각합니다… 물론 필요에 따라 patch나 buildeNumber가 가지는 범위를 줄이는 방식으로 개선해 나가도 상관없습니다.

Day4 - CI/CD의 날

day4

CI/CD는 앱을 테스트 및 배포하려면 필수적인 과정입니다. CI 환경을 구축하는 방법으로는 여러 가지가 있지만, 만화경에서는 Bitrise라는 서비스를 이용해서 CI 환경을 구축했습니다. Bitrise를 사용한 이유는 배달의민족 iOS팀에서 이미 사용하고 있고, 만화경에서는 Android와 iOS 모두 같은 플랫폼 위에서 구성하고 무엇보다도 쉬운 편의성 때문에 사용했습니다.

Bitrise

bitrise에서는 workflow라는 작업단위를 만들어서 사용합니다. 만화경에서는 이런 worklflow를 상황에 따라 쪼개서 사용 중인데 실제로 빌드 과정에 맞춰서 설명하겠습니다.

bitrise1

Bitrise에서는 Push, PR등에 트리거를 걸어서 원하는 타입의 빌드를 수행할 수 있습니다

이를 통해서 내가 원하는 트리거, 혹은 직접 선택해서 빌드를 할 수 있는데 만화경 안드로이드에서는 매 푸시 및 PR마다 항상 빌드를 돌려주는 편입니다. bitrise는 빌드 머신 수에 따라 가격을 내고 있으니 놀지 않고 계속 돌리는 것이 이득이 아니겠습니까..? 다만 release의 경우는 안전을 위해서 트리거를 사용하지 않고 직접 클릭해서 돌려주고 있습니다.

init

bitrise2

첫 단계는 init이라는 workflow를 거칩니다.

git에서 프로젝트를 clone을 받고 여러 가지 secret한 properties들을 가져오고 google-services.json을 다운받고 등등.. 프로젝트를 빌드하기 위한 기본적인 workflow 입니다. 이런 workflow에서 secret한 파일들이 있을 텐데요 그런 파일들은 bitrise에서는 따로 보관합니다.

bitrise3

bitrise4

Code Signing 탭에서는 keystore, google service 등 secret한 파일에 대해 저장을 할 때 사용합니다.

bitrise5

Secrets 탭에서는 파일이 아닌 key-value 형태로 값들을 저장해서 사용할 수 있습니다.

만화경의 경우는 Bitrise의 권한을 개발자만 가지고 있기에 PR 시 편의를 위해 Expose for PR을 체크한 상황입니다만 민감한 정보의 경우 체크를 해제하고 다른 전략을 가져가도 무방합니다. 이외에도 Env Vars탭에서 workflow별로 환경변수를 설정할 수 있는데 만화경에서는 프로젝트 내부에서 각 빌드타입 및 flavor별로 properties를 사용하여 잘 사용하지 않습니다. 취향껏 원하는 대로 구성하면 됩니다. 정답은 없으니까요~

각 build type 및 flavor 빌드

만화경에서는 debug, beta, state, release 총 4개의 빌드타입이 있습니다. 그리고 2개의 mock, real이라는 flavor dimesions가 있습니다. 이들 조합에 따라서 빌드를 다르게 해야 하기에 조합별로 각각 빌드를 해도 되지만 debug 빌드에서만 mock api를 사용하므로 mockDebug, realDebug 두 개로 구분하고 나머지는 beta, stage,release는 모두 real을 사용하게 되어 총 5개의 빌드를 사용합니다.

각 빌드별로 중복되는 항목들이 많으므로 가장 보편적인 release 빌드를 기준으로 설명하겠습니다.

bitrise6

먼저 2일 차에 설정해뒀던 린트들을 다시 한 번 돌려줍니다. 혹여 깃훅이 동작하지 않거나 잘못된 방법으로 푸시를 하면 코드 퀄리티를 떨어뜨릴 수 있으니까요. 그 후 테스트를 돌린 후 APK와 AAB(Android App Bundle)을 만들어줍니다. 왜 APK와 AAB 둘 다 만드느냐고 할 수 있는데 빌드 이후 테스터들이 사용할 Firebase App Distribution에서 APK만 지원해주기 때문에 APK빌드도 함께 합니다. 빌드 후 사이닝을 거쳐 Android Manifest에 있는 버전 정보를 가져오는 작업을 거칩니다.

Deploy, Tag, Slack

여태까지의 과정을 통해서 우리는 APK와 AAB를 얻었습니다… 이제 배포만이 남았군요

bitrise7

우선 Firebase App Ditribution을 통해 APK를 배포해줍니다. 그 외 Bitrise에서 제공하는 deploy기능도 사용합니다.

Firebase App Ditribution에 앱을 배포하게 되면 테스터 권한이 있는 사용자는 App Tester라는 앱을 다운받게 됩니다.

bitrise8

App Tester를 통해 빌드 및 버전별로 원하는 APK를 다운로드 가능합니다.

  • 개발단계에서 디자인 적용사항이 궁금한 디자이너 - debug
  • QA 엔지니어 - beta
  • 웹툰 피디님 - stage
  • 과거 버전이 궁금한 사람 - release

이런식으로 테스터분들께서 원하는 버전을 다운받아 확인할 수 있겠죠? debug, beta, stage는 위 firebase app distribution을 통해 deploy과정이 끝나지만, release 빌드의 경우 google play store의 내부 테스트로 배포를 한 단계 더 해줍니다. 그 후 git tag 과정을 거쳐준 후 slack에 배포의 성공/실패 여부를 알려주게 되면 모든 과정이 끝나게 됩니다!

bitrise9

모든 과정이 끝나면 슬랙에서 실패/성공 여부를 알 수 있습니다.

위 메시지 같은 경우는 테스트를 빼먹고 성급하게 푸시를 했던 저 자신을 돌아볼 수 있네요… 이런 식으로 CI/CD를 통해 잘못되었을 경우 슬랙을 통해서 피드백을 받을 수 있고 잘 통과했을 경우 테스터들에게 테스트 요청을 해서 간단하게 배포를 진행할 수 있는 장점이 있습니다.

물론 CI/CD 구성에서도 딱 정해진 규칙이나 정답은 없기에 팀에 좀 더 맞고 도움이 되는 방식으로 구성하면 되겠습니다~

Day5 - 기능구현의 세계로

day5

네 슬프게도 5일차부터는 바로 기능구현의 세계로 뛰어들었습니다.

마무리

구현하면서 아쉬웠던 점도 물론 있지만. 초반에 큰 틀을 잡고 갔기 때문에 무리 없이 개발해 나갈 수 있었던 것 같습니다.

또한, 개발하다 보니 아무래도 2년이 좀 안 되는 시간 동안 기존의 프로젝트에서 변경한 부분들도 많았습니다. 예를 들어

  • Fabric Beta가 없어져서 Firebase App Distribution으로 마이그레이션
  • RxJava2에서 RxJava3로 마이그레이션
  • Dagger Android에서 Dagger Hilt로 마이그레이션
  • 멀티모듈 적용
  • Gradle Kotlin DSL로 마이그레이션
  • FreeMaker에서 Intelij plugin으로 템플릿 마이그레이션

기억나는 것들은 이 정도지만 사실 더 많은 변화를 겪었습니다.

물론 변하는 것이 많지만 변하지 않는 한 가지 사실도 존재합니다…

입사 2년이 다 돼가지만, 여전히 저는 혼자 만화경 안드로이드를 개발하고 있습니다…

같이 만화경 안드로이드를 개발하실 멋진 개발자분들을 구합니다~!

이 외에도 개발하면서 도움이 되었던 여러 가지 것들이 있지만, 이것을 다 쓴다면 정말 포스팅이 길어질 수 있으므로 생략하겠습니다.

그러므로 슬슬 이제 글을 마무리하겠습니다. 긴 글 읽어주셔서 감사합니다!