제육's 휘발성 코딩
반응형

1. 목차

  • 기본 값 타입
  • 임베디드 타입 (복합 값 타입)
  • 값 타입 불변 객체
  • 값 타입 비교
  • 값 타입 컬렉션
  • 예제

1.1. 기본 값 타입

1.1.1. JPA 데이터 타입

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
    • 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
  • 값 타입
    • int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경 시 추적 불가

1.1.2. 값 타입 분류

  • 기본 값 타입
    • 자바 기본 타입 (int, double)
    • 래퍼 클래스 (Integer, Long)
    • String
  • 임베디드 타입 (복합 값 타입)
  • 컬렉션 값 타입

1.1.2.1. 기본 값 타입

  • 생명주기가 엔티티에 의존한다.
    • 회원을 삭제하면 나이, 필드도 함께 삭제와 같다.
  • 기본 값 타입은 복사하지, 공유되지 않는다
    • 회원 이름 변경 시 다른 회원 이름도 함께 변경되면 안된다.
    • 자바에서 String 이나 Integer 같은 래퍼클래스도 공유 가능한 객체이지만 변경할 수 없다.

1.2. 임베디드 타입

  • 새로운 값 타입을 직접 정의할 수 있다.
  • 임베디드 타입도 기본 값처럼 변경할 수 없도록 사용한다.

image

  • 새로운 타입을 만들어서 비슷한 요소들을 묶어 가독성 좋게 만들 수 있다.

1.2.1. 임베디드 타입 사용법

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수

1.2.2. 임베디드 타입의 장점

  • 재사용 가능하고, 응집도가 높다.
  • 값 타입만 사용하는 의미 있는 메서드를 만들 수 있고, 임베디드를 포함한 모든 값 타입은 소유한 엔티티에 생명주기를 의존한다.

1.2.3. 임베디드 타입과 테이블 매핑

image

  • 테이블 매핑은 다음과 같이 임베디드 안에 있는 값 타입을 그대로 넣는다.

1.2.4. 임베디드 사용 전 멤버 엔티티

<code />
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; }

1.2.5. 임베디드 사용 후 엔티티

1.2.5.1. 멤버 엔티티

<code />
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 를 적용 시킨다.

1.2.5.2. 주소 엔티티

<code />
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 적용과 기본 생성자는 반드시 지켜야한다.

1.2.5.3. JpaMain

<code />
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(); } }
  • 다음과 같이 실행해보면 정상적으로 임베디드 타입이 만들어진 것을 볼 수 있다.

1.2.6. 임베디드 타입 연관관계

image

  • 엔티티 타입 -> 임베디드 타입 -> 임베디드 타입의 연관 관계 가능
  • 엔티티 타입 -> 임베디드 타입 -> 엔티티 타입의 연관 관계 가능

1.2.7. @AttributeOverride

<code />
@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를 사용해야 한다.

1.3. 값 타입과 불변 객체

값 타입은 객체 세상을 단순화하려고 만든 개념이다. 따라서 단순하고 안전하게 다룰 수 있어야 한다.

image

  • 임베디트 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험할 수 있다.
<code />
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 를 임베디드 타입이 아닌, 엔티티로 설계해야한다.
<code />
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)로 설계하여 수정할 수 없게 만들어 부작용을 사전에 차단하자.

값을 변경하고 싶은 경우 새로운 인스턴스를 생성해서 사용하자.

1.4. 값 타입 비교

<code />
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() 사용

1.5. 값 타입 컬렉션

image

  • 데이터베이스는 테이블안에 컬렉션을 저장할 수 없다. 즉, 컬렉션을 저장하기 위한 별도의 테이블이 필요하다. 즉, 다음과 같이 Member를 기준으로 새로운 컬렉션 테이블을 만들어야 한다.
  • 값 타임 컬렉션이라는 것을 알려주는@ElementCollection와 테이블 명을 지정하는 @CollectionTable 사용한다.

1.5.1. 값 타입 컬렉션 예제

1.5.1.1. Member 엔티티

<code />
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 타입은 필드가 여러개이므로 변경할 수 없다.

1.5.1.2. JpaMain

<code />
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해도 연결된 값타입 컬렉션도 변경이 된다.

image

  • 다음과 같이 결과가 정상적으로 DB에 저장된 것을 확인할 수 있다.
<code />
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();
  • 컬렉션들은 기본적으로 지연로딩 전략을 사용한다.

1.5.2. 컬렉션 값 변경

<code />
Address a = findMember.getHomeAddress(); findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode())); // 치킨 -> 한식 findMember.getFavoriteFoods().remove("치킨"); findMember.getFavoriteFoods().add("한식");
  • 객체 타입의 컬렉션은 새로운 인스턴스를, String 타입의 컬렉션은 삭제 후 삽입을 통해 값을 수정한다. 마치 cascade + 고아객체 제거를 설정한 것과 비슷하다.
<code />
// 삭제를 위해선 동등성(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 과 중복이 있으면 안된다.

1.5.3. 값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려한다.
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용한다. cascade + 고아 객체 제거를 사용해서 값 타입을 컬렉션처럼 사용한다.

1.5.3.1. 일대다 관계 예시

Address 엔티티
<code />
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
<code />
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
<code />
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(); } }
  • 다음과같이 컬렉션 값 타입 대신 일대다로 풀어낼 수 있다.

정말 단순한 경우에만 값타입을 사용하고 이 외에는 일대다로 풀어내자.

값 타입은 정말 값 타입이라 판단될 때만 사용한다. 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티다.

1.6. 예제

image

  • 값 타입을 사용할만한 예제를 만들어보자. Member의 Address와 Delivery의 Address를 값타입으로 만들어보자.

1.6.1. Address 값 타입 생성

<code />
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을 정의해서 임베디드 값 타입 클래스를 생성하고, 값을 수정할 때 동등성 비교를 위한 equalshashCode를 재정의한다. 참고로 equals를 만들 때 필드값이 아닌 getter로 비교하는 경우 추후 프록시 상태의 객체에도 접근할 수 있다.
  • DeliveryMember@Embedded private Address address를 적용해준다.

본 포스팅은 인프런-김영한님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 참고하여 정리했습니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^