백엔드 개발자의 코틀린 입문기 - 코틀린이 얼마나 좋길래? 자바에서 옮겨가도 될까?

2022. 5. 6. 00:54IT/Review

들어가며

이 글은 Java Spring으로 백엔드 개발을 하던 개발자가 Kotlin으로 전환한 후기를 담은 글입니다. Java에서 Kotlin으로의 전환을 고민 중인 백엔드 개발자 분들에게 도움이 되면 좋겠습니다.

왜 자바에서 코틀린으로 넘어가나요?

우리나라에서 가장 대중적인 백엔드 개발 기술 스택은 단연컨대 자바 + 스프링일 것입니다. 그렇기에 백엔드 개발을 한다면 이 스택으로 경력을 이어나갈 확률이 매우 높은데요, JVM 기반으로, 자바와의 상호 호환성을 들고 나온 코틀린의 등장과 급부상으로 코틀린 + 스프링, 이른바 코프링도 최근에 많은 주목을 받고 있습니다. 저는 일찌기 코프링 도입을 간절히 원했기에 코틀린으로의 전환을 강력하게 건의해 왔는데요, 이럴 때 접하는 반응들 중엔 이러한 것들이 있었습니다.

  • 어차피 같은 프레임워크인데, 자바에서 코틀린으로 바꿀 이유가 있나요?
  • 코틀린이 자바와의 상호호환이 가능하다고 하는데, 정말 100% 확실한가요? 혹시라도 호환이 안 되면 어떡하죠?

이 글의 독자분들 중에서도 이러한 의문을 가진 분들이 계실 것이라 봅니다. 저는 최근에 실제로 코틀린 + 스프링 기반의 프로젝트를 구축하면서 이러한 질문들에 대한 답을 찾을 수 있었습니다. 그럼 이제부터 하나씩 알아봅시다.

잠깐! 이야기에 앞서서...

자바 생태계는 지금도 활발히 유지되고 있으며, 어느새 JDK의 버전은 17까지 올라오게 되었습니다. 그 사이에 오라클은 자바의 단점으로 지적받던 부분을 상당 부분을 개선해가고 있습니다. 대표적인 예로는 아래와 같은 것들이 있습니다.

  • 람다에서만 부분적으로 가능하던 타입추론이 var 키워드 도입으로 변수 선언에서도 가능해졌고
  • getter/setter 생성에 대한 부담을 줄일 수 있는 레코드(record)가 도입되었으며
  • 향후 가상 스레드(virtual thread) 도입으로 동시 처리 성능을 높일 계획을 세우고 있습니다.

2014년에 나온 jdk 8이 2022년 현재까지도 터줏대감을 맡고 있습니다.

그러나 안타깝게도 현재 업계에서 가장 많이 쓰이는 jdk는 다름아닌 8입니다. 이는 람다와 스트림을 사용할 수 있는 가장 최소한의 버전이며, 위에서 언급한 기능들을 포함하고 있지 않습니다. 그러므로 이 글에서 이후 언급되는 자바는 주로 "Java 8"을 의미한다는 것을 유념해주셨으면 합니다.

자바는 충분히 편한 언어가 맞을까?

예전에 자바와 스프링에 관련된 글을 하나 작성한 적이 있었습니다. 최대한 중립적인 시선에서 작성하고자 했으나, 제가 이 글을 작성했던 동기는 "자바의 불편함"에서 기인한 게 가장 컸습니다. 특히 요즘 주니어 개발자들은 node.js나 파이썬같은 언어로 시작하는 추세다보니 자바의 첫 인상은 주로 "불편하다, 장황하다"로 남는 듯합니다.

 

왜 자바 Spring만 뽑나요? 꼭 배워야 하나요?

들어가며 취준생 시절 때부터도 쭉 간직해 온 의문이 있었습니다. 내로라하는 대기업들의 구직 공고를 보면, 항상 따라오는 조건들이 있었죠. Java 언어 사용이 능숙한 분 Spring Framework 개발 경험

seolin.tistory.com

물론 윗 글에서 충분히 설명했듯이 자바는 좋은 언어가 맞습니다. 전 이제 node.js로 개발하는 것이 더 불편한 상태가 되었을 정도니까요. 특히나 시니어 개발자 분들은 자바의 변천을 이야기하면서 "자바 5 시절에 비하면 현재의 자바는 매우 세련된 언어다"라는 점을 강조하곤 합니다. 맞습니다. 5면 그 흔한 제너릭(Generics)도 없었기에 다형성을 제대로 살리지도 못 했으니까요.

