0. 문제의 발단
코틀린 입문 강의를 마치고 처음으로 코틀린 프로젝트를 시작했습니다. 기존에 자바로 작성된 코드를 코틀린으로 리팩토링하면서 여러 상황을 마주하게 되었습니다. 특히 엔티티 클래스를 새롭게 작성하면서 고민이 많았습니다.
기존 코드는 다음과 같았습니다.
@Entity
class Link(
name: String,
content: String,
isActive: Boolean,
) : Base() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "link_id")
var id: Long? = null
var name: String = name
var content: String = content
var isActive: Boolean = isActive
}
이 코드에는 몇 가지 개선 포인트가 보였습니다
- 2단계 초기화 구조 – 생성자에서 받은 값을 다시 필드에 대입하는 불필요한 과정
- setter 노출 문제 – 외부에서 필드가 쉽게 변경될 수 있음
1. 주 생성자에서 프로퍼티 선언
위 코드에서는 생성자 파라미터를 통해 값을 받고, 이를 다시 클래스 내부의 프로퍼티에 할당하는 구조입니다. 이 과정을 다음처럼 간결하게 바꿀 수 있습니다.
@Entity
class Link(
var name: String,
var content: String,
var isActive: Boolean = true,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var idx: Long? = null
) : Base()
- 생성자에서 바로 프로퍼티를 선언하여 초기화 과정을 간소화했습니다.
- isActive는 디폴트 값으로 true를 설정해 불필요한 파라미터 생성을 줄였습니다.
- 기본키는 관례적으로 idx로 통일해 @Column을 생략했습니다.
- 코틀린에서는 디폴트 값이나 자동 생성 필드를 생성자 마지막에 두는 것이 관례입니다.
2. 기본키 필드와 val 사용 고민
다음과 같이 기본키를 val로 선언할 수도 있습니다.
@Entity
class Link(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var idx: Long? = null
) : Base()
JPA는 리플렉션을 통해 필드 값을 설정하므로 val이어도 문제없이 동작합니다. 하지만 고민이 생깁니다
@Entity
class Link(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val idx: Long? = null
) : Base()
- val로 선언하여 불변성을 높일 것인가?
- var로 선언하여 JPA의 자연스러운 흐름을 따를 것인가?
저는 팀원들의 교육과 코드 리뷰로 관리 가능하다고 판단해 var를 사용하기로 했습니다.
3. 무분별한 setter 노출
다음은 setter가 노출되어 있는 코드입니다:
val link = Link("링크 이름", "링크 설명")
link.name = "변경된 이름"
엔티티 외부에서 필드를 쉽게 변경할 수 있게 됩니다. 이를 막기 위해 다음과 같이 setter 접근 제한을 시도했습니다.
@Entity
class Link(
name: String,
content: String,
isActive: Boolean = true,
) : Base() {
var name: String = name
private set
var content: String = content
private set
var isActive: Boolean = isActive
private set
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var idx: Long? = null
}
그러나 빌드 시 아래와 같은 오류가 발생합니다:
Private setters are not allowed for open properties
JPA는 프록시를 활용하기 위해 엔티티 클래스를 상속 가능하게 (open) 만들어야 하며, setter도 protected 이상이어야 합니다. 따라서 다음과 같이 수정해야 합니다.
@Entity
open class Link(
name: String,
content: String,
isActive: Boolean = true,
) : Base() {
var name: String = name
protected set
var content: String = content
protected set
var isActive: Boolean = isActive
protected set
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var idx: Long? = null
}
이제 외부에서는 setter를 사용할 수 없고, JPA의 프록시 기능에도 문제가 없습니다.
4. 그런데 이렇게까지 해야 할까?
리팩토링을 반복할수록 코드가 오히려 더 복잡해지고 지저분해질 수 있습니다.
코틀린은 자바보다 더 간결한 문법을 제공하지만, JPA 같은 자바 기반 프레임워크에 맞추다 보면 오히려 불편해지는 경우가 생깁니다.
그래서 아래처럼 간단한 방식으로 작성하는 것도 나쁘지 않다고 생각합니다.
@Entity
class Link(
var name: String,
var content: String,
var isActive: Boolean = true,
) : Base() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var idx: Long? = null
}
이렇게 하려면 두 가지 전제가 필요합니다.
- setter는 사용하지 않고 내부 메서드를 통해 값 변경
- 기본키 필드는 외부에서 변경하지 않도록 주의
이 두 가지는 대부분의 JPA/Spring 개발자라면 기본적으로 지켜야 하는 원칙입니다. 결국 코드의 가독성과 유지보수성 사이에서 팀 컨벤션이 중요해집니다.
5. 마무리하며
이번 글에서는 코틀린을 활용한 엔티티 설계에서 발생할 수 있는 고민들을 정리해 보았습니다. 이 과정을 통해 불변성, 가독성, JPA 호환성에 대해 다시금 생각할 수 있었습니다. 다음에도 좋은 고민거리를 함께 나누겠습니다. 감사합니다.