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을 사용하여 해결 가능 | val은 final과 유사하지만, 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
'BE > Kotlin' 카테고리의 다른 글
[Kotlin] 코루틴 (Coroutines) (1) | 2025.03.02 |
---|---|
[Kotlin] 함수형 프로그래밍과 불변성 (0) | 2025.02.24 |
[Kotlin] 코틀린 문법 6 (open, 상속, 인터페이스) (0) | 2025.02.20 |
[Kotlin] 코틀린 문법 5 (lateinit, lazy, companion object) (0) | 2025.02.19 |
[Kotlin] 코틀린 문법 4 (apply, let, also, run, with) (0) | 2025.02.18 |