[Kotlin] 자바 대신 코틀린 사용하기
- [ Languages ]/Kotlin
- 2023. 9. 1.
들어가며
최근 Toss 같이 자바+스프링 대신 코틀린+스프링을 사용하는 기업들이 증가하고 있다. 지금 속한 동아리의 백엔드 팀에서도 자바가 아닌 코틀린을 사용하고 있다. 동아리에서 일년 가까운 기간동안 일하며 코틀린을 사용했지만, 지금까지 코틀린으로 작성한 코드를 살펴보니 단순히 자바 코드를 코틀린으로 번역한 수준에서 크게 벗어나지 않다는 것을 느꼈다. 그래서 향후 코틀린을 의미있게 사용하기 위해서, 자바 대신 코틀린을 사용했을 때 가질 수 있는 이점들을 정리하고자 한다.
컴파일 프로세스와 롬복
자바의 컴파일 과정에 대해서는 이전에 설명한 적이 있다. 그렇다면 코틀린은 어떻게 컴파일되고 실행될까?
코틀린은 문법에서 차이가 있을 뿐, 자바와 완벽하게 상호 운영 가능하다는 특징을 가진다. 심지어 둘을 같이 사용할 수도 있다. 코틀린 또한 JVM 위에서 동작하며, 아래와 같은 과정을 거친다.
1. 코틀린 소스코드(*.kt)는 코틀린 컴파일러(kotlinc)에 의해서 바이트 코드(*.class)로 컴파일된다. 이때 코틀린 소스코드가 참조하는 Java 소스코드도 함께 로딩되어 사용된다.
2. 자바 소스코드(*.java)는 자바 컴파일러(JDK의 javac)에 의해서 바이트 코드(*.class)로 컴파일된다.
코틀린 파일도 결국 바이트코드(*.class)로 변환되기 때문에, JVM에서 인터프리터나 JIT 컴파일러를 활용하여 이진코드의 형태로 실행될 수 있다.
Null Safety
코틀린을 처음 사용했을때 가장 먼저 느꼈고, 지금까지도 제일 크게 체감하고 있는 부분이다. 자바로 비즈니스 로직 코드를 작성하다 보면 다음과 같은 상황들에 흔히 마주치게 된다.
if(article.getImage().equals(SPECIFIC_IMAGE)) {...}
게시글의 이미지가 특정 이미지일때 분기처리를 할 필요가 있다고 하자. 로직을 그대로 코드로 옮기면 위와 같고 컴파일러는 아무런 문제도 찾아내지 못하지만, 사실 해당 코드는 런타임에 의도치 않게 NPE을 발생시킬 위험이 있다. article.getImage()가 null일수도, article 자체가 null일수도 있기 때문이다.
'가장 좋은 에러는 컴파일 에러다'라는 말을 들어본 적이 있을 것이다. 프로그램이 실행 중 발생하는 예기치 못한 Throwable(런타임 예외나 에러 등)은 잡아내기도 힘들고, 복구에 많은 리소스가 들어갈 가능성이 있다. 자바는 모든 변수를 nullable하게 정의하고 있기 때문에, 컴파일 시점의 null safety를 위해서는 아래와 같이 분기시 추가로 검사하거나, try-catch로 예외를 잡아내는 코드를 추가해야 한다.
if(article!=null && article.getImage()!=null && article.getImage().equals(SPECIFIC_IMAGE)) {...}
이와 같은 자바의 특징은 로직과 관련없는 추가적인 코드를 작성하게 만들어 가독성을 해치고, 유지보수에까지 악영향을 끼칠 수 있다. (자바에서도 이러한 문제를 인지하여 Java 8부터 Optional이라는 타입을 도입하였으나, 기본적으로 모든 변수가 nullable하기 때문에 완벽한 null-safety를 보장하기 어렵다는 문제가 있고, 문법이 더 복잡하다.
반면 코틀린은 모든 객체변수를 not-nullable하게 정의하기에 변수가 null값을 가지기 위해서는 코드상으로 명시되어야 한다.
Class Article(
var id: Long?
var title: String
var image: String?
)
if(article.image == SPECIFIC_IMAGE) {...}
어떠한 사유로 image에는 null값이 들어갈 수 있었고, 따라서 article의 image는 nullable value로 선언했다. 이제 저 위에서 자바로 작성했던 코드를 코틀린으로 그대로 옮겨보았다. 자바로 작성했을 때는 아무런 오류가 발생하지 않았지만, 코틀린은 컴파일러가 오류를 발생시킨다.
기본값인 not-nullable value와, '?'키워드를 통해서 선언한 nullable value를 구분하고 있음을 알 수 있다. 이러한 코틀린의 특성을 이용하여 자바에서는 런타임에 발생할 수 있는 예외를 컴파일 시점에 잡아낼 수 있다.
Lombok과 Getter / Setter
자바는 getter와 setter의 작성을 개발자에게 위임하고 있다. 따라서 DTO, VO와 같이 getter나 setter가 필요한 모든 클래스에서 명시적으로 getter/setter를 선언해주어야 했고, 이러한 불편과 보일러 플레이트 코드들을 해소하기 위해서 Lombok이라는 라이브러리를 많이 사용한다.
하지만 코틀린은 lombok 라이브러리를 아예 지원하지 않는다. 지원하지 못한다고 표현하는 것이 맞는 것 같은데, Lombok 라이브러리는 자바 코드(.java)가 자바 바이트 코드(.class)로 컴파일될때 Lombok 어노테이션을 해당하는 부분의 자바 바이트코드로 자동으로 대체하는 방식으로 동작한다.
*.java파일과 *.kt파일이 각각의 컴파일러에 의해서 바이트 코드로 변환되는데, 코틀린이 먼저 컴파일된 후 자바 컴파일이 진행된다. 그런데 앞서 말했듯이 Lombok은 javac의 Annotation Processing 단계에서 어노테이션을 실제 바이트코드로 변환하기 때문에, 코틀린 컴파일 시점에는 롬복이 어노테이션 자체로만 존재하기에 동작하지 않는 것이다.
대신 코틀린은 언어 자체로 lombok의 기능들을 일부 지원해준다.
Class Article(
val id: Long
var title: String
var image: String?
)
위와 같은 클래스가 있다고 하자. 코틀린은 객체 프로퍼티에 한하여 val이 선언된 경우 getter를, var이 선언된 경우 getter와 setter를 자동으로 생성해준다. 자바와 같이 함수로 접근하기보다는 '.'을 이용하여 객체 프로퍼티에 접근하는 방식으로 표현된다.
article.id = 1L // val에 값을 할당하려고 하므로 컴파일에러 발생
article.title = "ex"
Static과 Object, Companion Object
자바에는 static 이라는 키워드가 존재한다. 기본적으로 자바의 static 키워드는 다음과 같은 의미와 특징을 가진다.
1. static 변수의 경우 객체 생성 시점에 생성되는 인스턴스 변수나 멤버변수와 다르게, 클래스 로딩 시점에 클래스 로더에 의해서 로딩된다.
2. 클래스 단위에서 논리적인 의미를 가지며, 인스턴스 생성 없이 접근 가능하다. 따라서 유틸리티 클래스에 static 메서드를 만들어 활용하는 경우가 많다.
3. Heap이 아닌 Static영역에 할당되며, GC에 의해 자동으로 메모리 할당이 해제되지 않는다.
그런데 코틀린에는 static 키워드가 존재하지 않고, 대신 Object와 Companion Object를 활용할 수 있다.
첫 방법은 코틀린의 싱글톤을 지원하는 object를 이용하는 방법이다. 자바에서 싱글톤 패턴의 구현을 위해서는 스프링과 같은 프레임워크를 사용하거나, 직접 구현해주어야 했다. private 생성자를 통해서 객체 생성을 방지하고, static변수를 활용하여 하나의 객체만 사용되도록 했다.
그런데 코틀린은 object 키워드를 사용하여 언어차원에서 싱글톤 선언을 지원한다.
object Event {
val coupons = ListOf()
fun registerCoupon(name: String): Car {
coupon.add(name)
}
}
위 코드에서 event는 싱글톤으로 동작하며, Event객체를 여러개 생성하더라도 내부의 coupons라는 리스트는 새로 생성되지 않는다. 이를 이용하면 object키워드를 이용하여 싱글톤 클래스를 생성하고, 내부에 있는 변수들은 자동으로 static 멤버와 동일하게 사용할 수 있다.
두 번째 방법으로는 companion object를 사용할 수 있다.
class Event {
companion object {
val coupons = listOf()
fun registerCoupon(name: String): Car {
coupon.add(name)
}
}
}
자바의 static과 거의 동일하지만, 단순히 클래스 레벨의 메서드나 변수를 선언하는 역할만을 하는 java의 static과 다르게, 클래스 내에 실제 'companion object'라는 객체의 형태로 존재한다.
궁금해요
그렇다면 왜 코틀린은 static 키워드를 사용하지 않고 object와 companion object라는 다른 형태로 제공할까? 내 결론은 다음과 같다.
사실 자바의 static은 논리적인 문제를 가지고 있다. 바로 static 변수나 static 메서드를 클래스명이 아닌 인스턴스에서 접근할 수 있다는 것.
자바는 클래스명이 아닌 인스턴스를 통해 static 멤버에 접근하는 것을 허용하고 있다. 나는 자바에서 static키워드는 논리적으로 대체하기 어려운 중요한 문법이라고 생각한다. 하지만 이러한 자바의 너그러움(?)으로 인해, 개발자는 인스턴스의 멤버에 접근할 때 이것이 static인지, non-static인지 알 수 있는 방법이 없다. IDE가 띄워주는 경고가 아니었으면 일일히 클래스에 들어가서 멤버에 static이 붙었는지 확인해야 할 수도 있다.
다시 처음으로 돌아가서, static은 '인스턴스에 의존적이지 않음'을 선언하는데 목적이 있다. 인스턴스가 아닌 클래스 단위로 의미를 가져야하는 사용처를 생각해보면 크게 두가지가 떠오른다. - 유틸리티 클래스와 싱글톤이 그것이다.
따라서 싱글톤은 object로, 유틸리티 클래스는 companion object라는 키워드를 통해 사용처를 분리하여 지원하는 것이라고 나는 이해했다.
따라서 코틀린에서는 인스턴스에서 static멤버를 호출하는 문제도 발생하지 않는다. object의 경우 인스턴스 생성 자체가 제한되니 당연하고, companion object 또한 클래스 내부에 존재하지만 인스턴스와의 독립성을 부여하여 인스턴스 단위의 호출을 막았기 때문이다.
참고자료
Kotlin 컴파일 프로세스: https://d2.naver.com/helloworld/6685007
'[ Languages ] > Kotlin' 카테고리의 다른 글
[Kotlin] 안드로이드 코틀린 연습 (0) | 2022.07.06 |
---|---|
[Kotlin] 기본 문법(2) - 심화 (0) | 2022.07.02 |
[Kotlin] 기본 문법(1) (0) | 2022.06.30 |