Naver Tech Concert Day-2

06. 자동화, 계륵에 살 붙이기 : Evolution of Android Automation Test

  1. UI Automation Test
  2. Automation Library
  3. Android Event Checker
  4. Resource Monitoring

UI Automation Test

사용자의 테스트 동작 중 화면상의 마우스 동작, 키보드 입력 등을 자동화 수행이 가능한 script 형태로 변환하여 이후 동일한 형태로 Replay 함으로써 Regression Test 를 지원하는 테스트 자동화 방식

  • 단순하고 반복적인 테스트
  • 유지보수 기간 길어질수록 Test Case도 많아짐
  • Side-effect 확인을 위한 회귀 테스트 (작은 수정에도 전체 확인이 가능)

N Tech Service에서의 히스토리

  • Calabash, ADB, Sikuri, Appium, BDD 방식을 활용한 자동화 사례
  • Uiautomator Stub 활용한 멀티 디바이스 제어 자동화 사례
  • WebKuli(NTS 자체 제작) 적용사례

자동화 결과는 좋지 않았음.

이슈를 실제 검출하는 경우도 적고 프로젝트 유지가 오래된 케이스도 저었음.

  • 기대에 미치치 못하는 Test Coverage
  • Tool 이해도 부족
  • 자동화 구현 및 유지보수 공수 상승 (리소스 절감을 위해 도입했다 더 리소스가 투입된 경우도 있음)
  • 정보 공유 부족

Automation Library

표준화 및 규칙화를 위해 라이브러리 제작.

제작 당시 안드로이드 프레임워크 비교 후 Appium을 선택함.

public class TestClass {
    public AppiumDriver<WebElement> driver;
    
    @BeforeClass
    public void setup() throws Exceptiom {
        // 플랫폼, 단말기 정보,테스트 타켓 패키지 정보
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, "Android");
        capabilities.setCapability(MobileCapabilityType.UDID, "UDID");
        capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "platform-version");
        capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, "UIAutomator2");
        capabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "package");
        capabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, "launchable-activity");
        
        // 안드로이드 드라이버 형성
        driver = new AppiumDriver<WebElement>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
        driver.manage().timeouts().implicitlyWait(15, TimeUnit.SECONDS);
    }
    
    @Test
    public void scenario_01() {
        // 동작 제어
        driver.findElement(By.id("tv_home_weekly_schedule_title")).click();
    }
    
    @Test
    public void scenario_02() {
        // 동작 제어
        driver.findElement(By.xpaht("//*[@text='요일별 연재']")).isDisplayed();
    }
    
    @AfterClass
    public void quit() {
        driver.quit();
    }
}

라이브러리 제작 전 고려사항

  • 자동화 테스트 환경 Setting (환경 일원화)
  • 자동화 구현 패턴 표준화 (스크립트 작성에 대한 표준화)
  • Appium Client API의 사용성 (가독성 높이고 타이밍 이슈에 대한 부분 커스텀마이징 추가)
  • 객관적 지표 산출

구성

  • Appium Server Builder
  • Appium Client Utilities
  • Report
public class TestClass extends Formatter {
    public AndroidUtil android;
    
    @BeforeClass
    public void setup() {
        android = (AndroidUtil) new Automation()
            .android()
            .mobileApp()
            .apkFile("apkPath")
            .packageName("Package_Name")
            .activityName("Activity_Name")
            .logLevel("loglevel")
            .start());            
    }
    
    @Test
    public void scenario_01() {
        android.click(By.id("tv_home_weekly_schedule_title"));
    }
    
    @Test
    public void scenario_02() {
        android.isElelmentPresent(By.xpath("//*[@text='요일별 연재']"));
    }
    
    @AfterClass
    public void quit() {
        android.quit();
    }
}

위처럼 커스텀하여 사용하며 운영도 실제 진행함.

UI 자동화 테스트 자체적으로 발생하는 이슈들 생김

  • UI Assertion 대한 신뢰성
    • 음악 스트리밍 앱에서 재생 중인지?
    • Audio Focus는 가져왔는지?
  • 자동화 시나리오 진행 중 안정성 문제
    • 자동화 테스트 중 Interrupt (SMS, Call 등)
    • 알 수 없는 간헐적 Fail Result

Android Event Checker

Event Checker

  • Log 출력 방식
  • Monitoring Start/Stop
  • Event Log

구조

  • Service
  • Command
  • Broadcast Receiver
  • Sensor : 기울기 센서를 이용하여 orientation 전환 확인
  • Audio Focus, Audio Manager : Audio Focus 선점 및 Audio 재생 확인
  • Notification Listener : Application Notification 정보 수집

약 30개 Event, 80개 Log

위와 같이 UI만 확인하던 UI 테스트에 기능적인 부분도 추가함.

Resource Monitoring

  • 동일 스텝
  • 동일 시간
  • 반복적

rMon Mobile Version

  • Battery 소모량
  • CPU 사용률
  • Memory 사용량
  • 발생 Traffic

rMon을 이용하여 앱을 켜둔 상태에서 테스트하며 성능 리포트를 남기게 함.

(Android 5.1.1 이후 사용 불가. rMon이 CPU나 Memory를 앱의 pid에 접근해야해서 불가됨.)

rMon PC Version

