안드로이드 8.0 (API Level 26) 전환 대응 체크리스트

Intro

안드로이드 8.0으로 마이그레이션 할 때 compileSdkVersion 과 targetSdkVersion 올리는 것 외에 호환성 관련 작업을 해야 하는 것들이 존재한다. 실무에서 버전별로 대응해야하는 Migration Checklist를 만들어 정리하려고 한다. 더 나아가 Android P (API level 28 ) 전환 대응 체크리스트에 대해서 가볍게 언급하며 마칠 예정이다.

compileSdkVerion vs minSdkVersion vs targetSdkVersion

시작하기에 앞서, compileSdkVersion, minSdkVersion, targetSdkVersion의 차이를 간략하게 설명하고 시작하겠다.

  • compileSdkVersion은 빌드가 컴파일 될 때 사용되는 버전이다. 이는 런타임 동작에 영향을 주지 않으며, 컴파일 타임에만 영향을 준다
  • minSdkVersion은 구글 플레이에서 어떤 디바이스까지 다운로드가 가능한지 사용되는 로우바운드라고 생각하면 된다. 런타임에 영향을 주며, lint를 통해서 개발자들에게 앱에 미칠 수 있는 영향에 대해서 알려준다
  • targetSdkVersion은 어느 버전까지 테스트가 완료되었는지 Android OS한테 알려주는 값이다. 이곳에서. 정의된 수치로 Android OS 기능들을 런타임에 판단하게 된다

만일 targetSdkVersion을 명시하지 않는 경우에는, minSdkVersion으로 대체되어서 사용된다. 허용되는 범위와 이상적인 수치는 아래와 같다

Checklist: Notifications

안드로이드 8.0 부터 Notification 동작 일관성을 보장하기위해서 하나 이상의 Notification Channel 을 구현해야하게끔 변경되었다. 선언하지 않은 경우 API Level 26을 사용하게 되면 푸쉬가 아예 안 오는 현상이 발견된다.

이와 같이 변경되는 스펙으로 인해 다양한 기능들이 영향을 받는데, 아래와 같다:
– Notification Dots (앱 런처 뱃지 구현)
– Notification Snoozing (특정 시간 뒤에 Notification이 다시 오게 구현)
– Notification Timeout (Notification 생성 시 특정 시간 이후에 자동으로 사라지게 구현)
– Notification Setting (setSettingsText() 통해 App Notification Setting 이동시 노출되는 문구 구현)
– Notification Dismissal (onNotificationRemoved() callback 통해서 사용자의 액션인지, OS 에서. 자체적으로 실행된건지에 따라서 동작 분기 구현)
– Notification Background colors (setColor(), setColorized() 를 통한 Notification 설정 고도화 기능 구현)
– Notification Message Style (Chat History 등 메세지 특화 기능 구현)

Checklist: Background Execution Limits

백그라운드 Services 와 Broadcast Receivers의 제한이 많이 생겼으며, 간단하게 백그라운드에서 자유도가 많이 떨어졌다. Android API level 26부터 startService() 호출을 백그라운드에서 실행 할 경우 IllegalStateException 로 앱이 멈추는 현상이 발견된다.

OS 에서 자체적으로 분류한 화이트리스트는 아래와 같다:
– High priority FCM/GCM notification 에서 호출되었을 경우
– SMS/MMS 수신의 broadcast 를 받는 경우
– Notification 에서의 Pending Intent 를 처리하는 경우
– VPN 앱을 foreground 호출 전 VpnService 를 실행하는 경우

또한 7.0 부터 제시되던 implicit broadcast 제한의 정도가 더 강해졌다. 하지만 여전히 몇개의 implicit broadcast 는 이 제한으로부터 자유로운데, 항목(https://developer.android.com/guide/components/broadcast-exceptions) 을 확인 해보는 것을 추천한다.

TOOD: 백그라운드에서 startService() 의 사용을 startForegroundService() 으로 전환. 백그라운드 작업이 필요한 경우 JobScheduler 로 로직 전환이 가능하다.

Checklist: The List.sort() & Collections.sort() method

안드로이드 7.x 에서 List.sort() 는 Collection.sort() 를 호출했다. 현재 8.0 부터는 Collections.sort() 에서 List.sort() 를 호출하게 되었다. 고로 API Level 26부터 List.sort()의 구현체에서 Collections.sort()의 호출이 존재한다면 무한 루프 혹은 ConcurrentModificationException 을 던질 것이다.

TOOD: List.class 를 상속받는 객체의 sort() 를 override 하는지 확인 후 최신 API에 맞게 대응.

Checklist: Privacy

안드로이드 8.0 (API level 26) 부터 프라이버시 관련 변화들이 생겼으며, net.dns1, net.dns2, net.dns3, or net.dns4 시스템 프로퍼티가 null 을 반환한다. 사용 하는 프로젝트의 경우 아래와 같이 콘솔 로그에서 발견이 가능하다:

TOOD: DNS 관련 작업이 필요한 앱의 경우 아래의 링크를 참고해서 Migration을 하면 된다.
– https://stackoverflow.com/questions/3070144/how-do-you-get-the-current-dns-servers-for-android/48973823#48973823
– https://github.com/creytiv/re/pull/142/files

Conclustion

위에 언급된 항목들 가장 흔하게 확인되는 케이스들이다. 일반적이지 않는 앱의 경우 구글 공식 migration document를 참고하여 Migration에 응해야한다.

  • [ ] Notification Channel 및 신규 기능 확인
  • [ ] Background Services and Broadcast Receivers 관련 확인
  • [ ] List & Collection sorting ConcurrentModificationException 관련 확인 작업
  • [ ] Privacy 관련 net.dns 관련 확인
  • [ ] Background location limits 관련 대응
    • [ ] Fused Location Provider, Geofencing, GNSS Measurements, Location Manager, Wi-Fi Manager 백그라운드에서 사용시 대응
  • [ ] Currency.getDisplayName(), Currency.getSymbol(), Locale.getDisplayScript() 변화 대응
    • [ ] Since API level 26 Locale.getDefault() -> Locale.getDefault(Category.DISPLAY) 대응
    • [ ] Currency.getDisplayName(null) NPE 대응
    • [ ] Alert windows SYSTEM_ALERT_WINDOW 변화 대응
    • [ ] Bluetooth ScanRecord.getBytes() 변화 대응
    • [ ] findViewById() 변화

Future Actions for Migration to Android P (API level 28)

  • [ ] Restrictions on non-SDK interfaces 대응
    • [ ] direct, via JNI, or via reflection call 확인
  • [ ] Removal of Crypto provider 대응
    • [ ] SecureRandom.getInstance(“SHA1PRNG”, “Crypto”) 호출 확인
  • [ ] Stricter UTF-8 decoder 대응
    • [ ] Base64.decode 관련 Util class 들 확인
  • [ ] Idle 상태에서의 카메라, 마이크, 센서 접근 확인
  • [ ] Wi-Fi location 관련 권한 확인
  • [ ] READ_CALL_LOG 권한 없이 모바일 폰 번호 및 상태 접근 확인
  • [ ] CALL_LOG, READ_CALL_LOG, WRITE_CALL_LOG, and PROCESS_OUTGOING_CALLS 권한 변경 확인
    • [ ] SecurityException 노출 확인
  • [ ] android.icu package 관련 코드 확인
    • [ ] SimpleDateFormat “zzzz” 포맷 변환 대응
    • [ ] DateFormatSymbols.getZoneStrings() 변환
    • [ ] NumberFormat.getInstance(ULocale, PLURALCURRENCYSTYLE).parse(String) 대응
  • [ ] TestSuiteBuilder addRequirements() 메소드 제거 대응 확인
  • [ ] Socket Tagging 관련 대응 확인

Reference

  • https://medium.com/google-developers/picking-your-compilesdkversion-minsdkversion-targetsdkversion-a098a0341ebd
  • https://developer.android.com/about/versions/oreo/android-8.0-changes
  • https://developer.android.com/about/versions/oreo/android-8.0-changes#privacy-all
  • https://developer.android.com/about/versions/oreo/android-8.0#notifications
  • https://developer.android.com/about/versions/oreo/android-8.0-changes#o-ch
  • https://stackoverflow.com/questions/3070144/how-do-you-get-the-current-dns-servers-for-android/48973823#48973823
  • https://medium.com/exploring-android/exploring-background-execution-limits-on-android-oreo-ab384762a66c

사내에서 눈치를 안 보고 오픈소스 프로젝트 하기

Intro

“Heroes must see to their own fame. No one else will.” —Gore Vidal, Jullian

자기 자신을 상대보다 낮춰서 상대가 알아주길 바라는 시대는 끝났다. 허세에서 비롯되는 자의식 과잉을 추구하라는 것은 아니다. 단지 15년 기준 SW 종사자가 25만 명을 바라보는 시점에, 셀프 마케팅과 셀프 프로모션은 때려야 땔 수 없는 생존 수단이 되어버린 것이다. 오픈소스는 이러한 소프트웨어 개발자에게 자신을 알릴 수 있는 통로로, 더 나아가 다양한 개발자들과 공유, 피드백, 수정하며 더 나은 제품을 만들어가는 삶의 방식이 되었다.

기업의 제품 또한 오픈소스의 매우 밀집한 관계가 있다. 네트워크 통신, 데이터베이스, 테스팅, 이미지 처리, 디버그… 대부분의 핵심 모듈에서 이미 오픈소스 라이브러리를 응용하고 있을 것이다. 개발자들은 제품 개발을 위해 오픈소스의 프로젝트를 사용할 때는 큰 걱정 없이 사용한다. 단지 막상 업무시간에 오픈소스 프로젝트를 만들어서 많은 개발자들에게 공유하려고 하지는 않는다.

“업무 시간에 오픈소스 프로젝트를 하면 스스로 눈치가 보이는 것 같아요. 개인 프로젝트인지 회사 업무인지 경계가 모호해서…”

회사 업무와 오픈소스 병행하기

회사 업무와 개인 프로젝트의 경계를 확실히 하기 위해선, 확고한 주관과 규칙이 필요하다. 아래의 몇 가지 항목들은 개인적으로 사용하는 철칙들이다.

1. 존재하는 솔루션에 시간 낭비 하지 않기

회사 제품의 기능을 구현할 때 좋은 솔루션이 이미 존재한다면, 굳이 지식을 뽐내기 위해서 시간 낭비를 하지 말라. 어차피 만들어도 개발자들이 굳이 사용하지 않을 것이다. 존재하는 솔루션을 찾기 위해서 수많은 검색과 웹 브라우징을 마쳐도 여전히 못 찾겠다면 다음 단계로 넘어갈 때이다.

구상한 방법을 하나의 모듈화된 솔루션으로 추상화시킬 수 있겠고, 유지보수, 사용성 면에서 더 뛰어나다면 그때 프로젝트를 공개해서 사람들의 의견을 수집해라.

충실하게 시간 낭비하기

2. 회사에서 사용하는 오픈소스 프로젝트의 Issue 티켓들을 모두 확인하라

계속 유지되고 있는 건강한 오픈소스 프로젝트일 경우, 티켓들만 확인하더라도 일정을 파악할 수 있다. 버그 수정이나 기능의 배포 예정일이 언제인지, 누가 진행하고 있는지를 파악해서 큰 그림을 파악해야 한다. 죽은 프로젝트일 경우 과거 Maintainer에게 연락을 해서 프로젝트의 권한을 넘겨받고 싶다고 연락도 해보아라. 물론 Fork를 떠서 PR부터 넣는 것부터 시작해야 할 것이다. 티켓들을 모두 확인하고 나면, 더 나은 솔루션을 만드는 것과 기존의 것을 고쳐서 사용하는 것의 장단점들이 파악될 것이다.

3. 이해관계의 조율에 신경을 써라

모든 업무에는 마감일이 있고, 다양한 이해관계들이 엮여있다. 오픈소스를 하기 위해 다른 일정을 연장한다던가, 개인의 욕구를 먼저 충족시키기 위해서 다른 요소들을 무시하는 건 지양해야 한다. 사내에서 오픈소스를 하는 것은 최우선으로 회사의 제품을 발전시키기 위함인 것을 잊으면 안 된다. 그걸 원치 않으면 오픈소스는 개인 시간에만 하고 회사를 그만두는 것이 양측에 도움이 될 것이다.

4. 솔루션을 만들었다면, 알려라!

제품을 만들고 홍보를 하지 않으면 많은 잠재적 사용자들을 놓치는 셈이다. 오픈소스 프로젝트 또한 마찬가지이다. 다양한 사용자들이 써야 피드백도 받고, 더 좋은 API도 개발하고, 제품이 성장해나가는 것이다. 공개 깃에 올려두기만 하고 아무도 쓰질 않는다면, 복붙 코드와 다를 바가 없다.

겸손의 시대는 가라: 셀프 마케팅 시대

1. README, CONTRIBUTING, LICENSE 작성

귀찮더라도 README, CONTRIBUTING, LICENSE, 등. 마크다운 파일들을 꼭 작성해두자. 요즘은 깃허브에서 쉽게 클릭 몇 번으로 작성을 가능하게 해줘서 훨씬 편한 편이다. 특히 README 의 경우 사용자가 가장 최초에 접근하는 페이지라서, 첫인상이 시각적으로 보기 편하면 사용 수가 확연하게 차이가 난다. 또한, 마크다운으로 작성해놓을 시 다양한 사이트에서 크롤링해서 가져가기 때문에 더 다양한 유입 채널이 생기는 것이다. 프로젝트들에 라이선스가 명시되지 않아서 사용을 못 하는 경우가 많이 존재한다, 라이선스는 꼭 명시하자!

2. gh-pages 이용한 페이지 개설

Gh-pages 브랜치를 만들어서 html 페이지를 추가한 뒤 푸쉬 하게 되면 .github.io/ 식의 무료 호스팅이 제공된다. 랜딩 페이지로 사용하게 되면 검색 엔진에서 노출 순위도 높아지고, 개발자가 아닌 직군이 프로젝트를 볼 때를 위해 접근성을 확장할 수 있다.

ButterKnife의 랜딩 페이지

3. 블로그 이용

개발 블로그가 있다면, 새로운 솔루션을 왜 만들게 되었고, 어떤 문제를 해결하였고, 예제 코드와 사용법 등을 설명하는 글을 쓰자. 검색에 노출되는 것은 물론이고, 사용자가 개발자의 공감대를 형성하는 과정이므로, 미래의 컨트리뷰터를 만나게 되는 몇 안 되는 수단일 수도 있다. 주의해야 할 점은, 유지 보수 시 관리 포인트가 증가하는 점이다. 깃과 블로그에서 설명하는 API가 다를 경우 많은 혼란을 줄 수 있으니 꼭 유의 해야 한다. 개인적으로 떠오르는 나쁜 예로는 SugarORM 프로젝트에서 버젼 관리와 문서 노후화이다.

너 덕에 고생 좀 했다…

4. 다양한 개발 채널 이용

개발 채널은 너무 많아서 정리하기가 힘들지만 크게 개인적으로 사용하는 것들은 아래와 같다:

a) GDGkr 채널, Facebook 생활 코딩, Twitter

