목차
33. 생성자 대신 팩토리 함수를 사용하라
우리는 다양한 방법으로 객체를 생성할 수 있다.
기본적으로 기본 생성자(primary constructor)를 이용해서 생성할 수도 있고, 세컨드 생성자를 이용해 객체를 생성할 수도 있다. 목적에 따라서는 자바에서처럼 정적 팩토리 메서드를 통해서 생성 할 수도 있다. 이에 대해서는 이펙티브자바의 아이템1번을 참고해보면 좋을 것 같다.
이런 자바의 정적 팩토리 메서드는 코틀린에서는 보통 companion 객체 팩토리 함수로 사용되는데, 코틀린의 팩토리 함수는 그 밖에도 존재한다.
1.
companion object 팩토리 함수
2.
확장 팩토리 함수
3.
톱레벨 팩토리 함수
4.
가짜 생성
5.
팩토리 클래스의 메서드
그럼 간단하게 하나씩 알아보자. (1번은 이펙티브자바에서 설명을 이미 했기에 생략한다.)
확장 팩토리 함수
•
언제 사용하는가?
◦
이미 companion 객체가 존재하고 이 객체의 함수처럼 사용할 수 있는 팩토리 함수가 필요한 경우
interface Tool {
companion object { ... }
}
fun Tool.Companion.createBitTool(...) : BigTool { ... }
...
Tool.createBigTool()
Kotlin
복사
◦
다만 위와 같은 방법은 companion object가 있어야 한다.(비어있더라도)
톱레벨 팩토리 함수
•
우리가 많이 사용하는 톱레벨 팩토리 함수
◦
listOf, setOf, mapOf
•
특정 클래스에 구현된 함수가 아닌 톱레벨로 함수를 제공하여 사용할 수 있다.
가짜 생성자
•
코틀린 1.1 부터 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 {
val list = ArrayList<T>(size)
repeat(size) { index -> list.add(init(index)) }
return this
}
Kotlin
복사
•
위와 같은 톱레벨 함수는 생성자처럼 보이고 생성자처럼 작동한다.
그러면서도 톱레벨 함수의 장점도 가져간다.
•
대부분의 개발자는 이게 함수인지 잘 모르기에 가짜 생성자(fake constructor)라 부른다.
•
다음과 같은 경우 사용한다.
◦
인터페이스를 위한 생성자를 만들고 싶을 때
◦
reified 타입 아규먼트를 갖게 하고 싶을 때
팩토리 클래스의 메서드
•
팩토리 클래스도 일종의 클래스이기 때문에 상태를 가질 수 있다.
•
상태를 활용해 다양한 기능을 도입하고 최적화가 가능하다. (Ex: 캐싱)
data class Student(val id: Int, val name: String, val surname: String)
class StudentsFactory {
var nextId = 0
fun next(name: String, surname: String) = Student(nextId++, name, surname)
}
val factory = StudentFactory()
val s1 = factory.next("catsbi", "Test")
println(s1) // Student(id=0, name=catsbi, surname=Test)
val s2 = factory.next("Pobi", "Crong")
println(s2) // Student(id=1, name=Pobi, surname=Crong)
Kotlin
복사
34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라
자바에서는 객체 생성 편의성을 위해 여러 생성자 패턴이 있었는데 가장 자주 활용되는 방식은 다음과 같이
•
점층적 생성자 패턴(telescoping constructor pattern)
•
빌더 패턴(builder pattern)
두 가지고 있다. 그런데 결론부터 얘기하면 코틀린에서는 두 패턴의 가치가 모두 자바에 비교해서 낮아지거나 필요 없어졌다.
예를 들어보자. 다음과 같이 피자라는 클래스가 있다고 하자.
class Pizza(
val size: String,
val cheese: Int,
val olives: Int,
val bacon: Int
)
Kotlin
복사
이를 자바식으로 생성한다고 가정하면 기본적으로 생성자에 모든 파라미터를 채워야 한다.
Pizza(10, 3, 5, 2)
Kotlin
복사
하지만 피자집의 매출이 대부분 치즈피자에서 나온다고 할 때 매번 피자 생성자의 모든 값을 채워주는건 불필요하다. 이 경우 점층적 생성자 패턴을 고려하면 다음과 같다.
class Pizza(
val size: String,
val cheese: Int,
val olives: Int,
val bacon: Int
){
constructor(size: String, cheese: Int):this(size, cheese, 0, 0)
}
Kotlin
복사
하지만 이런 방식은 그렇게 좋은 코드는 아니다. 매 번 점층적 생성자 클래스를 만들어 주기에는 비용낭비가 크다. 그럼 빌더패턴을 사용해야할까? 자바에서는 빌더패턴을 통해 2개 이상의 변수를 지닌 객체 생성의 편의성과 가독성을 높혔다.
하지만 코틀린에서는 그럴 필요가 없다. 코틀린에서는 디폴트 아규먼트를 제공할 뿐더러 이름 있는 아규먼트(named parameter)를 제공하기 때문에 빌더 패턴을 위한 빌더 객체와 세터들을 만들어주지 않아도 된다. 결국 두 방식 모두 없이 사용이 가능하다.
class Pizza(
val size: String = "L",
val cheese: Int = 0,
val olives: Int = 0,
val bacon: Int = 0
)
Kotlin
복사
이렇게 기본값을 작성하면 우리는 원하는 값만 설정해주면 나머지 값은 기본값(default value)으로 설정되어 객체가 생성되는 것을 볼 수 있다.
35. 복잡한 객체를 생성하기 위한 DSL을 정의하라
코틀린에서는 코틀린 DSL(Domain Specific Language)을 지원하고 또 직접 만들어서 사용할수도 있다. 그리고 이러한 DSL은 복잡한 객체나 하이라키 기반의 객체들을 정의할 때 유용하다.
물론, DSL을 만든다는 것은 난이도가 있지만, 잘만 사용한다면 상당히 높은 가독성을 제공할 수 있다.
다음은 대표적으로 보여주는 코틀린 DSL로 작성한 HTML 이다.
body {
div {
a("https://kolinlang.org") {
target = ATarget.blank
+"Main site"
}
}
+ "Sime content"
}
Kotlin
복사
사용자 정의 DSL 만들기
우선 Custom DSL을 만들려면 몇 가지 알고 넘어가야 하는 부분이 있다.
첫 번째로 리시버인데, 말이 어렵지 사실 간단하다.
fun String.appendPrefix(prefix: String): String {
return "${prefix}_${this}"
}
...
val name: String = "catsbi"
print(name.appendPrefix("pika")) // pika_catsbi
Kotlin
복사
•
String : Recevier 타입
•
appendPrefix: 확장 함수
위와 같이 리시버 타입(String)을 가진 함수(appendPrefix)를 리시버를 가진 함수 타입이라 부른다.
일반적인 함수와 비슷해보이지만 파라미터 앞 리시버 타입이 추가되었으며 dot(.) 기호로 구분되어 있다.
이러한 리시버를 가진 함수의 가장 크고 DSL에 필요한 특징이 this의 참조 대상을 변경할 수 있다는 것이다. 다음은 개발자 소개를 DSL로 하는 DeveloperBuilder를 만들어보자.
data class Developer(
val name: String,
val company: String?,
val skills: Skills?,
val languages: Languages?
)
@DeveloperMarker
class DeveloperBuilder : Builder<Developer> {
private lateinit var name: String
private var company: String? = null
private var skills: Skills? = null
private var languages: Languages? = null
fun name(value: String) {
name = value
}
fun company(value: String) {
company = value
}
fun skills(block: SkillsBuilder.() -> Unit) {
skills = SkillsBuilder().apply(block).build()
}
fun languages(block: LanguagesBuilder.() -> Unit) {
languages = LanguagesBuilder().apply(block).build()
}
override fun build(): Developer = Developer(
name = name,
company = company,
skills = skills,
languages = languages
)
}
fun introduce(block: DeveloperBuilder.() -> Unit): Developer = DeveloperBuilder().apply(block).build()
class DslTest : FunSpec({
test("kotlin dsl을 이용해 자기소개 정보를 작성할 수 있다.") {
val actual = introduce {
name("이한솔")
company("마이다스인")
skills {
soft("Java")
soft("javascript")
hard("Kotlin")
}
languages {
"Korean" level 5
"English" level 3
}
}
actual.name shouldBe "이한솔"
actual.company shouldBe "마이다스인"
actual.skills?.contains(SoftSkill("Java")) shouldBe true
actual.skills?.contains(HardSkill("Kotlin")) shouldBe true
actual.languages?.contains(Language(value = "Korean", level = 5)) shouldBe true
}
})
Kotlin
복사
개발자 객체인 Developer 객체를 생성하는 DSL과 이를 테스트하는 코트를 작성해봤다.
언제 사용해야 할까?
사실 DSL은 구현하기도 난이도가 있는편이며, dot(.)을 통한 메서드 작성시 지원해주는 코드 어시스턴트 기능도 제대로 동작하지 않는다. 그렇기에 별도의 사용법이나 문서를 보지 않는 한 사용이 쉬운편도 아니다. 즉, DSL을 제공한다는건 개발자의 혼란과 성능 비용 모두 문제가 될 수 있다.
그렇기에 DSL은 다음과 같이 명확한 경우에 유용하다.
•
복잡한 자료 구조
•
계층적인 구조
•
거대한 양의 데이터
대부분의 경우 빌더나 생성자만 가지고도 대부분 해결이 가능하다. DSL은 위와 같이 복잡해질수록 커질수록 반복이 많아질수록 DSL의 사용성이 높아지기에 그 때가 되고 나서야 DSL 사용을 고려해보는 것이 좋다.