Android 5.1.1 이후 대응을 위해 PC 버전으로 adb 기반으로 제작.

  • ADB를 활용한 Resource 측정 방식
  • Android Application의 소모 CPU, Memory, Traffic 측정
  • Android 단말기 내부에 rMon 명령어 추가
  • ADB > rMon 명령어 호출하여 리소스 데이터 수집

Android Automation Test

  • GitLab으로 Automation Test Project 관리
  • CI 서버를 통해 Daily Build 수행 (서버 이슈, 컨텐츠 이슈, 로그인/댓글 같은 부분 배포로 인한 이슈 확인 용. 출근 후 확인)

Maver Repository -> Coding -> Code Respository -> CI 서버 -> Target Device -> Result Report

nMobile

https://solution.navercorp.com/nmobile/

모바일 기기 원격 제어 솔루션

  • 화면 제어, 디버깅 등
  • Real Time에 가까운 응답속도
  • 실제 단말과 동일한 사용감 제공

nMobile 자동화 연동시 장점

  • 가상 단말 아닌 리얼 디바이스 대상 수행 가능
  • 다양한 단말 보유 (버전 별, 제조사 별, 국가 별)
  • ADB 연결 가능 / 다양한 API 지원
  • 물리적인 연결 없이 접근 가능 / 사용 용이성

PC Browser & iOS

nMobile Android 단말과 실제 iOS 단말, PC 크롬에서 채팅 네이버 카페 채팅방 테스트 가능

  • Step1 nMobile Android 단말기 실행 및 연결
  • Step2 nMobile Android 단말에서 Cafe 앱 실행
  • Step3 실제 iOS 단말에서 Cafe 앱 실행
  • Step4 PC에서 Chrome 실행 및 Cafe 홈페이지 진입
  • Step5 PC웹 및 Android/iOS 앱에서 테스트 대상 카페 진입
  • Step6 PC웹 및 Android/iOS 앱에서 테스트 대상 카페 채팅방 진입
  • Step7 각 플랫폼에서 채팅 입력 후 다른 플랫폼에서 전송된 채팅 확인 및 답장
  • Step8 연결 종료

Result

  • Library를 이용한 안정적인 자동화 프로젝트 생성
  • 외부 인터럽트 및 기능 Assertion에 대한 단점 보완
  • 자동화 특성 반영 Resource Monitoring

Q & A

Q : 네이버는 자동화 테스트와 QA 인력들의 수동 테스트 둘다 진행하는지?

A : 둘 다 진행함. 자동화 테스트를 한다고 비용이 절감되지 않고 증대되는 듯. 사람과 자동화와 서로 교차 테스트 됨으로 해결되는 부분들이 잇음.

Q : 라이브러리 제작 시간은?

A : 2016년도 제작하면서 각자 QA 담당하면서 시간 쪼개서 만든 것임. 제작에 전적으로 시간 투자하면 빨리 가능할 듯.

Q : Appium이 iOS도 지원하지만 앱 내부 구조가 다를텐데 따로 스크립트를 작성하는지?

A : 동일한 기능에도 따로 작성함. iOS는 아직은 자동화 테스트 구현에 어려움이 있음.

Q : 자동화된 UI 테스트가 라이브 서비스의 패포 파이프라인에 포함이 되어 테스트 통과가 되어야 배포가 되는것인지?

A : 그렇지 않음. 개발자들이 유닛 테스트들을 별도로 따로 작성 후 확인하고, 자동화 테스트는 QA에서만 사용.

Q : nMobile은 리얼 디바이스처럼 와이파이 환경일때나 모바일 네트워크 환경일 때, 또는 유심이 들어있는 상황에서만 할 수 있는 상황도 연출이 가능한지?

A : 그런 상황들도 테스트 가능함.

Q : CI/CD는 어떻게 하는지?

A : CI 서버는 따로 두고 젠킨스로 있음.

Q : 자동화 테스트 툴을 오픈할 계획이 있는지?

A : 아직 오픈되진 않았음.

Q : 테스트 시나리오 작성시 사용자 행위로 서버에 변경되어 저장될 수 있는 케이스는 어떻게 처리하는지?

A : 서버까지는 확인이 불가하며, 노출되는 텍스트 등으로 확인 정도까지만.

Q : QA에 개발 인력도 소속되어 별도의 자동화를 위한 개발이 이루어지는 것인지?

A : 따로 개발인력이 있지 않음. 몇몇의 QA 인력들이 모여 만듬.

Q : 실제 기능 구현 개발자는 자동화 테스트 코드를 작성하지 않는지?

A : 직접 작성하는 개발자도 있지만 많이는 없는 듯. 시간이 많이 들기 때문에.

Q : 리소스가 부족한 소규모 사업장, 1인 개발자 들이 UI 자동화 테스트에 접근하려 할 때 좋은 방법은?

A : Appium을 직접 커스텀하여 만들었지만 이를 커스텀화한 서비스들이 이미 있음. 그걸 이용하면 좋을 듯.

Q : Jira 같은 서비스를 이용해서 각각의 이슈를 만드는지? 별도의 QA 시트를 만들어서 빨리 전달하는지?

A : 지라를 이용하여 이슈 공유함. 깃헙을 사용하기도 함. 개발자와 협의해서 사용함. 별도의 QA 시트도 작성하고 테스트케이스도 작성하여 진척도를 메일로 공유하는 등 같이 사용함.

Q : 국가 변경 등 보통 개발자 옵션 페이지 등으로 접근하는 경우도 범용적으로 사용 가능한 노하우가 있는지?

