정의란?

C++에서 정의는 객체를 "실체"로 만들겠다는 뜻이다.

실체로 만들겠다 라는 것은 메모리에 할당하는 것을 의미한다. 메모리 상에 유효한 비트로 만들겠다는 것이다.

#include <iostream>

class CTest;

int g_Val;
int g_Arr[3];
CTest g_T;

int main()
{
	
    int v;
    int arr[3];
}

int Add(int a, int b)
{
	
    return a + b;
}

main 위에 정의한 전역변수는 데이터 영역에 , main 안에 있는 지역변수 스택 영역에 , Add함수 자체도 코드영역에 들어가기 때문에 모두 실체화 한거라고 볼 수 있고, 정의했다고 말할 수 있다.

이런 변수나 함수는 프로세스가 사용하는 가상 메모리이다.

class CTest
{
public:
	int m_Value;
    int MFunc();
}

이런 클래스 정의 자체는 프로세스와 상관 없이 컴파일러가 사용하는 메모리이다.

(함수도 같은거 아닌가? 둘 다 코드영역 아닌가? 함수는 포인터 형태로 프로세스가 사용하기 때문에 성격이 다른건가? 클래스 코드 자체는 컴파일 할 때만 참고하기 때문에 성격이 다르다고 하는건가?)

 

선언이란?

위에서 봤던 클래스, 전역변수, 지역변수, 함수 모두 선언한 것이다.

정의라고 해놓고 또 선언이란다. 무슨말이냐?

이유는 정의는 선언을 포함하기 때문이다. 정의를 한 순간 해당 개체는 알려지므로(선언되므로) 따로 선언해 줄 필요가 없다.

그럼 정의만 있어도 되는 거 아니냐?

선언이 꼭 필요한 경우가 있음.

// A.cpp
int g_Val;
int Func()
{
	return 1;
}
// Main.cpp
void main()
{
	g_Val = Func();
}

이렇게 사용하면 컴파일 에러가 발생한다.

g_Val과 Func()는 정의와 동시에 선언 되었지만, A.cpp 에 한정된다.

main도 g_Val과 Func()에 대해 알 수 있게 해주어야 한다.

// Main.cpp

extern int g_Val;
int Func();

void main()
{
	g_Val = Func();
}

따로 헤더파일을 만들어 include를 해주지 않아도, 각 cpp 파일이 obj파일로 만들어지고 심볼릭 테이블에서 정의를 찾아 매핑해주기 때문에 이 코드로도 컴파일/실행이 가능하다.

#include "A.cpp"

void main()
{
    g_Val = Func();
}

이렇게 하면 되는거 아니야?

아니다. 링킹 오류가 난다.

include는 전처리과정에서 복붙된다.

그래서 최종 obj 파일을 링킹할 때 정의가 2개씩 존재하기 때문에 링커가 선택할 수 없다.

extern & static

extern 외부에서 사용할 수 있음을 알려주고, static은 사용 범위가 한정된다는 의미를 알려준다.

extern은 선언할 때만 사용하는 것이 아니라, 정의할 때도 사용된다.

extern int a; // 선언만 됨.
extern int b = 3; // 선언과 정의가 동시에 됨.

int c; // 선언과 정의가 동시에 됨.
int d = 3; // 선언과 정의가 동시에 됨.

일반적으로 `extern int b = 3;`은 `int b = 3;`과 의미가 똑같기 때문에 생략한다.

// A.cpp
extern int g_Val;

// Main.cpp
#include <iostream>
extern int g_Val;

void main()
{
	printf("%d", g_Val);
}

그래서 이렇게 선언만 있는 경우, 링킹 오류가 발생한다. 심볼을 찾을 수 없다고 나온다.

클래스 선언과 정의

// A.cpp
class CTest
{
public:
	int m_Val;
};

// Main.cpp
class CTest; // 선언
void main()
{
	CTest t; // Compile Error
}

12 라인에서 컴파일 에러가 난다.

왜일까?

값형은 스택에 할당되고, 스택의 크기는 컴파일 시점에 결정된다.

CTest는 Main.cpp 입장에서는 선언만 돼있다. 어떤 멤버가 있는지, 크기가 몇인지는 알 수 없다.

그렇기 때문에 힙에 할당되는 포인터 형은 가능하다.

class CTest;
void main()
{
	CTest* pT;
}

사실 힙이라기보단 포인터 타입이 OS에 따라 4바이트 혹은 8바이트로 정의되어있기 때문이라고 할 수 있겠다.

