[kotlin] class, object, interface -2

dante Yoon·2022년 4월 2일
3

kotlin

목록 보기
3/9
post-thumbnail

kotlin in action을 보고 정리한 글입니다

심슨 형님 책장 넘기는 속도가 빠르시네요? 부럽습니다.

Declaring a class with nontrivial constructors or properties

자바에서는 한 개 이상의 constructor를 선언할 수 있다. 코틀린은 primary secondary로 constructor를 나누는데
primary는 class를 초기화 하고 class body 밖에서 선언되는 constructor 이며
secondary는 class body 내부에서 선언되는 constructor이며 initializer blocks에 추가적인 초기화 관련 로직을 작성하는 것을 가능하게 한다.

Initializing classes: primary constructor and initializer blocks

다음과 같이 curly braces {} 가 생략되고 parentheses () 만 사용해서 class를 선헌 하는 것을 primary constructor라고 한다.

class User(val nickname: string)

primary constructor는 두 가지 역할을 수행한다.
1. constructor parameters를 상세한다.
2. properties를 정의한다.

class User constructor(_nickname: String) {
  val nickname: String
  
  init {
    nickname = _nickname
  }
}

constructor keyword는 primary/secondary constructor를 declaration 하는데 사용한다.

init keyword는 initilizer block을 사용할때 사용한다.

initlizer block은 class instance가 생길 때 실행되며 한 class에 여러 개를 정의할 수 있다.

위의 코드에서는 class property인 nickname을 초기화 하는 코드를 initializer block에서 작성했는데 아래 코드처럼 initialization block을 생략하고 작성할 수 있다. 또한 primary constructor에 annotation, visibility modifier (public, protected 같은)게 없기 때문에 constructor 키워드도 생략할 수 있다.

class User(_nickname: String) {
  val nickname = _nickname
}

좀 더 간략하게 작성해보겠다.
constructor parameter를 이용해 property가 정의된다면 다음처럼 val keyword를 통해 primary constructor로 초기화 할 수 있다.

class User(val nickname: String)

이 때 val의 의미는 nickname property는 constructor parameter를 통해 정의된다는 뜻을 가진다.

default parameter도 사용할 수 있다.

class User(val nickname: String, val isSubscribed: Boolean = true)

class instance를 만들 때 new keyword를 생략한다.

val alice = User("Alice")
println(alice.isSubscribed)
//true

class가 superclass를 가진다면 primary constructor에서 superclass를 초기화 해야 한다.

open class User(val nickname: String){ ... }
class TwitterUser(nickname: String): User(nickname) { ... }

superclass의 constructor를 제공하지 않으면 parameter가 없더라도 명시적으로 호출해줘야 한다.

class RadioButton: Button()

정의한 class가 다른 곳에서 초기화 되는 것을 원치 않는다면 private constructor를 만들어준다.

class Secretive private constructor() {}

Seconsary constructors: initializing superclass in different ways

자바와는 다르게 overload constructor 대신에 코틀린에서는 default paramete, named argument를 사용할 수 있기에 코틀린에서는 어떤 상황에서 secondary constructor를 사용하는게 좋을지 알아보자

처음 볼 코드는 프레임워크에서 제공하는 class를 extend할때이다.

open class View {
  constructor(ctx: Context) {
    // some code
  }
  constructor(ctx: Context, attr: AttributeSet) {
    // some code
  }
}

위 코드에서 관찰할 수 있는 바는 다음과 같다.

먼저 이 View class는 class name에 ()가 없기 때문에 primary constructor가 선언되지 않았다.

secondary constructor는 원하는 만큼 선언할 수 있다.

secondary constructor는 constructor keyword와 함께 선언된다.

class MyButton: View {
  constructor(ctx: Context)
    : super (ctx) {
    // ...
  }
  constructor(ctx: Context, attr: AttributeSet)
    : super (ctx, attr) {
  }
}

MyButton class는 두 개의 constructor가 있는데 각 constructor에서 super keyword를 통해 superclass의 constructor를 호출한다.
이를 superclass의 constructor에게 delegate(위임)한다고 한다.

kotlin in action p.82 Figure 4.3

다음과 같이 본인 class의 다른 constructor에게 delegate할 수도 있다.

class MyButton: View {
  constructor(ctx: Context): this(ctx, MY_STYLE) {
   // ...
  }
  
  constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
  // ...
  }
}

이를 그림으로 표현하면 다음과 같다.

kotlin in action p.83 Figure 4.4

class가 primary constructor가 없으면, 각 secondary constructor가 base class를 초기화 하거나 다른 class의 constructor에게 delegate해야 한다. superclass를 가지고 있는 class에서는 항상 superclass로의 delegate를 나타내는 화살표가 하나 이상 존재해야 하는 것이다.

