Blog

이펙티브 코틀린(5)

이펙티브 코틀린 (5)

5. 객체 생성

코틀린은 순수 함수형 스타일로 작성할 수도 있지만, 자바처럼 객체 지향 프로그래밍 (Object Oriented Programming, OOP) 스타일로도 작성할 수 있다
코틀린은 정적 메소드를 사용할 수 없지만, top-level 함수와 companion 객체 함수 등을 대신 활용할 수 있다

5-1. 생성자 대신 팩토리 함수를 사용하라

클래스으 인스터를 만들게 하는 가장 일반적인 방법으로는 기본 생성자 (primary constructor) 를 사용하는 방법이다
생성자의 역할을 대신 해 주는 함수를 팩토리 함수라고 부른다
팩토리 함수는 기본 생성자가 아닌 추가적인 생성자 (secondary constructor) 와 경쟁 관계이다
장점
함수에 이름을 붙일 수 있다
함수가 원하는 형태의 타입을 반환할 수 있다
다른 객체를 생성할 때 사용할 수 있다
인터페이스 뒤에 실제 객체의 구현을 숨길 때 유용하게 사용할 수 있다
호출될 때마다 새 객체를 만들 필요가 없다
함수를 사용해서 객체를 생성하면 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 최적화를 위해 캐싱 메커니즘을 사용할 수도 있다
아직 존재하지 않는 객체를 반환할 수 있다
어노테이션 처리를 기반으로 하는 라이브러리에서는 팩토리 함수를 많이 사용한다
프로젝트를 빌드하지 않고도 앞으로 만들어질 객체를 사용하거나, 프록시를 통해 만들어지는 객체를 사용할 수 있다
객체 외부에 팩토리 함수를 만들면 가시성을 원하는 대로 제어할 수 있다
팩토리 함수는 인라인으로 만들 수 있으며, 파라미터들을 reified 로 만들 수 있다
생성자로 만들기 복잡한 객체로 만들어 낼 수 있다
원하는 때에 생성자를 호출할 수 있다
제한
서브클래스 생성에는 슈퍼클래스의 생성자가 필요하기 때문에, 서브클래스를 만들 수 없다
팩토리 함수로 슈퍼클래스를 만들기로 했다면, 서브 클래스에도 팩토리 함수를 만들면 되기 때문에 우회해서 사용할 수 있다
팩토리 함수 종류
companion 객체 팩토리 함수
확장 팩토리 함수
톱레벨 팩토리 함수
가짜 생성자
팩토리 클래스의 메서드
예시 코드

Companion 객체 팩토리 함수

이름을 가진 생성자 (Named Constructor Idiom)
예시 코드
일반적으로 많이 쓰이는 이름 규칙
from
파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수를 나타낸다
예시 코드
of
파라미터를 여러 개 받고, 이를 통합해서 인스턴스를 만들어 주는 함수를 나타낸다
예시 코드
valueOf
from 또는 of 와 비슷한 기능을 하면서도, 의미를 조금 더 쉽게 읽을 수 있게 이름을 붙인 함수이다
예시 코드
instance or getInstance
싱글톤으로 인스턴스 하나를 반환하는 함수이다
예시 코드
createInstance or newInstance
함수를 호출할 때마다 새로운 인스턴스를 만들어서 반환한다
예시 코드
getType
팩토리 함수에서 반환하는 타입이다
예시 코드
newType
팩토리 함수가 다른 클래스에 있을 때 사용하는 함수이다
예시 코드
companion 객체 멤버를 단순한 정적 멤버처럼 사용하는 경우가 많지만, 더 많은 기능을 갖고 있다
companion 객체는 인터페이스를 구현할 수 잇으며, 클래스를 상속받을 수 있다
예시 코드
추상 companion 객체 팩토리는 값을 가질 수 있다
캐싱을 구현하거나, 테스트를 위한 가짜 객체 생성(fake creation) 을 할 수 있다

확장 팩토리 함수

이미 companion 객체가 존재할 때, 이 객체의 함수처럼 사용할 수 있는 팩토리 함수를 만들어야 할 때가 있다. 이 때 companion 을 직접 수정할 수 없고, 다른 파일에 함수를 만들어야 한다면 확장 함수를 활용하면 된다.
예시 코드

톱레벨 팩토리 함수