A : 개발서버에서 테스트하기 때문에 특정 언어를 바꾸거나, nMobile에서 제공하는 다른나라 단말 등을 사용함.

Naver Tech Concert Day-2

05. 안드로이드에서 코루틴은 어떻게 적용할 수 있을까? : 코루틴 적용 및 ReactiveX(RxJava/RxKotlin)와 비교한다면?

  • 발표자 : 권태환 (요기요 / 안드로이드 개발)

  • 동영상 : https://tv.naver.com/v/4635469/list/272653

  • 슬라이드 : https://www.slideshare.net/NaverEngineering/25-121499000

  • 세션설명 : 1.3에 정식으로 포함될 코루틴! 안드로이드에서 코루틴의 적용은 어떻게 할 수 있으며, ReactiveX(RxJava/RxKotlin)과 비교 한다면 좋은점과 부족한 점, 그리고 실무 프로젝트에 적용한 코루틴을 소개해본다.

  • 목차 :

    1. 코루틴 소개(서브루틴과 코루틴)
    
    2. 코루틴을 통해 할 수 있는것?
    
    3. 코틀린 코루틴에서 제공하는 주요 기능들
    
    4. 실제 업무에 적용한 코루틴 소개
    
    5. 코루틴 써보니? RxJava와 비교
    

코루틴 적용 및 ReactiveX(RxJava)와 비교

Kotlin coroutines

coroutines이란?

Subroutine의 사전적 의미 : 평소 사용하는 함수, 메서드 등.

subroutine의 return이 불러지기 전까진 다음 라인을 실행하지 않음.

private fun MutableList<Int>.sum(): Int = this.sumBy{ it }  // sum이 끝나야 return

@Test
fun test() {
    val sum = (0..10).toMutableList().sum()
    println(sum)	// sum이 끝나야 println
}

Coroutines : 1958년 어셈블리에서 언급되었고, 1963년 논문을 통해 설명됨.

코틀린 뿐만 아니라 파이썬, C#에서도 지원함.

  • Entry point 여러 개 허용하는 subroutine (쉽게 쓰레드라 생각하면 됨.)
  • 언제든 일시 정지하고 다시 실행 가능
  • event loops, iterators, 무한 리스트, 파이프 같은 것을 구현하는데 적합

private suspend fun Int.countDown(currentIndex: Int) {
    for(index in this downTo 1) { //countdown from 10 to 1
        tv_message.test = "Now index $currentIndex Countdown $index"	// update text
        delay(200)
    }
    Log.i("TEMP", "Now index $currentIndex Done!")
}

var currentIndex = 0
fab.onClick {
    CoroutineScope(Dispatchers.Main).launch {
        10.countDown(++currentIndex)
    }
}

suspend라는 키워드를 넣어야 코루틴으로 동작되고 delay(기존 Thread.sleep과는 다름)를 사용함.

코루틴에서는 CPU와 관계를 최소한으로 줄여 루틴마다 스위치가 일어날 때 CPU 영향을 적게 받음.

여러 쓰레드에서 하던 부분을 단일 쓰레드에서 처리할 수 있게 해줌.

Dispatchers.Main 지정으로 여러번 click해도 메인 쓰레드에서 동작함.

Kotlin coroutines vs RxJava

Android onClick - RxJava vs coroutines

kotlin coroutines

var currentIndex = 0
fab.onClick {
    CoroutineScope(Dispatchers.Default).launch {	// 새로운 scope를 생성하고 default로 launch (일반 Work thread)
        val job = launch(Dispatchers.Main) {		// launch를 Main thread로 변경
            10.countDown(++currentIndex)
        }
        job.join()							// join()으로 UI thread 종료하기 전까지 대기
    }
}
private suspend fun Int.countDown(currentIndex: Int) {		// 상위 scope thread에 따름(여기선 UI)
    for(index in this downTo 1) { //countdown from 10 to 1
        tv_message.test = "Now index $currentIndex Countdown $index"	// update text
        delay(200)
    }
    Log.i("TEMP", "Now index $currentIndex Done!")
}

RxJava

private val clickEventSubject = PublishSubject.create<View>()	// onClick 처리를 위한 Subject 생성
private var currentIndex = 0
fab.setOnClickListener {
    clickEventSubject.onNext(it)		// 클릭이 이루어지면 onNext를 호출하며 View를 넘겨줌.
}
clickEventSubject
.throttleFirst(500, TimeUnit.MILLISECONDS)	// 첫 번째만 처리하기 위한 throttleFirst (500ms 이후에 동작하게 함.)
.observeOn(Schedulers.io())					// 스케쥴러를 가지고 io 쓰레드에서 동작하게 함.
.map {
    currentIndex++							// index 증가.
}
.switchMap {									// switch에 2개의 zip을 묶음.
    Observable.zip(Observable.range(0, 10),		// 1 부터 10 출력을 위한 range
                  Observable.interval(200, TimeUnit.MILLISECONDS),	// 200ms interval
                   BiFunction<Int, Long, Int> { range, _ ->
                       10 - range
                   })
}
.observeOn(AndroidSchedulers.mainThread())		// UI thread로 변경하고, 메시지 노출
.subscribe({
    tv_message.text = "Now index $currentIndex Countdown $index"
}, {})

Subject, ObserveOn 등등 10 몇가지를 알아야 이해를 할 수 있음.

