사이드 프로젝트를 리팩터링 하는 과정에서 Kotlin과 Java를 같이 사용하는 방식을 선택하게 됐다. 어떤 이유로 이런 방식을 사용하게 되었는지에 대한 내용을 정리해 보려 한다.
프로젝트 리팩터링 레포는 아래 링크에서 확인할 수 있다.
https://github.com/TEAM-SAMSION/Backend-refactor
먼저 Presentation 계층부터 Domain 계층까지는 Kotlin을 사용하려 한다. 기존 Java에서 Kotlin으로 넘어가려는 이유는 다음과 같다.
(아키텍처는 현재 리팩터링 중인 레포에 있는 Convention 관련 위키에서 확인할 수 있다.)
Java를 사용하다 보면 NullPointException을 자주 만나게 된다. Java는 Nullable을 표현하는 타입 시스템이 없기 때문에, 개발자가 직접 Null 여부를 체크해야 한다. 그러나 Kotlin에서는 모든 변수를 not-null로 정의하기 때문에 자바 버그의 일반적인 원인인 Null 포인터 예외를 방지하는 데 도움이 된다.
실제로 Kotlin에서 다음과 같이 코드를 작성하면 컴파일 에러가 발생한다.
val name: String = null
또한 Kotlin은 Nullable 타입 (널 입력 가능 타입) 을 지원 하기 때문에 Optional 타입을 사용할 필요가 없다. 기존 Java 코드를 Kotlin으로 바꿔보면 다음과 같다.
private User readUser(Supplier<Optional<User>> method){
return method.get()
.orElseThrow(() -> new UserNotFoundException(UserError.USER_NOT_FOUND));
}
private fun readUser(findMethod: () -> User?): User {
return findMethod() ?: throw UserNotFoundException(UserError.USER_NOT_FOUND)
}
Kotlin에서는 Optional 대신 엘비스 연산자와 ? 를 사용해 좀 더 직관적인 코드를 작성할 수 있다.
Kotlin을 사용하게 되면 코드가 매우 간결해지고 가독성이 높아진다. 관련된 내용 몇 가지를 이야기해보겠다.
💡 Immutable
Kotlin은 불변성을 기본으로 한다. Kotlin은 Java에 final 키워드를 붙이는 것과 같은 val라는 키워드를 통해 변수를 선언할 수 있기에 코드를 작성하는데 있어 안정감을 느낄 수 있다.
💡 Type Inference
Kotlin에서는 타입을 명시하지 않아도 선언될 때 자동으로 할당된 값의 형태로 어떤 자료형을 가지는지 추론해준다.
💡 Lambda
Kotlin에서는 매개변수가 하나일 경우 표기를 생략하고 it이라는 암묵적 변수명으로 표현식을 작성할 수 있다.
fun modifyNickname(userId: Long, newNickname: String){
userRepository.findByIdOrNull(userId)?.let {
it.changeNickname(newNickname)
userRepository.update(it)
}
}
💡 Getter/Setter
Kotlin에서는 객체 프로퍼티에 대하여 val이 선언되어 있을 경우 getter를, var가 선언되어 있다면 getter/setter를 자동 생성해 준다. 추가적으로 Data Class를 활용한다면, getter/setter 외에 equals와 hashCode까지 자동으로 생성해 준다. 따라서 Kotlin에서는 롬복을 쓸 이유가 없다. @Getter @Setter @Data 등의 어노테이션이 이미 코틀린 언어 자체에 내장되어 있기 때문이다.
Infrastructure 계층에서는 Java를 사용하려 한다. 이 결정을 하게 된 이유는 프로젝트에서 사용하는 ORM 기술과 연관되어 있다.
현재 우리 프로젝트에서는 ORM 기술로 JPA를 사용하고 있다. JPA는 Java를 기준으로 나온 ORM 표준이다. Kotlin 또한 Java 진영에 속하기 때문에 문제가 없다 생각할 수 있지만, Kotlin 언어의 특성과 JPA의 특성 사이에서 충돌 나는 경우가 있다.
💡 Immutable
Kotlin에서는 open 키워드를 사용하지 않은 이상 모든 클래스가 기본적으로 final 이다. 또한 data class와 var 보다는 val을 사용하는 것을 권장한다. 반면 JPA는 엔티티를 선언할 때 final 키워드를 사용하지 않도록 요구하고 런타임에 상태가 언제든지 변할 수 있다는 것을 자연스럽게 생각한다. 이는 Kotlin이 가지는 언어적 특징과 JPA의 특징이 완전히 반대된다는 것을 알 수 있다.
💡 Private Setter
JPA를 사용해 엔티티를 선언할 때는 내부 프로퍼티는 private으로 선언하고, setter를 노출하지 않도록 구현하는 게 기본적인 방법이다. Java에서는 롬복을 통해 Getter만 사용할 수 있도록 설정이 가능하나, Kotlin에서는 롬복을 사용하지 않기 때문에 위에 방식을 구현하려면 val로 프로퍼티를 선언해야 할 것이다.
class User(
@Column
val nickname: String,
@Column
val imageUrl: String,
)
그러나 val 을 사용하면 해당 프로퍼티는 클래스 내부에서도 수정이 불가능하고, JPA의 변경 감지 기능도 사용할 수 없다. 이를 해결하기 위해 var ****키워드를 사용하고, setter를 protected로 선언하는 방법을 사용할 수 있다.
class User(
nickname: String,
imageUrl: String,
) {
@Column
var nickname = nickname
protected set
@Column
var imageUrl = imageUrl
protected set
}
그러나 이렇게 구현하면 생성자와 클래스 내부, 두 번의 Property 선언과 Property마다 protected set 문구가 필요하기 때문에 작성해야 할 코드가 늘어나게 된다.
결론적으로 JPA와 Kotlin을 같이 사용하려면 Kotlin의 언어적 특징을 해치며 사용할 수밖에 없다.
위에 같은 이유로 ORM 기술에 종속되어 있는 Infrastructure 계층에서는 Java를 사용해 구현하기로 결정했다. 우리 프로젝트에서는 각각 계층을 모듈로 묶은 멀티모듈 형태를 사용해 의존성도 잘 분리하고 있으므로 Infrastructure 계층에서는 ORM 기술에 최적화되어있는 언어를 사용해도 문제가 없을 거라 생각한다.