객체를 만드는 흔한 방법 중 하나로 톱레벨 팩토리 함수를 이용하는 방법이 있다
e.g) listOf, setOf, mapOf
listOf(1, 2, 3)List.of(1, 2, 3) 보다 훨씬 읽기 쉽고 때문에 객체 생성에 톱레벨 함수를 사용한 것이다
단점
Public 톱레벨 함수는 모든 곳에서 사용할 수 있으므로, IDE 가 제공하는 팁을 복잡하게 만든다
톱레벨 함수의 이름을 클래스 메서드 이름처럼 만들면, 다양한 혼란을 일으킬 수 있다

가짜 생성자

코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용된다
예시 코드
톱레벨 함수처럼 참조될 수 있다 ( 생성자 레퍼런스는 함수 인터페이스로 구현한다 )
예시 코드
톱레벨 함수는 생성자처럼 보이고, 생성자처럼 작동한다. 하지만 팩토리 함수와 같은 장점을 갖고 있다
이러한 특성 때문에 가짜 생성자 (fake constructor) 라고 부른다
가짜 생성자를 만드는 이유
인터페이스를 위한 생성자를 만들고 싶을 때
reified 타입 아규먼트를 갖게 하고 싶을 때
이를 제외하면 가짜 생성자는 진짜 생성자처럼 동작해야 한다.
생성자처럼 보여야 하며, 생성자와 같은 동작을 해야 한다
가짜 생성자를 선언하는 다른 방법
invoke 연산자를 갖는 companion 객체를 사용하면 비슷한 결과를 얻을 수 있다
예시 코드
invoke 함수의 복잡성
생성자
val f: ()->Tree = ::Tree
Kotlin
가짜 생성자
val f: ()->Tree = ::Tree
Kotlin
invoke 함수를 갖는 companion 객체
val f: ()->Tree = Tree.Companion::invoke
Kotlin
가짜 생성자는 톱레벨 함수를 사용하는 것이 좋다
기본 생성자를 만들 수 없는 상황 또는 생성자가 제공하지 않는 기능으로 생성자를 만들어야 하는 상황에만 가짜 생성자를 사용하는 것이 좋다
e.g) reified 타입 파라미터 등

팩토리 클래스의 메서드

팩토리 클래스는 클래스의 상태를 가질 수 있다는 특징 때문에 팩토리 함수보다 다양한 기능을 갖는다
팩토리 클래스는 프로퍼티를 가질 수 있기 때문에 이를 활용하면 다양한 종류로 최적화하고, 다양한 기능을 도입할 수 있다
캐싱을 활용하거나, 이전에 만든 객체를 복제해서 객체를 생성하는 방법으로 객체 생성 속도를 높일 수 있다
예시 코드

정리

객체를 생성할 때는 특징을 잘 파악하고 사용해야 한다
가짜 생성자, 톱레벨 팩토리 함수, 확장 팩토리 함수 등 일부는 신중하게 사용해야 한다
팩토리 함수를 정의하는 가장 일반적인 방법은 companion 객체를 사용하는 것이다

5-2. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라

객체를 정의하고 생성할 때 가장 기본적인 방법은 기본 생성자 (primary constructor) 를 사용하는 것이다
예시 코드

점층적 생성자 패턴

점층적 생성자 패턴: 여러가지 종류의 생성자를 사용하는 패턴
예시 코드
디폴트 아규먼트가 점층적 생성자보다 좋은 이유
코드를 단순하고 깔끔하게 만들어 준다
파라미터들의 값을 원하는대로 지정할 수 있다
아규먼트를 원하는 순서로 지정할 수 있다
명시적으로 이름을 붙여서 아규먼트를 지정하므로 의미가 훨씬 명확하다
예시 코드

빌더 패턴

