티스토리 뷰

개요

코틀린을 공부하다 보면 이펙티브 자바라는 책에서 제안했던 베스트 프랙티스 오버랩되어 보이는 경우가 종종 있는데 코틀린이 자바에 불편한 내용을 해결하기 위해 탄생했기 때문인가 싶다.
코틀린과 자바를 비교해서 보다 보면 오히려 자바에 대해 더 잘 알게 되는 게 아닌가 싶은데 서로가 다른 그 지점이 자바에 있었던 문제에 해결책 이여서다.
그래서 이펙티브 자바와 이펙티브 코틀린의 서로 연관된 챕터를 비교 정리해 보면 좋지 않을까 싶어 시리즈로 글을 작성해나갈 예정이다.

생성자 대신 정적 팩토리 메서드(함수)를 고려하라

자바에서는 메서드 코틀린에서는 함수라고 얘기하지만, 매번 소괄호치고 둘 다 쓰기는 난잡해지는 것 같아 정적 팩터리 함수로 통일해 쓸 생각이에요,
이 주네는 이펙티브 자바에서는 Item. 01 이였고 이펙티브 코틀린에서는 Item. 33에 내용인데, 두 내용 모두 구현 방식은 언어별로 조금씩 다르지만
장단점이 같아 크게 비교할 내용이 없었다. 다만 조금 흥미로웠던 것은 이펙티브 자바에서 동반 객체(Companion Object)라는 단어가 등장하였는데, 알아보니 과거에 인터페이스 내에 정적 함수를 선언할 수 없었던 자바가
인터페이스 내에 동반 객체를 통해 정적 팩토리 함수를 사용했다고 한다.

인터페이스 정적함수는 자바8부터 지원되었다고 하는데, 자바8 출시일이 2014년이고 젯브레인에서 코틀린을 최초로 공개한 날짜가 2011년인 것을 보아 아마 자바 7 에서의 내용을 코틀린이 가져온 게 아닌가 싶다.
자바 6부터 자바 8까지 버전이 2개 업데이트되는데 약 8년 정도 시간 동안 들었던 기간에 젯브렌인이 안 되겠다 우리가 만들게 라고 하며 개발한 것이 코틀린이고 코틀린에서 여기저기 보면 충분히 가능성이 있어 보인다.

정적 팩터리 함수가 필요한 이유

이팩티브 자바와 코틀린에서 정적 팩터리 함수가 필요한 이유에 대해 아래와 같이 설명하고 있다.

  1. 이름을 가질 수 있다.
    • 생성자와 매개변수만으로는 생성되는 인스턴스의 특성이나 용도를 정확히 설명하기는 무리가 있다. 때문에 함수 이름을 직접 정의할 수 있는 정적 팩토리 함수의 경우 이름을 통해 그 인스턴스의 특징을 명확히 할 수 있다는 장점을 가지고 있다
  2. 호출 시마다 인스턴스 생성을 매번 할 필요가 없다.
    • 만약 생성하고자 하는 인스턴스가 싱글턴 객체처럼 불변 객체이고 값이 결정되어 있다면 매번 인스턴스를 생성하기보다 미리 준비된 인스턴스를 정적 팩터리 메소드를 통해 반환해주면 된다. Enum의 각 인스턴스는 오직 한 개만 조재하며 변경도 불가능하니 가장 적절한 예가 아닌가 싶다.
  3. 반환 타입의 하위 타입 객체를 반환할 수 있다.
    • 자바와 코틀린은 상위 타입과 하위 타입이 존재해서 하위 타입은 언제나 상위 타입으로 표현될 수 있다. 때문에 상위 타입은 반환 타입의 정적 팩터리 메소드를 통해 다양한 그 하위 타입의 인스턴스를 제공해 다형성을 활용할 수 있다..
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다
    • 이팩티브 코틀린에서는 "생성자 호출 이전에 다른 작업을 포함할 수 있다"라고 표현하고 있는데 사실 같은 의미이다. 최종적으로인스턴스 생성은 생성자를 통해서만 가능하다. 때문에 정적 팩토리 함수 역시 내부에서는 객체의 생성자를 사용하고 있는데, 생성자를 통해 인스턴스 생성하기 전후에 작업을 추가해 매번 다른 클래스 객체를 반환할 수도 값을 검증할 수도 있다.