그러나 그들이 주장하는 "자바는 많이 발전했으므로 충분히 사용하기 편한 언어다" 라는 주장에는 동의하지 않습니다. 프레임워크와 어노테이션 프로세서에 감춰져서 제대로 인지하지 못할 뿐, 자바는 수없이 많은 보일러 플레이트를 만들어냅니다.

  • 게터(getter)와 세터(setter)조차도 컴파일러 자체적으로 생성하지 못해 롬복(Lombok)의 도움을 받아야만 하며
  • 생성자에 네임드 패러미터(named parameter)를 줄 수 없어서 빌더(Builder)를 개발자가 스스로 개발해줘야만 합니다.

또한 다른 언어들이 기본적으로 제공하는 편한 문법들도 자바에 존재하지 않습니다.

  • 확장 함수(Extension Function)를 지원하지 않습니다. xxxUtils 같은 별도의 유틸리티 클래스를 써야만 합니다.
  • Destructive Declaration을 지원하지 않습니다. 객체를 일단 받아온 후에 일일이 getter를 호출해야만 합니다.
  • null에 대한 처리가 다양하지 않습니다. @NotNull 어노테이션도 구현을 컴파일러가 아닌 프레임워크에 맡기고 있으며, 대안으로 나온 Optional도 올바르게 사용하기 어렵습니다.

위의 요소들은 C#, Python 등에서는 이미 기초 문법으로 자리잡고 있는 것들입니다. 이것들이 정확히 무엇인지는 아래에서 설명하겠습니다.

제트브레인(JetBrains)은 왜 코틀린을 개발했을까?

예전에 네이버 그린팩토리에서 코틀린을 소개하는 세션이 열린 적이 있었습니다. Kotlin In Actions의 저자이자 제트브레인에서 코틀린 제작에 참여했던 팀원인 스베트라나 이사코바(Svetlana Isakova)가 직접 강의를 했었는데요, 그녀는 코틀린의 제작 동기를 아래와 같이 요약했습니다.

 

Kotlin in Action - YES24

코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀린 언어의 가장

www.yes24.com

 

| The JetBrains Blog

We’ve been working on a new K2 Kotlin compiler for quite some time. The new compiler aims to speed up the development of new language features, unify all the platforms Kotlin supports, bring performance improvements, and provide an API for compiler exten

blog.jetbrains.com

  • 제트브레인은 자바를 포함하여 JVM 베이스인 여러 언어의 IDE를 만드는 회사이며, 대부분의 소스 코드가 자바로 쓰여있다.
  • 그러나 우리는 더 많은 생산성(more productivity)을 원했으며, 그렇다고 이미 쓰여진 자바를 포기할 수도 없었다.
  • 그래서 우리는 자바와의 상호 호환성을 전제로 하는 언어인 코틀린을 개발하기 시작했다.

이는 제트브레인 공식 블로그에서도 확인 가능한 사항입니다. 물론 이는 코틀린이 만들어진 수 없이 많은 이유 중 하나일 뿐이지만, "자바 IDE에서 가장 많은 점유율을 내고 있는 회사가, 생산성을 높이기 위해 새로운 언어를 개발했다"는 사실은 여러가지 메시지를 내포하고 있다 생각합니다.

 

Why JetBrains needs Kotlin | The Kotlin Blog

The question of motivation is one of the first asked when someone learns that someone else is working on a new programming language. Kotlin documentation offers a fairly detailed overview of why the l

blog.jetbrains.com

그렇다면 코틀린은 자바보다 뭐가 좋나요?

이쯤되면 한 가지 의문이 드실 법합니다. "코틀린이 자바에 비해 얼마나 편하길래 이렇게 밑밥을 깔지?" 하는 의문이죠. 이사코바는 코틀린의 디자인 철학에 대해 아래와 같이 설명했습니다.

  • 코틀린은 자바의 고질적인 문제였던 널(null) 안정성을 해결하고자 했다
  • 개발자들은 평생동안 코드를 쓰는 데보다 읽는 데에 더 많은 시간을 할애한다. 그러므로 코틀린은 가독성에 초점을 뒀다.

