이펙티브 코틀린 (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 의 참조 대상을 변경할 수 있다
◦
this 는 apply 함수에서 리시버 객체의 메서드와 프로퍼티를 간단하게 참조할 수 있게 해준다
예시 코드
Table DSL 예시
언제 사용해야 할까?
•
DSL 이 유용한 경우
◦
복잡한 자료 구조
◦
계층적인 구조
◦
거대한 양의 데이터
•
DSL 은 많이 사용되는 구조의 반복을 제거할 수 있게 해준다
정리
•
DSL 은 언어 내부에서 사용할 수 있는 특별한 언어이다
•
DSL 구현은 해당 DSL 이 익숙하지 않은 개발자에게 혼란과 어려움을 줄 수 있다
◦
DSL 은 복잡한 객체를 만들거나, 복잡한 계층 구조를 갖는 객체를 만들 때만 활용하는 것이 좋다
•
좋은 DSL 을 만드는것은 어렵지만, 한번 잘 만들어진 DSL 은 프로젝트에 굉장히 큰 도움을 준다