휘발성이 좀 강하긴 하지만, 사용자 접근이 쉬운 채널들이다. 개인정보와 함께 노출되어서 좋아하지는 않지만, 국내에서 영향력은 확실히 있는 편.

b) Androiddev Subreddit, MaterialUp – UpLabs

Reddit 의 androidDev subreddit 은 다양한 분야의 정보 공유 채널로 이용되는데, 안드로이드의 경우 다양한 셀프 프로모션 글들이 올라온다. 건설적 피드백도 많이 주는 편이라서 애용하는 편이다. MaterialUp – UpLabs 의 경우 특성상 디자이너들이 많이 보게 되며, 클라이언트 라이브러리 위주로 많이 올라온다. 시각적으로 매력적인 샘플 앱을 작성하고 README.MD 파일에 추가 해두면, 관리자가 자체적으로 긁어간다.

c) Github issue 댓글, Android Arsenal

Android Arsenal 의 경우 안드로이드 라이브러리의 집합소 같은 곳인데, 이미지 규격, 글 길이, 등 구체적인 제한 사항들이 있어서 업로드 전에 잘 확인 해야 한다. 업로드가 끝나고, 관리자의 검수가 끝나야 페이지에서 노출이 된다.

깃허브 댓글은 해당 문제가 있는 라이브러리의 이슈 티켓에 다는 것이 좋다. 프로젝트가 더는 관리되지 않거나, 어떤 사유로 포함이 안 되었을 경우, 별도로 포팅 시켜서 공유하면 필요한 사람들이 사용하게 되는 것이다.

d) RSS 피드와 뉴스레터

본인의 블로그 RSS 피드를 활성화해놓을시, 스크랩하는 사용자들도 있기 때문에 같은 맥락에서 블로그 운영을 주기적으로 하는 것을 추천한다. 또한, 국내 개발자 블로그 및 RSS 피드를 정리하는 오픈 리포같은곳에 추가가 되어있다면, 더 많은 사용자에게 노출된다.

안드로이드의 경우 뉴스레터가 크게 두 가지가 있는데, Android Dev Digest 와 Android Weekly 이다. 두 곳 모두 투고 형식으로 글이 올라가는데, 최종 검수자가 일주일 치 투고된 글들과 라이브러리들을 종합해서 매주 발행하는 방식이다. 경쟁이 좀 있는 편이라, 안 뽑히는 경우가 많으며, 뉴스레터 특성상 트랜드가 지났을 경우에는 선정 안 되는 경우가 발생하니, 지속해서 지켜봐야 한다.

오픈소스를 하면 생기는 장점들

오픈소스 프로젝트에 참여하게 되면 개발자로서 많은 경험을 할 수 있게 된다. 수많은 회사를 거쳐서 쌓을 수 있는 축적된 노하우들을 오픈소스 프로젝트를 진행함으로써 간접적으로 체험할 수 있다.

코드 리뷰

다양한 개발자들에게서 코드 리뷰를 받을 수 있게 되며, 수많은 사고방식과 디자인 패턴 등에 노출되면서 자연스레 코드를 비판적으로 보는 실력이 는다. 다양한 언어로 자기 생각을 풀어내는 경험은 아무 회사에서 쉽게 경험하지 못하며, 다양한 실력 군의 사람들과 의사소통 하는 경험은 어느 다국적 대기업에서 경험하는 것 그 이상이다. 또한, 공개된 장소에 코드를 공유하는 만큼 더 신중하고 깊게 코드를 작성하고 공유하게 되는 습관이 생기며, 이것은 더 성숙한 개발자가 되는 과정에 필수 요소라고 생각한다.

프로젝트의 성장 과정 경험

