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

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

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

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

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

출처: 다재다능 코틀린

상속을 받는 구조에서 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()
  }
}

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

 

 

object

코틀린에서 object 키워드는 싱글톤 객체를 만들 때 사용된다.

ojbect 클래스 안에는 val, var, method 모두 가질 수 있고, 상속을 받는 것도 가능하다.

object Util : Runnable {
    val width = 300
    var time = 0
    fun doSomething() { }
    override fun run() { }
}

사용할 때는 Util.width, Util.doSomething() 이런 식으로 사용할 수 있다.

object 는 객체를 생성할 수 없다. 코틀린 컴파일러는 object를 클래스로 취급하지 않고 이미 객체인 상태로 취급한다.

내부적으로는 Util클래스의 static 인스턴스라고 표현한다.

companion object

companion object 는 클래스 안에 정의한 싱글톤이다.

class Example {
    companion object {
    	val width = 300 
    }
}

클래스의 companion object 멤버에 접근하려면, 클래스 이름으로 참조하면 된다.

Example.width 이런 식으로 접근할 수 있다.

만약 companion object 객체 자체에 대한 참조가 필요하다면 Example.Companion 이런 식으로 할 수 있다.

또는 companion object에 이름을 붙여서 접근할 수도 있다.

class Example {
    companion object Inner {
    }
}

Example.Inner로 companion object를 참조할 수 있다.

companion object의 멤버가 클래스의 static 멤버가 되는 것은 아니다.

companion object의 멤버에 접근하면 코틀린 컴파일러는 싱글톤 객체로 라우팅한다.

@JvmStatic 어노테이션을 사용해서 static 멤버로 만들 수 있다.

 

차이점

  • 초기화 시점
    • object는 사용할 때 초기화된다.
    • companion object는 클래스가 로드될 때 초기화된다.

'Programming > 언어' 카테고리의 다른 글

[Kotlin] Coroutine 기본 개념, 동작 원리  (0) 2023.03.19
[Kotlin] Delegation  (0) 2023.03.01
[Kotlin] Generics 공변성, 반공변성(out, in)  (0) 2023.02.26
[Kotlin] Data Class  (0) 2023.02.15
[Kotlin] Collection의 View  (0) 2023.02.13

Kotlin에서 List, Map, Set같은 Collections 클래스를 사용할 때, List<String> 이런식으로 제네릭 타입을 지정할 수 있다.

제네릭을 사용하면 타입 안정성을 얻을 수 있다.
하지만 제네릭으로 지정한 타입 이외에도 좀 더 유연하게 사용하고 싶을 수 있다.
그래서 코틀린에서는 공변성과 반공변성을 허용한다.

  • 공변성이란, 파라미터 타입 T의 자식클래스를 T의 자리에 사용할 수 있도록 허용하는 것이다.
  • 반공변성이란, 파라미터 타입 T의 부모클래스를 T의 자리에 사용할 수 있도록 허용하는 것이다.
open class Fruit
class Banana : Fruit()
class Orange : Fruit()

fun receiveFruits(fruits: Array<Fruit>) {
    println("${fruits.size}")
}

receiveFruits함수 안에서는 fruits의 크기만 출력하고 있다.

하지만, 파라미터로 Array<Banana>, Array<Orange>를 전달할 수 없다.

왜냐하면 함수 안에서 fruits.add(Orange()) 을 한다고 할 때, Array<Banana>를 전달했다면 타입 에러가 나기 때문이다.

하지만 fun receiveFruits(fruits: List<Fruit>)라면 어떨까? (Array -> List)

된다. 왜?

  • Array는 Mutable 하지만, List는 Immutable하기 때문이다.
    • 따라서 함수 내부에서 fruits.add(Orange()) 이런 연산 자체를 하지 못한다.
  • 각 타입이 정의되는 곳을 보면 Array는 Array<T>, List는 List<out T>로 정의한다.

out 에 대해 알아보자.

out은 공변성을 사용할 수 있게 해주는 키워드이다. 즉, 파라미터 타입으로 T의 자식타입을 사용할 수 있게 해준다.
어떻게 그렇게 하는 걸까?