Rx는 observeOn 등 몇가지 지정을 하고 subject를 통해야 실행을 함.

Coroutines은 CoroutineScope를 지정하고 launch를 하면 실행이 됨.

suspend는 RxJava 처럼 subscribe가 올 때 처리하고 싶을때 선언하고 사용하면 되고, CoroutineScope 안에서만 호출이 가능함.

RxJava

  • 장점
    • Observable과 Streams
    • 기존 Thread보다 간단한 코드로 처리 (어디선가 callback이 올거라 보고 처리해야 함.)
    • stream을 통해 데이터 처리 용이
    • Thread간 교체가 간단
    • RxJava를 활용한 수 많은 라이브러리 활용 가능 (Retrofit 등등)
      • 예제도 많고 문서도 잘 되어 있음.
  • 단점
    • 용어를 모르면 코드 활용 이유를 알 수 없음
    • 처음 학습 비용이 높음

Kotlin coroutines

  • 장점
    • 함수 형태라 읽기 쉬움
    • light-weight threads
    • 모든 routine 동작을 개발자가 처리 가능
    • 처음 학습 비용이 낮음
  • 단점
    • 아직은 필요한 라이브러리를 구현해서 사용해야 함. (RxView와 같은 라이브러리 개발 필요)
      • 직접 만들 수 있고, 문서도 잘 되어 있음.

Kotlin도 Coroutines 처럼 처음은 쉽지만 그 다음부터 어려워짐.

Kotlin coroutines의 장단점

  • 장점
    • 함수 형태라 읽기 쉬움
    • light-weight threads
    • 모든 routine 동작을 개발자가 처리 가능
  • 단점
    • 아직은 필요한 라이브러리를 구현해서 사용해야 함. (RxView와 같은 라이브러리 개발 필요)

Kotlin coroutines 이 제공하는 것들

Kotlin 1.3과 함께 coroutines 1.0 정식 릴리즈됨.

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'

주의할 점 : 코루틴 0.25버전에서부터 deprecated 되었던 부분들이 1.0에서 다 삭제됨. 릴리즈 문서 참고.

  • Kotlin 다양한 platform 제공

    • Server-side

    • Desktop

    • Mobile Application

  • 다양한 언어에서 제공하던 주요 라이브러리 제공

    • C#, ECMAScript : async/await
    • Go : channels, select
    • C#, Python : generators/yield

Kotlin coroutines guide

Coroutines : https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

Android Coroutines - codelab : https://codelabs.developers.google.com/codelabs/kotlin-coroutines/index.html

blocking과 no-blocking

@Test
fun test() {
    GlobalScope.launch {
        delay(300L)			// non-blocking
        println("World!")
    }
    pirntln("Hello,")
    Thread.sleep(500L)		// block main thread. UI 쓰레드에서 동작이라 화면이 멈춤
}
  • Thread.sleep() : Thread 처리 전까지 Main Thread가 멈춤. UI 쓰레드에선 최대한 안써야 함.
  • delay : 별도의 coroutine에서 동작하여 멈추지 않고 넘어감.

그래서 runBlocking을 제공함. (아래 두가지 코드는 동일하게 동작함.)

@Test
fun test() {
    CoroutineScope(Dispatchers.Unconfined).launch {
        delay(300L)
        println("World!")
    }
    pirntln("Hello,")
    runBlocking {
        delay(500L)
    }
}
@Test
fun test() = runBlocking {
    CoroutineScope(Dispatchers.Unconfined).launch {
        delay(300L)
        println("World!")
    }
    pirntln("Hello,")    
    delay(500L)
}

runBlocking

  • runBlocking 은 함수의 처리가 끝날때까지 대기
  • delay()를 걸어두면 delay 시간만큼 대기하고 return
    • Android에서 runBlocking을 UI에서 잘못 사용하면 멈추는 현상 발생
  • delay를 이용해 non-blocking을 할 수 있음
  • runBlocking, CoroutineScope, GlobalScope 안에서 동작해야 함

CoroutineScope, GlobalScope

CoroutineScope

  • 가장 기본적인 Scope
  • Thread 형태를 지정(Main, Default, IO 등을 지정)
    • CoroutineScope(Main, Default, IO, ...)
  • launch, async 등을 통해 scope를 실행
  • 중요한 점
    • Activity/Fragment LifeCycle에 따라야 함
      • onDestroy() : cancel하도록 코드 추가
    • CoroutineScope(/* thread type */).launch { } 로 실행
    • launch { }의 return job의 동작을 지정 가능
      • join() : scope 동작이 끝날때까지 대기하며, suspend에서 호출 가능
      • cancel() : 동작을 종료하도록 호출
      • start() : scope가 아직 시작하지 않을 경우 start, scope의 상태를 확인

CoroutineScope의 interface 정의

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job())


public interface CoroutineScope {
    @Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
    public val isActive: Boolean
    	get() = coroutineContext[Job]?.isActive ?: true
    
    /** Context of this scope. */
    public val coroutineContext: CoroutineContext
}

CoroutineScope 활용 - Delay 대신 job.join()

@Test
fun test() = runBlocking {
    val job = CoroutineScope(Dispatchers.Unconfined).launch {	// 새로운 scope를 생성하고 default로 launch. (Work Thread로 동작함)
        //launch에서 CoroutinScope에서 지정한 default Thread로 사용. (별도 지정 없으면 부모를 따라감)
        delay(300L)
        println("World!")
    }
    pirntln("Hello,")
    // delay(500L) <-- 이 부분은 CoroutineScope에서 네트워크 같은 처리시 의미가 없어짐.
    job.join()		// join()으로 default thread 종료하기 전까지 대기
}