한 회사에서 장기간 근속을 하는 경우, 기발하던 코드 뭉치들이 레거시코드가 되는 과정을 지켜보는 경험을 할 수 있겠지만, 상대적으로 경험이 부족한 개발자들은 이러한 일을 쉽게 경험하지 못한다. 오픈소스는 이러한 기술 부채가 생기는 과정, 레거시 코드가 생기는 과정 등을 보며, 어떤 방향으로 프로젝트가 변화하는지 이해할 수 있다.

오픈소.. 읍읍!

바람직한 API 설계 습관

API 의 백포팅, 함수 매개변수 설정, 상황에 적합한 패턴 판단하기, Fast Failing API, 등. 사용자 (개발자) 입장에서 설계하는 습관을 들이게 된다. 다른 개발자들이 빌드를 깨트리는 것을 방지하기 위해서 Testable 코드를 작성하는 것이 우선시 되게 된다. 결과 우선주의가 통용되는 기업 문화에서는 경험하기 힘든 것들을 자유로운 오픈소스에서는 당연시하게 되는 것이다.

결론

빠르게 변화하는 개발의 세계인만큼 오픈소스가 선택이 아닌 필수가 되는 시대가 올 것이라고 조심스레 상상해본다. 깨어있는 시간의 반을 회사에서 보내는 개발자로서, 넓은 세상의 좋은 코드들을 개인 시간에만 만져 볼 수 있다면, 얼마나 슬플까. 세상은 넓고, 좋은 프로젝트는 많다!

Reference

  • https://android.jlelse.eu/things-i-wish-i-knew-when-i-started-building-android-sdk-libraries-dba1a524d619
  • http://jakewharton.github.io/EffectiveOpenSource/#/overview

RxJava 1 과 비교해서 정리한 RxJava 2

RxJava 1 적용한지 얼마 안 되었는데, 벌써 RxJava 2 라니, 말도 안 돼! 심지어 3.X 에 대한 정보도 심심찮게 들리는데…

서론

RxJava1과 RxJava2 를 먼저 비교하기에 앞서, 왜 이 진입 장벽 높은 라이브러리가 버전업을 했는지부터 알아야 할 필요가 있다. Reactive Extension (Rx)은 옵져버 패턴을 이용해서 이벤트 기반 비동기식 프로그래밍을 할 수 있게 만들어준 라이브러리다. 다양한 언어에서 이 Rx의 프로토콜을 이용할 수 있게 기준이 되는 것이 Reactive Stream Specification인데, RxJava 1은 이 기준에 맞지 않는 것들이 많아서, 버전업을 시키면서 완전히 처음부터 다시 작성하기로 결정된 것이다.

다른점

RxJava 2 에서 변경된 것들은 많지만, 일단 대분류로 그 중 인상적인 것들만 모아봤다. 이들을 제외하고도 달라진 점들은 꽤 많으니, 직접 진행하는 프로젝트에 일단 넣어보고 고민하는 것도 괜찮은 방법이다. 목록이 긴 관계로 관심 있는 대분류를 클릭해서 보면 편할 것이다.
Observable.create
Null
Functional Interfaces
Testing
Observable vs Flowable
Single, Completable and Maybe

결론

RxJava1 을 사용하면서 이미 Backpressure 에 대한 이해가 있었다면, 빠른 시일 내에 업데이트하는 게 좋을 것 같다. Dagger 1 에서 Dagger 2로 넘어가는 것만큼 사고의 변환이 크지 않고, Technical Debt 기간이 길어질수록 이자가 쌓여서, 어차피 나중에 전환할 때 더 힘들어지기 때문이다. 이 블로그와 같이 90% 이상이 RxJava 1 베이스인 경우라면, PR을 여러 기능별로 끊어서 넣고 배포 때 플레이스토어의 Staged Rollout 기능을 통해서 예상치 못하는 이슈들을 잡아나가면서 진행하는 걸 추천한다.

Observable.create()

RxJava 1의 가장 큰 진입 장벽의 하나던 .create() 함수가 전면적으로 개편되었다. API를 처음 배울 때 가장 처음 확인하게 되는 .create() 함수를 올바르게 쓰기가 너무 어려운 관계로 .just().defer() 을 함께 많이 이용했는데, RxJava 2에서부터는 Flowable 의 경우 .create() 의 경우 BackpressureStrategy 를 미리 지정해줘야 하게 된다. Back pressure 을 책임져야 하는 존재는 source이어야 하며, chain이 아니라는 철학이 반영되었다.

Null

RxJava 1 에서 자주 보이던 Observable.just(null), 특히 RxBinding 을 같이 쓰는 프로젝트에서 아주 자주 보게 되는데, RxJava 2 에서는 더는 null을 정상적인 값으로 받거나 넘기지 않게 된다.

위와 같이 null을 값으로 받거나 넘기게 될 경우, NullPointerException 으로 취급해서 에러로 핸들 하게 된다. Observable 의 경우 오로지 완료되거나 오류를 던지는 것 외에는 값을 넘길 수 없게 되며, API 제작자들은 enum 타입으로 분리하던 객체를 이용해서 Observable 처럼 내려줄 객체를 정의해야 한다.

Functional Interfaces

RxJava 1.X 와 2.X 모두 Java 6의 호환을 고려하고 설계되어서 Java 8의 java.util.function.Function 에서 제공하는 Functional Interface 들을 사용하지 못해서, RxJava 내부에서 별도로 정의했었다. 단지, 이제 RxJava 2.X부터는 Exception을 핸들 하게 되어서, 더는 .map() 안에서 try-catch 코드를 보지 않아도 된다! 예를 들면, 아래와 같다:

Testing

RxJava 1.X의 TestSubject 가 드디어 없어졌다! TestSchedulerPublishSubject 를 통해서 손쉽게 테스팅을 할 수 있게 되었다. 개인적으로는 유닛 테스트 작성할 때 PublishSubjectRxJavaPlugins로 구현했었는데, 이제 injection을 통해서 스케쥴러를 주입할 수 있게 짜는 방향으로 가게 될 것 같다.

Observable vs Flowable

상용 프로젝트 환경에서 Rx 스트림 내에서 MissingBackpressureException 혹은 OutOfMemoryError 을 겪게 되면 매우 대응하기가 난감하다. 특히 RxJava 1 에서는 백프레셔를 대응할 수 있는 오퍼레이터인지 아닌지, 그리고 어떻게 대응을 할지에 대해서 알기 위해선 일단 한번 당하고(?) 문서를 확인하며 배우는 방식이다. 근본적으로 Hot Observable은 올바르게 백프레셔를 처리하기 힘들며, 애초에 RxJava 1.X에서는 Hot Observable과 Cold Observable으로 명확하게 나뉘어있지 않기 때문이다.

RxJava 2.X에서는 이 둘을 명확하게 나누기 위해서 백프레셔가 일어나지 않는 Observable과 백프레셔를 주의해야 하는 Flowable로 나누게 되었다. 아래는 RxJava 위키에서 정의하는 Observable과 Flowable 을 사용해야 할 때를 정리한 것이다:

Observable을 사용 해야 할 때

  1. 흐르는 객체에 따라 다르겠지만, OOM이 일어날 수 없을 정도의 객체의 개수를 관리할 때
  2. 터치이벤트나 클릭 좌표 이동 등 sampling 또는 debouncing으로 쉽게 핸들 할 수 있는 GUI 이벤트 사용할 때
  3. 이때 항상 유의해야 하는 것은, Observable이 Flowable 보다 적은 자원을 필요로 하다는 점이다

Flowable 을 사용 해야 할 때

  1. 수많은 객체들이 생성되며 흘러내려 올 때 chain이 생성하는 소스의 수를 제한할 수 있을 때
  2. 디스크에서 읽기 혹은 파싱 해서 객체를 내려줄 때
  3. JDBC를 통해서 데이터베이스에서 읽고 내려줄 때
  4. 네트워크 IO를 통해서 값을 내려줄 때

Single, Completable and Maybe

Single

싱글 타입은 하나의 onSuccess 혹은 onError 만을 내려줄 수 있는 타입이다. 소비자 역할이 되는 SingleObserver은 3개의 메소드만 가지고있는 인터페이스이며 API는 아래와 같다:

Completable

Completable 타입은 onComplete 혹은 onError 만을 내려줄 수 있는 타입이다. 소비자 역할이 되는 CompletableObserver은 3개의 메소드만 가지고 있으며 API 는 아래와 같다:

Maybe

RxJava 2.0.0-RC2 에서 Maybe 라는 새로운 베이스 타입을 소개했다. 이론적으로는 위의 Single 과 Completable 을 합친것과 같다. API 는 아래와 같다:

결론

결론다시보기

Reference

  • https://msdn.microsoft.com/en-us/library/hh242985(v=vs.103).aspx
  • http://www.reactive-streams.org/
  • https://github.com/ReactiveX/RxJava/wiki/What’s-different-in-2.0
  • https://artemzin.com/blog/reply-to-kaushik-gopals-aricle-rxjava-1-rxjava-2-understanding-the-changes/
  • http://blog.kaush.co/2017/06/21/rxjava1-rxjava2-migration-understanding-changes/
  • https://blog.mindorks.com/migrating-from-rxjava1-to-rxjava2-5dac0a94b4aa

300만 다운로드를 통해서 배운 교훈들

2011년 1월 여름방학에 인턴십을 구하거나 친구들을 만나서 시간을 보내지 않고, 방구석에서 Class Timetable의 첫 번째 버전을 만들었다. 2011년으로부터 딱 1년 전부터 사용하기가 쉽고 단순한 시간표 관리 앱을 찾았으나, 기존의 앱들은 너무 복잡하고 사용하기가 힘들었다. 그날로부터 나는 종이로 만든 시간표보다 더 쉽고 편리한 솔루션을 만들기 위해서 약 500시간을 넘게 사용한 거 같다.