해결 방법은?

Main.cpp에도 CTest를 정의해주면 된다. 클래스는 중복 정의를 허용한다.

그래서 보통 헤더 파일에 클래스 정의를 만들고, cpp 파일에서 인클루드 해서 사용한다.

중복 정의를 할 수 있기 때문에 중복된 정의가 다른 경우 문제가 발생할 수 도 있다.

클래스 크기는 같지만, 멤버 함수 순서가 달라 메모리 할당 순서가 바뀌는 경우

 

Kotlin으로 코드를 작성하다보면 보게되는 연산자들이 있습니다.

?. , ?: , !! 모두 Null 타입을 처리하기위해 사용하는 연산자들입니다.

Java와 비교하면서 천천히 알아보겠습니다.

In Java
private void doSomething(String str) {
    System.out.println(str.length());
}

위의 코드는 안전한 코드일까요?
.
.
.
아닙니다.

str 이 null 일 가능성이 있기 때문입니다. null에 대해 length()를 호출하면 NullPointerException이 발생하죠.

private void doSomething(String str) {
    if (str != null) {
        System.out.println(str.length());
    }
}

이렇게 써야 안전한 코드라고 할 수 있겠습니다.

In Kotlin

Kotlin은 어떨까요?

private fun doSomething(str: String) {
    println(str.length)
}

위의 코드는 안전한 코드일까요?
.
.
.
맞습니다.

왜일까요?
Kotlin은 null 이 가능한 타입과 불가능한 타입을 확실히 구분합니다.

String 타입은 절대 null이 될 수 없는 타입입니다.
String? 타입이 nullable 타입입니다.

따라서 str은 String 타입이므로 null check를 안해도 됩니다.

private fun doSomething(str: String?) {
    if (str != null) {
        println(str.length)
    }
}

String? 같은 nullable 타입일 때만 null check를 해주면 됩니다.
하지만 코틀린에서 제공해주는 ?. , ?: , !! 를 사용하면 더 섹시하게 처리할 수 있습니다.

?. Safe call operator
private fun doSomething(str: String?) {
    val length = str?.length  
    println(length)
    // println(str?.length)
}

str?.lengthlength 변수에 할당해서 사용했습니다.
?.는 safe call operator라고 합니다.

str.length 를 호출 하면 str이 null일 수 도 있기 때문에 NPE가 발생할 확률이 있습니다.
(Kotlin에선 컴파일 오류)

애초에 NPE가 왜 발생할까요?
우선 length() 는 String 클래스 안에 정의된 함수(메소드)입니다.

str변수에 String 인스턴스가 할당돼있지 않은 상태에서 String 클래스의 함수를 호출하기 때문입니다.
null.length() 를 호출하는 꼴입니다. null에는 당연히 length() 라는 함수가 없습니다.
그래서 NPE가 발생합니다.

safe call operator를 사용해서 str?.length 호출하게되면 str이 null일 경우 뒤의 호출을 무시하고 식의 전체 결과가 null로 반환됩니다.

val doubleLength = str?.length?.toDouble()이렇게 chaining하여 사용할 수도 있습니다.

또, Kotlin은 Scope function 이라는 걸 제공합니다.
https://kotlinlang.org/docs/scope-functions.html

private fun doSomething(str: String?) {
    str?.let { notNullStr ->
        println(notNullStr)
    }
}

let 을 사용해서 null 안정성을 확보해도 됩니다.

질문: str?.length의 타입은 무엇일까요?

?: Elvis operator

세워서 보면 엘비스 프레슬리를 닮아서 이름 지어진 연산자입니다.

?: 널이라면? 으로 해석하시면 편할 거 같습니다.

private fun doSomething(str: String?) {
    val str2 = str ?: "Hello"
    println(length)
}

str 이 널이라면 str2"Hello" 를 할당한다. 라는 뜻입니다.

자바로 보면 대충 이렇겠죠.

String str2 = "";
if (str == null) {
    str2 = "Hello";
} else {
    str2 = str;
}

물론 safe call과 함께 이용할 수도 있습니다.
val doubleLength = str?.length?.toDouble() ?: 0.0

값 대신 함수를 종료하거나 예외를 던질 수도 있습니다.
val str2 = str ?: return
val str2 = str ?: throw Exception()

!! Not-null assertion operator

얘는 절대 null 아님으로 해석하시면 될 거 같습니다.

