프로그래밍 노트/Effective 시리즈

[Effective Kotlin] 5장. 객체 생성

깡냉쓰 2023. 12. 17. 15:00
728x90
반응형

객체를 생성하는 방법에 어떤 것들이 있는지, 각각의 방법에는 어떤 장점과 단점이 있는지 살펴보자.

[Effective Kotlin. 33] 생성자 대신 팩토리 함수를 사용하라.

객체 생성 방법

  1. 생성자
  2. 별도의 함수를 통해 생성

생성자의 역할을 대신 해주는 함수를 팩토리 함수라 부르며, 아래와 같은 장점들이 생김

  1. 생성자와 다르게 함수에 이름을 붙일 수 있음 (가독성이 좋아짐)
    • ArrayList(3) : ArrayList.withSize(3)
  2. 함수가 원하는 형태의 타입을 리턴할 수 있음
  3. 호출될 때마다 새 객체를 만들 필요가 없음
    • 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 최적화를 위해 캐싱 메커니즘을 사용할 수 있음
  4. 아직 존재하지 않는 객체를 리턴할 수 있음
  5. 가시성을 원하는 대로 제어할 수 있음
  6. 인라인으로 만들 수 있음
  7. 생성자로 만들기 복잡한 객체도 만들 수 있음
  8. 생성자는 즉시 슈퍼클래스 또는 기본 생성자를 호출해야 하지만 팩토리 함수는 원하는 때에 생성자를 호출할 수 있음

1. Companion 객체 팩토리 함수

companion 객체를 사용하여 팩토리 함수를 정의하는 가장 일반적인 방법 (정적 팩토리 메소드 함수)

  companion object {
    fun <T> of(vararg elements: T): MyLinkedList<T>? {
      /*...*/
    }
  }
}

// 사용
val list = MyLinkedList.of(1, 2)

팩토리함수에는 아래와 같은 이름들이 많이 사용 되니 참고

  • from : 파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수
    • val date: Date = Date.from(instant)
  • of : 파라미터를 여러개 받고, 이를 통합해서 인스턴스를 만들어 주는 함수
    • val faceCards: Set = EnumSet.of(JACK, QUEEN, KING)
  • valueOf : form, of 와 비슷한 기능을 하면서도, 의미를 조금 더 쉽게 읽을 수 있게 이름을 붙인 함수
    • val prime: BigInteger
  • instance/getInstance : 싱글턴으로 인스턴스 하나를 리턴하는 함수. 아규먼트를 기반으로 하는 인스턴스를 리턴
    • val luke: StackWalker = StackWalker.getInstance(options)
  • createInstance/newInstance : getInstance처럼 동작하지만, 싱글턴이 아닌 호출할 때 마다 인스턴스를 생성하여 리턴
    • val newArray = Array.newInstance(classObject, arrayLen)
  • getType : getInstance처럼 동작하지만, 팩토리 함수가 다른 클래스에 있을때 사용하는 이름
    • val fs: FileStore = Files.getFileStore(path)
  • newType : newInstance처럼 동작하지만, 팩토리 함수가 다른 클래스에 있을때 사용하는 이름
    • val br: BufferedReader = Files.newBufferedReader(path)

2. 확장 팩토리 함수

이미 companion 객체가 존재할 때, 이 객체의 팩토리 함수를 만들어야할 때 사용
companion 객체를 직접 수정할 수는 없고, 다른 파일에 함수를 만들어야 한다면 확장 함수를 활용할 수 있음

// Tool 인터페이스는 교체할 수 없는 상황 
interface Tool {
  companion object { /*...*/ }
}

companion 객체를 활용하여 확장 함수를 정의할 수 있음