현재 Class Timetable 앱은 300만 이상의 다운로드와 많은 긍정적인 앱 스토어 평가를 받았으며, 한때에는 나의 가장 큰 수입원이기도 했다. 아직 다양한 국가에서 홍보 및 관리를 활발하게 하지 않아서 못 들어 봤을 수도 있지만, 호주, 뉴질랜드, 영국의 학생들에게는 꽤 인기가 있다.

Class Timetable App

앱 스토어의 추천을 받고 하루에 10만 다운로드 이상을 기록하며 대박을 친 앱들에 대한 정보를 여러 블로그에서 접했다. 그 앱들과 비교하면 나의 앱은 그저 적당한 성공일 것이다. Class Timetable은 한 번도 앱 스토어 1등 자리까지 간 적이 없으며, 나 또한 하루아침 사이에 떵떵거리는 부자가 되지 않았고, 실패한 횟수가 성공한 횟수보다 훨씬 많았다. 심지어 주말 사이에 만들어서 대박을 친 앱들과 비교하면 나는 훨씬 더 많은 시간을 소모했으며, 비록 300만 다운로드는 축하해야 하는 수치이긴 하지만, 이것은 하루아침이 아닌 6년 이상의 시간이 걸렸다.

Class Timetable 다운로드 통계

대신, 나의 “적당한 성공” 이야기는 느리고 꾸준한 성장 이야기에 더 가깝다. 신화처럼 접하게 되는 다른 성공 사례들보다 가장 현실에 가까운 이야기일 것이다. Class Timetable은 6년째 꾸준히 성장 하고 있으며, 그 자체만으로 이미 조금 특별하다. 많은 1위의 앱들은 수명이 짧은 경우가 허다하기 때문이다. 나는 지금부터 지난 몇 년 동안 배운 몇 가지 사실을 알려 드리고자 한다. 아무리 많은 성공을 경험했든, 아직 경험하지 못한 사람이든 유용하게 활용할 수 있는 곳이 있기를 바란다.

하나의 성공한 앱을 만들기에 위에 수많은 실패한 앱들이 존재했다

나는 여전히 무수히 많은 실패한 앱들 중 일부는 훌륭한 아이디어라고 생각한다. 단지 시기적절한 마케팅이 아니여서, 혹은 운이 없어서 일 수도 있다는 미련을 아직도 가지고 있다. 그중 ‘Ginge-O-Meter’ 이라 불리는 앱이 있었는데 정말 많은 시간과 노력을 투자했었다. 이 앱은, 사람의 사진을 찍으면 그 사람의 머리가 얼마나 빨간지 측정해주는 앱이었다 (의역: 19세기부터 서양권에서는 빨간 머리의 사람들을 Ginger 이라고 불러왔으며, 현재는 영혼이 없다는 둥 많은 농의 대상이 되기도 한다.). 유머성 앱이 아닌, 실제 이미지 인식 기술을 사용해서 색상 분석 기술을 사용해서 작동하는 앱이었다.

아쉽게도 별로 인기는 없었으며, 금액으로는 50만 원도 수익 내지 못했다. 솔직한 심정으로는 정말 많은 노력을 부었기에, 실패했을 때는 어마어마하게 실망스러웠다. 하지만 거기서 멈추지 않아서 지금의 Class Timetable이 존재하게 되었다. 어쨌든, 여기서 내 요점은 야심에 찬 아이디어가 실패하더라도 좌절하지 않고, 다시 도전하고.. 다시 도전하고.. 다시 도전하는 것이 중요하다는 것이다: 다음 아이디어는 성공할 것이라는 걸 믿으며.

최초 사용자로서 모든 디자인을 설계해라

앱의 기능을 못 사용하겠다고 소리를 지르며 고치라고 항의하는 이메일을 받는다고 상상해보라. 꽤 짜증이 날 것이다. 이런 유사한 이메일을 여러 번 받다 보면, 더 사용하기 쉽고 간단한 제품을 만들어야겠다고 생각이 든다. 내가 이제껏 느낀 것은, 제품을 기획하고 디자인 할 때는 컴맹을 사용자 대상으로 지정하는 것이다. 간단하게 만들기 위해서는, 잘 못 사용하는 경로를 최소화 하는 것이 매우 중요하다. 모든 기능이 명확하고 간단한 흐름을 가지게 설계를 하면, 고객지원에 소모하는 시간이 줄 것이며, 전반적으로 사용자들 또한 행복해할 것이다.

Class Timetable이 최초로 일 평균 1000 다운로드를 기록할 때에, 나는 평균적으로 주당 20통 가까이 이메일을 받았다. 사용자들이 항의하는 기능 위주로 앱을 개선해 나가다 보니, 현재에는 주당 한두 통 받으며, 대부분은 항의 이메일이 아니고 새로운 기능에 관한 제안들이다. 그리고 정말 가끔은 팬레터도 받는다 (진짜로!)

비평가들의 말은 듣되, 그들의 의견을 맹목적으로 믿지는 말아라

나는 여태껏 고객들에게서부터 수백 개가 넘는 새로운 기능에 대한 요청을 받았다. 이 모든 기능을 다 넣었다면, Class Timetable은 매우 방향성이 모호한 앱이 되었을 것이다. 만약 새로운 기능 요청 중에서 현실적인 요청들만 포함 시켰더라도, 결과는 크게 다르지 않을 것이다. 사용자들이 마음에서 우러나온 제품에 대한 방향을 제시하더라도, 이 해결책이 항상 정답이지는 않기 때문이다. 그렇다면 우리는 무엇을 해야 할까?

사용자들의 의견을 들어라. 그들이 표현하는 불만과 경험하는 문제들을 자세히 관찰하다 보면, 더 깊은 곳에 근본적인 원인이 존재할 것이다. 그리곤 그 원인을 제거 / 개선하며 제품의 방향 앞으로 발전해 나가는 것이다. 때론 매우 좋은 기능 요청도 제품의 전체적 완성도에 해가 될 경우가 있으며, 이럴 경우 과감하게 쳐내야 한다.

Class Timetable의 가장 큰 장점은 간단함과 좋은 사용성이었다. 제안받은 몇몇 기능들은 기간에 걸쳐 추가되었으면 좋았겠지만, 대부분은 앱의 복잡성만 늘렸을 것이다. 나는 간결함을 중점으로 개발을 진행했으며, 이 간결미는 Class Timetable을 매력적으로 만들었다.

많은 다운로드 수를 통한 눈속임보다, 하나의 좋은 제품이 더 월등하다

Class Timetable은 단 한 번도 앱 스토어 매인 화면에 올라가지 못했다. 하루에 10만 다운로드도 이루지 못했지만, 이것은 나한테 아무런 의미가 없었다. 많은 앱은 차트에서 1위를 해도, 일 년 뒤엔 버려지는 경우가 허다하다. 기발한 이름이나 바이럴 마케팅을 통해서 사용자들을 끌었어도, 사용자들의 실제 문제를 해결하지 않기 때문이다.

진정으로 좋은 제품을 만드는 것은 사용자들이 계속해서 재사용하는 제품을 기획하는 것이다. 다수의 사용자는 눈치조차 못 채는 기능에 시간과 노력을 투자해라. 사용자들이 가지고 있는 진짜 문제를 해결하는 것에 집중해서, 그 사용자들이 다른 사용자들을 끌어 오게끔 하여라. 재방문하는 사용자들이 많은 것은 제품에 대한 좋은 청신호이다.

인자해져라

Class Timetable이 처음으로 앱 스토어에 올라갔을 때는 1달러짜리 유료 앱이었다. 나는 내가 500시간 이상을 투자한 제품을 사용자들이 1달러로 사용하는 것은 거저 가져가는 거나 마찬가지라고 생각했다. 첫 주에 단 네 명의 사람이 다운로드를 했다. 심지어 매주 지날수록 신규 다운로드 수는 계속 떨어졌다. 잭팟을 터트리는게 어떤 느낌인지는 몰랐지만, 적어도 이건 아니라는걸 알았다. 500시간을 변기 속으로 버리는 기분을 느꼈다!

이때 나에겐 두 가지 결정권이 있었다:
1. 이대로 내버려둬서 매우 천천히 앱을 죽이거나
2. 앱을 무료로 풀어서 많은 사용자가 계획표를 작성할 때 겪는 문제들을 해소한다

무료로 앱을 공개하는 순간 다운로드 수는 하루에 50건, 100건, 1,000건 … 으로 상승하기 시작했다. 얼마 지나지 않아 나는 추가 기능을 풀 수 있는 결제 기능을 배포했으며, 꽤 많은 사용자가 이 기능을 사용한다. 뭔들 주당 1달러 보다는 나았을 것이다! 결론은, 쪼잔하게 굴지 마라! 아무도 안 쓰는 비싼 제품보다는 많은 사람이 쓰는 무료 제품이 더 좋다! 기존의 사용자에게 업셀링을 하는 것이, 새로운 사용자에게 결제를 유도하기보다 더 쉽기 때문이다.

한보 물러서서 생각해라, 매우 자주