Implementing properties declared in interfaces

코틀린에서는 abstract property declaration을 가질 수 있다.

interface User {
  val nickname: String
}

User interface를 사용해 implementation 하는 class는 nickname을 얻을 수 있는 로직을 구현해야 한다.

interface는 nickname이 backing field를 가질지, getter를 통해 값을 얻어야 하는지에 대한 정보를 제공하지 않는다. 따라서 어떤 state도 가지지 않는다.

그렇기 때문에 class에서 해당 property에 대한 로직을 작성해줘야 한다.

class PrivateUser(override val nickname: String) : User

class SubscribingUser(val email: String) : User {
  override val nickname: String
    get() = email.substringBefore("@")
}

class FacebookUser(val accountId: Int): User {
  override val nickname = getFacebookName(accountId)
}

println(PrivateUser("test@kotlinlang.org").nickname)
// test@kotlinlang.org
println(SubscribingUser("test@kotlinlang.org").nickname)
// test

PrivateUser class는 User의 abstract property를 구현(implementation)했기에 override를 사용했다.

SubscribingUser는 nickname property를 custom getter를 사용해서 구현했다.

FacebookUser는 다른 곳에 구현했다고 가정한 메소드를 이용해 구현했다.
이 때 FacebookUser는 backing field를 사용했다.

interface는 backing field를 가지지 않는다는 가정하에 getter, setter를 가질 수 있다.

interface User {
  val email: String
  val nickname: String
    get() = email.substringBefore("@")
    
}

email은 abstraact property 이고 implementation class에서 정의되어야 하며, nickname은 override 하지 않아도 된다.

Accessing a backing field from a getter or setter

class User(val name: String) {
  var address: String = "unspecified"
    set(value: String) {
      println("""
        Address was changed for $name:
        "$field" -> "$value""".trimIndent()
      )
    }
}

val user = User("Alice")
user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
// "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".

setter가 재 정의 되었기 때문에 logging 로직이 실행된다. 이 때 field identifier를 사용해 backing field에 접근했다. getter의 경우 읽기 연산만 가능하고 setter는 쓰기 연산 또한 가능하다.

var keyword property에 대해서 getter, setter중 하나만 변경할 수 있다.

Changing accessor visibility

accessor visibility는 default로 property의 visibility를 따라가지만 임의로 변경할 수 있다.

class LengthCounter {
  var counter: Int = 0
    private set
  
  fun addWord(word: String) {
    counter += word.length
  }
}

Compiler-generated methods: data classes and class delegation

Universal object methods

코틀린 class에는 override 하고싶을 수 있는 toString, equals, hashCode와 같은 메소드가 있다. 코틀린이 어떻게 이런 메소드를 자동을 ㅗ만드는지 알아보려고 한다.

toString

class Client(val name: String, val postalCode: Int)

Client instance에 대해 toString 연산을 수행하면 Client@5e9f23b4와 같은 문자열을 반환하는데 다른 문자열을 얻고 싶다.

class Client(val name: String, val postalCode: Int) {
  override fun toString() = "Client (name=$name, postalCode=$postalCode)"
}

equals

자바에서 == 연산자는 primitive와 reference type을 비교한다.

코틀린에서 == 연산자는 두 object를 비교할 때 사용한다. 그 이면에는 equals 메소드를 호출해서 비교하는 것이다. reference를 비교하고 싶다면 === 를 사용한다.

class Client(val name: String, val postalCode: Int) {
  override fun equals(other: Any?): Boolean {
    if (other == null || other !is Client)
      return false
    return name == other.name &&
       postalCode == other.postalCode
  }
}

hashCode

hashCode 메소드는 항상 equals와 함께 override 되어야 한다.

val processed = hashSetOf(Client("Alice", 342562))
println(processed.contains(Client("Alice", 342562)))
// false

위의 코드에서 false가 나오는 이유는 hashCode 메소드가 없기 때문이다. 만약 두 객체가 동일하다면 같은 hash code를 가져야 하기 때문에 이 조건에 맞지 않는다. HashSet collection은 내부적으로 두 객체의 hash code가 맞는지 먼저 확인한 이후 객체의 내용을 비교하기 때문에 이러한 일이 일어나는 것이다.