코틀린 Only

  1. 팩토리 함수는 인라인으로 만들 수 있으며, 파라미터를 reified로 만들 수 있다.
    • 팩토리 함수를 제네릭으로 만들 수 있다는 의미이다. 자바에서는 제네릭 타입 T 로 인스턴스 생성하는 것은 불가능해 정 필요하다면 타입 토큰을 받아서 생성해야 했다. 정적 팩토리 함수는 아니지만 스프링에 ApplicationContext.getBean(Type)이 훌륭한 예시이다. 하지만 코틀린에서는 제네릭을 가진 팩토리 메서드를 만들 수 있다.
  2. 코틀린은 탑레벨에 함수를 생성할 수 있고 가시성 제어를 자유롭게 할 수 있다.
    • 팩토리 함수를 접근할 수 있는 영역을 같은 모듈, 같은 파일 등 다양한 가시성을 제공해줄 수 있다.

정적 팩토리 함수는 가독성, 성능, 확장성 등 다양한 부분에서 이점을 얻을 수 있다.

코틀린에서 정적 팩토리 함수 구현하는 방법

Companion 객체 팩토리 함수

class Person(  
    val age: Int,  
    val name: String,  
) {  
    companion object {  
        fun fixture(): Person {  
            return Person(  
                age = 25,  
                name = "Hero",
            )  
        }  
    }  
}

코틀린에서는 static 키워드가 존재하지 않고, companion object내에 선언한 함수를 컴파일하면static 이 추가되어 있다. 때문에 companion object를 사용해 정적팩터리 함수를 만들 수 있다.
하지만, 문제가 존재하는데 Companion Object 내에 함수를 호출하고자 하면 Type.Companion.function 처럼 Companion 키워드 혹은 이름이 있는 경우 그 이름을 추가해주어야 한다.
물론 이 문제는 @JvmStatic 어노테이션을 사용하면 해결되지만 어노테이션을 사용하는 것은 자바와의 상호 운용을 위해 추가했을 뿐 코틀린의 결에 맞는다고 보기는 힘들어 보인다.

확장 팩토리 함수

fun Person.Companion.of(): Person {  
    return Person(  
        name = "Hero",  
        age = 25,  
    )  
}

class Person(  
    val age: Int,  
    val name: String,  
)

fun main() {
    val person = Person.of()
}

외부 라이브러리에 companion object가 추가되어 있다면 확장 함수를 활용할 수도 있는데, Type.Companion.functionName() 의 형태로 확장 함수를 사용하면 외부 라이브러리에 확장함수를 활용해 정적 팩토리 함수를 만들 수 있다.
잘 생각해보면 테스트 코드에도 유용할 수 있다. Object Mother Pattern 을 사용하기 위해 테스트 모듈에서 확장 함수를 통해 팩토리 함수를 추가하면 프로덕션 코드에 영향을 주지 않고도 픽스쳐를 만들 수 있는 팩토리 메소드를 추가할 수 있다.

탑 레벨 팩토리 함수

fun create(): Person {  
    return Person(  
        name = "Hero",  
        age = 25,  
    )  
}  

class Person(  
    val age: Int,  
    val name: String,  
)

코틀린의 결에 맞는 팩토리 함수 추가를 위한 방법은 탑 레벨 팩토리 함수가 가장 적합해 보인다. 사실 companion object 는 정적 필드나 함수를 만드는 용도 외에도 다양한 방법으로 사용될 수 있다.
어차피 나중에 너무 많은 책임을 가졌다는 이유로 골칫덩어리가 되거나 밖으로 쫒겨날 운명일 수도 있다. 따라서 애초에 정적 팩토리 메소드를 탑 레벨에 둬야 한다. 좋은 예시로 List 를 만들어주는 listOf(...) 가 그 예시가 있다.
하지만 listOf 와 같이 그 이름을 명확하게 함수 명에 추가해주어야 한다. 그렇지 않으면 그 Type을 오해하는 경우가 생길 수도 있다.

댓글