가끔은 좋은 해결책이 없어서 한 문제에 막혀서 오래 고민하는 때도 있을 것이다. 이 고민은 작성하고 있는 코드에 관한 것일수도 있고, 앱의 마케팅을 어떻게 해야 할지에 관한 고민일 수도 있다. 그럴 땐 한보 물러서서 더 큰 그림을 보도록 노력해라. 설계를 올바르게 하면 코드에 관한 문제는 해결되는 것처럼, 마케팅 문제 또한 더 능력과 경험이 있는 사람이 쉽게 해결할 수 있는 문제일 수도 있다.

나의 전체 소프트웨어 개발 커리어중에서 단 한 번도 물러서서 생각하는 것을 후회한 적이 없다. Class Timetable을 만들면서 그 필요성에 대해서 더 강하게 느끼게 되었다. 예를 들어 Class Timetable 1.0 버전을 만들면서 나는 완전히 몰입하며 코딩만 하며 시간을 많이 사용했었다. 코드 관련 문제가 있을 때마다, 한보 물러서서 생각하지 않고, 구현하는 것에 더 집중해서 문제를 해결했었다. 몇 년 뒤에 나는 너무나 많은 코드 품질 문제로 코드베이스를 통째로 다시 짜야 하는 노고를 겪게 되었다.

막혔을 때는 한 걸음 물러서서 생각해라!

결론

오늘날까지 Class Timetable은 잘 유지되고 있다. 나는 iOS 업데이트 혹은 Class Timetable의 추후 방향과 같은 고민을 하며 계속해서 미래를 준비하고 있다. 혹시 이 글을 읽고 있는 당신이 학교에서 공부를 하고 있다면 언제든지 유용하게 사용 하길 바란다.

참조

이 문서는 외부 블로그 글을 번역한 내용입니다.

AsyncTask와 비교해서 당신이 RxJava를 당장 써야하는 이유

AsyncTask를 이용해서 네트워크 코드 혹은 데이터베이스 읽고 쓰는 코드 등을 잘 개발해왔는데, 최근들어 RxJava 가 새로운 트렌드 같습니다. 그냥 단순한 트렌드 인가요? 혹은 이 러닝 커브를 극복해야할 만큼 장점이 있는지 잘 모르겠습니다.

Intro

AsyncTask 는 설계상 수 많은 문제점들이 존재한다. 제대로 된 의존성 관리가 안되어서 테스트를 하기가 매우 어렵고, IO 관련 로직을 UI 레이어에서 처리해야 하는 점, 메인 스레드 외에서는 시작을 할 수 없는 것, 메모리 누수가 일어나기 매우 쉬운점, 등 수많은 불만과 이슈들이 존재한다.

먼저 확실히 하자면, RxJava는 AsyncTask의 문제를 해결하지 않는다. 단지 AsyncTask 가 설계적으로 가지고있는 문제점들이 극복하는데 사용되는 노력과 코드가 훨씬 적을 뿐이다. 더군다나, 훨씬 더 유연하고 강력한 도구가 생겼는데, 굳이 오래되고 녹이 다 슬어 버린 도구를 지향 할 이유는 하나도 없다. 새로운 변화와 배움을 받아들이는 자세가 중요한 것이 아닌가 생각이 든다.

Case Study: RxJava & AsyncTask

연쇄 API 호출, 평행 API 호출, 테스팅, UI 로직 handling, 등 수 많은 예제들이 있지만, 가장 많이 사용되는 연쇄 API 호출에 대해서 설명하겠다.

연쇄 API 호출

AsyncTask 에 비교해서 RxJava의 유연함을 가장 쉽게 보여 줄 수 있는 예제는 아마 연쇄된 네트워크 통신일 것이다. 아래의 코드는 가상의 서버에 User API 를 호출한 뒤에, 그 사용자의 ID 값을 다시 서버에 보내서 유효한 사용자인지 아닌지 알아야하는 상황이라고 가정하자. AsyncTask 로 구현하면 아래와 같을 것이다.

AsyncTask

이에 반해, RxJava 에서의 구현은 훨씬 더 간결하다. (RxJava 외에 Retrofit, Retrolambda 처럼 시너지가 좋은 라이브러리들을 사용했다고 가정했다)

RxJava

여러 API 는 꼭 네트워크 호출이 아니여도 상관이 없다. 사용자의 버튼입력에 따른 로직 추가도 RxBinding 을 이용하면 몇줄의 코드로 구현이 가능하고, 코드 리뷰를 해야하거나 예전에 짠 코드를 다시 읽어야 할때도 보다 편하게 흐름을 파악 할 수 있다.

RxJava 에서 수 많은 Operator 들의 조합으로 수만가지의 상황을 해결하는 도구상자가 완비되는 것이다. Operator, 즉 연산자를 연마하는데 걸리는 시간만 극복하고 나면 모든 곳에서 적용을 하고싶을 정도로 즐거워진다. 비교하자면, 어릴적 구구단을 외울때는 고통스러웠으나, 시간이 흘러 응용 문제들을 풀며 기쁨을 느낀 시절과 비슷하다.

Conclusion

개발은 공예다. 비록 다른 공예에 비해 역사적으로 기간이 길지는 않지만, 개발 또한 인간의 생활주변에서 주로 사용되며, 미적 효과를 가진 도구를 제작하는 과정이기 때문이다. 통일신라 시절 도자기를 빚던 도예가들이 자연 시유의 기술을 시도 하지 않았었다면, 우리가 흔히 높은 품질로 자랑하는 고려청자는 세상에 존재하지 못했을 것이다. AsyncTask의 망령에 잡혀 사는 당신, 도예가들의 정신을 이어 받아, 새로운 장을 받아들이는건 어떨까

I am an ordinary developer. If this blog post contains any wrong information or improvements, please feel free to add comments! I would love to hear it, to improve and learn together. 😀

Reference

  • https://realm.io/news/gotocph-jake-wharton-exploring-rxjava2-android/
  • https://www.reddit.com/r/androiddev/comments/63f78o/can_someone_explain_why_is_rxjava_is_better_than/
  • https://stackoverflow.com/questions/2735102/ideal-way-to-cancel-an-executing-asynctask
  • http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html
  • http://blog.daum.net/_blog/BlogTypeView.do?blogid=0Tc6T&articleno=25&categoryId=4&regdt=20101227214814

RxRealcase 2장: 네트워크가 불안정한 존을 위한 Exponential Backoff

About

RxRealcase 시리즈는 가상의 개발자와 가상의 앱을 이용해서 RxJava Operator 들의 실제 적용 사례를 정리한 글이다. 가볍게 읽을 수 있으며, 필요할때 적용 할 수 있는 의미로 작성하고 있다. 이번 장은 RxJava를 적용하며 불안정한 네트워크에서 비교적 우아하게 서버에 재호출을 하는 방법에 대해서 고민을 하는 글이다. https://github.com/wotomas/Findeed 에서 전체 코드가 언젠가 공개될 예정이다. #experienceMatters

시나리오

이상

존은 성공한 사업가로서 필수적으로 설치를 해야한다는 Findeed 라는 앱을 자주 사용한다. 사업차 해외로 출장을 자주 가는 존은 간혹 네트워크가 불안정한 나라에서도 Findeed 를 아무 문제없이 사용한다. 네트워크가 불안정한 곳에서 접속 할때에는 존은 향긋한 헤이즐넛 라떼를 마시며 기분 좋게 기다린다.

하지만…

현실

존은 가끔 출장 중에 Findeed 라는 앱을 사용하지만 네트워크가 불안정한 곳에서는 실행이 정상적으로 안되는 앱을 보면서 한숨만 푹푹 쉰다. 투자자의 연락을 확인해야하는 존은 수동으로 재시도를 몇번이나 시도한 끝에 겨우 접속에 성공하였다. 답변하기 버튼을 클릭하는 순간 네트워크가 불안정하다며 앱이 종료되어버린다.

해결

개발도상국에서 Findeed 의 접속자가 많아 지기 시작하자 앱 개발자는 네트워크 에러 핸들링을 개편하기로 결정한다. 기존에 실패시에 1초마다 3번 재시도를 하는 로직을 수정하기로 결정 한 것이다. 재시도 횟수에 따라 호출의 간격이 길어지면 좋겠다고 생각하며 개발자는 다시 한번 Rx의 심해로 빠져보기로 한다.

1. repeatWhen()

개발자는 repeatWhen() 을 사용하니 네트워크 호출이 정상적으로 돌아와도 다시한번 호출을 하게 되는것을 확인하였다. 네트워크 에러가 생길시에만 재호출을 하고 싶지만, repeatWhen()onComplete()이 호출될때 다시 Subscription 이 생성되는걸 확인했다. 이번 구현에는 부적합하다고 판단한 개발자는 다른 걸 찾아 보기로 했다.

2. retryWhen() + timer()

조금만 더 찾아보다 보니 개발자는retryWhen()을 발견했다! onError() 이 발생할때 다시 Subscription 을 생성할 수 있게 되니 개발자는 써보기로 결정했다. timer() 과 합쳐서 구현하면 간단한 재시도는 구현할 수 있겠다고 판단했다. 하지만 더 우아한 해결책을 원한 개발자는 더 찾아 보기로 결정한다.

3. retryWhen() + zipWith() + range() + timer()

개발자는 여러개의 operator들을 묶으면 구현 할 수 있겠다고 판단하였다. retryWhen()range()zipWith() 시켜서 재시도 횟수를 구현하고, timer() 을 이용해서 인터벌이 늘어 날 수록 다음 시도 간격을 증가 시키는 방법으로 결정하였다.

구현

코드가 조금 긴 관계로 Gist 를 확인하면 완성된 코드를 확인 할 수 있다.

Unit Test

RetryWithExpoentialBackOff 의 경우 testSubscriber 을 이용하여 다양한 상황을 테스트 한다.