정말 그럴까요? 아래에서 확인해보겠습니다.

널 안정성

이 코드는 Java로 쓰여졌습니다. 여기에서 4번 줄의 분기는 컴파일 시에 아무 문제가 없으나, 런타임에서는 문제가 생길 수 있습니다. user가 null일 수도 있고, user.getName()이 null을 return할 수도 있기 때문이죠.

Java는 기본적으로 모든 객체 변수를 nullable로 정의하고 있습니다. 그렇기에 null 안정성에 대한 검사는 온전히 개발자의 임무가 되고, if문을 통해 검사해주거나, try-catch로 한번 감싸주지 않으면 컴파일러는 지체없이 NPE를 던질 것입니다. 자바로 개발해본 모든 분들은 이 상황에 마주한 적이 있을 것입니다. 참고로 프로그래밍 언어에 Null Reference를 처음 도입한 당사자 Tony Hoare마저 이는 10억달러짜리 실수(The Billion Mistake)였다며 도입을 후회하기까지 했습니다.

 

Null References: The Billion Dollar Mistake

Tony Hoare introduced Null references in ALGOL W back in 1965 "simply because it was so easy to implement", says Mr. Hoare. He talks about that decision considering it "my billion-dollar mistake".

www.infoq.com

그렇다면 코틀린은 널 안정성을 어떻게 확보하고 있을까요? 일단, 코틀린은 자바와 정반대로 기본적으로 모든 객체 변수를 not-null로 정의합니다. 그렇기에 불필요한 null 값들이 여기저기에 들어갈 여지를 어느 정도 차단해줍니다.

이 코드는 kotlin으로 쓰인 코드입니다. Java 버전 코드와 플로우에서 큰 차이는 나지 않으나, email을 검증하는 로직을 하나 더 추가했습니다. 그리고 User class는 String? 으로 정의되어 있는데, 이는 nullable String이라는 의미입니다. 즉, id, name은 not null value, email은 nullable value라는 의미입니다.

이 코드는 Java와 달리 컴파일조차 실패할 것입니다. 왜냐하면 14번째 줄에서 null일 지도 모르는 email에 대해 String의 메소드 split()을 호출하고 있기 때문입니다. 그래서 개발자는 email을 not null value로 바꿔주거나, 자신이 받은 user 객체의 email이 null인지 아닌지를 검증하는 절차를 수행해야만 합니다. Java에서는 반강제적으로 모든 property에서 해줘야 했던 검증을, Kotlin에서는 오로지 nullable로 선언된 property에만 적용해도 된다는 의미입니다.

또한, 코틀린은 특정 값이 null일 경우에 별도로 분기를 탈 수 있는 엘비스(elvis) 연산자를 지원하고 있습니다.

위 코드는 user의 수입(income)을 판단하는 로직을 담고 있습니다. 하지만 income이 nullable long이기에, 11번째 줄의 비교 로직을 태우기 전에 null 검사를 수행해줘야만 합니다. 이럴 때 유용한 것이 바로 elvis(:?) 연산자로, null 값에 대한 기본값(default value)을 지정해줄 수 있습니다. user의 income이 null이 나올 경우, long 0로 치환되는 것이죠.

자바의 Optional 클래스에서 제공하는 orElseGet()과 유사하지만, 훨씬 간결한 문법으로 처리할 수 있다는 점이 다릅니다.

엘비스 연산자는 오직 변수의 값을 대입할 때만 사용할 수 있으나, 연산자의 호출에도 적용 가능한 ?. 연산자가 있습니다. a?.b() 는 a가 null이 아니라면 b()를 호출하고, 아니라면 null을 return하라는 의미입니다.

위는 json 형태의 값을 읽어들여 User class로 변환 후, 주소를 알아내는 코드입니다. 클라이언트 단에서는 매우 자주 일어나는 경우죠. Java였다면 5단계의 depth를 들어가 값을 받아오기 위해서 5번의 null 검증을 거쳐야하지만, 코틀린에서는 ?. 와 ?: 연산자의 도움으로 간편하게 값을 가져올 수 있습니다.

물론 이런 요소들이 npe의 발생 가능성을 원천 차단해주진 않습니다. 그러나 자바보다 훨씬 적은 품을 들이면서 널 체크를 할 수 있게 된다는 것은 확실합니다.

언어의 간결함

