[JPA] 9장 값 타입

diveintoo·2022년 3월 19일
0

본 글은 김영한님의 <자바 ORM 표준 JPA 프로그래밍>을 읽고 공부한 내용을 정리한 글입니다.

  1. 기본값 타입
  2. 임베디드 타입
  3. 값 타입과 불변 객체
  4. 값 타입의 비교
  5. 값 타입 컬렉션

JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다.

  • 엔티티 타입

    • @Entity로 정의하는 객체
  • 값 타입

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

1. 기본값 타입

@Entity
public class Member { // 엔티티 타입
	@Id @GeneratedValue
    private Long id;
    
    private String name; // 값 타입
    private int age; // 값 타입
  • 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존한다.
  • 값 타입은 공유하면 안 된다.

2. 임베디드 타입

새로운 값 타입을 직접 정의해서 사용한다.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    
    private String name;
    private int age;
    
    @Embedded Period workPeriod; // 임베디드 타입
}
    
@Embeddable
public class Period {
	@Temporal(TeporalType.DATE) java.util.Date startDate;
  	...
}

임베디드 타입 어노테이션

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
    ✔️ 둘 중 하나는 생략해도 된다.

2.1 테이블 매핑


임베디드 타입은 엔티티의 값이기 때문에 속한 엔티티의 테이블에 매핑한다.
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.

2.2 연관관계

임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있다.

  • 엔티티를 참조한다 - 공유될 수 있으므로
  • 값 타입을 포함한다 - 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로
@Entity
public class Member {
	
    @Embedded Address address; 		   // 임베디드 타입 포함
    @Embedded PhoneNumber phoneNumber; // 임베디드 타입 포함
}
    
@Embeddable
public class Address {

	String street;
    String city;
	@Embeddable Zipcode zipcode; // 임베디드 타입 포함
  	...
}

@Embeddable
public class Zipcode {

	String zip;
  	...
}

@Embeddable
public class PhoneNumber {

	@ManyToOne PhoneServiceProvider provider // 엔티티 참조
  	...
}

@Entity
public class PhoneServiceProvider {
	
    @Id String name;
    ...
}

2.3 속성 재정의

@AttributeOverride : 임베디드 타입에 정의한 매핑 정보를 재정의한다.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Address homeAddress;
    @Embedded Address companyAddress; // 주소 추가
}

테이블에 매핑하는 컬럼명이 중복된다.
@AttributeOverride 를 이용해서 매핑 정보를 재정의한다.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Address homeAddress;
    
    @Embedded
    @AttributeOverrides({
    	@AttributeOverride(name="city", column=@Column(name = "COMPANY_CITY")),
        @AttributeOverride(name="street", column=@Column(name = "COMPANY_STREET")),
        @AttributeOverride(name="zipcode", column=@Column(name = "COMPANY_ZIPCODE"))
    })
    Address companyAddress; // 주소 추가
}

@AttributeOverride는 엔티티에 설정해야 한다.
❓ 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.❓

2.4 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이다.

3. 값 타입과 불변 객체

3.1 공유 참조

임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 안 된다.

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); // 회원1의 address 값을 공유해서 사용 -> 회원1의 주소도 변경
member2.setHomeAddress(address);

이러한 부작용을 막으려면 값을 복사해서 사용해야 한다.

3.2 갑 타입 복사

member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();

newAddress.setCity("NewCity");
member2.setHomeAddress(newAddress);

임베디드 타입은 자바의 기본 타입이 아니라 객체 타입이다.

기본 타입 - 값을 복사해서 전달
객체 타입 - 참조 값을 전달


Address a = new Address("OldCity")

Address b = a.clone(); // 항상 복사해서 넘겨야 한다.
// Address b = a // 이렇게 참조만 넘기면 공유 참조의 부작용이 발생한다.

b.setCity("NewCity");

객체를 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있다.
하지만 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다.

객체의 공유 참조는 피할 수 없다.
⇒ 객체의 값을 수정하지 못하게 막는다.
ex) setter와 같은 수정자 메소드를 제거한다.

3.3 불변 객체

부작용을 원천 차단하기 위해서 값 타입은 되도록 불변 객체로 설계해야 한다.
불변 객체의 값은 조회 ⭕️, 수정 ❌

불변 객체도 결국 객체이다.
⇒ 인스턴스의 참조 값 공유를 피할 수 없다.
⇒ 하지만 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않는다.

불변 객체 만들기!
생성자로만 값을 설정하고 수정자를 만들지 않는다.

만약 값을 수정해야한다면 새로운 객체를 생성해서 사용해야 한다.

4. 값 타입의 비교

int a = 10;
int b = 10;

Address a = new Address("가", "나", "다");
Address b = new Address("가", "나", "다");
  • 동일성 비교 : 인스턴스의 참조 값을 비교, == 사용
  • 동등성 비교 : 인스턴스의 값을 비교, equals() 사용

값 타입을 비교할 때는 a.equals(b)를 이용해서 동등성 비교를 해야 한다.

5. 값 타입 컬렉션

값 타입을 하나 이상 저장하려면 컬렉션에 보관하고 @ElementCollection, @CollectionTable 을 사용한다.

@Entity
public class Member {
	@Id @GeneratedValue
    private Long id;
    private String name;
    
    @Embedded Address homeAddress;
    
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOODS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<String>();
    
    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayList<Address>();
    
}
  • @ElementCollection : 값 타입 컬렉션
  • @CollectionTable : 컬렉션 테이블 매핑


관계형 데이터베이스의 테이블은 컬럼 안에 컬렉션을 포함할 수 없기 때문에 별도의 테이블을 추가해야 한다.

5.1 값 타입 컬렉션 사용

값 타입 컬렉션 등록

Member member = new Member();

// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));

// 기본 값 타입 컬렉션
member.getFavoriteFoods().add("r");
member.getFavoriteFoods().add("s");

// 임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("1"));
member.getAddressHistory().add(new Address("2"));

em.persist(member);

값 타입 컬렉션은 영속성 전이와 고아 객체 제거 기능을 필수로 가진다.

값 타입 컬렉션 수정

Member member = em.find(Member.class, 1L);

// 임베디드 값 타입
member.setHomeAddress(new Address("ㄱ","ㄴ","ㄷ"));

// 기본 값 타입 컬렉션
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove(r); // String 타입을 수정할 수 없다.
favoriteFoods().add("s");

// 임베디드 값 타입 컬렉션
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("1")); // 값 타입은 불변하므로 삭제 후 새로 등록한다.
addressHistory.add(new Address("2"));

5.2 값 타입 컬렉션의 제약사항

값 타입 컬렉션에 보관된 값들은 별도의 테이블에 보관되므로 이 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다.

이 문제를 해결하기 위해 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면

  • 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제한다.
  • 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다.

따라서 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 새로운 엔티티를 만들어서 일대다 관계로 설정한다.

0개의 댓글