BE/Kotlin

[Kotlin] JPA에서 필드에 final을 붙이지 않는 이유 (val, var)

baek-dev 2025. 2. 21. 19:47

Spring Boot의 JPA 엔티티에서는 일반적으로 필드를 private으로 선언하고 final을 붙이지 않는 이유JPA의 프록시(proxy) 및 리플렉션(reflection) 메커니즘 때문.

 

✅ 1. JPA는 엔티티 객체를 리플렉션(Reflection)으로 생성해야 한다

📌 final을 사용하면 JPA가 필드를 초기화할 수 없음

 

JPA는 기본 생성자(No-Arg Constructor)를 사용하여 엔티티 객체를 리플렉션으로 생성합니다.

즉, JPA가 객체를 만들 때 필드에 값을 할당할 수 있어야 합니다.

 

🚨 private final을 사용하면 발생하는 문제

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private final Long id; // ❌ final 사용

    private final String name; // ❌ final 사용

    protected User() { } // 기본 생성자

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

👉 위 코드에서 private final 필드가 있기 때문에 JPA는 이 필드를 초기화할 수 없음.

💥 결과: org.hibernate.PropertyAccessException 예외 발생

 

❗ JPA는 프록시 객체를 생성할 때 기본 생성자를 사용하여 값을 채워 넣는데, final이 있으면 값을 할당할 수 없기 때문에 오류 발생! 🚨

 

✅ 2. JPA의 “프록시 객체” 동작 방식

JPA는 **지연 로딩(Lazy Loading)**을 위해 프록시 객체를 생성할 수 있습니다.

즉, 데이터베이스에서 값을 가져오기 전에 빈 객체를 생성하고 나중에 값을 채워 넣음.

 

🚨 final 필드가 있으면 프록시 객체를 만들 수 없음

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private final Long id; // ❌ final 사용하면 프록시 객체 생성 불가

    protected User() {} // 기본 생성자 필요

    public User(Long id) {
        this.id = id;
    }
}

JPA는 먼저 빈 객체를 만든 후 ID 값을 설정해야 하지만, final 필드는 생성자에서 한 번만 값을 설정할 수 있기 때문에 오류 발생!

 

결론: final 필드를 사용하면 JPA가 객체를 동적으로 조작할 수 없어서 사용하지 않음.

 

✅ 3. Spring에서는 일반적으로 “Setter 없이 불변 객체”를 권장함

JPA 엔티티에서는 final을 사용하지 않지만,

대신 불필요한 setter 메서드를 제공하지 않음으로써 변경을 방지하는 방식을 사용합니다.

 

📌 JPA 엔티티에서는 final 대신 “Setter 없이 불변성을 유지”

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // ✅ final 제거

    private String name;

    protected User() { } // JPA 기본 생성자

    public User(String name) { // Setter 대신 생성자 사용
        this.name = name;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
}

 

✅ final 없이도 불변성을 유지할 수 있는 방법

1. 필드를 private으로 선언 (직접 수정 불가능)

2. Setter 메서드를 제공하지 않음

3. 값을 변경하려면 생성자를 통해 객체를 새로 생성해야 함

 

✅ 4. Kotlin에서는 val을 사용하여 JPA 엔티티를 만들면?

Java에서는 final을 사용할 수 없지만,

Kotlin에서는 val을 사용하면 불변성을 유지하면서도 JPA에서 정상적으로 작동할 수 있음.

 

📌 Kotlin 엔티티 (JPA 최적화 버전)

@Entity
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null, // ✅ JPA가 자동으로 값 할당 가능

    val name: String // ✅ 생성자를 통해 값 설정 (불변성 유지)
)

val을 사용하면 Java의 private final과 유사한 효과

하지만 JPA가 ID를 자동 할당할 수 있도록 val id: Long? = null 설정

 

💡 Kotlin에서는 val을 사용하면 불변성이 유지되므로 final을 사용하지 않아도 됨.

 

✅ 5. 결론 (JPA에서 private final을 사용하지 않는 이유)

이유 설명
JPA는 리플렉션을 사용하여 객체를 생성 final 필드가 있으면 값 할당 불가능 → JPA에서 객체 생성 불가 🚨
프록시 객체 생성 문제 final이 있으면 Hibernate가 프록시를 만들 수 없음 🚨
Setter를 제공하지 않아도 불변성을 유지 가능 JPA에서는 final 대신 Setter 제거로 불변성을 유지
Kotlin에서는 val을 사용하여 해결 가능 valfinal과 유사하지만, JPA에서 자동 ID 할당 가능

 

코틀린에서 필드는 val로 설정. var를 써야하는 경우엔 필드에서 기본값을 설정하거나, 기본생성자를 통해 값을 설정.

JPA 엔티티에서 lateinit var 는 안쓰는게 좋다 (null을 허용하지 않음)

 

@Entity
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column(unique = true, nullable = false)
    val username: String,

    @Column(nullable = false)
    var password: String, // ✅ 비밀번호는 변경 가능하므로 var 사용

    @Column(nullable = false)
    var profileImgUrl: String = "" // ✅ 기본값 설정, 프로필 사진은 변경 가능할 수 있음
)

 

필드 val 사용 var 사용
ID (id) ✅ JPA가 자동 생성하는 값이므로 val 사용 ❌ 보통 필요 없음
고유 값 (username, email) ✅ 한 번 설정되면 변경되지 않음 ❌ 변경이 필요한 경우가 거의 없음
비밀번호 (password) ❌ 변경 가능성이 높음 ✅ 사용자가 변경할 가능성이 있음
프로필 사진 (profileImgUrl) ❌ 변경될 가능성이 있음 ✅ 사용자가 변경할 가능성이 있음
상태값 (status) ❌ 상태 변경이 필요할 수 있음 ✅ 예를 들어 ACTIVE → INACTIVE 변경 가능

 

 

 

 

 

출처 : ChatGPT