코틀린은 앞서 언급했듯이 가독성에 특히나 더 많은 신경을 쓴 언어입니다. 대표적인 것들은 아래와 같습니다.

타입 추론

자바 때와는 달리, var/val 키워드만 사용하면 알아서 타입 추론을 통해 해당 변수의 타입을 정할 수 있습니다. 위 코드에서 value는 String임을 명시하지 않았음에도, getValue의 return type이 String이기에 자동으로 String 변수로 인식되었습니다. 이후 String의 메소드 replace()를 사용해도 정상적으로 호출이 됩니다.

코틀린의 타입추론에 몇 가지 제약은 있으나, 적재적소에 잘 활용하면 코드의 양을 줄이면서 리뷰하기도 좋은 코드를 만들 수 있습니다.

람다

자바의 람다는 매개변수를 명시해야만 했으나, 코틀린에서는 매개변수가 하나일 경우 표기를 생략하고 "it"이라는 암묵적 변수명으로 표현식을 작성할 수 있습니다. filter에 사용된 it이 그 예시입니다.

Getter/Setter 생성

앞서 언급했듯이, 자바는 getter/setter 작성을 온전히 개발자에게 맡기고 있으므로 Lombok의 도움을 받는 경우가 많습니다. 코틀린에서는 객체 프로퍼티에 대하여 val이 선언되어 있을 경우 getter를, var가 선언되어 있다면 getter/setter를 자동 생성해줍니다. 즉, 위 코드에서 16번째 줄인 user.name = "zerobell"은 Java 코드에선 user.setName("zerobell")로 변환됩니다. 또한, User의 id가 getter만 생성하는 val로 선언되어 있으므로 15번째 줄은 컴파일이 되지 않습니다. setter가 선언되지 않은 상태이기 때문이죠.

여기에 더해 Data Class를 활용한다면, getter/setter 외에 equals와 hashCode까지 자동으로 생성해줍니다. 19번째 줄은 Data Class끼리 동등성(equality) 비교를 하고 있습니다.(자바와 달리 코틀린에선 == 연산자는 곧 equals() 호출과 같습니다) 그렇기에 height와 weight 모두가 동일한 값이라면, 저 비교문은 true가 됩니다.

즉, 코틀린에서는 롬복을 쓸 이유가 전혀 없습니다. @Getter @Setter @Data 등의 어노테이션이 이미 코틀린 언어 자체에 내장되어 있으니까요.

싱글톤 객체 - object

스프링 환경에서는 프레임워크가 빈 생성을 담당하기에 자주 잊곤 하지만, 자바에서 싱글톤 객체를 만들기 위해서는 약간의 스킬이 필요합니다. 가장 정석적으로 활용되는 것은 lazy loading으로, JVM이 클래스 로딩을 하는 시점에 맞춰 인스턴스를 만들도록 하여 싱글톤을 보장하게 하는 것이죠. 코틀린에서는 이러한 고민을 할 필요가 없습니다. object 키워드를 이용해 선언하면 해당 클래스는 하나의 인스턴스만 갖는 싱글톤 객체가 됩니다.

object를 자바로 decompile하면 이런 코드가 됩니다.

불변성

앞서 var/val에 대해 잠깐 언급을 했습니다. var/val은 getter냐 setter냐의 문제뿐 아니라, 불변성에도 연관이 있습니다. val은 var와 달리 재할당이 불가능한 변수에 사용됩니다. 즉, 자바로 치면 final 키워드가 붙은 셈인데요, 특히나 불변성을 이용한 코딩이 좀 더 선호되는 요즘 시대에 final을 하나하나 다 붙여주는 것보다는 val이라는 짧은 키워드로 대신할 수 있다는 것은 꽤 큰 장점입니다.

더 나아가, Data Class 같은 경우는 불변성을 전제로 하는 클래스이므로, copy() 메소드를 이용하여 일부 프로퍼티만 변경하여 새로운 복제본을 만들어낼 수 있습니다. 자바에서 이를 이용하기 위해서는 롬복의 toBuilder()와 build()를 적절히 활용하거나, 스프링의 ObjectUtils 를 활용해야만 했던 것과는 대조적입니다.

확장된 문법들

맨 처음 언급했던 "자바에서 제공하지 않는 문법 요소"에 대해서 이야기 해보고자 합니다. 코틀린에서는 아래와 같은 것들이 가능합니다.

