기본값 타입
JPA에서 데이터 타입은 크게 두 가지로 분류할 수 있습니다: 엔티티 타입과 값 타입입니다.
엔티티 타입
- 정의: @Entity로 정의된 객체.
- 특징:
- 데이터가 변해도 식별자(ID)로 지속해서 추적 가능.
- 식별자를 통해 동일한 객체를 유지.
- 예를 들어, 회원 엔티티에서 키나 나이 값이 변경되어도 동일한 회원으로 인식 가능.
값 타입
- 정의: 자바 기본 타입이나 객체로 단순히 값으로 사용.
- 특징:
- 식별자가 없고 값만 있음.
- 값이 변경되면 추적 불가.
- 예를 들어, 숫자 100을 200으로 변경하면 완전히 다른 값으로 인식.
값 타입 분류
기본 값 타입
- 예시: String name, int age
- 특징:
- 생명 주기가 엔티티에 의존.
- 예를 들어, 회원을 삭제하면 이름과 나이 필드도 함께 삭제됨.
- 값 타입은 공유 불가.
- 예를 들어, 회원 이름을 변경하면 다른 회원의 이름도 변경되면 안됨.
- 자바의 기본 타입은 절대 공유되지 않음.
- 기본 타입은 항상 값을 복사.
- Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체지만 변경되지 않음.
임베디드 타입은 JPA에서 값 타입을 재사용하고 응집력을 높이며, 엔티티의 일부로서 다루기 위한 중요한 개념입니다. 임베디드 타입을 사용하는 방법과 장점에 대해 정리해 보겠습니다.
1. 임베디드 타입 정의
임베디드 타입은 @Embeddable 어노테이션을 사용하여 정의합니다. 이 클래스는 값 타입을 표현하며, 엔티티의 일부로 사용됩니다. 임베디드 타입 클래스에는 기본 생성자가 필요합니다.
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// Getters and Setters
}
2. 엔티티에서 임베디드 타입 사용
엔티티 클래스에서 @Embedded 어노테이션을 사용하여 임베디드 타입을 포함시킵니다. 이렇게 하면 해당 엔티티는 임베디드 타입의 속성을 자신의 속성처럼 사용할 수 있습니다.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Embedded;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address address;
public Member() {}
public Member(String name, Address address) {
this.name = name;
this.address = address;
}
// Getters and Setters
}
임베디드 타입의 장점
- 재사용성: 한 번 정의한 임베디드 타입을 여러 엔티티에서 재사용할 수 있습니다. 예를 들어, Address 타입을 여러 엔티티에서 사용할 수 있습니다.
- 높은 응집도: 관련된 데이터를 하나의 값 타입으로 묶어 응집도를 높일 수 있습니다. 이는 코드의 가독성과 유지보수성을 높여줍니다.
- 의미 있는 메소드 추가: 임베디드 타입 내에서 해당 타입에만 관련된 비즈니스 로직 메소드를 추가할 수 있습니다. 예를 들어, Period 타입에 isWork() 메소드를 추가할 수 있습니다.
- 엔티티의 생명주기에 의존: 임베디드 타입은 그것을 포함하는 엔티티의 생명주기에 의존합니다. 엔티티가 영속화되거나 삭제되면, 임베디드 타입도 함께 처리됩니다.
임베디드 타입과 테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이며, 이를 사용하기 전과 후의 테이블 매핑은 동일합니다.
- ORM 애플리케이션을 잘 설계하면, 매핑된 테이블 수보다 클래스 수가 많아지는 경우가 많습니다. 이는 객체와 테이블을 세밀하게 매핑할 수 있게 해주기 때문입니다.
@AttributeOverride: 속성 재정의
@AttributeOverride는 엔티티에서 같은 값 타입을 여러 번 사용할 때 컬럼 이름이 중복되는 문제를 해결하기 위해 사용됩니다. @AttributeOverrides와 함께 사용하여 특정 컬럼의 속성을 재정의할 수 있습니다.
예제: @AttributeOverride 사용법
Address 클래스 (임베디드 타입)
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
// Getters and Setters
}
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
})
private Address workAddress;
public Member() {}
public Member(String name, Address homeAddress, Address workAddress) {
this.name = name;
this.homeAddress = homeAddress;
this.workAddress = workAddress;
}
// Getters and Setters
}
설명
- Address 클래스: @Embeddable로 정의된 임베디드 타입입니다. city, street, zipcode 필드를 가지고 있습니다.
- Member 클래스: @Entity로 정의된 엔티티입니다. homeAddress와 workAddress라는 두 개의 Address 임베디드 타입을 가지고 있습니다.
- @AttributeOverride: workAddress 필드에 적용되어, 기본 컬럼 이름이 중복되지 않도록 각각의 컬럼 이름을 재정의합니다. homeAddress와 workAddress의 각 필드는 서로 다른 컬럼 이름을 가집니다.
임베디드 타입과 null
- 임베디드 타입의 값이 null: 임베디드 타입 자체가 null이면 해당 임베디드 타입이 매핑된 모든 컬럼 값이 null로 저장됩니다.
예를 들어, Member 엔티티의 homeAddress가 null이면, homeAddress에 매핑된 city, street, zipcode 컬럼 값도 모두 null로 저장됩니다.
값 타입과 객체 타입의 한계 및 해결 방법
값 타입의 개념
- 값 타입은 복잡한 객체 세상을 단순화하려고 만든 개념입니다.
- 값 타입은 단순하고 안전하게 다룰 수 있어야 합니다.
- 값 타입은 식별자가 없고, 단순히 값으로서의 의미를 가집니다.
- 대표적인 예: int, String, Integer 등
값 타입의 공유 참조 문제
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 발생할 수 있습니다.
- 값 타입을 공유할 경우, 한 엔티티에서 값을 변경하면 다른 엔티티에서도 값이 변경되는 문제가 발생할 수 있습니다.
값 타입 복사
- 값 타입의 실제 인스턴스를 공유하는 것은 위험합니다.
- 대신 값 타입을 사용할 때는 값을 복사해서 사용해야 합니다.
객체 타입의 한계
- 기본 타입은 값을 복사하여 사용하기 때문에 안전합니다.
- 객체 타입은 참조를 공유하기 때문에 부작용이 발생할 수 있습니다.
// 기본 타입
int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4; // a는 여전히 10
// 객체 타입
Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("New"); // a도 "New"로 변경됨
불변 객체 (Immutable Object)
- 객체 타입의 한계를 극복하기 위해 불변 객체로 설계할 수 있습니다.
- 불변 객체는 생성 시점 이후 값을 변경할 수 없는 객체입니다.
- 불변 객체로 설계하면 부작용을 원천 차단할 수 있습니다.
불변 객체로 설계하는 방법
- 생성자에서만 값을 설정하고, 수정자(Setter)를 제공하지 않습니다.
- 예제: Address 클래스를 불변 객체로 설계
@Embeddable
public final class Address {
private final String city;
private final String street;
private final String zipcode;
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
public String getStreet() {
return street;
}
public String getZipcode() {
return zipcode;
}
}
- 불변 객체는 생성자에서 모든 값을 설정하고 이후에는 값을 변경할 수 없도록 합니다.
- 자바의 대표적인 불변 객체로는 Integer, String 등이 있습니다.
값 타입의 비교
- 동일성(Identity) 비교: 두 객체의 참조 값을 비교합니다. 이는 객체가 동일한 인스턴스를 참조하는지를 확인합니다. == 연산자를 사용합니다.
- 동등성(Equivalence) 비교: 두 객체의 실제 값을 비교합니다. 이는 객체가 동일한 데이터를 포함하는지를 확인합니다. equals() 메소드를 사용합니다.
- 값 타입의 객체는 equals() 메소드를 적절하게 재정의해야 합니다. 일반적으로 모든 필드를 사용하여 재정의합니다.
값 타입 컬렉션
- 값 타입 컬렉션은 값 타입을 하나 이상 저장할 때 사용합니다.
- @ElementCollection과 @CollectionTable 어노테이션을 사용합니다.
- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없으므로, 별도의 테이블을 만들어 저장합니다.
- 값 타입 컬렉션은 지연 로딩(Lazy Loading) 전략을 사용합니다.
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// getters, setters, equals(), hashCode() 정의
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@ElementCollection
@CollectionTable(name = "member_address", joinColumns = @JoinColumn(name = "member_id"))
private List<Address> addresses = new ArrayList<>();
// getters, setters
}
값 타입 컬렉션의 제약사항
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장합니다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 합니다. 이는 null 입력 및 중복 저장을 방지합니다.
- 값 타입은 엔티티와 다르게 식별자 개념이 없고, 값이 변경되면 추적이 어렵습니다.
값 타입 컬렉션 대안
- 실무에서는 값 타입 컬렉션 대신 일대다 관계를 고려할 수 있습니다.
- 일대다 관계를 위한 엔티티를 만들고, 이 엔티티에서 값 타입을 사용합니다.
- 영속성 전이(Cascade)와 고아 객체 제거(orphanRemoval)를 사용하여 값 타입 컬렉션처럼 사용합니다.
값 타입 컬렉션 대신 일대다 관계 사용
@Entity
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private String city;
private String street;
private String zipcode;
// getters, setters
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "member_id") // 연관관계의 주인 설정
private List<AddressEntity> addresses = new ArrayList<>();
// getters, setters
}
엔티티와 값 타입 정리
엔티티 타입의 특징
- 식별자 (Identifier): 고유 식별자를 가집니다.
- 생명 주기 관리: 엔티티는 영속성 컨텍스트에 의해 생명 주기를 관리받습니다.
- 공유 가능: 엔티티는 여러 곳에서 참조되고 공유될 수 있습니다.
값 타입의 특징
- 식별자 없음 (No Identifier): 값 타입은 고유 식별자를 가지지 않습니다.
- 엔티티에 의존적인 생명 주기: 값 타입은 이를 소유한 엔티티의 생명 주기에 의존합니다.
- 공유하지 않는 것이 안전: 값 타입은 복사하여 사용하고, 공유하지 않는 것이 안전합니다.
- 불변 객체로 설계: 값 타입은 생성 이후에 변경되지 않는 불변 객체로 설계하는 것이 안전합니다.
값 타입 사용 시 주의사항
- 값 타입은 정말 값 타입이라 판단될 때만 사용해야 합니다.
- 엔티티와 값 타입을 혼동하여 엔티티를 값 타입으로 만들면 안 됩니다.
- 식별자가 필요하고, 지속적으로 값을 추적하거나 변경해야 한다면 이는 값 타입이 아니라 엔티티로 설계해야 합니다.
잘못된 예 : 엔티티를 값 타입으로 사용
@Entity
public class Address {
@Id @GeneratedValue
private Long id;
private String city;
private String street;
private String zipcode;
// getters, setters, equals, hashCode
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Address address; // 값 타입처럼 사용하려고 하는 예
// getters, setters
}
올바른 예: 값 타입으로 정의
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
// getters, setters, equals, hashCode
}
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Address address; // 올바른 값 타입 사용
// getters, setters
}
결론
- 엔티티는 고유 식별자를 가지며, 생명 주기를 관리받고, 여러 곳에서 공유될 수 있습니다.
- 값 타입은 식별자를 가지지 않으며, 소유한 엔티티의 생명 주기에 의존하고, 공유되지 않도록 복사하여 사용하며, 불변 객체로 설계하는 것이 좋습니다.
- 식별자 필요, 지속적 값 추적 및 변경이 필요한 경우에는 값 타입이 아니라 엔티티로 설계해야 합니다.
'JPA' 카테고리의 다른 글
[JPA] JPA 활용1 - 도메인 분석 설계 (0) | 2024.06.30 |
---|---|
[JPA] JPA 활용1 - 프로젝트 환경설정 (0) | 2024.06.30 |
[JPA] JPA 프록시와 연관관계 관리 (0) | 2024.06.30 |
[JPA] JPA 고급 매핑 (0) | 2024.06.29 |
[JPA] JPA 연관관계 매핑 (2) (0) | 2024.06.29 |