Java와 Kotlin 함께 사용하기

장세은·2024년 3월 30일
post-thumbnail

사이드 프로젝트를 리팩터링 하는 과정에서 Kotlin과 Java를 같이 사용하는 방식을 선택하게 됐다. 어떤 이유로 이런 방식을 사용하게 되었는지에 대한 내용을 정리해 보려 한다.

프로젝트 리팩터링 레포는 아래 링크에서 확인할 수 있다.
https://github.com/TEAM-SAMSION/Backend-refactor

Kotlin

먼저 Presentation 계층부터 Domain 계층까지는 Kotlin을 사용하려 한다. 기존 Java에서 Kotlin으로 넘어가려는 이유는 다음과 같다.

(아키텍처는 현재 리팩터링 중인 레포에 있는 Convention 관련 위키에서 확인할 수 있다.)

Null Safety

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 대신 엘비스 연산자와 ? 를 사용해 좀 더 직관적인 코드를 작성할 수 있다.

Simplicity

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 등의 어노테이션이 이미 코틀린 언어 자체에 내장되어 있기 때문이다.

Java

Infrastructure 계층에서는 Java를 사용하려 한다. 이 결정을 하게 된 이유는 프로젝트에서 사용하는 ORM 기술과 연관되어 있다.

JPA with Kotlin

현재 우리 프로젝트에서는 ORM 기술로 JPA를 사용하고 있다. JPA는 Java를 기준으로 나온 ORM 표준이다. Kotlin 또한 Java 진영에 속하기 때문에 문제가 없다 생각할 수 있지만, Kotlin 언어의 특성과 JPA의 특성 사이에서 충돌 나는 경우가 있다.

💡 Immutable

Kotlin에서는 open 키워드를 사용하지 않은 이상 모든 클래스가 기본적으로 final 이다. 또한 data classvar 보다는 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의 언어적 특징을 해치며 사용할 수밖에 없다.

JPA with Java

위에 같은 이유로 ORM 기술에 종속되어 있는 Infrastructure 계층에서는 Java를 사용해 구현하기로 결정했다. 우리 프로젝트에서는 각각 계층을 모듈로 묶은 멀티모듈 형태를 사용해 의존성도 잘 분리하고 있으므로 Infrastructure 계층에서는 ORM 기술에 최적화되어있는 언어를 사용해도 문제가 없을 거라 생각한다.

0개의 댓글