확장함수

확장 함수는 이미 존재하는 클래스에 별도의 함수를 추가하는 기능입니다. C#에는 확장 메서드라는 이름으로 비슷한 개념이 있으며, js에서도 (비록 안티패턴이지만) prototype에 직접적으로 정의하여 사용할 수 있습니다.

위의 코드에서는 흔히 쓰이는 자료구조인 List에 "원소들이 중복없이 저장되어 있는지"를 확인하기 위한 메소드 hasOnlyUniqueElements() 를 추가했습니다. 이후 다른 곳에서 list 객체를 다룰 때, 마치 List 안에 실제로 그 메소드가 추가된 것처럼 호출하여 사용할 수 있습니다.

위의 코드에서는 String 내부에 dollar() 함수를 추가하여, Dollor("4") 가 아닌 "4".dollar() 형태로 사용할 수 있도록 만들었습니다. 이 기능은 몇 가지 제약조건이 있고, 남용하면 코드의 파편화를 유발할 수도 있다는 단점이 있으나, 적절히 활용한다면 가독성과 생산성을 높일 수 있습니다.

when문

자바의 switch문에 대응하는 것이 코틀린의 when 문입니다. 그러나 switch와 다른 점이 몇 가지 있습니다.

  • 별도로 break를 적지 않아도 됩니다
  • 변수 선언 때 바로 사용할 수 있습니다
  • Smart Cast를 지원합니다.

위 코드의 세번째 케이스가 바로 Smart Cast의 한 예입니다. 처음 가져올 때 something은 Any(Java에서는 Object와 같습니다) 타입이었으나, when 절에서 is 를 사용하면서 자동으로 변수형이 변환되었습니다. 그래서 is String에서는 something에 String 메소드 사용이 가능해지고, is Int에서는 더하기가 가능해지는 것입니다. Java였다면 intanceof 연산자와 명시적 캐스트를 사용해야만 했던 부분이 간소화됩니다.

Destructive Declaration

js와 파이썬에서도 지원되는 기능입니다. 각 프로퍼티의 순서에 맞춰서 좌변에 변수들을 준비하면, 각 위치에 대응하는 값들이 대입됩니다. Pair, Triple 같은 간단한 자료구조와 같이 활용한다면 생산성이 더 높아집니다.

Map 생성

개인적으로 자바에서 가장 불편했던 점은 Map 만들기였습니다. 자바는 딕셔너리형이 존재하지 않아서 json으로 값을 파싱해오거나 별도로 json을 만들어야 할 때, DTO를 사용하지 않는다면 Map<String, Any>이 유일한 옵션이었는데요. 코틀린도 근본 구조는 jvm이기에 딕셔너리형이 존재하지는 않으나 적어도 Map 객체 생성은 자바보다 훨씬 간단하게 할 수 있습니다. kotlin의 기본 자료구조인 Pair의 Collection을 바로 map으로 변환할 수 있어 stream을 억지로 늘어놓거나, new Map 선언 후에 add를 일일이 호출해줄 필요가 없습니다.

Named Arguments

Python에서는 kwargs 라는 이름으로 자주 사용되던 녀석입니다. 생성자 혹은 함수에 argument를 위치가 아닌 변수명으로 직접 지정하여 넘겨줄 수 있습니다. 이는 property가 많은 클래스의 생성자에 매우 유용하고, 함수의 불필요한 오버로딩을 줄일 수 있다는 장점도 있습니다. Java에서는 Builder 패턴의 사용이나 Command 객체만이 유일한 해결책이었던 것과는 대조적입니다.

Operator Override

Java를 쓰던 당시에는 BigDecimal을 애용했었는데요, 연산을 할 때 add()와 subtract() 메소드를 계속해서 체이닝해서 쓰다보니 가독성이 좋지 않아 실수를 자주 했었습니다.

Kotlin에서는 연산자 오버라이드를 통해 각 클래스가 연산자를 만날 때 어떤 동작을 할 지를 정의할 수 있습니다. 위의 코드에서 Dollar 클래스는 더하기/빼기/곱하기에 해당하는 연산자를 오버라이딩하게 되어, 36번째 줄에서 보다시피 + 기호를 놓기만 해도 자동으로 plus() 메소드가 실행되는 것을 알 수 있습니다.