private fun doSomething(str: String?) {
    println(str!!.length)
}

이렇게 하면 컴파일 오류 없이 length 를 호출할 수 있습니다.
하지만 런타임에서 NPE가 터질 확률이 있죠. 그래서 웬만하면 사용하지 말라는 연산자이고,
공식문서에도 for NPE-lovers라는 표현이 나옵니다.

여러분은 어떻게 생각하시나요? !! 은 무조건 쓰면 안되는 걸까요?

0. 코루틴이란?

  • 코루틴은 비동기 실행을 간단하게 하기위한 concurrency design pattern이다.
    • suspendable computation 기법을 사용한다.
      • 함수의 실행을 특정 지점에서 일시 중단하고 나중에 다시 실행할 수 있는 기법
  • 코루틴(Coroutine)은 코틀린(Kotlin)에만 존재하는 것이 아니다.
    • Python, Go, C#, Js 등에서도 사용하는 개념이다.

1. Main Routine, Sub Routine, Co Routine

Subroutine vs Coroutine

  • 일반적으로 한 함수에서 다른 함수를 실행할 때, 호출되는 함수를 서브루틴이라고 부른다.
fun register() {
	...
	getDeviceToken()
	...
}

fun getDeviceToken() {
	...
}
  • getDeviceToken() 함수는 register() 함수의 서브루틴이다.

 

  • 코루틴은 호출한 함수의 안에서 수행되는 것이 아니라 호출한 함수와 함께 수행되는 루틴이다.

  • coroutine1안에서 coroutine2가 실행되는 것이 아니다.
  • 각 코루틴은 하나의 routine이며 쓰레드를 공유하며 함께 실행되는 것이다.
    • 따라서 병렬적으로 처리하는 것이 아닌 동시적으로 처리하는 기법이다.

Concurrency vs Parallelism

  • CPU의 코어는 1개의 프로세스만 실행할 수 있다.
  • 싱글 코어 CPU에서는 멀티태스킹을 Concurrent하게 처리한다.
    • 실제로 프로세스가 동시에 실행되지 않는다.
    • 시분할을 통해 동시에 실행되는 것 처럼 동작한다.
 

  • 멀티태스킹을 Parallel하게 하려면 코어나 CPU가 여러개 있어야한다.
    • 같은 시간에 여러 프로세스가 실행된다.

2. 동작방식 - H/W

Thread

  • 쓰레드는 같은 프로세스 내에서 Heap, Code, Data를 공유하며, Stack영역을 독립적으로 가진다.
    • Thread단위로 작업하면 context switching 비용이 프로세스에 비해 적어진다.

Coroutine

  • 코루틴의 작업 단위는 Coroutine Object로 JVM의 Heap에 적재된다.
  • 한 쓰레드에서 Coroutine Object만 변경되기 때문에 context switching이 필요없다.
    • 프로그래밍적으로 switching을 할 수 있다.
    • Coroutine을 Lightweight Thread라고도 한다.

3. 사용

코루틴은 크게 3가지로 구성된다.

  • Coroutine Scope
  • Coroutine Context
  • Coroutine Builder

코루틴이 어떤 범위에서 동작하는 지에 대한 Scope를 정하고, 어떤 쓰레드에서 코루틴을 실행할 지 Context를 설정하고, 코루틴 객체를 만드는 Builder로 구성된다.

Scope

  • CoroutineScope
    • 코루틴이 필요할 때 만들고, 필요하지 않을 때 취소할 수 있는 Scope이다.
    • 안드로이드에서는 라이프사이클을 따라간다.
  • GlobalScope
    • Application이 종료될때 까지 유지되는 Scope이다.
    • Singleton으로 만들어져 있다.
    • 메모리 누수가 생기기 쉽다.

Context

Dispatchers

코루틴이 실행이되는 쓰레드를 지정

  • Default
    • 많은 CPU 연산이 필요한 작업을 하기위한 백그라운드 쓰레드
  • IO
    • File, Network, DB IO를 위한 쓰레드
  • Main
    • UI 쓰레드

Builder

launch

  • Job 객체 반환
val job: Job = CoroutineScope(Dispatchers.IO).launch {
 // 비동기함수()
}

async

  • Deferred 객체 반환
    • 람다 안에 마지막 줄이 반환된다.
    • await() 메소드가 호출되면 async가 실행되고 실행을 완료하고 결과를 반환할 때 까지 기다린다.
