해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.
JPA의 데이터 타입은 크게 엔티티 타입과 값 타입으로 나눌 수 있다. 엔티티 타입은 @Entity
로 매핑되는 테이블 객체이고, 값 타입은 단순히 값으로 사용되는 자바 기본 타입이나 객체를 말한다.
엔티티 타입은 식별자가 있어 추적이 가능하지만, 값 타입은 속성만 존재하므로 추적이 불가능하다. 값 타입은 다음과 같이 분류할 수 있다.
@Entity
public class Example {
@Id @GeneratedValue
private Long id;
// 기본 값 타입
private String name;
private int number;
}
위 예제의 String, int가 값 타입이며, Example 엔티티는 id라는 식별자 값과 생명주기가 있지만, name, number와 같은 값 타입은 식별자가 없으며, 생명주기 또한 엔티티에 의존한다. 또한 값 타입은 java의 기본 타입(primitive type)과 같이 공유되면 안된다.
JPA에서는 새로운 값 타입을 직접 정의해서 사용할 수 있으며, 이를 임베디드 타입(embedded type)이라고 한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// 주소
private String city;
private String street;
private String zipcode;
}
위의 Member 엔티티가 주소 값을 가지고 있는 것 처럼 엔티티 객체가 상세 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며, 응집력만 떨어뜨린다. 이를 아래와 같이 임베디드 타입으로 분리시킬 수 있다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded
private Address address;
}
@Embeddable
public class Address {
@Column(name = "city")
private String city;
private String street;
private String zipcode;
}
새로 정의한 값 타입은 재사용이 가능하며, 응집도도 아주 높다. 또한 값 타입 객체 내에 해당 값 타입만 사용하는 의미 있는 메서드들도 만들 수 있다. 임베디드 타입을 사용하기 위한 어노테이션을 다음과 같으며, 둘 중 하나는 생략이 가능하다.
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시임베디드 타입은 기본 생성자가 필수이다.
임베디드 타입은 엔티티의 값일 뿐이기에 값이 속한 엔티티의 테이블에 매핑되며, 사용하기 전과 후의 테이블의 형태는 같다. 임베디드 타입을 통해 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능해지기에 보다 객체지향 모델을 설계하기에 편리해진다.
임베디드 타입이 null일 경우 매핑한 컬럼 값은 모두 null이 된다.
임베디드 타입은 아래 예제와 같이 값 타입을 포함하거나 엔티티를 참조할 수 있다.
@Entity
public class Member {
...
@Embedded
private Address address;
}
@Embeddable
public class Address {
private String city;
private String street;
@Embedded
private Zipcode zipcode;
}
@Embeddable
public class Zipcode {
private Zip zip;
private String plusFour;
}
@Entity
public class Zip {
@Id
private String code;
}
@AttributeOverride
: 속성 재정의임베디드 타입에 정의한 매핑 정보를 @AttributeOverride
로 재정의할 수 있다.
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
AttributeOverride(name = "city", columne = @Column(name = "COMPANY_CITY"),
AttributeOverride(name = "street", columne = @Column(name = "COMPANY_STREET"),
AttributeOverride(name = "zipcode", columne = @Column(name = "COMPANY_ZIPCODE"))
})
private Address companyAddress;
}
값 타입은 객체를 조금이라도 단순화하기 위해 만든 개념이다. 그렇기에 언제나 단순하고 안전하게 다룰 수 있어야 한다.
임베디드 타입 같은 값 타입을 여러 엔티티에서 그대로 참조해서 사용할 경우 값 타입의 값 변경시 해당 인스턴스를 참조하는 모든 엔티티의 값이 변경되는 사이드 이펙트가 발생할 수 있다. 그렇기에 하나의 값 타입을 여러 엔티티에서 사용할 경우 참조가 아닌 복사를 통해 사용해야 한다.
자바는 기본 타입에 값을 대입하면 항상 값을 복사해서 전달한다. 그렇기에 원본과 복사본은 완전히 독립된 값을 가지며, 부작용도 없다.
그러나 객체 타입은 항상 참조값을 전달하며, 원본과 복사본이 같은 인스턴스를 공유한다. 그렇기에 원복 값 변경시 복사본의 값 또한 변경된다. 이를 막기 위해 객체를 대입할 때마다 항상 인스턴스를 복사해서 대입하면 되지만, 자바에서 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 근본적인 문제가 있다. 이러한 근본적인 문제를 해결하기 위해서는 setter와 같은 수정자 메서드를 모두 제거해야 한다.
값 타입은 부작용 걱정 없이 사용해야 한다. 그렇기에 될 수 있으면 불변 객체(immutable Object)로 설계해야 한다. 불변 객체도 결국 객체기 때문에 인스턴스의 참조 값 공유를 피할 수 없지만 해당 값을 수정할 수 없기 때문에 부작용이 발생하지는 않는다.
자바가 제공하는 객체 비교는 인스턴스의 참조 값을 비교하는 동일성(Identity) 비교(==)와 인스턴스의 값을 비교하는 동등성(Equivalence) 비교(equals()) 두 가지가 있다.
값 타입은 서로 다른 인스턴스일지라도 그 안에 값이 같으면 같은 것으로 봐야 한다. 따라서 값 타입의 equals()
메서드를 재정의하고, 동등성을 비교해야 한다. 또한 자바에서 equals()
메서드를 재정의하면 hashCode()
도 재정의해야 해시를 사용하는 컬렉션(HashSet, HashMap)들이 정상 동작한다.
값 타입을 하나 이상 저장하기 위해서는 컬렉션에 보관하고 @ElementCollection
, @CollectionTable
어노테이션을 사용한다.
@Entity
public class Member {
...
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<Address>();
}
관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없기 때문에 위와 같은 값 타입 컬렉션을 가진 객체를 테이블로 매핑하려면 별도의 테이블을 추가하고 @CollectionTable
을 사용해야 한다.
값으로 사용되는 컬럼이 하나일 경우 @Column
을 사용해서 컬럼명을 지정할 수 있으며, @AttributeOverride
를 사용해서 매핑정보를 재정의할 수도 있다.
값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가지며, 조회시 페치 전략을 선택할 수 있다. 기본 전략은 LAZY이다.
값 타입은 불변해야 하기 때문에 엔티티의 값 타입 수정은 다음과 같다.
1. 임베디드 값 타입 : 엔티티에 새로운 임베디드 인스턴스 생성 후 엔티티만 UPDATE한다.
2. 기본값 타입 컬렉션 : 수정하고자 하는 기본값 타입을 제거하고 새 값을 추가한다.
3. 임베디드 값 타입 컬렉션 : 수정하고자 하는 임베디드 값 타입을 제거하고 새 값을 추가한다. eqauls()
와 hashCode()
를 꼭 구현해야 한다.
값 타입은 식별자라는 개념이 없는 단순한 값들의 모음이기 때문에 값을 변경해버리면 데이터베이스에서 저장된 원본 데이터를 찾기 어렵다.
특정 엔티티에 소속된 값 타입은 소속된 엔티티를 데이터베이스에서 찾고 변경하면 되지만, 값 타입 컬렉션은 별도의 테이블에 보관되기 때문에 값이 변경되면 원본 데이터를 찾기 어렵다는 문제가 있다. 그렇기에 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면 값 타입 컬렉션이 매핑된 테이블의 모든 연관 데이터를 삭제하고 현재 객체에 있는 값들을 다시 저장하며, 이 때 해당 컬렉션 길이만큼 INSERT된다. 그렇기에 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 일대다 관계를 고려해야 한다.
또한 값 타입 컬렉션을 매핑하는 모든 컬럼을 묶어서 기본 키를 구성해야 하기 때문에 컬럼에 null을 입력할 수 없고, 중복된 값을 저장할 수 없다는 제약이 있다.