fun Tool.Companion.createBigTool( /*...*/ ): BigTool { // ... }

// 호출
Tool.createBigTool()

companion 객체를 활용하라면, 적어도 비어 있는 컴패니언 객체가 필요하다는 것을 기억해야 함

3. 톱레벨 팩토리 함수

대표적인 예로 listOf, setOf, mapOf가 있음
companion 객체로 팩토리 함수를 만들 수 있지만, 톱레벨 함수로 사용할 때가 읽기 쉬운 부분이 있을 수 있음

listOf(1, 2, 3)
ArrayList.of(1, 2, 3)

하지만 톱레벨 함수를 만들 때는 이름을 신중하게 생각해서 잘 지정해야 함

4. 가짜 생성자

List와 MutableList는 인터페이스이지만, List를 생성자처럼 사용하는 코드가 존재한다.

List(4) { "User$it" } [User0, User1, User2, User3]

다음과 같은 함수가 stdlib에 포함되어 있기 때문

public inline fun <T> List(
  size: Int,
  init: (index, Int) -> T
): List<T> = MutableList(size, init)

public inline fun <T> MutableList(
  size: Int,
  init: (index, Int) -> T
): MutableList<T> {
  val list = ArrayList<T>(size)
  repeat(size) { index -> list.add(init(index)) }
  return list
}

이러한 톱레벨 함수는 생성자처럼 보이고, 생성자처럼 작동한다. 하지만 팩토리 함수와 같은 모든 장점을 갖음
많은 개발자가 이 함수가 톱레벨 함수인지 몰라 이것을 가짜 생성자(fake constructor)라고 부른다.

가짜 생성자를 만드는 이유

  • 인터페이스를 위한 생성자를 만들고 싶을 때
  • reified 타입 아규먼트를 갖게 하고 싶을 때(?)

가짜 생성자를 선언하는 방법이 또 하나 있긴 한데, 추천하지는 않는 방식임

class Tree<T> {
  companion object {
    operator fun <T> invoke(size: Int, generator: (Int) -> T): Tree<T> {
      // ...
    }
  }
}

// 사용
Tree(10) { "$it" }
  • 연산자 오버라이드의 의미가 맞지 않음
  • invoke는 호출한다는 의미며 객체 생성과 의미가 다름
    • 이런식으로 연산자를 오버로드하면, 원래 의미와 차이가 발생

5. 팩토리 클래스의 메서드

점층적 생성자 패턴과 빌더 패턴은 코틀린에서 의미가 없음
팩토리 클래스는 클래스의 상태를 가질 수 있다는 특징 때문에 팩토리 함수보다 다양한 기능을 갖음

data class Student(
  val id: Int,
  val name: String,
  val surname: String
)

class StudentsFactory {
  var nextId = 0
  fun next(name: String, surename: String) = Student(nextId++, name, surname)
}

val factory = StudentFactory()
val s1 = factory.next("깡", "냉")
val s2 = factory.next("corn", "world")

팩토리 클래스는 프로퍼티를 가질 수 있기 때문에 이를 활용하여 다양한 종류로 최적화, 다양한 기능을 도입할 수 있음
예를 들어 캐싱을 활용하거나, 이전에 만든 객체를 복제해서 객체를 생성하는 방법으로 객체 생성 속도를 높일 수 있음

 

반응형

 

[Effective Kotlin. 34] 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라.

기본 생성자가 좋은 방식인 이유를 이해하려면, 아래 패턴들을 이해하는 것이 우선이다.

  • 점층적 생성자 패턴(telescoping constructor pattern)
  • 빌더 패턴(builder pattern)

점층적 생성자 패턴

여러 가지 종류의 생성자를 사용하는 간단한 패턴

class Pizza{
  val name : String
  val cheese : Int
  val olview : Int
  val bacon : Int
}

constructor(size: String , cheese : Int , olives : Int , bacon : Int){
  this.size = size
  this.cheese = cheese
  this.olives = olives
  this.bacon = bacon
}
constructor(size: String , cheese : Int,olives : Int): this (size, cheese, 0)
constructor(size: String , cheese : Int): this (size, 0 )

위와 같은 코드는 좋지 않음.
코틀린에서는 일반적으로 디폴트 아규먼트(default argument)를 사용하여 이 문제를 해결함

class Pizza(
  val size: String,
  val chees : Int = 0
  val olives : Int = 0
  val bacon :Int = 0
)

코드를 단순하고 깔끔하게 만들어 줄 뿐만 아니라, 훨씬 다양한 기능을 제공함

  • 파라미터들의 값을 원하는 대로 지정할 수 있음
  • 아규먼트를 원하는 순서로 지정할 수 있음
  • 명시적으로 이름을 붙여서 아규먼트를 지정하므로 의미가 훨씬 명확
val villagePizza = Pizza(
  size = "L"
  cheese = 1,
  olives = 2,
  bacon = 3
)

빌더 패턴

자바는 이름 있는 파라미터와 디폴트 아규먼트를 사용할 수 없어 빌더 패턴을 사용한다.
빌더 패턴을 사용하면 다음과 같은 장점이 있음

  • 파라미터에 이름을 붙일 수 있음
  • 파라미터를 원하는 순서로 지정할 수 있음
  • 디폴트 값을 지정할 수 있음

이러한 장점은 코틀린의 디폴트 아규먼트와 이름 있는 파라미터가 갖고 있음
빌더 패턴을 사용하는 것보다 이름 있는 파라미터를 사용하는 것이 좋은 이유는 아래와 같음

  1. 더 짧음
    • 구현하기 쉬울 뿐만 아니라 읽는 사람의 입장에서도 읽기 쉬움
    • 코드를 수정하는 것도 어려움
  2. 더 명확함
    • 객체가 어떻게 생성되는지 확인하고 싶을 때, 빌더 패턴은 여러 메서드들을 확인해야 함
    • 디폴트 아규먼트가 있는 코드는 생성자 주변 부분만 확인하면 됨
  3. 더 사용하기 쉬움
    • 기본 생성자는 기본적으로 언어에 내장된 개념
    • 빌더 패턴은 언어 위에 추가로 구현한 개념이므로, 추가적은 knowledge가 필요
  4. 동시성과 관련된 문제가 없음
    • 코틀린의 함수 파라미터는 항상 immutable 반면 빌더 패턴에서 프로퍼티는 mutable 임

[Effective Kotlin. 35] 복잡한 객체를 생성하기 위한 DSL을 정의하라

  • DSL에 대한 지식이 부족하여 나중에 정리하도록 한다.
728x90
반응형