fun copyFromTo(from: Array<out T>, to: Array<T>) 처럼 선언 했을 때, 함수 안에서 from에 "쓰기" 작업을 하지 않는다는 보장이 있어야 한다. "쓰기"작업이 있는지 없는지는 컴파일러가 판단해 있다면 컴파일 시간에 오류를 준다.

이렇게 Array라는 제네릭 클래스를 정의한 곳이 아닌 사용하는 쪽에서 out으로 공변성을 이용하는 것을 사용처 가변성 또는 타입 프로젝션이라고 부른다.

List와 같이 제네릭 클래스를 정의할 때 out으로 공변서을 이용한다면 선언처 가변성이라고 한다.

예시

fun copyFromTo(from: Array<Fruit>, to: Array<Fruit>) {
    for (i in 0 until from.size) {
        to[i] = from[i]
    }
}

val fruits = Array<Fruit>(3) { _ -> Fruit() }
val bananas = Array<Banana>(3) { _ -> Banana() }
copyFromTo(bananas, fruits)

이 경우 from은 "읽기"만 하고있어 로직 상으로는 전혀 문제가 없지만, Array<Banana>Array<Orange>타입이 들어갈 수 없다.
Fruit의 자식 클래스도 전달할 수 있게 만드려면 from: Array<out Fruit>으로 선언하면 된다.

in; 반공변성은 무엇인가.

공변성과 반대로, 파라미터 타입에 부모클래스를 사용하고 싶을 수 있다.

위의 예시에서 to위치에 Fruit의 부모클래스를 받고 싶다면 in 키워드를 사용하면 된다.

to: Array<in Fruit> 로 선언하면 Fruit의 부모타입도 받을 수 있다.

'Programming > 언어' 카테고리의 다른 글

[Kotlin] Delegation  (0) 2023.03.01
[Kotlin] object, companion object  (0) 2023.02.26
[Kotlin] Data Class  (0) 2023.02.15
[Kotlin] Collection의 View  (0) 2023.02.13
[Kotlin] Unit, Any, Nothing 클래스  (0) 2023.02.12

안드로이드에서 사용하는 단위 6가지를 비교해보자.

dp, sp, pt, px, mm, in이 있다.

기본 지식

픽셀(pixel)은 무엇일까?

디스플레이 화면의 최소단위이다. 

https://news.samsungdisplay.com/305

 

 

11.0인치

2388 x 1668 display

264ppi

 

 

 

11.0인치 : 화면의 물리적인 크기를 나타낸다. (대각선 길이)

2388 x 1668 : 화면의 픽셀 수(가로x세로)

264ppi : pixel per inch; 1인치 안에 들어있는 픽셀 수, dpi(dot per inch)라고 쓰기도 한다.

https://news.samsungdisplay.com/305

안드로이드의 크기 단위

dp 

Density-independent Pixel

dp = px * (160dpi / 기기 dpi)

위와 같은 식을 통해 구할 수 있다.

이렇게 해상도가 다른 2개의 휴대폰이 있을 때,
pixel로 크기를 정의하면 휴대폰마다 보이는 크기가 달라지게 된다.

dp는 화면의 해상도에 상관없이 이미지를 같은 비율로 표시할 수 있다.

안드로이드에서는 160dpi 를 기준으로 사용하며, 이때 1dp는 대략 1px과 같다.

화면의 밀도가 160dpi보다 늘어난다면 사용되는 pixel수는 화면의 dpi에 따라 확장된다.

화면에 상관없이 같은 비율로 보이는 단위

sp

텍스트의 크기에 사용되는 단위로 dp 단위와 같다.

다만, 시스템 글꼴 설정에따라 크기가 달라질 수 있는 단위이다.

pt

화면 밀도와 상관없이 모든 기기에서 1pt = 1/72 inch 이다.

72pt = 1 inch

px

화면에 실제 픽셀에 대응되는 단위

in, mm

실제 물리적인 inch, mm 길이

 

 

 

+ Recent posts