안녕하세요! 최근 지인들과 코틀린 스터디를 시작하게 되어서 코틀린에 대해서 학습을 해보고 있었습니다.
아직 깊이있게 이해했다라기 보다는 학습해나가는 중인데요. 자바에서 단점이라고 느꼈던 여러 문제들을 해결하기 위해 젯브레인사에서 얼마나 공들였는지 조금은 이해가 된달까요...
뭔가 Java + TS 느낌도 나구요.. ㅎㅎ 아무튼 코틀린의 매력에 빠져서 재미있게 시간을 보내고 있습니다.
스터디 과제가 자바와의 비교를 통해 코틀린에서 중요한 점을 정리해보는 것이여서 이를 정리할 겸 제가 느낀 코틀린의 첫인상과 자바와의 비교를 통한 가치를 글로써 남겨보고자 합니다.
코틀린에 대해 잘 모르는 자바 개발자가 쓴 글이니 지적은 언제나 환영입니다!
코틀린은 JPA랑 왜 같이 쓰면 안될까?
기존에 자바 + 스프링 + JPA의 스택으로 이루어진 작은 토이 프로젝트를 코틀린으로 전환을 해보면서 학습을 진행하였습니다.
위에서도 잠깐 언급하였듯, 코틀린은 자바 기반의 코드에서 발생하는 여러 문제들을 해결하기 위해 노력하였답니다.
하지만 JPA는 이름부터 Java Persistence API로 완전히 자바 친화적인 기술이죠. 이러다보니 코틀린에서 제공하고자 하는 가치를 훼손시키는 여러 작은 문제들이 있음을 알게되었습니다(!)
아래에서 하나씩 살펴보도록 하겠습니다.
1. Null 안전성
자바의 단점을 말해보아라 라고 하면 대표적으로 제시되는 단점이 Null 안정성에 대한 것들입니다.
자바에서는 NullPointerException을 방지하기 위해 더 철저히 유효성 검사를 해야하고, Optional로 처리를 하는 등 많은 노력이 필요했습니다.
하지만 코틀린에서는 이러한 단점을 해결하기 위해 Null 안전성을 강제하도록 설계되었습니다.
하지만, JPA를 사용하면 엔티티의 필드를 Null로 설정하거나, 데이터베이스에서 Null 값을 반환하도록 기본적으로 설계가 되었습니다. 이로 인해 코틀린에서 JPA를 사용하게 되면 nullable 타입과 non-nullable 타입 간의 변환이 필요하게 됩니다.
import javax.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@Column
private String email;
}
이러한 User 엔티티가 있다고 간단히 name과 email 속성을 가지고 있습니다. 이 코드를 코틀린으로 바꾸면 어떻게 될까요?
import javax.persistence.*
@Entity
data class User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false)
val name: String,
@Column(nullable = true)
val email: String?
)
이런식으로 기본적으로 null을 허용하지 않는 코틀린의 특성상 JPA와의 조합을 위해 nullable 타입을 강제로 사용해야 합니다.
이는 Null 안정성을 제공하고자 하는 코틀린 자체의 취지와는 어울리지 않는 부분이죠.
사실 이 부분은 KotlinX Serialization 직렬화 라이브러리를 사용하면 어느정도 커버가 가능하다고는 합니다.
그래도,, 추가적인 복잡도가 생기는 건 적절하지 않겠죠?
2. 기본 생성자의 명시적 작성
JPA를 사용하기 위해서는 엔티티 클래스에 기본 생성자를 명시적으로 작성해야합니다. 내부적으로 엔티티 클래스를 인스턴스화 할 때 생성자가 필요하기 때문이죠.
코틀린에서는 기본적으로 생성자 없이 클래스를 만들 수 있습니다. 하지만 이는 JPA와 호환되지 않고 문제를 일으키게 되므로 명시적으로 필요없는 생성자 메서드를 생성해야 합니다.
import javax.persistence.*
@Entity
class User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column(nullable = false)
var name: String,
@Column(nullalbe = true)
var email: String?
) {
constructor() : this(null, "", null)
}
3. 프록시와 리플렉션
JPA는 엔티티의 프록시 객체를 생성하기 위해서 리플렉션을 사용합니다. 코틀린은 기본적으로 final 클래스를 생성하여 객체를 불변하게 관리합니다. 하지만 이러한 코틀린의 불변성이 JPA와 조합되었을 때 프록시 생성을 어렵게 만들 수 있습니다.
이를 해결하기 위해 open 키워드를 통해 명시적으로 객체를 가변한 상태로 만들게 되는데요.
이는 결국 코틀린의 불변성이라는 장점을 살리지 못하게 되고 비즈니스 로직 내에서 값을 안전하게 핸들링하기 어렵게 됩니다.
import javax.persistence.*
@Entity
open class User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column(nullable = false)
var name: String,
@Column(nullable = true)
var email: String?
) {
constructor() : this(null, "", null)
}
4. JPA와 데이터 클래스의 호환성
코틀린의 데이터 클래스는 편리한 기능을 제공합니다.
데이터 클래스는 객체에서 거의 필수적으로 사용되는 메서드 equals, hashCode, toString 메서드를 자동으로 생성합니다. 하지만 이러한 추상화된 동작이 JPA 엔티티의 동작과 충돌을 일으킬 수 있습니다. 따라서 대신 일반 클래스로 선언후 해당 메서드들을 직접 선언하여야 하는 번거로움이 생깁니다.
이러한 점도 코틀린의 장점을 포기하게되는 것이죠.
5. getter/setter의 일관성
JPA는 엔티티의 필드에 접근할 때, 직접 접근하거나 private으로 선언되어있는 필드로의 접근은 getter와 setter 메서드를 통해 접근합니다. 하지만 이런식으로 선언하는 자바 스타일과는 다르게 코틀린에서는 필드를 val과 var 키워드를 사용하여 정의합니다. JPA는 이러한 필드에 접근하는데 오류가 생길 수 있습니다.
이를 해결하기 위해서 코틀린의 @JvmField 나 @JvmStatic 어노테이션을 사용하여 자바 스타일의 접근을 강제해야합니다.
@Entity
class User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@JvmField var id: Long? = null,
@Column(nullable = false)
@JvmField var name: String,
@Column(nullable = true)
@JvmField var email: String?
) {
constructor() : this(null, "", null)
}
그렇다면 이러한 문제를 어떻게 해결할 수 있을까?
그렇다면 이러한 문제를 어떻게 해결할 수 있을까요?
가장 편하게 접근할 수 있는 것은 위에서 이야기했던 방법들을 사용해 코틀린 내부에서 코틀린의 장점이나 특징을 어느정도 포기하고 JPA와 호환성을 챙기도록 코드를 작성하는 것입니다.
하지만 JPA를 사용하지 않고 다른 ORM 기술을 사용하여 해결하는 방법도 있습니다.
Spring Data JDBC
Spring Data JDBC는 JPA보다 간단한 ORM 기술입니다. 코틀린과의 호환성이 더 좋기도 합니다.
해당 라이브러리를 사용하면 JPA에 비해 더 적은 리플렉션을 사용하기 때문에 문제가 발생할 확률이 줄어듭니다.
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table
@Table("users")
data class User(
@Id val id: Long? = null,
val name: String,
val email: String?
)
Exposed
Exposed는 JetBrains에서 만든 코틀린을 위한 SQL 프레임워크 입니다. 완전히 코틀린 친화적인 기술로 DSL 스타일로 SQL 쿼리를 작성하고 데이터베이스와 상호작용할 수 있습니다.
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
object Users : Table() {
val id = long("id").autoIncrement().primaryKey()
val name = varchar("name", 50)
val email = varchar("email", 50).nullable()
}
data class User(val id: Long, val name: String, val email: String?)
transaction {
SchemaUtils.create(Users)
Users.insert {
it[name] = "John Doe"
it[email] = "john.doe@example.com"
}
Users.selectAll().map {
User(it[Users.id], it[Users.name], it[Users.email])
}
}
Jooq
Jooq은 타입 안전성을 극대화하기 위한 SQL 빌더로 데이터베이스 스키마를 기반으로 코드를 생성하게 합니다. 자바 코틀린 두 언어 모두와 사용해도 안전하게 사용이 가능합니다.
import org.jooq.impl.DSL
import org.jooq.impl.SQLDataType
val create = DSL.using(configuration)
create.createTable("users")
.column("id", SQLDataType.BIGINT.identity(true))
.column("name", SQLDataType.VARCHAR(50).nullable(false))
.column("email", SQLDataType.VARCHAR(50).nullable(true))
.execute()
create.insertInto(DSL.table("users"))
.columns(DSL.field("name"), DSL.field("email"))
.values("John Doe", "john.doe@example.com")
.execute()
이러한 여러 기술을 통해서 JPA 와 코틀린의 호환성 문제를 해결할 수 있습니다.
항상 이야기하지만 프로그래밍에는 은탄환은 없습니다. 각자의 필요에 따라 맞는 방법을 통해 조금더 안전하고 코틀린스러운 어플리케이션을 작성하기 위해 노력해야겠습니다! 💪💪
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