JPA의 데이터 타입은 크게 엔티티 타입과 값 타입이 있다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
(영한님은 여기서 엔티티 타입은 살아있는 생물이고, 값 타입은 단순한 수치 정보라고 표현했다.)
JPA는 값 타입을 표현하는 방법을 몇가지 더 제공한다.
아래는 JPA에서 제공하는 값 타입의 목록이다.
기본값 타입(primitive, wrapper, String)
임베디드 타입
값 타입 컬렉션
값 타입은 기본적인 특징은 아래와 같다.
식별자가 없다
생명주기가 엔티티에 의존한다
공유하면 안된다
값 타입
자바의 primitive 타입, Wrapper 클래스, String 클래스를 말한다.
@Entity
class Member{
@Id
private Long id;
private String name;
private int age;
}
임베디드 타입(복합 값 타입)
괄호안에 써놨듯이, 여러개의 값 타입을 묶어서 하나의 값 타입으로 정의하는 방법이다.
우선 값 타입을 적용하기 전의 코드는 아래와 같다.
@Entity
class Member{
@Id
private Long id;
// 근무기간
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
// 집주소
private String city;
private String street;
private String zipCode;
}
위 처럼 엔티티가 모든 속성을 flat 하게 가지는 것은 객체지향적이지 않다. 근무기간, 집주소로 묶을 수 있다면 더 좋을 것이다.
그런데 JPA가 그것을 지원해준다! ㅋㅋ
@Entity
class Member{
@Id
private Long id;
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
}
@Embeddable
class Period{
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;
public boolean isWork(Date date){
// 값 타입을 위한 메서드 또한 작성 가능
}
}
@Embeddable
class Address{
@Column(name = "city") // 매핑할 컬럼 지정 가능
private String city;
private String street;
private String zipcode;
}
작성한 값 타입은 다른 곳에서 재사용 될수도 있고, 값 타입만을 위한 메서드도 작성 가능하다.
엔티티가 더욱 의미있고 응집력있게 변했다(!!)
이러한 임베디드 타입을 정의하려면 아래의 2가지 어노테이션이 필요하다.
@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
임베디드 타입과 테이블 매핑
이렇게 작성한 임베디드 타입은 테이블에 아래와 같이 매핑된다.
![임베디드 타입과 테이블 매핑]()
임베디드 타입은 엔티티의 값일 뿐이다. 그러므로 보다시피 임베디드 타입을 사용하기 전과 후에 매핑되는 테이블은 같다.
ORM을 사용하지 않았더라면 객체와 테이블은 대부분 1:1로 매핑되었을 것을,
ORM을 사용함으로써 객체와 테이블을 더 세밀하게 매핑할 수 있다.
(잘 설계한 ORM 어플리케이션은 매핑힌 클래스의 수가 테이블의 수보다 더 많다)
임베디드 타입의 포함과 연관관계
임베디드 타입은 다른 임베디드 타입을 포함할 수 있고, 다른 엔티티를 참조할 수도 있다.
@Entity
class Member{
// ...
@Embedded
private Address address;
@Embedded
private PhoneNumber phoneNumber;
}
class Address{
private String city;
private String street;
@Embedded // 포함 가능
private Zipcode zipcode;
}
@Embeddable
class Zipcode{
String zip;
Strign code;
}
class PhoneNumber{
String areaCode;
String localNumber;
@ManyToOne // 연관관계 가능
PhoneServiceProvider phoneServiceProvider;
}
@Entity
class PhoneServiceProvider{
@Id
private String name;
}
name에는 Address 내의 필드명을 써주고, column에는 @Column 어노테이션을 써서 재정의 해주면 된다.
어노테이션이 너무 많이 사용되서 지저분하긴 하지만, 다행히(?) 이렇게 한 엔티티에 중복해서 임베디드를 사용할 일이 많이 없다.
@AttributeOveride는 엔티티에 설정해야 한다. 임베디드 타입이 임베디들 타입을 가지고 있어도 엔티티에 설정해야 한다.
임베디드 타입과 null
임베디드 타입이 null 이면 매핑한 컬럼 값을 모두 null 이 된다(!!)
// city, street, zipcode가 모두 null이 됨
member.setAddress(null);
임베디드 타입의 딜레마
임베디드 타입을 만들어서 값 타입으로 사용한것 까진 괜찮았다. 하지만 여기서 근본적인 문제점이 있다.
임베디드 타입이 값 타입이긴하지만, 그건 jpa의 입장이고, java에서는 그냥 일반적인 객체라는 것이다.
즉, 자바의 기본 값 타입처럼 바로 사용할 수 없다. 그러므로 그런 딜레마를 해결해줘야 한다.
발생하는 딜레마는 아래와 같다.
불변성
언급했듯이, 임베디드 타입은 그냥 일반 객체이다. 그러므로 아래와 같은 상황을 막을 수 없다.
Address address1 = new Address("city", "street", "zipcode1");
Member member1 = Member.builder()
.name("joont1")
.homeAddress(address1)
.build();
Address address2 = address1;
address2.setZipcode("zipcode2");
Member member2 = Member.builder()
.name("joont2")
.homeAddress(address2)
.build();
em.persist(member1);
em.persist(member2);
값 타입이라면 값이 공유되지 않을테니,
member1에 zipcode1, member2에 zipcode2가 저장되어야 한다.
알다시피 자바의 기본 타입은 기본적으로 값을 복사한다
int a = 10;
int b = a;
a = 20;
assertThat(a, is(20));
assertThat(b, is(10));
하지만 Address는 자바 기본 값 타입이 아니므로 값을 복사하지 않는다.
그러므로 기대한대로 동작하게 하려면 아래와 같이 사용해야 하는데,
Address address1 = new Address("city", "street", "zipCode1");
Member member1 = Member.builder()
.name("joont1")
.homeAddress(address1)
.build();
Address address2 = address1.clone(); // 복사
address2.setZipcode("zipcode2");
Member member2 = Member.builder()
.name("joont2")
.homeAddress(address2)
.build();
em.persist(member1);
em.persist(member2);
중요한건 항상 이렇게 사용되도록 강제할 수도 없고, 이를 무시하고 직접 참조를 전달하는 행위를 막을 방법도 없다는 거다.
그러므로 이럴떄는 그냥 객체 자체를 불변객체로 만들어서 부작용이 발생하지 않도록 만들어버리는게 낫다.
자바 값 타입은 불변객체로 만드는 것이 좋다.
자바에서 객체를 불변객체로 만드는 방법은 여러가지가 있지만, 가장 간단한 방법은 setter를 만들지 않는것이다.
@AllArgsConstructor // 전체 프로퍼티를 받는 생성자
@Embeddable
class Address{
private String city;
private String street;
private String zipcode;
}
여전히 address 참조를 전달할 수 있지만, 전달해도 값을 수정할 수 없으니 부작용이 발생하지 않는다.
비교
값 타입이라면 동일성 비교(==)나 동등성 비교(equals)가 동작해야 한다.
하지만 현재 Address는 그것이 보장되지 않으므로, equals 메서드를 재정의 해줘야한다.
@AllArgsConstructor
@EqualsAndHashCode // 전체 필드에 대해 equals와 hashCode 재정의
@Embeddable
class Address{
private String city;
private String street;
private String zipcode;
}
임베디드 타입의 equals 메서드를 재정의 할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
그리고 equals 메서드를 재정의하면 hashCode까지 같이 재정의 해주는 것이 좋다.
그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)에서 문제가 발생할 수 있기 때문이다.
값 타입 컬렉션
여러개의 값 타입을 저장할 떄 사용한다. 그러려면 컬렉션에 저장해야 하는데, RDB에서는 컬럼에 컬렉션을 저장할 수 없다.
그러므로 값 만을 저장하는 테이블을 따로 만들어서 사용해야 한다.
![값 타입 컬렉션 ERD]()
위 ERD를 엔티티에서 매핑하면 아래와 같다.
@Entity
class Member{
// ...
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "member_id")
)
@Column(name = "food_name")
private List<String> favoriteFoodList = new ArrayList<>();
@ElementCollection
@CollcetionTable(
name = "ADDRESS_HISOTRY",
joinColumns = @JoinColumn(name = "member_id")
)
private List<Address> addressHistory = new ArrayList<>(); // Address는 위와 동일
}
@ElementCollection으로 값 타입 컬렉션 인것을 알려주고, @CollectionTable로 해당 값들을 저장한 테이블을 알려주면 된다(외래키랑 같이). favorite_food처럼 값으로 사용되는 컬럼이 하나일 경우 @Column을 사용해서 컬럼명을 지정할 수 있다.
값 타입 컬렉션 사용
저장
Member member = new Member();
member.setHomeAddress(new Address("city", "street", "zipCode4"));
member.getFavoriteFoodList().add("pork");
member.getFavoriteFoodList().add("beef");
member.getAddressHistory().add(new Address("city1", "street1", "zipcode1"));
member.getAddressHistory().add(new Address("city2", "street2", "zipcode2"));
member.getAddressHistory().add(new Address("city3", "street3", "zipcode3"));
em.persist(member);
member : insert 1번
homeAddress : 임베디드 값 타입이므로 member에 포함됨
favoriteFoodList : insert 2번
addressHistory : insert 3번
조회
값 타입 컬렉션도 조히할 때 패치 전략을 사용할 수 있다. Default는 LAZY이다.
@ElementCollection(fetch = FetchType.LAZY)
조회 방식은 일반적인 @OneToMany 조회 할때와 동일하다.
직접 사용할 때 조회된다.
값 타입은 식별자가 없는 단순한 값들의 모음으로 테이블에 저장된다.
그래서 여기에 저장된 값이 변경되면 데이터베이스에 저장된 원본 데이터의 값을 찾기 어렵게 된다.
이러한 문제로 인해 JPA는 값 타입 컬렉션에 변경사항이 발생하면,
값 타입 컬렉션에 매핑된 테이블의 모든 데이터를 삭제하고, 현재 값 타입 컬렉션에 있는 모든 값을 다시 데이터베이스에 저장한다.
즉, 위와 같은 코드에서는 아래와 같이 쿼리가 발생한다.
/** address_history **/
delete
from
ADDRESS_HISTORY
where
member_id=1
insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city1", "street1", "zipcode1")
insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city2", "changed street", "zipcode2") -- insert modifired data
insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city3", "street3", "zipcode3")
/** favorite_food **/
delete
from
FAVORITE_FOOD
where
member_id=1
insert
into
FAVORITE_FOOD
(member_id, food_name)
values
(1, "changed pork"); -- insert modifired data
insert
into
FAVORITE_FOOD
(member_id, food_name)
values
(1, "changed beef"); -- insert modifired data
이러한 비효율적인 특징이 있으므로 만약 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해보는 것이 좋다.
게다가 값 타입 컬렉션은 모든 컬럼을 묶어서 기본키를 구성하므로, 컬럼에 null을 입력할 수 없고, 중복된 값을 입력할 수 없는 제약조건도 있다.
JPA의 데이터 타입은 크게
엔티티 타입
과값 타입
이 있다.엔티티 타입은
@Entity
로 정의하는 객체이고, 값 타입은int, Integer, String
처럼 단순 값으로 사용하는 자바 기본 타입이나 객체를 말한다.(영한님은 여기서 엔티티 타입은 살아있는 생물이고, 값 타입은 단순한 수치 정보라고 표현했다.)
JPA는 값 타입을 표현하는 방법을 몇가지 더 제공한다.
아래는 JPA에서 제공하는 값 타입의 목록이다.
값 타입은 기본적인 특징은 아래와 같다.
값 타입
자바의 primitive 타입, Wrapper 클래스, String 클래스를 말한다.
임베디드 타입(복합 값 타입)
괄호안에 써놨듯이, 여러개의 값 타입을 묶어서 하나의 값 타입으로 정의하는 방법이다.
우선 값 타입을 적용하기 전의 코드는 아래와 같다.
위 처럼 엔티티가 모든 속성을 flat 하게 가지는 것은 객체지향적이지 않다.
근무기간
,집주소
로 묶을 수 있다면 더 좋을 것이다.그런데 JPA가 그것을 지원해준다! ㅋㅋ
작성한 값 타입은 다른 곳에서 재사용 될수도 있고, 값 타입만을 위한 메서드도 작성 가능하다.
엔티티가 더욱 의미있고 응집력있게 변했다(!!)
이러한 임베디드 타입을 정의하려면 아래의 2가지 어노테이션이 필요하다.
임베디드 타입과 테이블 매핑
이렇게 작성한 임베디드 타입은 테이블에 아래와 같이 매핑된다.
![임베디드 타입과 테이블 매핑]()
임베디드 타입은 엔티티의 값일 뿐이다. 그러므로 보다시피 임베디드 타입을 사용하기 전과 후에 매핑되는 테이블은 같다.
ORM을 사용하지 않았더라면 객체와 테이블은 대부분 1:1로 매핑되었을 것을,
ORM을 사용함으로써 객체와 테이블을 더 세밀하게 매핑할 수 있다.
(잘 설계한 ORM 어플리케이션은 매핑힌 클래스의 수가 테이블의 수보다 더 많다)
임베디드 타입의 포함과 연관관계
임베디드 타입은 다른 임베디드 타입을
포함
할 수 있고, 다른 엔티티를참조
할 수도 있다.속성 재정의: @AttributeOverride
아래와 같이 정의하고 싶을 수 있다.
ORM 에서만 객체로 묶을 뿐, 테이블 레벨에선 flat하게 펴지므로 위와 같이 정의하는 것은 불가능하다.
컬럼명이 중복되기 때문이다.
이럴땐
@AttributeOverride
를 통해 컬럼명을 재정의해야 한다.name에는 Address 내의 필드명을 써주고, column에는 @Column 어노테이션을 써서 재정의 해주면 된다.
어노테이션이 너무 많이 사용되서 지저분하긴 하지만, 다행히(?) 이렇게 한 엔티티에 중복해서 임베디드를 사용할 일이 많이 없다.
임베디드 타입과 null
임베디드 타입이 null 이면 매핑한 컬럼 값을 모두 null 이 된다(!!)
임베디드 타입의 딜레마
임베디드 타입을 만들어서 값 타입으로 사용한것 까진 괜찮았다. 하지만 여기서 근본적인 문제점이 있다.
임베디드 타입이 값 타입이긴하지만, 그건 jpa의 입장이고, java에서는 그냥 일반적인
객체
라는 것이다.즉, 자바의 기본 값 타입처럼 바로 사용할 수 없다. 그러므로 그런 딜레마를 해결해줘야 한다.
발생하는 딜레마는 아래와 같다.
불변성
언급했듯이, 임베디드 타입은 그냥 일반 객체이다. 그러므로 아래와 같은 상황을 막을 수 없다.
값 타입이라면 값이 공유되지 않을테니,
member1에 zipcode1, member2에 zipcode2가 저장되어야 한다.
하지만 Address는 자바 기본 값 타입이 아니므로 값을 복사하지 않는다.
그러므로 기대한대로 동작하게 하려면 아래와 같이 사용해야 하는데,
중요한건 항상 이렇게 사용되도록 강제할 수도 없고, 이를 무시하고 직접 참조를 전달하는 행위를 막을 방법도 없다는 거다.
그러므로 이럴떄는 그냥 객체 자체를 불변객체로 만들어서 부작용이 발생하지 않도록 만들어버리는게 낫다.
자바에서 객체를 불변객체로 만드는 방법은 여러가지가 있지만, 가장 간단한 방법은 setter를 만들지 않는것이다.
여전히 address 참조를 전달할 수 있지만, 전달해도 값을 수정할 수 없으니 부작용이 발생하지 않는다.
비교
값 타입이라면 동일성 비교(==)나 동등성 비교(equals)가 동작해야 한다.
하지만 현재 Address는 그것이 보장되지 않으므로, equals 메서드를 재정의 해줘야한다.
임베디드 타입의 equals 메서드를 재정의 할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
그리고 equals 메서드를 재정의하면 hashCode까지 같이 재정의 해주는 것이 좋다.
그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)에서 문제가 발생할 수 있기 때문이다.
값 타입 컬렉션
여러개의 값 타입을 저장할 떄 사용한다. 그러려면 컬렉션에 저장해야 하는데, RDB에서는 컬럼에 컬렉션을 저장할 수 없다.
그러므로 값 만을 저장하는 테이블을 따로 만들어서 사용해야 한다.
![값 타입 컬렉션 ERD]()
위 ERD를 엔티티에서 매핑하면 아래와 같다.
@ElementCollection
으로 값 타입 컬렉션 인것을 알려주고,@CollectionTable
로 해당 값들을 저장한 테이블을 알려주면 된다(외래키랑 같이).favorite_food
처럼 값으로 사용되는 컬럼이 하나일 경우 @Column을 사용해서 컬럼명을 지정할 수 있다.값 타입 컬렉션 사용
저장
조회
값 타입 컬렉션도 조히할 때 패치 전략을 사용할 수 있다. Default는
LAZY
이다.조회 방식은 일반적인 @OneToMany 조회 할때와 동일하다.
직접 사용할 때 조회된다.
수정
값 타입은 식별자가 없는 단순한 값들의 모음으로 테이블에 저장된다.
그래서 여기에 저장된 값이 변경되면 데이터베이스에 저장된 원본 데이터의 값을 찾기 어렵게 된다.
이러한 문제로 인해 JPA는 값 타입 컬렉션에 변경사항이 발생하면,
값 타입 컬렉션에 매핑된 테이블의 모든 데이터를 삭제하고, 현재 값 타입 컬렉션에 있는 모든 값을 다시 데이터베이스에 저장한다.
즉, 위와 같은 코드에서는 아래와 같이 쿼리가 발생한다.
이러한 비효율적인 특징이 있으므로 만약 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해보는 것이 좋다.
게다가 값 타입 컬렉션은 모든 컬럼을 묶어서 기본키를 구성하므로, 컬럼에 null을 입력할 수 없고, 중복된 값을 입력할 수 없는 제약조건도 있다.