GlobalScope

전역에서 돌아가야할 경우

  • CoroutineScope 상속 받아 구현

  • Demon, Application 등에서 사용

  • Application의 lifetime에 따라 동작하는 scope에서 사용 추천

  • GlobalScope는 Dispatchers.Unconfirned(worker thread) 에서 동작

  • GlobalScope.launch(/* thread type */) { } 로 실행.

    위에서 thread가 이미 지정되어 있어 launch에서 쓰레드 타입을 지정. 기본은 Default임.

GlobalScope API 코드

object GlobalScope : CoroutineScope {
    /**
     * @suppress **Deprecated**: Deprecated in favor of top-level extension property
     */
    @Deprecated(level = DeprecationLevel.HIDDEN, 
                message = "Deprecated in favor of top-level extension property") 
    override val isActive: Boolean
    	get() = true
    
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
    	get() = EmptyCoroutineContext
}

GlobalScope 사용 예

fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = 
GlobalScope.produce(Dispatchers.Unconfined) {
    for(number in this@sqrt) {
        send(Math.sqrt(number.toDouble()))
    }
}

suspend

  • suspend를 활용하여 함수 분할을 할 수 있음.
  • suspend로 만들어진 함수는 사용하기 전까지는 동작하지 않음.
  • suspend 키워드를 사용하는 함수는 CoroutineScope에서만 사용할 수 있음.

suspend 사용 예

suspend fun CorountineScope.loadData(body: suspend CoroutineScope.(item: String) -> Unit) {
    val item = ""
    delay(100L)
    body(item)
}
CoroutineScope(Dispatchers.Main).launch {
    loadData { item ->
        // Coroutine scope 정의
    }
}

Job

  • CoroutineScope의 return에는 job 객체를 넘겨줌.
  • job을 통해 routine의 취소, 실행, 종료를 대기할 수 있음
    • job.cancel() : 종료하도록 유도함.
    • job.join() : 종료를 대기하며 호출은 suspend에서 가능함.
    • job.start() : coroutine의 시작을 확인할 수 잇으며, 시작 상태라면 true

Android onClick 잘 활용하기

연속해서 빠르게 입력하는 경우를 막을 경우

RxJava 버튼 처리

  • 첫 번째 이벤트만 허용하기 위해서

    • throttleFirst 활용

      • 시간으로 first의 시간을 지정하여 문제 발생 외 처리 필요.

        (문제점 : 특정 시간 후에 다음 이벤트가 발생함)

Coroutine으로 처리

  • 첫 번째 이벤트만 허용하기 위해서
    • 버튼의 상태를 변경해서 처리?
  • coroutine에서는 그럴 필요 없음.
  • GlobalScope + actor 활용

Coroutine GlobalScope.actor<T> 활용하기

private fun View.onClick(action: suspend (View) -> Unit) {
    // 5. 이 때 Higher-Order function 정의는 suspend가 포함되어야 함.
    val event = GlobalScope.actor<View>(Dipatchers.Main) { // 1. Singletone의 GlobalScope 활용
        for(event in channel) {						// 2. actor 이용 event 받기
        	action(event) // 4. 받은 event를 Higher-Order function으로 넘겨서 정의하도록 함.
        }
    }
    setOnClickListener {
        event.offer(it)								// 3. actor에 offer로 event 보내기
    }
}
var currentIndex = 0
fab.onClick {
    10.countDown(currentIndex++)				// 6. 람다 표현으로 countDonw 구현
}

Android Coroutines

안드로이드에서 코루틴 활용

  • UnitTest/Main Thread에서 활용하기 쉽게 Dispatchers 하나로 활용
    • UnitTest에서 Main thread를 활용할 수 없기에 기본으로 처리 할 수 있도록 작성
  • CoroutineScope를 Base에 작성하여 release를 쉽게 하도록 처리
  • onClick에 coroutine 활용을 위한 GlobalScope 적용

Dispatchers 정의(UnitTest/Default Thread)

sealed class DispatchersProviderSealed {
    open val main: CoroutineContext by lazy { Dispatchers.Main }
    open val default: CoroutineContext by lazy { Dispatchers.default }
}

/**
 * 기타 Thread를 위한 Dispatchers 정의
 */
object DispatchersProvider: DispatchersProviderSealed()

/**
 * Unit Test를 위한 Dispatchers 정의
 */
// Rx 기준으로 볼때 테스트시 전부 백그라운드 쓰레드로 돌려야할 경우
object TestDispatchersProvider: DispatchersProviderSealed() {
    override val main: CoroutineContext = Dispatchers.Unconfined
    override val default: CoroutineContext = Dispatchers.Unconfined
}

CoroutineScope를 상속받아 구현

abstract class CoroutineScopeActivity: AppCompatActivity(), CoroutineScope {
    private val job: Job = Job()	// Job을 미리 생성하여 CoroutineContext에 미리 지정할 수 있음
    