빌더 패턴의 장점
파라미터에 이름을 붙일 수 있다
파라미터를 원하는 순서대로 지정할 수 있다
기본 값을 지정할 수 있다
빌더 패턴 예시 코드
디폴트 아규먼트가 빌더 패턴보다 좋은 이유
디폴트 아규먼트가 있는 생성자 또는 팩토리 메소드가 빌더 패턴보다 구현하기 쉽다
객체가 어떻게 생성되는지 확인하고 싶을 때 디폴트 아규먼트가 있는 코드는 생성자 주변 부분만 확인하면 되기 때문에 훨씬 이해하기 쉽다
기본 생성자는 기본적으로 언어에 내장된 개념이기 때문에 훨씬 이해하기 쉽다
동시성과 관련된 문제가 없다
코틀린의 함수 파라미터는 항상 immutable 하다
빌더 패턴에서 프로퍼티는 mutable 하다
빌더 패턴의 빌더 함수를 Thread-Safe 하게 구현하는것은 어렵다
무조건 빌더 패턴 대신 기본 생성자를 사용해야 한다는 것은 아니다
DSL (Domain Specific Language) 을 사용하는게 좋다
예시 코드
빌더 패턴을 팩토리로 사용할 수 있다
예시 코드
팩토리 메서드를 기본 생성자처럼 사용하게 만들려면 커링을 활용해야 하지만, 코틀린은 커링을 지원하지 않는다
대신 객체 설정을 데이터 클래스로 만들고, 데이터 클래스로 객체를 만들어두고 이를 copy 한뒤 필요한 설정들을 일부 수정해서 사용하는 형태로 만든다
예시 코드
코틀린에서 빌더 패턴을 사용하는 경우
빌더 패턴을 사용하는 다른 언어로 작성된 라이브러리를 그대로 옮길 때
디폴트 아규먼트와 DSL 을 지원하지 않는 다른 언어에서 쉽게 사용할 수 있게 API 를 설계할 때

정리

코틀린에서는 점층적 생성자 패턴, 빌더 패턴을 거의 사용하지 않고 디폴트 아규먼트를 활용한다

5-3. 복잡한 객체를 생성하기 위한 DSL 을 정의하라

DSL (Domain Specific Language) 는 복잡한 객체, 계층 구조를 갖는 객체들을 정의할 때 굉장히 유용하다
예시 코드
DSL 을 만드는것은 약간 힘들지만, 한 번 만들고 나면 보일러플레이트와 복잡성을 숨기면서 개발자의 의도를 명확하게 표 현할 수 있다
DSL 은 자료 또는 설정을 표현할 때도 활용될 수 있다
코틀린 DSL 은 Type-Safe 하기 때문에 여러가지 유용한 힌트를 활용할 수 있다

사용자 정의 DSL 만들기

함수 타입은 함수로 사용할 수 있는 객체를 나타내는 타입이다
예시 코드
함수 타입 예시
()->Unit: 아규먼트를 갖지 않고, Unit 을 반환하는 함수
(Int)->Unit: Int 를 아규먼ㅌ트로 받고, Unit 을 반환하는 함수
(Int)->Int: Int를 야규먼트로 받고, Int 를 반환하는 함수
(Int, Int)->Int: Int 2개를 아규먼트로 받고, Int 를 반환하는 함수
(Int)->()->Unit: Int 를 아규먼트로 받고, 다른 함수를 반환하는 함수
다른 함수는 아규먼트로 아무것도 받지 않고, Unit 을 반환
(()->Unit)->Unit: 다른 함수를 아규먼트로 받고, Unit 을 반환하는 함수
다른 함수는 아규먼트로 아무것도 받지 않고, Unit 을 반환
함수 타입 만드는 기본적인 방법
람다 표현식
익명 함수
함수 레퍼런스
익명 함수는 일반적인 함수처럼 보이지만, 이름을 갖고 있지 않다
람다 표현식은 익명 함수를 짧게 작성할 수 있는 표기법이다
익명 확장 함수 예시
리시버를 가진 함수 타입
확장 함수를 나타내는 특별한 타입
예시
리시버를 가진 익명 확장 함수람다 표현식을 호출하는 방법
일반적인 객체처럼 invoke 메서드를 사용
확장 함수가 아닌 함수처럼 사용
일반적인 확장 함수처럼 사용
예시 코드
리시버를 가진함수 타입의 가장 중요한 특징은 this 의 참조 대상을 변경할 수 있다
thisapply 함수에서 리시버 객체의 메서드와 프로퍼티를 간단하게 참조할 수 있게 해준다
예시 코드
Table DSL 예시

언제 사용해야 할까?

DSL 이 유용한 경우
복잡한 자료 구조
계층적인 구조
거대한 양의 데이터
DSL 은 많이 사용되는 구조의 반복을 제거할 수 있게 해준다

정리

DSL 은 언어 내부에서 사용할 수 있는 특별한 언어이다
DSL 구현은 해당 DSL 이 익숙하지 않은 개발자에게 혼란과 어려움을 줄 수 있다
DSL 은 복잡한 객체를 만들거나, 복잡한 계층 구조를 갖는 객체를 만들 때만 활용하는 것이 좋다
좋은 DSL 을 만드는것은 어렵지만, 한번 잘 만들어진 DSL 은 프로젝트에 굉장히 큰 도움을 준다