suspend fun doWorld() {  // this: CoroutineScope
  val deferredInt: Deferred<Int> = CoroutineScope(Dispatchers.IO).async {
		println(1)
		2
	}
  println(3)

	val result: Int = deferredInt.await()
  
  println("$result")
  println(4)
}

// 결과
3
1
2
4

runBlocking

  • 현재 쓰레드를 block하고, 코루틴 함수를 실행한다.

withContext

  • Dispatcher 변환
    • Main 스레드에서 작업하는 중에 DB작업 같은 IO작업이 필요할때 사용할 수 있다.
CoroutineScope(Dispatchers.Main).launch {
	updateUI1()
	updateUI2()
	withContext(Dispatchers.IO) {
		insertUserData()
	}
	updateUI3()
}

Job & Deferred

  • 코틀린에서는 Coroutine Object를 Job이나 Deferred로 정의한다.
    • Deferred는 결과값을 가지는 Job이다.
  • 코루틴의 상태를 가지고 있고 흐름을 관리할 수 있다.
    • start()
      • 중단 없이 실행됨
      • 실행을 기다리지 않는다. 호출한 쓰레드가 종료되면 같이 종료
    • join()
      • Job의 동작이 완료될 때 까지 코루틴 일시 중단
    • cancel()
      • 코루틴을 종료하도록 유도한다. 대기하지 않는다.
    • cancelAndJoin()
      • 코루틴 종료 신호를 보내고 종료할 때 까지 기다린다.
    • cancelChildren()
      • 자식 코루틴 종료
  • Job states cycle
                                          wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+

예외처리

  • CoroutineExceptionHandler를 사용하여 예외처리를 할 수 있다.
    • 코루틴 안에서 예외 발생 시 exception을 handler로 전달하여 처리하는 흐름을 만들 수 있다.
    • launch vs async
val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
    throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
    throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)

4. 예시

  • 앱에 로그인 했을 때, Sendbird User를 만들고 디바이스 토큰을 등록하는 상황
    • 유저등록이 완료된 후, 디바이스 토큰을 등록해야 한다.
  • callback
fun initializeSendbird() {
	createUser() {
		registerToken() {
			doSomething() 
		} 
	}
}
  • rx
fun initializeSendbird() {
	Observable
		.just(...)
		.observeOn(MAIN_Thread)
		.subscribeOn(IO_Thread)
		.flatMap { () -> createUser() }
		.flatMap { () -> registerToken() }
		.subscribe( { () -> doSomthing() }, { fail() })
}
  •  coroutine
    • 비동기 작업을 순차적으로 작성할 수 있다.
      • initializeSendbird() 함수가 코루틴이기 때문에, createUser()를 호출하고, 코루틴을 빠져나간다. (다른 스레드에서 실행)
      • createUser()가 완료되면 코루틴으로 들어와서 다음 함수를 호출한다.
suspend fun initializeSendbird() {
    val result = createUser()
    registerToken(result)
    doSomething()
}

 

5. 동작방식 - S/W

  • 코루틴은 어떻게 suspendable하게 동작하는가?
  • 코루틴은 CPS(Conrinuation Passing Style)이라는 형태의 코드로 변환된다.
    • label로 중단점을 나누고, 함수 실행 완료되면, resume을 호출하여 코루틴을 재개한다.
suspend fun myCoroutine(cont: MyContinuation) {
    val userData = fetchUserData()
    val userCache = cacheUserData(userData)
  updateTextView(userCache)
}

⬇️

fun myCoroutine(cont: MyContinuation) {
    when(cont.label) {
        0 -> {
            cont.label = 1
            fetchUserData(cont)
        }
        1 -> {
            val userData = cont.result
            cont.label = 2
            cacheUserData(userData, cont)
        }
        2 -> {
            val userCache = cont.result
            updateTextView(userCache)
        }
    }
}

fun fetchUserData(cont: MyContinuation) {
    val result = "[서버에서 받은 사용자 정보]"
    cont.resumeWith(Result.success(result))
}

출처

[Coroutine] Coroutine(코루틴)과 Subroutine(서브루틴)의 차이 - Coroutine이란 무슨 뜻일까?

Coroutine, Thread 와의 차이와 그 특징

알기쉬운 코루틴 이론::Android Studio에서 Kotlin으로#28

코틀린 코루틴(coroutine) 개념 익히기 · 쾌락코딩

Coroutine exceptions handling | Kotlin

[Kotlin] Coroutine - 코루틴의 내부 구현