    override val coroutineContext: CoroutineContext	// Activity에서 사용할 기본 Context를 정의
    	get() = Dispatchers.Main + job
    
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()				// onDestroy()에서 job을 종료하도록 함
    }
}
  • CoroutineScope를 상속받아 구현하면 기본 CoroutineScope로 정의되어 있음.
    • launch, actor<E> 를 사용하면 코드 간결 및 자동으로 종료 처리해 줌.
    • Default 상위 Activity에서 정의한 CoroutineScope의 Thread를 활용함.
    • 필요시 launch, actor에서 Thread를 언제든 변경 가능함.
  • 다양한 CoroutineScope 구현
    • ViewModel
    • LifeCycleObservable
    • Fragment

CoroutineScope 상속받은 Activity에서 routine 사용하기

abstract class CoroutineScopeActivity: CoroutineScopeActivity() {
    launch {
        // UI Thread에서 처리
    }
    launch(Dispatchers.Default) {
        // Default Thread에서 처리
    }
    actor<Generic Type> {
        // UI Thread에서 event 처리
        for(event in channel) action(event)
    }
    actor<Generic Type>(Dispatchers.Default) {
        // Default Thread에서 처리
        for(event in channel) action(event)
    }
}

onClick 처리

  • Higher-Order function + kotlin Extensions을 활용.
  • GlobalScope을 활용하거나, CoroutineScope을 활용 가능.
    • 동작 범위에 따라서 GlobalScope, CoroutineScope을 선택함이 좋음.

onClick 만들기, background에서 처리하고, UI에 던져주기

class CoroutinesSendChannelOnClickEvent<E>(
    private val view: View,							// View: click을 위한 View
    private val bgBody: suspend (item: View) -> E,	// background : Higher-Order Function
    private val dispatcherProvider: DispatchersProviderSealed = DispatchersProvider,	// Provider : 지정
    private val job: Job? = null) {					// Job : 종료 처리를 위한 job 추가
    
    fun consumeEach(uiBody: (item: E) -> Unit): CoroutinesSendChannelOnClickEvent<E> {
        val clickActor = CoroutineScope(dispatcherProvider.main + (job ?: EmptyCoroutineContext)).actor<View> {
            this.channel.map(context = dispatcherProvider.default, transform = bgBody).consumeEach(uiBody)
        }
        view.setOnClickListener { clickActor.offer(it) }	// Offer 처리를 위한 CoroutineScope 생성
        return this
    }
}
fun <E> View.onClick(dispatcherProvider: DispatchersProviderSealed = DispatchersProvider,
                     job: Job? = null, bgBody: suspend (item: View) -> E): CoroutinesSendChannelOnClickEvent<E> =
CoroutinesSendChannelOnClickEvent(this, bgBody, dispatcherProvider, job)

infix fun <E> CoroutinesSendChannelOnClickEvent<E>.consume(uiBody: (item: E) -> Unit) {	// 생성을 간단하게 하기 위한 function 2개
    this.consumeEach(uiBody)
}
fab.onClick(job = job) {		// click을 처리하고, background에서 loadnetwork()
    loadNetwork()
} consume {
    tv_message.text = it
}

private suspend fun loadNetwork(): String {		// Temp load network
    delay(300)
    return "currentIndex ${currentIndex++}"
}

Retrofit2 kotlin coroutines adapter

  • JakeWharton 배포

  • Dependency 추가

    • implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
  • Deferred<E>를 활용하여 return을 처리할 수 있음.

    interface RetrofitService {
        @GET("/posts")
        fun getPosts(): Deferred<Response<List<Post>>>
    }
    // 초기화시키는 코드
    object RetrofitFactory {
        const val BASE_URL = “https://url"
        
        fun makeRetrofitService(): RetrofitService {
            return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build().create(RetrofitService::class.java)
        } 
    }
    // 사용하는 코드
    launch {
        val request = service.getPosts() 
        val response = request.await() 
        if (response.isSuccessful) { 
            // Pass in the response.body() to your adapter 
        } else {
            toast("Error ${response.code()}") 
        }
    }

Q & A

Q : Dispatcher.Unconfined는 부모 코루틴 컨텍스트에서 코루틴을 실행하고 제일 처음 suspend 함수 이후에는 해당 suspend에서 사용했던 쓰레드에서 동작하는 걸로 알고 있음. 예제에서 Unconfined를 쓰셨던데 안드로이드에서 활용 가능한 부분이 있는지?

A : 테스트 코드에서 활용하기 위해 넣은 거임. UI Thread(Main Thread)에서 활용하기 위해 넣어야 함.

Q : Rx를 안쓰고 Coroutine 기반 라이브러리들로 코드 베이스를 변화시키거나 변화시킬 계획인지?

A : Coroutine을 조금씩 적용할 예정이며 Retrofit을 사용하기 위해 새로 팔 예정임. onClick에 먼저 적용 중.

Q : 코루틴이 rx java의 operator 기능들의 유연함을 못따라간다고 들었는데 어떤지?

A : 유연함을 못따라가는 것은 맞음. 아직 라이브러리들이 없음. 기본적인 틀만 제공 중인 상태.

'IT > 행사' 카테고리의 다른 글

[행사] Naver Tech Concert Day-1 요약  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 06  (1) 2019.01.14
[행사] Naver Tech Concert Day-2 04  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 03  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 02  (0) 2019.01.14

Naver Tech Concert Day-2