연산자 오버라이딩은 사칙연산에 그치지 않고, collection에서 볼 수 있는 get/set에도 적용될 수 있습니다. 이 경우에는 [] 기호를 사용하게 되어 실제 array의 몇번째 요소, 혹은 dictionary 형에서 어떤 키를 직접적으로 접근하는 듯한 느낌을 낼 수 있습니다. 위 코드에서 Accounts는 내부적으로 accountMap을 가지고 있는 래핑 클래스지만, get과 set를 오버라이드 함으로써 실제 Map과 유사하게 동작을 하게 됩니다.

이런 특성들은 각 객체들이 실제 코틀린에 원래부터 있던 것처럼 보일 정도로 잘 녹아들게 함으로써, 설계할 때나 실제로 사용할 때에도 많은 도움을 줍니다.

비동기 처리 용이

자바의 고질병 중 하나는 "비동기 처리가 아쉽다"는 점이었습니다. Future를 거쳐 CompletableFuture를 제공해줬으나, 자바를 통해 서비스를 개발하는 개발자들 대부분은 CompletableFuture를 직접 활용하기 보단 RxJava 같은 Reactive Streaming 프레임워크를 사용해왔습니다. 그러나 리액티브 프로그래밍은 러닝 커브가 높다는 단점이 있었습니다.

코틀린은 이에 대한 대안으로 코루틴(coroutine)을 제시합니다. 코루틴은 기존의 코드들과 동일하게 절차적으로 코드를 작성하면서도 비동기 처리를 할 수 있도록 하는 또 하나의 패러다임입니다. 코루틴은 기존의 Reactive Streaming과의 호환성도 고려하여 만들어졌으므로 RxJava/Reactor와도 함께 사용할 수 있으며, 특히나 이를 채택하고 있는 Spring Webflux와의 상성도 꽤나 좋습니다.

위는 간략하게 작성해본 Java 언어에서의 Spring Webflux의 코드입니다. (기억에 의존해서 작성했으므로 문법적으로 맞지 않을 수도 있습니다.) Reactive 프로그래밍은 로직 전개를 함수간의 연계로 풀어낼 것을 강제하기에 map, doOnNext, onError 등의 수없이 많은 중간연산 메소드들을 체이닝해서 사용해야만 합니다. 이는 조금만 비즈니스 요건이 복잡해져도 코딩의 난이도를 급상승하게 만드는 원인이 됩니다.

코틀린의 코루틴은 suspend 키워드를 이용해 async로 동작해야 할 지점을 지정하고, 육안으로 볼 때는 평범한 절차적 코드로 보이게 하되, 컴파일 될 때에만 reactive하게 작동되도록 바꿔주는 기믹을 가지고 있습니다.

위의 코드는 Java로 쓰여졌던 Webflux 코드를 coroutine으로 바꾼 결과입니다. 코드 상으로 봤을 때에는 async 호출이 존재 하긴 하는 것인지조차 알 수 없습니다만, 실제 컴파일러는 suspend가 붙은 함수들을 reactive 하게 동작하도록 바꿉니다. 어떤 면에서는 javascript ES6의 async/ await와 유사해 보이기도 합니다.

물론 코루틴 또한 제대로 된 활용을 하기 위해서는 배워야 할 부분이 많으나, 대부분의 경우에서 리액티브보다 더 좋은 편의성을 제공합니다.

jetbrain이 제시한 coroutines vs rxjava. 결국 마스터하기는 둘 다 어려우나, 진입 장벽은 coroutines가 훨씬 낮다는 것이 jetbrain의 주장입니다.

코틀린 좋은 건 알겠습니다. 그런데 정말 JAVA 코드와 100% 호환이 되나요?

이제 두 번째 질문에 답해보고자 합니다. 코틀린은 .java 파일과의 상호 호환이 가능합니다. 실제로도 gradle에서 추가한 많은 디펜던시가 java 코드 기반으로 작성되어 있음에도 불구하고 코틀린 프로젝트에서 해당 라이브러리들이 정상적으로 호 출되곤 합니다.