델리게이션은 객체지향 프로그래밍의 디자인 방식이다.

상속과 델리게이션 모두 클래스를 다른 클래스로 부터 확장시키는 것이다.

델리게이션은 상속보다 유연한 개념으로, 객체가 자신이 처리해야할 일을 다른 클래스의 인스턴스에게 위임하거나 넘겨버릴 수 있다. 서로 다른 클래스의 인스턴스끼리 위임을 할 수 있다.

델리게이션을 써야하는 상황

  • 다형성이 필요하면 상속을 사용하라.
  • 클래스의 객체가 단순히 다른 클래스의 객체를 사용만 해야한다면 델리게이션을 사용하라.

출처: 다재다능 코틀린

상속을 받는 구조에서 Candidate 클래스의 인스턴스는 내부에 BaseClass의 인스턴스를 같이 가지고 있다고 볼 수 있다.

BaseClass타입으로 Candidate인스턴스를 사용할 수 도 있다. 이런 상속을 통한 다형성을 얻을 수 있다.

하지만, 상속받은 클래스를 자식 클래스에서 마음대로 바꾸려고 하면 오류를 일으킬 있다. 

또한 "자료형 S가 자료형 T의 서브타입라면 필요한 프로그램의 속성의 변경 없이 자료형 T의 객체를 자료형 S의 객체로 교체할 수 있어야 한다"라는 리스코프 치환원칙(LSP)를 지키려면 제약사항이 생기게 된다.

위 오른쪽 그림에서 Candidate 클래스가 DelegationObject로 델리게이션을 하면, Candidate 클래스의 인스턴스는 델리게이션의 참조를 갖는다.

Caller가 Candidate에게 요청을 보내면 Candidate인스턴스는 적절하게 위임하여 처리한다.

상속과 델리게이션 선택

상속

"개는 동물이다"와 같이 포함관계에 있는 다른 클래스로 대체할 때 상속을 사용한다.

델리게이션

오직 다른 객체의 구현을 재사용하는 경우라면 델리게이션을 사용한다.
Manager는 Assistant를 가지고 있고, Assistant에게 일을 넘기기만 한다면 델리게이션을 사용하라.

상속에서 델리게이션으로

상속을 이용해서 작은 문제를 설계해보고, 상속이 방해요소로 변하는 시점과 문제해결을 위해 델리게이션을 사용하는 이유를 알아보자.

상속의 문제

기업의 소프트웨어 프로젝트를 시뮬레이션하는 어플리케이션을 만들 것이다.

먼저 일을 할 작업자인 Worker 인터페이스를 만들자.
Worker는 2가지를 할 수 있다. 1. 일을 하거나, 2. 휴가를 떠난다.

interface Worker {
    fun work()
    fun takeVacation()
}

Worker가 될 수 있는 JavaProgrammer와 CSharpProgrammer 두 개의 클래스를 구현해보자.

또한 이 Worker들을 관리하기 위한 매니저가 필요하다.

class JavaProgrammer : Worker {
    override fun work() = println("...write Java...")
    override fun takeVacation() = println("...code at the beach...")
}

class CSharpProgrammer : Worker {
    override fun work() = prinln("...write C#...")
    override fun takeVacation() = println("...branch at the raanch...")
}

class Manager

매니저는 아무일도 하지 않는다.

매니저를 어떻게 설계해야할까?

일단 매니저는 Worker 들에게 work(), takeVacation()을 명령할 수 있어야한다.

 1. 상속을 통해 구현하기

class Manager : JavaProgrammer()
val manager = Manager()
manager.work()
  • JavaProgrammer, CSharpProgrammer를 상속받아 매니저를 구현하면, Manager클래스에서 메소드를 다시 작성할 필요가 없다.
  • 그냥 부모클래스의 함수를 호출하기만 하면 된다.

이런 방식은 문제가 많다. 매니저가 특정 클래스에 갇혀버리게 된다. 특정 언어마다 매니저가 존재해야한다.

우리가 원하는 것은 Manager가 모든 worker에게 일을 시킬 수 있는 구조이다.

이것을 델리게이션을 통해 할 수 있다.

2. 델리게이션 사용하기 (Java 방식)

class Manager(val worker: Worker) {
    fun work() = worker.work()
    fun takeVacation() = worker.takeVacation()
}