Reference

  • 글에서 사용된 Findeed 라는 앱은 아직까지 공개 / 개발 되지 않은 가상의 앱이다.
  • https://en.wikipedia.org/wiki/Exponential_backoff
  • http://blog.danlew.net/2016/01/25/rxjavas-repeatwhen-and-retrywhen-explained/
  • https://gist.github.com/sddamico/c45d7cdabc41e663bea1
  • https://stackoverflow.com/questions/22066481/rxjava-can-i-use-retry-but-with-delay/25292833#25292833
  • http://leandrofavarin.com/exponential-backoff-rxjava-operator-with-jitter

RxRealcase 1장: 성격이 급한 존을 위한 끊김 없는 데이터 전송

About

RxRealcase 시리즈는 가상의 개발자와 가상의 앱을 이용해서 RxJava Operator 들의 실제 적용 사례를 정리한 글이다. 가볍게 읽을 수 있으며, 필요할때 적용 할 수 있는 의미로 작성하고 있다. 이번 장은 RxJava를 적용하며 비교적 빠른 디스크 캐시와 비교적 느린 네트워크 호출을 끊김 없이 사용자들에게 가공해서 보여주는 방법에 대해서 고민을 하는 글이다. https://github.com/wotomas/Findeed 에서 전체 코드가 언젠가 공개될 예정이다. 당당하게 말하라, 불필요한 로딩 화면은 게으른 기획이다. #experienceMatters

시나리오

이상

존은 아침에 일어나며 Findeed 라는 앱을 킨다. 로딩 화면을 보면서 존은 느긋하게 아침 커피를 내리며 로딩이 끝나기를 기다린다. 로딩이 마치면 존은 향긋한 아침 커피를 마시며 피드를 읽으며 기분좋게 하루를 시작한다.

하지만…

현실

존은 아침에 일어나며 가끔 Findeed 라는 앱을 키지만, 로딩 화면을 보면서 존은 앱을 종료하고싶다는 충동을 느끼며 기다린다. 심지어 인터넷이 느린곳을 가거나 로딩중에 인터넷이 불안정하면 로딩이 정상적으로 끝나지도 않는다. 대부분의 사람들은 존 처럼 로딩이 끝날때까지 기다릴 정도로 자애롭지 않을 것이다.

해결

성격이 급한 존을 위해서 앱 개발자는 안드로이드 로컬 데이터베이스에 캐싱 하기로 한다. 존의 핸드폰이 피드를 요청 할 시에, 상대적으로 빠른 경우가 많은 캐쉬를 먼져 보여준 다음에 네트워크 피드가 들어올 경우에 마술처럼 업데이트를 시키는 방법을 택하기로 했다.

1. Concat

.concat() 을 사용하자니 첫번째 옵져버블이 끝나기 전에는 두번째 옵져버블은 시작도 안하는것을 확인하였다. 개발자는 네트워크 요청과 캐쉬 요청이 동시에 시작했으면 좋겠기 때문에 다른 걸 찾아 보기로 했다.

2. Merge

Operator 의 심해에서 뒤적거리다보니 개발자는.merge()을 발견했다! 개발자는 동시에 요청을 보내고 여러개의 스트림을 종합해서 받을 수 있으니 최고라고 생각했다. 10년이 넘은 먼지 쌓인 핸드폰으로 테스트 도중 로컬 캐쉬 결과가 더 늦게 오는 경우에 네트워크을 통해 새로 들어온 데이터가 덮어 씌여지는 경우를 확인했다! 불안정하다고 생각한 개발자는 다른 방법이 있을꺼라고 생각하고 더 찾아 보기로 했다.

3. Publish & Merge & TakeUntil

구석에 쳐다보고 관심 주지 않았던 .publish()가 관심을 가져달라고 얼쩡거렸다. 개발자는 selector 을 받을 수 있는 .publish().merge().takeUntil() 을 조합하면 뭔가 나올 것 같다는 느낌을 받았다. 상대적으로 느릴 확률이 높은 네트워크 호출을 publish() 하여 캐쉬 호출을 selector로 지정하지만, takeUntil() 을 이용해서 네트워크가 오기 전까지만 캐쉬를 쓰게끔 구현하는 방식으로 결정하였다.

구현

Unit Test

PublishSubject를 이용해서 시간차로 호출이 돌아오는 경우를 5가지 케이스로 나눈다.
1. 네트워크 데이터가 캐쉬 데이터보다 빠르게 올 경우
2. 캐쉬 데이터가 네트워크 데이터보다 빠르게 올 경우
3. 네트워크 데이터가 유실 되었고, 캐쉬 데이터만 있을 경우
4. 캐쉬 데이터가 유실 되었고, 네트워크 데이터만 있을 경우
5. 둘다 없을 경우

Reference

  • 글에서 사용된 Findeed 라는 앱은 아직까지 공개 / 개발 되지 않은 가상의 앱이다.
  • concat(), concatEager(), merge() 모두다 많이 사용되는 개념이지만, 대부분 네트워크가 무조건 더 느리다는 전재가 필요하기때문에 이것이 충족이 안될시 잘못된 state에 빠지게된다. (merge() out-of-order problem)
  • https://github.com/kaushikgopal/RxJava-Android-Samples
  • http://blog.kaush.co/2015/01/21/rxjava-tip-for-the-day-share-publish-refcount-and-all-that-jazz/

안드로이드에서 Clean Architecture의 흔적과 장점

불필요한 코드 삭제할때 흔한 경우

Intro

위의 GIF 파일을 보고 실소를 한 개발자들이 많을 것이다. 그 만큼 불필요한 코드라고 판단하고 리펙토링 했는데 전혀 뜬금없는 곳에서의 에러를 겪은 상황이 만연하게 일어난다는 증거이기도 하다.

As the number of unintended defects rose … they feared the changes would do more harm than good. Their production code began to rot.
-Robert C. Martin (Clean Code)

클린코드 에서 Robert C. Martin (a.k.a Uncle Bob) 이 코드가 썩어가는 과정을 비약적으로 표현한 예제이다. 예상치 못한 고장이 많아지기 시작하며, 개발자들은 수정을 두려워 하기 시작하며, 코드는 점차 썩어버리는 것이다. 왜 프로젝트가 커질수록 저런 경우가 많아지는것일까? 책의 제목이 설명하듯 Clean Code 이지 않아서이며, 더 나아가 Clean Architecture 이 아니기 때문이다.


Architecture

Clean Architecture 에서 사용되는 Architecture의 정의를 한번 살펴보자.

The process and product of planning, designing and constructing concepts

잘 만들어진 건물의 도면도를 보면 추상적으로 공간이 어떻게 사용되는지 상상이 가능하다. 건축학도가 도면도만 보아도 그 공간이 학교인지 가정집인지 머리속에서 상상 할 수 있듯이, 개발자들도 서비스의 도면도를 보고 어떤 기능을 제공하는 서비스인지 상상 할 수 있어야 한다. 마찬가지로, 깨끗하게 구현된 서비스의 코드를 보고, 도면도로 그려 낼 수 있어야 한다. 도면도로 도저히 분리시켜 낼 수 없을때, Dirty Architecture로 구현하고 있는지 확인해야 할 때다.

즉, 새로운 개발자 (건축가)가 프로젝트에 참여 할 경우, 도면도만 보고 어떤 서비스인지 머리속에서 그려진다면 Clean Architecture 인 것이다.
건물의 평면도


Clean Architecture

소프트웨어 개발이 발전하면서 코드가 썩는것을 방지하면서 유지보수가 가능한 프로젝트를 만들기 위해 수많은 Architecture 들이 정의 됬으며, 이들은 모두 Separation of Concerns 라는 공통된 목표를 가지게 되었다. 유지보수를 위해 레이러로 나눌때의 최적화된 규칙을 찾는것인데, Uncle Bob의 Clean Architecture은 하나의 아우르는 규칙을 제시하는데, 이것이 The Dependency Rule 이다.
그래서 이게 무슨뜻이야!


The Dependency Rule

Dependency Rule이란 위의 그림을 기준으로 코드의 의존성은 오로지 안으로만 향하는 것이다. 단적으로 예를들면, 밖의 레이어에서 정의된 모든 객체는 안의 레이어에서 언급조차 되지 않는것이다. 즉, 함수, 객체, 변수의 이름에서 조차 다른 레이어와의 연결고리를 끊는것이다. 간단하게 안드로이드의 예로는 Framework Layer에 포함되는 Activity, Fragment, Context 등이 Interface Adapter Layer에 포함되는 Controller, Presenter, Repository 등에서 찾을 수 없어야 한다는 뜻이다.

그렇다면 거꾸로 Presenter 에서 Activity로 무언가를 알려줘야 할 경우에는 어떻게 해야할까? 여기서 사용되는 개념이 Dependency Inversion Principle이다. 인터페이스를 이용해서 의존관계와 반대로 흐름을 제어 할 수 있게 하는 방법인데, Presenter 에서 View Interface 를 이용해서 동작을 제어하는것, Repository Interface 를 이용해서 데이터베이스의 데이터를 가공하는것 등이 있다. 더 자세하게 알고싶다면 Uncle Bob’s Clean Architecture 을 꼭 읽어보길 추천한다!


Example

플리토의 앱을 부분적으로 분석해보았다. 화살표의 방향이 의존관계를 나타내며, 플리토 앱은 현재 Clean Architecture 에 의거하여 분리시키며 리펙토링을 진행하는 중이다.
Flitto App 평면도 나열


