떠오르는 언어(?) 코틀린으로 스프링 웹 백엔드를 구현해보고자는 열망이 있었던 찰나에 코틀린과 스프링에 대해서 공부할 수 있는 영상이 있어서 공부한 내용을 적어보고자 한다.
아이템 1. 코틀린 표준 라이브러리를 익히고 사용하라.
"표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여려분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다." 라는 명언을 통해 자바 코드 대신 코틀린 표준 라이브러리를 사용하라고 적극 권장하고 있다.
코틀린에는 읽기 전용 컬렉션과 변경 가능한 컬렉션을 구분해서 제공한다. 플랫폼에서 사용하는 컬렉션들이 읽기/쓰기 전용에 맞게 구분되어 제공되어진다. 코틀린에는 좋은 기능들이 많다. 애용하자.
자바와 관련된 코드들을 줄여나가다 보면 자연스럽게 코틀린스러운 코드를 작성할 수 있게 된다.
아이템 2. 자바로 역컴파일하는 습관을 들여라.
코틀린 숙련도를 향상시키는 가장 좋은 방법 중 하나는 작성한 코드가 자바로 어떻게 표현되는지 보는 것이다.
역컴파일을 통해 예기치 않은 코드를 방지할 수 있고 문제가 발생했을 때 빠르게 확인할 수 있다.
IntelliJ IDEA 에서 Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile 기능을 통해 역컴파일할 수 있다.
아이템 3. 롬북 대신 데이터 클래스를 사용하라.
코틀린과 자바를 혼용하여 사용하면 코틀린 먼저 class 파일로 변환이 되고 java 파일들을 읽어 애너테이션 프로세싱을 거치게 된다. 즉 애너테이션 프로세서는 코틀린 컴파일 이후에 동작하기 때문에 롬북에서 생성된 자바 코드는 코틀린 코드에서 접근할 수 없다. 그래서 코틀린 코드에서는 롬북 코드를 사용하지 못 한다(?) 라는 이야기가 나오게 되었다.
자바와의 호환성을 제공하고자 코틀린 진영에서도 롬북을 사용할 수 있게 1.5.20 부터 롬북 컴파일러 플러그인이 실험적으로 추가되기도 했다. 코틀린 코드보다 자바 코드를 먼저 컴파일하도록 빌드 순서를 조정하면 롬북 문제는 해결될 수도 있다. 하지만 이 역시 자바 코드에서 코틀린 코드를 호출할 수 없게 되는 단점이 생긴다. lombok 을 delombok 으로 순수 자바 코드로 풀어버리는 방법이 있지만, 코틀린의 data class 를 사용하는 것을 추천한다.
data class 를 사용하면 컴파일러가 알아서 equals, hashCode, toString, copy 함수등을 자동으로 생성해준다.
주 생성자에 있는 매개변수를 기반으로 생성되므로 하나 이상의 매개변수가 있어야 하며 모든 매개변수는 val 이나 var 로 표시해야 한다. copy 함수를 적절히 사용하면 데이터 클래스를 불변으로 관리할 수도 있다.
TIP. all-open 컴파일러 플러그인을 사용하라.
코틀린의 폐쇄적인 클래스 정책 때문에 SpringBoot main 함수가 실행이 되지 않는다. 자바로 컴파일해보면 Application 클래스에 final 이라는 키워드가 붙어 있다. @SpringBootApplication 은 @Configuration 을 포함하고 있는데 스프링은 기본적으로 CGLIB 을 사용하여 @Configuration 클래스에 대한 프록시를 생성한다.
CGLIB 에서 대상 클래스를 상속해서 프록시를 구현하는 문제 때문에 코틀린의 폐쇄적인 클래스 정책과 충돌이 나는 것이다. final 로 지정된 클래스와 메서드들은 상속하거나 오버라이드를 할 수 없기 때문이다.
(스프링 5.2부터 @Configuration 의 proxyBeanMethod 옵션을 사용하여 프록시 생성을 비활성화하는 기능을 사용하면
동작이 된다고 한다.)
상속을 허용하고 오버라이드를 허용하려면 open 이라는 변경자를 추가해야 한다. 그런데 spring 에 관련된 모든 클래스와 메서드에 open 이라는 지시자를 붙여야 한다면 코틀린의 매력도가 떨어지게 된다.
코틀린에서는 이러한 문제점을 해결하고자 all-open 플러그인을 제공한다. all-open 플러그인을 사용하면 지정한 애너테이션이 있는 클래스와 모든 멤버에 open 변경자를 추가한다. 스프링을 사용할 경우 이 all-open 플러그인을 랩핑한 kotlin-spring 컴파일러 플러그인을 사용하면 된다. @Component, @Transactional, @Async 등이 기본적으로 제공된다.
적용되는 애너테이션 항목들을 확인하려면 Intellij IDEA 에서 File -> Project Structure -> Project Settings -> Modules -> Kotlin Compiler Plugins 에서 확인하면 된다.
아이템 4. 필드 주입이 필요하면 지연 초기화를 사용하라.
생성자를 통해 의존성을 주입하는 것이 당연시 되는 오늘이지만, 때로는 필드를 통해 주입해야 될 때가 있다.
이 때 코틀린에서는 backing field 가 존재하는 프로퍼티라면 인스턴스화될 때 초기화해야 된다. null 로 초기화 할 수는 있지만 null 이 있는 타입은 항상 null 연산자를 통해 체크로직이 필요하다는 불편함이 있다. 코틀린에서는 lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.
TIP. 역직렬화를 위해 잭슨 모듈이 코틀린을 지원하고 있다.
역직렬화를 하려면 매개변수가 없는 생성자가 필요하다. 그런데 코틀린에서는 매개변수가 없는 생성자를 만드려면 모든 매개변수에 기본 인자를 넣어야 한다. 잭슨 코튤린 모듈은 매개변수가 없는 생성자가 없더라도 직렬화와 역직렬화를 지원한다. 스프링 부트에서 기본적으로 이 모듈을 포함하고 있다고 한다.
아이템 5. 변경 가능성을 제한하라.
폐쇄적으로 작성할 수록 안전하기 때문에 모든 멤버를 val 로 선언할 것을 권장하고 있다. (나중에 var로 바꾸면 되므로!) 스프링 부트 2.2 부터 스프링 프로퍼티 클래스에서 생성자 바인딩이 가능하다. (@EnableConfigurationProperties 또는 @ConfigurationPropertiesScan 사용) Configuration 클래스에 적극 활용하자.
클래스에 개념적으로 동일하지만 하나는 공개하고 다른 하나는 구현 세부사항으로 구현할 때 private 프로퍼티 이름의 접두사로 밑줄을 사용한다. 이를 뒷받침하는 프로퍼티(backing property) 라고 한다. 공개되는 쪽에는 읽기 전용의 자료구조를 사용하고 내부에서 사용할 때는 수정이 가능한 자료구조를 사용한다. JVM 에서 기본 getter 와 setter 가 있는 private 프로퍼티에 대해서 함수 호출 오버헤드를 방지하도록 최적화되어 있다.
TIP. No-arg 컴파일러 플러그인
역직렬화와 마찬가지로 엔티티 클래스를 생성하려면 매개변수가 없는 생성자가 필요하다. no-arg 컴파일러 플러그인은 지정한 애너테이션이 있는 클래스에 매개변수가 없는 생성자를 추가한다. 자바 또는 코틀린에서 직접 호출할 수는 없지만 리플렉션을 사용하여 호출할 수 있다. no-arg 컴파일러 플러그인을 랩핑한 kotlin-jpa 컴파일러 플러그인을 사용할 수 있다. 마찬가지로 스프링 부트에서 자동으로 셋팅되며 @Entity, @Embeddable, @MappedSuperclass 등이 기본적으로 제공된다.
Q. kotlin("plugin.jpa")구문만 있으면 될 것 같은데 Entity와 MappedSuperclass에 allOpen 애너테이션이 붙은 이유는?
프록시를 못 만들기 때문이다. JPA 에서 프록시를 못 만든다는 뜻은 지연로딩을 할 수 없다는 뜻과 같다. 컴파일 에러나 런타임 에러는 발생하지 않는데 성능 문제가 발생하기 시작한다. 지금 당장 로딩이 필요없는 수많은 데이터들이 즉시 로딩되면서 성능 문제가 발생하는 것이다.
아이템 6. 엔티티에는 데이터 클래스 사용을 피하라.
lombok 의 @data 와 같은 맥락이다. 양방향 연관관계의 경우 toString, hashcode 를 호출할 때 무한 순환 참조가 발생한다.
아이템 7. 사용자 지정 getter를 사용하라.
JPA 에 의해 인스턴스화 될 때 초기화 블록이 호출되지 않기 때문에 영속화하지 않는 필드는 초기화된 프로퍼티가 아닌 사용자 지정 getter 를 사용해야 한다.
왼쪽 코드처럼 영속화되지 않는 필드에 초기화 구문을 넣어도 데이터베이스에서 값을 가져올 때 boolean 이나 null 이 될 수 없음에도 호출해보면 null 이 들어가 있다. 오른쪽 코드처럼 getter 를 사용자 정의해서 프로퍼티에 접근할 때마다 호출할 수 있게 구성하여야 한다. 뒷받침하는 필드가 존재하지 않기 때문에 AccessType.FIELD 이더라도 @Transient 를 사용하지 않아도 된다.
아이템 8. 널이 될 수 있는 타입은 빠르게 제거하라.
null이 될 수 있는 타입을 사용하면 null 검사를 넣거나 !! 연산자를 통해 assert 검사를 해야 한다.
0 또는 빈 문자열로 초기화하면 null이 될 수 있는 타입을 제거할 수 있다. 확장 함수를 사용해 반복되는 null 검사를 제거할 수도 있다. 의문이 드는 건 0 또는 빈 문자열로 초기화해도 검사해야 되지 않나이다. 자연스레 0 이나 빈 문자열을 두어도 상관이 없는 괜찮은 코드라면 무방할 수도 있겠다. 확장함수를 사용해 반복되는 null 검사를 제거할 수도 있다.