val manager = Manager(JavaProgrammer())
manager.work()
  • 매니저 클래스의 생성자로 JavaProgrammer() 를 넘겨주었다.
    • Manager가 JavaProgrammer클래스에 강하게 묶이지 않는다.
    • Worker를 구현한 어떤 클래스든 넘길 수 있다.
    • Manager의 인스턴스는 Worker인터페이스를 구현한 클래스에 인스턴스에게 위임할 수 있다.
  • JavaProgrammer 클래스에 open을 적지 않아도 된다.

하지만, 이 방식에도 문제가 있다.

매니저의 work()메소드는 Worker의 인스턴스를 호출만 하고 있다.

개방-폐쇄 원칙(OCP)를 생각해보면, 확장에는 열려있고, 변경에는 닫혀있어야한다.

현재 구조에서 Worker인터페이스에 메소드를 추가한다면 Manager클래스도 해당 메소드를 호출하는 메소드를 추가해야한다.
이런 방식은 OCP를 위반한다.

3. 코틀린의 by 키워드를 사용한 델리게이션

코틀린에서는 개발자가 직접 손대지 않고도 컴파일러에게 코드를 라우팅하도록 할 수 있다.

class Manager() : Worker by JavaProgrammer()
val manager = Manager()
manager.work()

Manager는 어떤 메소드도 가지고 있지 않다. 

코틀린 컴파일러는 Worker에 속하는 Manager클래스의 메소드를 바이트코드 수준에서 구현하고, by 키워드 뒤에 나오는 JavaProgrammer 인스턴스로 호출을 라우팅한다.

by왼쪽에는 인터페이스, 오른쪽에는 인터페이스를 구현한 클래스가 필요하다.

파라미터에 위임하기

위 예제의  문제점

  1. Manager 클래스의 인스턴스는 오직 JavaProgrammer의 인스턴스에만 요청할 수 있다.
  2. Manager의 인스턴스는 델리게이션에 접근할 수 없다.

이런 제약은 인스턴스를 생성할 때, 생성자 파라미터로 델리게이션을 전달하면 해결할 수 있다.

class Manager(val staff: Worker) : Worker by staff {
    fun meeting() = println("organizing meeting with ${staff.javaClass.simpleName}")
}

val manager1 = Manager(JavaProgrammer())
val manager2 = Manager(CSharpProgrammer())

메소드 충돌 관리 

델리게이션을 사용할 때 메소드가 충돌되는 경우는 어떤게 있을까?

  1. 델리게이션 메소드와 클래스 내부 메소드의 선언이 같은 경우
  2. 두 개 이사의 인터페이스를 델리게이션하는 경우

델리게이션 메소드와 클래스 내부 메소드의 선언이 같은 경우

코틀린 컴파일러는 델리게이션에 사용되는 클래스마다 델리게이션 메소드를 위한 Wrapper를 만든다.

Worker 인터페이스는 work()메소드를 가지고 있다. 만약 Manager 클래스도 work() 메소드를 가지고 있다면 둘 중에 어느 것을 호출해야할까?

class Manager(val staff: Worker) : Worker by staff {
    override fun work() = println("work")
}

클래스 안에 델리게이션 인터페이스와 같은 이름, 시그니처의 메소드를 사용하려면 override 키워드를 명시적으로 사용해야한다.

이렇게 함으로써 코드를 읽는 사람은 해당 메소드가 어쩌다보니 같은 이름으로 생성된 게 아니라, 인터페이스의 메소드를 구현했다는 사실을 명확히 알 수 있다.

코틀린 컴파일러는 work() 메소드의 wrapper를 생성하지 않고, takeVacation()의 wrapper만 생성한다.

두 개 이상의 인터페이스를 델리게이션 하는 경우

코틀린에서 클래스는 여러 개의 인터페이스를 델리게이션 할 수 있다.

이런 인터페이스 사이에서 메소드 충돌이 있다면 어떻게 될까?

interface Worker {
    fun work()
    fun takeVacation()
    fun fileTimeSheet() = println("Hello")
}

interface Assistant {
    fun doChores()
    fun fileTimeSheet() = println("Hi")
}

class DepartmentAssistant : Assistance {
    override fun doChores() = prinltn("HHHH")
}

같은 메소드 선언을 갖는 인터페이스가 2개 있을 때,

class Manager(val staff: Worker, val assistant: Assistant) :
  Worker by staff, Assistant by assistant {
  
  override fun fileTimeSheet() {
    assistant.fileTimeSheet()
  }
}

충돌된 메소드를 오버라이딩 해야한다.

 

 

+ Recent posts