Framework & Drivers

가장 상위층인 4층에는 가능한 최소한의 로직들이 담긴 클래스들이 포함되어있다. 오로지 아래층으로 데이터를 넘겨 줄 수 있는 접착제 같은 역할만 분담하고 있으며, 안드로이드의 경우 Context 가 필요한 모든 존재들을 이 레이어에서 해결한다. 그 외에도 외부 모듈이나 제어가 불가능한 존재들은 모두 이곳으로 포함시켰다.
Flitto Example: Activities, Fragments, Services, Network, Analytics, Database, etc
4층 평면도


Interface Adapter

3층에서는 아래층과 윗층이 소통을 할 수 있게 데이터를 변환시키는 레이어다. 4층에 대해서 전혀 몰라야 하며, 이는 특정 화면을 Activity에서 Fragment로 변환시킨다고 3층 아래의 코드가 수정 될 일이 전혀 없어야 하는 것이다. 같은 맥락으로 데이터베이스를 다른걸로 봐꾸던, 센서를 다른걸 이용하더라도 수정될 일이 없어야 한다.
Flitto Example: Presenters, Views, MediaWrapper, Repositories, OrientationManager, RetrofitController, etc
3층 평면도


Application Business Rules

2층에서는 구축하는 시스템의 Use Case 가 모두 정의 되어있는 층이다. 3층과 마찬가지로, 3층의 코드가 수정이 된다고 해도, Use Case의 정의가 수정되는 일은 없어야한다. 즉, Repository 에서 기존에 데이터베이스에서 바로 데이터를 주던, 메모리 케쉬를 통해서 최적화를 해서 주던, Use Case의 로직은 수정이 되지 않아야 하는 것이다.
Flitto Example: 플리토의 경우 Presenter 가 들고있는 subscription 을 각각의 Use Case로 정의했다. 코드 리펙토링을 거쳐 재사용성을 위해 추후에 Use Case 로 분리 할 예정이다.
2층 평면도


Enterprise Business Rules

1층의 경우 서비스의 비즈니스 객체가 경우가 많다. 안드로이드의 경우 Plain Old Java Object (POJO) 가 되는 경우가 많으며, 기획 단계에서 비즈니스 로직의 수정이 있지 않는 이상, 서비스의 다른 층의 수정으로 이 계층이 수정 될 일은 없어야 한다.
Flitto Example: Model POJOs
1층 평면도


Flitto App Blueprint

Conclusion

위의 그림은 여러 층의 평면도가 합쳐서 만들어진 정리된 플리토 앱의 도면도이다. 마치 왼쪽의 주택의 도면도 처럼 층별로 분리가 되어있으며, Dependency RuleDependency Inversion Principle 을 이용하여 객체간 책임감 분리는 넘어서 계층간의 책임감도 분리시켜냈다.

Clean Architecture을 이용하여 새로운 개발자가 들어왔을때 시스템에 적응 기간을 효과적으로 단축 시킬 수 있으며, 리펙토링의 방향을 올바르게 정할 수 있게 되는 것이다. 시간이 지나며 코드가 썩는것을 방지하는 방부제가 되는 것이다. 레이어간의 책임감을 분리 시킴으로서 새로운 기능을 추가하거나 기존의 기능을 수정 해야 할때 예상치 못한 곳에서 오류가 생기는 것을 미연에 방지하는 예방책인 셈이다. 위에서 언급했듯이 더 자세하게 알고싶다면 Uncle Bob’s Clean Architecture 을 꼭 읽어보길 추천한다!

플리토의 앱을 발전 방향을 잡는 나침판이 되었으면 좋겠고, 모두 클린 코드에 대한 전반적 정보 공유를 위해 글을 썼으며, 틀린 정보가 있다면 같이 수정하며 배워 나가고 싶다.
I am an ordinary developer. If this blog post contains any wrong information or improvements, please feel free to give feedbacks! I would love to hear, improve and learn together. 😀


Extra

여담으로 플리토에서 함께 안드로이드 앱을 개발 하실 개발자를 찾습니다. 함께 좋은 서비스를 위해 고민하고 안드로이드 개발을 좋아하시는 분이라면 환영합니다! 같이 토론하고 지식을 공유하고 지속적으로 실력이 늘고싶습니다!

많이 지원해주세요!


Reference

  • https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
  • https://fernandocejas.com/2015/07/18/architecting-android-the-evolution/
  • https://github.com/android10/Android-CleanArchitecture
  • https://github.com/googlesamples/android-architecture/issues/190
  • Clean Architecture Getting Clean, Keeping Lean by Joe Birch from Boston DroidCon 2017

Android Debug Tools: 디버깅 도구 총정리

안드로이드 개발에 사용되는 수많은 디버깅 툴 중에서 편리한 도구 몇 가지를 정리하기 위함. 아래에 사용된 앱은 DebugTools 참조 가능.

디버깅 툴 List Up

Stetho

