반응형
목차
- 기본 값 타입
- 임베디드 타입 (복합 값 타입)
- 값 타입 불변 객체
- 값 타입 비교
- 값 타입 컬렉션
- 예제
기본 값 타입
JPA 데이터 타입
- 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 값 타입
- int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가
값 타입 분류
- 기본 값 타입
- 자바 기본 타입 (int, double)
- 래퍼 클래스 (Integer, Long)
- String
- 임베디드 타입 (복합 값 타입)
- 컬렉션 값 타입
기본 값 타입
- 생명주기가 엔티티에 의존한다.
- 회원을 삭제하면 나이, 필드도 함께 삭제와 같다.
- 기본 값 타입은 복사하지, 공유되지 않는다
- 회원 이름 변경 시 다른 회원 이름도 함께 변경되면 안된다.
- 자바에서 String 이나 Integer 같은 래퍼클래스도 공유 가능한 객체이지만 변경할 수 없다.
임베디드 타입
- 새로운 값 타입을 직접 정의할 수 있다.
- 임베디드 타입도 기본 값처럼 변경할 수 없도록 사용한다.
- 새로운 타입을 만들어서 비슷한 요소들을 묶어 가독성 좋게 만들 수 있다.
임베디드 타입 사용법
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수
임베디드 타입의 장점
- 재사용 가능하고, 응집도가 높다.
- 값 타입만 사용하는 의미 있는 메서드를 만들 수 있고, 임베디드를 포함한 모든 값 타입은 소유한 엔티티에 생명주기를 의존한다.
임베디드 타입과 테이블 매핑
- 테이블 매핑은 다음과 같이 임베디드 안에 있는 값 타입을 그대로 넣는다.
임베디드 사용 전 멤버 엔티티
package hellojpa;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
// 기간 Period
private LocalDateTime startDate;
private LocalDateTime endDate;
// 주소 Address
private String city;
private String street;
private String zipcode;
}
임베디드 사용 후 엔티티
멤버 엔티티
package hellojpa;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
// 기간 Period
@Embedded
private Period workPeriod;
// 주소 Address
@Embedded
private Address homeAddress;
}
- 임베디드를 적용할 클래스를 만들어서
@Embedded
를 적용 시킨다.
주소 엔티티
package hellojpa;
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;
}
}
@Embeddable
적용과 기본 생성자는 반드시 지켜야한다.
JpaMain
package hellojpa;
import javax.persistence.*;
import java.util.List;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("hello");
member.setHomeAddress(new Address("city", "street", "10000"));
member.setWorkPeriod(new Period());
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 다음과 같이 실행해보면 정상적으로 임베디드 타입이 만들어진 것을 볼 수 있다.
임베디드 타입 연관관계
- 엔티티 타입 -> 임베디드 타입 -> 임베디드 타입의 연관 관계 가능
- 엔티티 타입 -> 임베디드 타입 -> 엔티티 타입의 연관 관계 가능
@AttributeOverride
@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;
- 한 엔티티에서 같은 값 타입을 사용하려면
@AttributeOverrides
를 사용해야 한다.
값 타입과 불변 객체
값 타입은 객체 세상을 단순화하려고 만든 개념이다. 따라서 단순하고 안전하게 다룰 수 있어야 한다.
- 임베디트 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험할 수 있다.
package hellojpa;
import javax.persistence.*;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 다음과 같은 예시를 보자. 같은 주소를 가진 회원 중 한 회원이 주소를 바꾸면 값이 모두 바뀔 수 있다. 즉. 부작용(side effect)이 발생한다. 같이 공유하고 싶은 경우 Address 를 임베디드 타입이 아닌, 엔티티로 설계해야한다.
package hellojpa;
import javax.persistence.*;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);
Address copyAddress = new Address(address.getCity(), address.getCity(), address.getZipcode());
Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 다음과 같이 새로운 인스턴스를 생성해서 넣어줘야 독립적으로 관리를 할 수 있다.
직접 정의한 값 타입은 컴파일 상에서 오류를 잡을 수 없어, 항상 값을 복사해서 사용해야한다는 한계가 있다.
값 타입을 애초에 불변 객체(Setter 생성X)로 설계하여 수정할 수 없게 만들어 부작용을 사전에 차단하자.
값을 변경하고 싶은 경우 새로운 인스턴스를 생성해서 사용하자.
값 타입 비교
int a = 10;
int b = 10;
System.out.println("(a==b) = " + (a==b));
Address address1 = new Address("city", "street", "10000");
Address address2 = new Address("city", "street", "10000");
System.out.println("address = " + (address1==address2));
System.out.println("address = " + (address1.equals(address2)));
- 프리미티브형은 값이 같지만, 논프리미티브타입은 참조값을 이용하기 때문에 다르다. 마지막
.equals()
는 어떨까? 당연히 false가 된다. 왜냐하면Address
객체에서 별도로equals()
를 재정의 하지 않았기 때문이다. 재정의하여 사용하면 true를 반환한다.
동일성(Identity) 비교 : 인스턴스의 참조 값을 비교,
==
사용동등성(equivalence) 비교 : 인스턴스의 값을 비교,
equals()
사용
값 타입 컬렉션
- 데이터베이스는 테이블안에 컬렉션을 저장할 수 없다. 즉, 컬렉션을 저장하기 위한 별도의 테이블이 필요하다. 즉, 다음과 같이
Member
를 기준으로 새로운 컬렉션 테이블을 만들어야 한다. - 값 타임 컬렉션이라는 것을 알려주는
@ElementCollection
와 테이블 명을 지정하는@CollectionTable
사용한다.
값 타입 컬렉션 예제
Member 엔티티
package hellojpa;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
@Column(name = "FOOD_NAME") // 값이 하나이므로 컬럼명 변경가능
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressHistory = new ArrayList<>();
}
JoinColumn
은 외래키로 값을 적용 시켜준다.favoriteFoods
는 필드가 한개이다. 필드가 한 개인 경우에만 한해서 컬럼명을 변경(디폴트는 컬렉션명)할 수 있다. 즉, Address 타입은 필드가 여러개이므로 변경할 수 없다.
JpaMain
package hellojpa;
import javax.persistence.*;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "20000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- Member 만 persist해도 연결된 값타입 컬렉션도 변경이 된다.
- 다음과 같이 결과가 정상적으로 DB에 저장된 것을 확인할 수 있다.
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "20000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===== START =====");
Member findMember = em.find(Member.class, member.getId());
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
System.out.println("address = " + address.getCity());
}
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
System.out.println("favoriteFood = " + favoriteFood);
}
tx.commit();
- 컬렉션들은 기본적으로 지연로딩 전략을 사용한다.
컬렉션 값 변경
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
// 치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
- 객체 타입의 컬렉션은 새로운 인스턴스를, String 타입의 컬렉션은 삭제 후 삽입을 통해 값을 수정한다. 마치 cascade + 고아객체 제거를 설정한 것과 비슷하다.
// 삭제를 위해선 동등성(equals 재정의)필요
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
/* 출력 로그
Hibernate:
/* delete collection hellojpa.Member.addressHistory */ delete
from
ADDRESS
where
MEMBER_ID=?
Hibernate:
/* insert collection
row hellojpa.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
Hibernate:
/* insert collection
row hellojpa.Member.addressHistory */ insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
*/
- Address와 관련되 데이터를 모두 삭제 후 남아 있는 데이터의 개수 만큼 INSERT 쿼리문을 날린다. 즉, 현재 Address에 남아 있는 데이터는 2개이므로 INSERT 쿼리문을 2번 실행을 한 것이다.
값 타입은 엔티티와 다르게 식별자 개념이 없어, 변경하면 추적이 어렵다.
값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. 즉, null 과 중복이 있으면 안된다.
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
- 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용한다. cascade + 고아 객체 제거를 사용해서 값 타입을 컬렉션처럼 사용한다.
일대다 관계 예시
Address 엔티티
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity(String city, String street, String zipcod) {
this.address = new Address(city, street, zipcod);
}
public AddressEntity() {
}
}
Member
package hellojpa;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
@Column(name = "FOOD_NAME") // 값이 하나이므로 컬럼명 변경가능
private Set<String> favoriteFoods = new HashSet<>();
// @ElementCollection
// @CollectionTable(name = "ADDRESS", joinColumns =
// @JoinColumn(name = "MEMBER_ID")
// )
// private List<Address> addressHistory = new ArrayList<>();
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
- addressHistory를 일대다관계로 설계하고, cascade와 고아 객체 제거를 적용한다.
JpaMain
package hellojpa;
import javax.persistence.*;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "20000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===== START =====");
Member findMember = em.find(Member.class, member.getId());
tx.commit();
} catch (Exception e) {
tx.rollback();
e.printStackTrace();
} finally {
em.close();
}
emf.close();
}
}
- 다음과같이 컬렉션 값 타입 대신 일대다로 풀어낼 수 있다.
정말 단순한 경우에만 값타입을 사용하고 이 외에는 일대다로 풀어내자.
값 타입은 정말 값 타입이라 판단될 때만 사용한다. 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.
예제
- 값 타입을 사용할만한 예제를 만들어보자. Member의 Address와 Delivery의 Address를 값타입으로 만들어보자.
Address 값 타입 생성
package jpabook.jpashop.domain;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.util.Objects;
@Embeddable
public class Address {
@Column(length = 10)
private String city;
@Column(length = 20)
private String street;
@Column(length = 5)
private String zipcode;
public String fullAddress() {
return getCity() + " " + getStreet() + " " + getZipcode();
}
public String getCity() {
return city;
}
private void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
private void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(getCity(), address.getCity()) && Objects.equals(getStreet(), address.getStreet()) && Objects.equals(getZipcode(), address.getZipcode());
}
@Override
public int hashCode() {
return Objects.hash(getCity(), getStreet(), getZipcode());
}
}
@Embeddable
을 정의해서 임베디드 값 타입 클래스를 생성하고, 값을 수정할 때 동등성 비교를 위한equals
와hashCode
를 재정의한다. 참고로equals
를 만들 때 필드값이 아닌getter
로 비교하는 경우 추후 프록시 상태의 객체에도 접근할 수 있다.Delivery
와Member
에@Embedded private Address address
를 적용해준다.
본 포스팅은 인프런-김영한님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 참고하여 정리했습니다.
반응형