class Client(val name: String, val postalCode: Int) {
  ...
  override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

위의 코드와 같이 Client class를 수정하고 나면 의도대로 동작할 것이다.

코틀리 컴파일러가 이러한 부가적인 로직을 작성하는 것을 대신해줄 수 있는데 어떻게 할 수 있는지 알아보자.

Data classes: autogenerated implementations of universal methods

class가 data를 담을 수 있는 편한 보관소가 되게 하고 싶다면 toString, equals, hashCode 메소드를 적절하게 수정해야 한다. 매번 만들 필요가 없게 코틀린 컴파일러의 도움을 받는 방법이 있다.

data modifer를 이용해 class를 만들어보자.

data class Client(val name: String, val postalCode: Int)

data class를 만듦으로써 다음 메소드가 자동으로 override되었다.

  • equals는 instance를 비교하게 되었다.
  • hashCode는 class를 HashMap과 같은 hash-based container의 키로 사용한다.
  • toString declaration의 순서대로 모든 필드들을 string으로 만들어준다.

Data classes and immutability: the 'copy' method

data class instance를 만드는데 var keyword를 사용할 수 있다. data class instance를

  • HashMap과 같은 instance를 key로 사용하는 container의 경우, container가 정의된 이후 instance가 바뀌어 버리면 잘못된 state를 가리킬 수 있기 때문에, 또한
  • 멀티스레드 프로그래밍에서는 다른 스레드로 접근할때 기존 스레드의 레퍼런스가 바뀌지 않는 것이 좋기 때문에 immutable하게 작성하는 것을 권장한다.

data class instance를 immutable 한 객체로 사용하는 것을 용이하게 도와주기 위해 data class는 copy라는 메소드를 추가로 제공하는데, 직접 구현한다고 가정한다면 다음과 같이 생겼다.

class Client(val name: String, val postalCode: int)  {
  ...
  fun copy(name: String = this.name,
           postalCode: Int = this.postalCode) = Client(name, postalCode)
}

Class delegation: using the "by" keyword

superclass - sub clas간 의존성 결합도에 따라 프로젝트 규모가 커질 수록 superclass implementation 변경에 따른 side effect가 발생하기 쉽다.

코틀린은 이를 최소화 하기 위해서 class를 기본적으로 final로 규정한다. 확장성을 염두에 두고 설계된 class에 한해서만 명시적으로 상속을 할 수 있도록 한 것이다.

하지만 실용적인 측면에서, 확장성을 고려하지 않고 설계된 class 또한 추후 다른 기능을 추가해야 하는 경우가 있다. 이를 해결하는 방법으로 Decorator Pattern이 있는데 같은 인터페이스를 갖는 다른 새로운 class를 생성하고 기존 class의 field에 해당 instance를 가지게 하는 것이다.

이 패턴의 단점은 많은 양의 boilerplate가 필요하다는 것이다.

Collection interface에 어떤 변경도 가하지 않는다는 가정하에 다음의 코드가 작성되어야 한다.

class DelegatingCollection<T>: Collection<T> {
  private val innerList = arrayListOf<T>()
  
  override val size:Int get() = innerList.size
  override fun isEmpty():Boolean = innerList.isEmpty()
  override fun contains(element: T): Boolean = innerList.contains(element)
  override fun iterator(): Iterator<T> = innerList.iterator()
  override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}

코틀린은 자체 기능으로 delegation에 대한 first-class 지원 기능을 제공한다.
interface를 implementation 할때마다 (class를 정의할 때마다), by keywoard를 통해 interface implementation을 다른 객체에게 delegating(위임) 할 수 있다.

class DelegatingCollection,T>(
  innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}

by keyword를 붙임으로 인해 컴파일러가 모든 boilerplate를 대신 만들어준다.

이제 몇가지 메소드 동작에 변경을 가해야 할 때, override할 수 있으며 기존에 만들어진 메소드 대신에 이 코드들이 불릴 것이다.

class CountingSet<T>(
  val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
  var objectsAdded = 0
  
  override fun add(element: T): Boolean {
    objectsAdded++
    return innerSet.add(element)
  }
  
  override fun addAll(c: Collection<T>): Boolean {
    objectsAdded += c.size
    return innerSet.addAll(c)
  }
}

val cset = CountingSet<Int>()
cset.addAll(listOf(1, 1, 2))
println("${cset.objectsAdded} objects were added, ${cset.size} remain")
// 3 objects were added, 2 remain

구체적인 구현은 초기화할 때 사용되는 innerSet의 구현에 위임하고 추가 기능이 필요한 add, addAll에 대한 기능만 변경했다.

여기서 관찰할 수 있는 중요한 포인트는 innerSet이 어떻게 구현되었는지에 대한 세부적인 요소에 관심을 기울이지 않고 (addAll이 어떻게 구현되었는지, for문을 통해 add를 호출하는지 아닌지 등 ) innerSet의 api 문서만 보고 필요에 따른 구현을 할 수 있다는 점이다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글