Facebook 에서 만든 안드로이드 개발용 디버깅 플랫폼. Chrome inspect tools (chrome://inspect/) 를 사용해서 앱의 UI, database, SharedPreference, network status 등을 디버그할 수 있게 함.

사용법

앱 실행 후 크롬에서 chrome://inspect/ 확인 시 하단 inspect button 클릭 후 디버깅 화면으로 진입 가능

Chrome Inspect Button

네트워크 디버깅

Network Call Example

Stetho는 네트워크 호출이 있을 때마다 intercept를 통해 사용자에게 정보를 제공 할 수 있다. Response 정보뿐만 아니라 Header 정보, latency, size, 이미지 미리 보기 까지 한눈에 볼 수 있어서 네트워크 관련 디버깅 툴로 매우 효율적이다.

Network Header

데이터베이스 디버깅

Stetho 의 경우 Database inspection 또한 매우 간편하게 이뤄진다.
Resources → Web SQL → 해당 DB를 찾아서 클릭할 경우 직접 쿼리문 작성 후 실시간 값 변화 확인 가능합니다.

Database Query Example

Shared Preference도 database와 마찬가지로 매우 간편하게 확인 가능. Resources → Local Storage → 해당 pref name 찾아서 클릭 시 수치 확인 후 수정 가능 (더블클릭 후 값 수정)

SharedPreference Example

DebugDrawer

이름 그대로 Debug module 들을 모아놓은 drawer이다. 다양한 디버깅 모듈을 포함해서 자신에게 필요한 모듈을 장착한 customized drawer 을 가질 수 있게 되며 필요한 기능이 있을 시 언제든지 확장이 가능하다. 피카소, 글라이드 등 이미지, OkHttp, Location, 네트워크 모듈 등이 자주 사용되며, custom 모듈도 제작 가능하다.

Debug Drawer Example

사용법

Custom Module 추가

Debug Drawer의 가장 큰 장점은 custom module을 추가할 때 나타난다. 앱 디버깅 중에 특정 안드로이드 버전에서만 특정 fragment가 생성될 때 focus가 뺏기는 문제가 있었던 적이 있는데, 어떤 뷰가 도대체 왜 포커스를 뺏어가는지 상상이 안 되어서 해결하는 데 사용했었던 모듈이다.

Custom Focus Module Example

StrictMode

개발자가 실수하는 것을 감지하고 해결 할 수 있도록 돕는 모드. 일종의 경고 알람 같은 면에서는 메모리 리크를 감지해주는 Leak Canary와 비슷하다.
– 디스크 쓰기
– 디스크 읽기
– 네트워크 사용
등을 감지 할 수 있는데, StrictMode를 사용하면 java.io.*, android.database.sqlite.*, java.net.* 클래스들에게 후크를 걸게 되어서 감지가 가능하게 된다. 플래시 메모리를 사용하는 안드로이드라서 디스크 쓰기/읽기의 속도가 빠를것이라고 예상하지만, 용량이 채워질수록 큰 폭으로 느려진다.

사용법

TinyDancer

FPS tracking 유틸이다. 인간이 연속된 동작으로 인식할수있는 FPS 는 24 FPS부터다. 일반적인 사람은 30 부터 60 사이의 FPS는 차이를 못 느끼며, 매우 부드러운 동작으로 인식한다. 즉, Measure과 draw를 16.66 ms 안에 그려내야 하는 안드로이드 경우 dropped frame per second 수에 따라서 FPS가 결정이 된다. 부드러운 사용자 경험을 위해서 디버그 빌드와 UI Test를 연동 시켜 FPS를 프로파일링 하는 식으로 많이 사용된다.

Tiny Dancer Demo

사용법

LeakCanary

Leak Canary 경우 메모리 누수를 감지해주는 디버깅 툴이다. 액티비티에서 메모리 누수가 발견될경우 사용자에게 알람을 띄워주며, 별도의 프래그먼트에서 누수를 감지하고싶으면 refWatcher로 집어넣어준다음에 onDestory 가 호출될때 refWatcher도 별도로 호출해야한다.

사용법

Chuck

Chuck 는 안드로이드 앱 내에서 사용할 수 있는 HTTP Inspector 이다. Stetho 를 이용해서 네트워크를 확인할시, 크롬 인스펙터가 구동되기 전에 생긴 네트워크 호출일 경우 확인하기 불편하다는 점을 보안할 수 있다. 디버그 빌드에서 앱이 네트워크 호출을 할 시, Chuck 라는 앱이 호출되면서 앱 내에서 전반적인 네트워크 호출에 관한 정보들을 확인할 수 있다.

Chuck Example

사용법

Telescope

Telescope 은 앱의 루트 레이아웃에 TelescopeLayout 이라는 뷰를 추가함으로써, 테스트 사용자들 (개발자 혹은 회사 내부 사람들)이 언제든지 쉽게 스크린샷과 함께 버그 리포트를 손쉽게 보내게 해주는 툴이다. 지정된 손가락 갯수를 특정시간동안 누르고 있으면 dev 이메일로 리포트가 전송되게 만들어져있는 라이브러리다.

Telescope Example

사용법

Process Phoenix

Process Phoenix 는 앱의 특정 액티비티를 완전히 깔끔한 상태로 시작하고싶을때 손쉽게 사용 할 수 있는 디버깅 라이브러리다. 불사조가 다시 태어나듯이, context 만 쥐어주면 깨끗한 상태로 액티비티가 만들어지는데, 주로 staging 이나 dev build 에서 release build로 설정을 봐꾸거나 할때 사용한다. 앱을 완전히 삭제하고 다시 설치할 필요없이 필요 할 시에 호출해서 사용할 수 있는 것이다.

사용법

Hugo

Hugo 는 annotation-based 로깅 툴이다. 사용하기도 매우 간편하다. Log.d(TAG, message) 를 호출하기보다, @DebugLog annotation 하나만 추가하면, 메소드가 실행되는데 걸린 시간부터 파레메터로 받은 변수들 까지 다 손쉽게 볼 수 있는 것이다.

사용법

ClassyShark

ClassyShark는 안드로이드 라이브러리도 별도로 추가해야하는 것이 아니고, 외부로 존재하는 .jar 프로그램이다. Apk 파일을 업로드 하면 자신의 apk파일이 어떤 라이브러리들로 이뤄져있으며, 용량 차지, 메소드 카운트 부터 proguard 적용은 잘 되었는지 등을 확인 할수있는 바이너리 inspection 툴이다. .dex, .aar, .so, .apk, .jar, .class 파일들 을 다 열어서 확인 해볼 수 있으며, 안드로이드 xml 파일들까지 다 확인 할 수있다.

Classy Shark 화면

사용법

java -jar ClassyShark.jar실행 후 확인 하고 싶은 파일 open 후 확인

좋은 개발자? 뛰어난 안드로이드 개발자가 되는 방법!

지나친 비약으로 들릴 순 있겠지만, 무언가에 뛰어나다는 것은 매우 상대적인 개념이다. MMORPG 게임을 하다 보면 상위 랭커가 되기 위해서는 남들보다 많은 시간 투자와 효율적인 사냥 동선이 최고임을 알 것이다. 개발에도 마찬가지로 적용된다. 남들보다 더 많은 시간과 노력을 어떻게 더 효율적으로 사용함에 따라 뛰어난 안드로이드 개발자와 평균적인 안드로이드 개발자가 나뉜다. 구체적으로 어떻게 효율적으로 사용해야 하는 걸까?

효율적인 사냥 동선

서론

누구나 할 수 있는 말이겠지만, 뛰어난 개발자가 되기 위해서는, 학습연습을 남들보다 많이 무한 반복하면 된다. 그렇다면 뛰어난 안드로이드 개발자가 되기 위해서 어떤 것들을 학습해야 하고 연습해야 할까?

학습

뉴스레터

뉴스레터

뛰어난 안드로이드 개발자가 되기 위해선 안드로이드에 대해서 많이 알아야 한다. 유기적인 플랫폼을 잘 이해하기 위해서는 정보를 많이 접해야한다. 신기술이 무엇인지, 커뮤니티에서 화두가 되는 주제가 무엇인지, 기술이 어떤 방향으로 발전하고 진화하고있는지를 알기 위해서는 주기적으로 발행되는 뉴스레터가 매우 효율적이다. 이메일 주소만 기입하면 주 단위로 새로운 정보를 발송해주며, 나아가 더 빠르게 정보를 접하고 싶다면, androiddev subreddit 혹은 활발하게 활동하는 안드로이드 개발자들 트위터를 팔로우 하는것을 추천한다.
* Action Items
* Kotlin weekly 구독하기
* Android Weekly 구독하기
* Android DevDigest 구독하기
* /r/androiddev/ 시작 홈페이지로 설정하기
* 활발한 안드로이드 개발자 팔로우 하기

팟캐스트

팟캐스트

한국에는 상대적으로 덜 발전한 미디어 매체인 팟캐스트는 사실 안드로이드 정보로 가득 차 있다. 능동적으로 시간을 투자하고 정보를 접해야하는 글이나 블로그 포스트와 다르게, 팟캐스트 같은 경우는 수동적으로 노래 듣는것 처럼 정보를 접할 수 있다. 운동, 운전, 산책 등 노래를 들을 수 있는 상황에서는 팟캐스트 한편 듣는 것을 추천한다.
* Action Items
* Fragmented 구독하기
* Android Intelligence 구독하기
* Test Talks 구독하기
* Android Developer Backstage 구독하기

온라인 수업

빠르게 변화하고 발전하는 안드로이드 코드베이스에 맞춰서 지속적으로 업데이트되는 온라인 수업들도 존재한다. Udacity 와 CodePath 인데, 자기만의 페이스에 맞춰서 공부를 할 수 있으며, 같은 기능을 다양하게 개발 방법을 볼 수 있다.
* Action Items
* Udacity
* Advanced Android App Development 수업 듣기
* Gradle for Android and Java 수업 듣기
* Material Design for Android Developers 수업 듣기
* Android Development for Beginners 수업 듣기
* CodePath

세미나 및 컨퍼런스

세미나 및 컨퍼런스 참가 하는 것 또한 매우 중요하다. 분야의 전문가들이 제공하는 정보를 접할수 있게 되며, 새로운 기술의 응용에 대해서 배울 수 있다. 잘 준비된 세미나 한번 보는것은 그 주제에 관해서 농축된 한약을 한 사발 마시는것과 같다고 생각한다. 국내/국외 세미나가 정리되어있는 사이트들은 이러하다:
* Action Items
* 국외 컨퍼런스 참가하기
* 국내 컨퍼런스 참가하기

실행

오픈소스

실제 팀 단위 개발 환경과 가장 유사한 경험은 오픈소스 프로젝트에 참여 하는 것이다. 프로젝트가 유지 되는 방식이나 개발이 진행되는 방식, 등 혼자 개발하는 것 보다 훨씬 많은 경험을 할 수 있으며, 토론과 협동을 통해 유기적으로 발전하는 프로젝트를 지켜보는 것 또한 매우 좋은 경험이다. 참여를 하지 않더라도, 다양한 오픈 소스 프로젝트를 뜯어보는 것 만으로도 디자인 패턴이 안드로이드에서는 어떻게 적용되는지 등을 배울 수 있다.
* Action Items
* 오픈소스 프로젝트 PR 넣기
* 오픈소스 프로젝트 이슈 만들기
* 안드로이드 오픈소스 프로젝트 뜯어보기
* MovieGuide
* Archi
* MVP u2020

코드리뷰

개발은 팀 스포츠라고 봐도 무방하기 때문에, 코드리뷰를 잘 하는것 또한 개발 실력에 큰 영향을 끼친다. 코드리뷰 실력이 늘게 되면, 코드의 문제점을 더욱 더 빠른 시점에서 파악할 수 있고, 다른 개발자들과의 소통을 통해 다양한 아이디어를 접하기가 쉬워진다. 또한, 타인의 시선에서 코드를 보는 실력이 늘어서 일관성있는 설계와 유지보수 하기 쉬운 코드를 짜게 된다.
* Action Items
* 오픈소스 프로젝트 코드리뷰에 참가하기
* 본인 코드도 코드리뷰 하기

테스팅

테스팅은 자연스레 개발자들이 “시간이 남으면 하는 것”이라는 인식이 강하지만, 아무리 버그가 없는 코드를 썼어도, 테스팅이 없다면 불안정한 코드이다. 마치 수술실의 의사가 시간이 없다고 검증도 하지않고 환자의 환부를 봉합하는 것과 마찬가지로 책임감이 걸여되는 행동이다. 테스트 코드를 작성하는 기간도 개발 기간의 일부로 잡아야 하며, 테스팅을 습관화 하면 개발을 하며 자연스레 더 깨끗한 코드를 짜기 시작하는 본인을 발견할 것이다.
* Action Items
* Unit Test 작성하기
* UI Test 작성하기
* Pref Test 작성하기
* CI 연동하기

데이터 수집

사용자 행동 데이터를 분석해서 본인의 앱을 지속적으로 발전시켜 나가야 개발 결정을 하는 실력 또한 발전한다. 비록 초반에는 직관적으로 기능 개발이 가능 하더라도, 성숙해져가는 제품의 방향을 직관에 맡기기에는 직관이 틀리는 경우가 너무나 많다.
* Action Items
* 개인 프로젝트에 Fabric 연동하기
* Classy Shark로 apk 분석해보기

결론

뛰어난 개발자가 되는 방법에는 많은 길들이 존재할 것이고, 위에 제시한 방법은 그 수많은 길들중 일부분일 것이다. 나 또한 뛰어난 개발자가 되고싶은 평범한 개발자 중 한명이며, 같은 방향을 추구하는 다른 개발자들과 공유 하고싶은 마음에 쓰는 글이니, 틀린 정보가 있다면 같이 수정하며 배워 나가고 싶다. 많은 내용은 Droidcon Boston 2017의 First Do No Harm 을 참고 + 영감 받아 쓴 글이다.

I am an ordinary developer. If this blog post contains any wrong information or improvements, please feel free to add comments! I would love to hear it, to improve and learn together. 😀