그러나 100%라고는 말하기 어려운 부분이 있습니다. 특히나 gradle plugin이나 annotation processor들이 그런 경우가 많은데요, 이런 디펜던시들은 대체로 리플렉션을 통해 자바 코드를 직접 건드리는 경우가 많아 코틀린 쪽에서 예상하지 못한 문제를 낳기도 합니다. 이러한 이유로 Lombok이 코틀린에서 사용이 불가능한 적이 있었고(물론, 현재의 코틀린에서는 롬복이 필요 없습니다) 테스트에 주로 사용되는 모킹 프레임워크인 Mockito가 Kotlin object를 모킹하지 못하는 등의 이슈들이 있었습니다.

그러나 코틀린의 사용층이 점점 넓어지고 있으며, 이에 따라 기존 라이브러리들이 코틀린에 대응하여 추가적인 업데이트를 하거나, 다른 개발자들이 새로운 대체재를 만들어내고 있으므로 이 또한 큰 문제는 아닙니다. Slf4j를 대신할 kotlin-logging이란 라이브러리와 코틀린에서 사용 가능한 모킹 프레임워크인 MockK 등이 그 예시입니다.

따라서 코틀린으로 개발하더라도 기존 자바코드와의 호환성 문제를 겪을 가능성은 매우 낮다고 말씀드릴 수 있습니다.

Spring과 코틀린을 같이 사용한다는 것은

코틀린은 현재 "안드로이드 개발용 언어" 정도의 포지션을 잡고 있으나, 그 출발은 안드로이드와 무관했습니다. 오히려 코틀린 역사에서 스프링이 공식적으로 지원된 것이 안드로이드보다 더 빠릅니다. 따라서 스프링 + 코틀린은 결코 낯선 조합이 아니며, 충분히 현업에서도 도입할 수 있습니다.

그러나 스프링도 위의 "호환성 문제가 있는 라이브러리"처럼 자바의 바이트코드들을 직접 손대고 있다보니, 몇 가지 플러그인의 도움을 받는다는 전제 하에서만 올바른 작동을 보장합니다. kotlin-spring 플러그인은 바로 코프링을 돌리기 위한 가장 필수적인 플러그인입니다.

코프링에서는 빈 생성을 할 수 없다고?

Java와 정반대로 Kotlin의 모든 class는 기본값이 final입니다. open 키워드를 붙여야만 상속 가능입니다.

코틀린의 class들은 기본적으로 final로 선언되며, open이라는 키워드를 붙여줘야만 상속 가능한 클래스가 됩니다. 기본값이 상속 가능이고, final을 별도로 붙여줘야만 하는 자바와는 정 반대의 스펙이라 할 수 있습니다.