04. 안드로이드 웹뷰의 모든것

  • 발표자 : 이형욱 (NAVER / Whale Core)

  • 동영상 : https://tv.naver.com/v/4635576/list/272653

  • 슬라이드 : https://www.slideshare.net/NaverEngineering/24-121486425

  • 세션설명 : 안드로이드에서 웹컨텐츠를 렌더링 하는데 많이 사용하고 있는 웹뷰의 동작 원리 대한 설명과 이를 바탕으로 한 웹 성능 최적화를 위한 여러가지 툴 사용법을 설명하는 세션입니다.

  • 목차 :

    1. History of Android WebView
    
    2. Summary of how browsers work
    
    3. Rendering pipeline overview
    
    4. How chromium makes a frame?
    
    5. VSync aligned touch input & frame output
    

1. Android WebView overview

1.1 안드로이드 웹뷰란?

안드로이드에서 제공하는 뷰

사용하는 케이스

  • 안드로이드 기본 브라우저에서 사용
  • 앱에서 거의 대부분의 배너 광고에서 사용
  • 웹(HTML) 기반 앱에서 사용
  • 안드로이드 네이티브 뷰와 함께 사용

혹자는 안드로이드에 숨어있는 제 3의 플랫폼이다 라고도 함.

1.2 안드로이드 웹뷰의 역사

J(젤리빈 / 4.1 / API 16~18) 이하

custom WebKit-based "classic" WebView

K(킷캣 / 4.4 / API 19)

Chromium 30/33-based WebView

L(롤리팝 / 5.0 / 21) 이상

Unbundled Evergreen WebView

1.3 안드로이드 웹뷰와 파편화

  • 안드로이드 제조사에서 경쟁적으로 WebKit을 Customization

  • 안드로이드 버전별 서로 다른 WebKit 사용

    • Android 4.3 이전 버전은 WebKit 기반의 렌더링 엔진을 사용
    • Android 4.4 부터는 Blink 기반의 렌더링 엔진으로 교체

  • 현재는 사용자 웹뷰 업데이트 유무에 따라 서로 다른 버전을 사용 (앱 업데이트 없이도 이슈 생길 수 있음)

1.4 WebKit 브라우저 엔진의 역사

KHTML, KJS(1999.05 ~)

Apple Forks KHTML, KJS in 2001 -> WebKit (2001 ~), Open Source in 2005

Google Forks WebKit in 2013 -> Blink (2013.04 ~)

Webkit을 2008년 크롬 브라우저에서 사용하면서부터 유명해짐.

구글에서 개발인력이 더 늘면서 Blink로 포크 뜨게 됨.

1.5 안드로이드 웹뷰의 구조

Blink 엔진의 구조도

크로미움에서 브라우저 프레임워크 및 컴포넌트를 제공하며, 이를 브라우저 앱들이 사용함.

1.6 크롬과 크로미움의 차이점

크로미움 : 오픈소스

크롬 : 크로미움으로 상품화, 유료 비디오 코덱, 어도비 플래시, QA

2. How chromium works

2.1 브라우저는 어떻게 동작하는가?

크로미움은 브라우저 엔진으로, HTML을 처리하여 화면에 렌더링 역할.

HTML, CSS, JS를 입력으로 받아 파싱하여 렌더링함.

2.2 HTML Parser

<html>
   <head>
       <title>NAVER</title>
    </head>
    <body>
        <div>
            <h1>Hello</h1>
            <p>World</p>
        </div>
    </body>
</html>

DOM : Document Object Model

  • Docuement = HTML, well-formed XML
  • Object Model = Data + Method

DOM Tree로 만들어 놓으면 가공하기가 쉬워짐.

2.3 CSS Parser

body { font-size:16px }
p { font-weight: bold }
span { color:red }
p span { display:none }
img { float:right }

CSSOM : CSS Object Model

  • DOM 과 비슷하게 CSS도 CSSOM이 있음
  • CSS는 HTML Element의 스타일을 정의
  • 외부 링크로 정의된 경우 렌더링이 블로킹 됨
  • Cascade down 개념을 구현하기 위해 트리 구조 (상속개념)

2.4 Java Script Engine

구글에서 포크 뜨면서 자바스크립트 엔진을 다르게 썼었음.

  • 웹킷 : JSC (JavaScript Core)
  • 구글 : V8

두 엔진이 한동안 다르게 동작하였으나, 현재는 위 그림대로 동작하게 됨.

2.5 Render Tree

Rescalculate Style의 결과

  • Render Tree = DOM Tree + CSSOM Tree
  • DOM Tree와 Render Tree는 1:1 관계가 아님
  • 화면에 보이는 요소들을 중심으로 정리 (HEAD나 아래의 p span 등은 안보이는 요소라 트리에 없음)
body { font-size:16px }
p { font-weight: bold }
span { color:red }
p span { display:none }
img { float:right }

2.6 Layout

렌더링 전에 레이아웃팅(좌표 계산)을 거쳐야 함.

Layout 알고리즘

  • 각 박스의 넓이는 viewport (ICB) 기준
  • 각 박스의 높이는 contents (fonts)를 기준
  • 윈도우 사이즈를 변경하거나 폰트 변경시 Global Layout
  • Dirty bit system으로 incremental layout

2.7 Paint

결정된 레이아웃팅 결과를 가지고 페인팅.

브라우저의 페인팅은 프린트와 유사함. 굉장히 오래 걸리고 로드가 많이 걸리는 잡임.

2.8 VSync 기반 멀티 쓰레드 렌더링

브라우저도 안드로이드와 마찬가지로 VSync 기반으로 렌더링함. (16.6ms 내에 한 프레임을 찍어야 함)