(출처 : https://stackoverflow.com/questions/29182797/how-exactly-spring-use-jdk-proxy)

바로 이 부분에서 생기는 문제점이 하나 있습니다. 스프링은 CGLib이라는 라이브러리를 통해 빈을 생성합니다. 이는 특정 클래스를 상속받는 프록시 객체를 만드는 것으로 이뤄지는데요, 앞서 말했듯이 코틀린의 클래스들은 기본값이 final이기에 CGLIB이 빈을 만들 수 없습니다. 해결 방법은 빈에 해당하는 모든 클래스들을 open class로 선언하거나, kotlin-spring 플러그인을 사용하는 것입니다.

kotlin-spring plugin은 내부적으로 kotlin-allopen 이라는 플러그인을 사용하고 있습니다. 이는 특정 어노테이션이 붙은 클래스들을 컴파일 시에 open class로 강제 변환시키는 플러그인이며, spring plugin은 이를 이용해 스프링의 스테레오타입에 해당하는 어노테이션(@Component, @Repository, @Bean, @Configuration, @Service 등등)이 붙은 클래스들을 자동으로 상속 가능한 클래스로 바꿉니다.

이외에도, reflection이 제대로 일어날 수 있도록 kotlin-reflect 플러그인의 추가도 필요합니다. (예전에 이걸 빼먹고 작업했다가 의존성 주입이 안 일어나서 하루 날렸던 적이 있네요.)

이런 방법까지 동원해서 스프링을 쓰는 게 맞나요?

스프링의 정체성 자체가 자바의 특성을 최대한으로 끌어내 여러 디자인 패턴으로 정교하게 빚어낸 프레임워크이니만큼, 코틀린에서 스프링을 바로 사용하기에는 당연히 무리가 있습니다. 그렇기 때문에 이런 플러그인들을 도입해 그 문제를 해결한 것인데요, 개인적으로는 코틀린과 자바는 같은 JVM 기반 언어라고 해도 명백히 코딩 스타일을 다르게 가져가고 있는 상황이라 코프링으로 작업하다보면 '어쩔 수 없이 코틀린의 특성을 조금 포기하고 자바스럽게 짜야하는' 경우가 생기곤 했기에 아쉬움을 느끼곤 합니다.

Jetbrain에서는 Kotlin을 이용한 Http 서버 및 클라이언트 프로그래밍에 대한 다른 대안으로 자신들이 직접 제작하는 Ktor를소개하고 있습니다. '이왕 코틀린으로 넘어가는 거, 프레임워크도 새로운 것을 원한다!' 싶으신 분들은 이 쪽을 알아보는 것도 좋은 방법일 듯합니다.

 

Ktor: Build Asynchronous Servers and Clients in Kotlin

Kotlin Server and Client Framework for microservices, HTTP APIs, and RESTful services

ktor.io

그러나 위에서 언급한 아쉬운 경우를 제외한 대부분의 경우에는 같은 스프링으로 개발한다 하더라도 코틀린의 경우가 훨씬 더 생산성이 높았기에 저는 추천드리고 싶습니다. 장황한 getter setter 호출, 피곤한 null 값 검사, lombok 어노테이션 떡칠 등으로부터 자유로워질 수 있다는 것은 크나큰 축복입니다.

앞으로의 코틀린, 자바, 그리고 스프링은?

Spring Boot 3부터는 Java 17 이상의 버전으로만 프로그래밍을 할 수 있습니다.

우리나라에서 스프링의 위상은 꽤나 높습니다. 스타트업, 도전적인 몇 기업들을 제외하고는 다 백엔드에 스프링을 쓴다고 봐도 무방할 정도인데요, 그렇기에 구직자 입장인 프로그래머들에겐 Java 이외 다른 언어를 택하는 것 자체가 매우 위험한 선택으로 여겨지곤 했습니다.

Kotlin은 JVM 기반 언어 중 가장 성공한 '자바 아종'이라 불러도 손색이 없습니다. Scala처럼 매우 어렵지도 않으면서, 전폭적인 커뮤니티의 지원을 받고 있어 인력 풀도 넓고, 레퍼런스도 많으니까요. 그렇기에 코틀린은 "스프링을 버릴 수 없으나, 새 언어를 갈망하는 개발자들"에게 매우 희망같은 언어일 것이라 생각합니다. 이러한 이유로 앞으로도 코틀린 + 스프링 조합은 계속해서 증가하지 않을까 싶습니다.

반면, 자바의 역습 또한 기대해볼만 하다 생각합니다. 이 글이 마치 코틀린은 신문물/ 자바는 구세대 유물처럼 보이도록 쓰여진 느낌을 받으셨을 수도 있겠는데요, 이는 제가 비교 대상을 2014년에 릴리즈된 "자바 8"으로 잡았기 때문입니다. 이후 자바는 17까지 버전이 올라가면서 이 글에서 언급된 "자바의 단점"을 꾸준히 개선해왔습니다. 하지만 이 글을 읽으시는 독자분들도 "최신의 자바"가 얼마나 발전했는지에 대해서는 감이 잘 안 오실 듯합니다. 왜냐하면 아직까지도 전세계에서 가장 많이 쓰이는 자바는 8 버전이기 때문이죠.

Spring 측에서는 "Spring 6 == Spring Boot 3부터는 최소 JDK 버전을 17로 올릴 예정"이라고 발표했습니다. 몇 폐쇄적인 업계에서는 해당하지 않는 이야기겠지만, 좋은 IT 서비스 업계는 스프링의 버전 업을 따라가려고 하기에 이번 Spring의 발표는 자바의 메이저 버전을 8에서 17로 격변시키는 계기가 될 수도 있습니다. 자바 17 기반으로 만들어진 스프링 생태계는 과연 어떻게 될지, 떠오르는 샛별 코틀린 스프링을 타도할 수 있을지는 조금 더 지켜봐야 할 것 같네요. 그 때가 기대됩니다.

 

Preparing for Spring Boot 3.0

<div class="paragraph"> <p>Spring Boot 2.0 was the first release in the 2.x line and was published on Feburary 28th 2018. We’ve just released Spring Boot 2.7 which means that, so far, we’ve been maintaining the 2.x line for just over 4 years. In total

spring.io