현실은 요새 같이 웹이 표현할게 많고 고도화된 상태에서 60FPS 한 프레임에 표현하기 어려워짐.

크로미움쪽은 Compositor Thread를 도입함. (빠르게 스크롤시 흰 화면이 보이는 경우가 해당됨)

Raster Thred도 도입됨.

기존에 메인 스레드에서만 할 수 있는 Paint에서 Draw를 직접 안하고 Recording만 하고,

Raster에서 Bitmap만드는 과정을 분리하여 처리함.

안드로이드에서 2D 그래픽스를 담당하는 오픈소스.

크로미움에서도 사용 중.

3. How chromium powered WebView works

3.1 안드로이드에서 크로미움은 어떻게 동작하는가?

크로미움 브라우저는 멀티프로세서 기반의 브라우저.

UI 담당하는 프로세스와 렌더링를 담당하는 프로세스, GPU를 담당하는 프로세스 등 4가지 프로세스 구성

  • Browser 프로세스 : 크롬 액티비티 등이 동작하는 앱 프로세스.
  • Renderer 프로세스 : 안드로이드 서비스를 활용한 UI가 없는 프로세스. 렌더링이나 로케이팅 등 처리.
    • 보안에 취약하기 때문에 샌드박싱 되어 있음. (메모리와 CPU 리소스만 사용함)
  • GPU 프로세스 : CPU 자원을 접근해야 해서 샌드박스가 안되어 있음.
  • OS Winodw system : 안드로이드 SufaceView를 사용해 브라우저 화면을 렌더링함.

3.2 웨일 브라우저의 뷰 구조

SurfaceView만 사용하는 것은 아님.

3.3 크로미움 웹뷰의 구조적 차이점

Graphics components
  • Whale (Chrome)

    • SufaceView 사용 : 렌더링 성능을 위함. 자체적인 렌더링 싸이클을 가짐. (안드로이드 뷰보다 빠름)
    • Vsync based rendering
    • Async uploads using EGLImage and glTexSubImage2D() (OpenGL 함수들 사용)
  • Chromium powered webview

    • "Draw functor" : inject draw calls into system GL context

      • 안드로이드에서 하드웨어 엑셀레이션 동작시 HWPUI가 동작함. 안드로이드 뷰를 호출해 렌더링함.

        이를 Draw functor에 위임하게 됨.

    • Android based rendering

    • Private API. The WebView injects a callback onto the display list. (일반적인 웹뷰에선 못 씀.)

Architecture
  • Whale (Chrome)

    (Multi-process)

    • UI thread
    • GPU Process
    • Texture upload thread
    • Per renderer process :
      • Blink thread
      • Compositor thread
      • Raster thread
  • Chromium powered webview

    (Single-process/Multi-process)

    • Combined UI + Compositor thread
    • Android RenderThread (+ in-porcess GPU thread)
    • Canvas/WebGL GPU thread
    • Blink thread
    • Raster thread

큰 차이는 Compositor thread가 Whale(Chrome)의 경우 독립적이나 Chromium의 경우 UI thread와 합쳐짐.

(Chromium의 경우 View이기 때문에)

3.4 안드로이드 렌더링 파이프 라인 (KitKat 4.4 이하)

KitKat까지는 메인 쓰레드에서 렌더링함.

메인 쓰레드에선 보통 I/O를 담당해서 빠르게 처리하게 하는데, 렌더링도 메인 쓰레드에서 했지만 메인 쓰레드에서 할 일이 너무 많아짐.

3.5 크로미움 웹뷰 렌더링 파이프 라인 (KitKat 4.4 이하)

onDraw시 Private API인 DrawFunctor를 통해 함수 포인트를 연결해 하드웨어 캔버스가 호출됨.

그리고 콜백이 호출되어 Record, Raster, Composite 들이 돌게 됨.

KitKat까지는 Composite를 메인 쓰레드에서 하느라 좋은 구조는 아니었음.

3.6 안드로이드 렌더링 파이프 라인 (Lollipop 5.0 이상)

렌더 쓰레드가 도입됨. (크로미움의 Raster 쓰레드와 비슷함.)

크로미움과 안드로이드의 기술 발전 흐름은 유사함. (크로미움에서 괜찮으면 안드로이드에도 적용되는 느낌)

3.7 크로미움 웹뷰 렌더링 파이프 라인 (Lollipop 5.0 이상)

onDraw 때 Record와 Raster 쓰레드만 돌고 메인 쓰레드는 거의 안돌게 됨.

Render 쓰레드에서 플레이백하여 Composite하는 구조가 됨.

메인 쓰레드가 많이 Free해짐. 성능 좋아짐.

이 때부터 unbundled임. (AOSP에 없음. 크로미움 참조해야 함.)

쓰레드가 많아져서 쓰레드간 동기나 제어가 중요함.

크로미움엔 스케쥴러 모듈이 있어 메인 쓰레드에서 제어 및 스케쥴링을 하게 됨.

크로미움 웹뷰의 완성도가 많이 올라갔고, HTML5도 많이 지원하게 됨.

'IT > 행사' 카테고리의 다른 글

[행사] Naver Tech Concert Day-2 06  (1) 2019.01.14
[행사] Naver Tech Concert Day-2 05  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 03  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 02  (0) 2019.01.14
[행사] Naver Tech Concert Day-2 01  (0) 2019.01.14

+ Recent posts