자바 ORM 표준 JPA 프로그래밍 - 기본편 챕터 9 정리

정종일·2023년 6월 16일
0

Spring

목록 보기
14/18

Chapter 9. 값 타입


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

1. 기본 값 타입


  1. 생명주기를 엔티티에 의존
    • 회원삭제시 이름, 나이 등등 필드들이 함께 삭제됨
  2. 값 타입은 공유하면 안됨
    • Side Effect 발생 가능
    • 회원 이름 변경시 다른 회원 이름도 변경
      int a = 20;
      int b = a;
      b = 10;
      
      result > a = 20, b = 10;
      Integer a = new Integer(10);
      Integer b = a;
      a.setValue(20);
      
      result > a = 20, b = 20; // side Effect !

2. 임베디드 타입


  • 새로운 값 타입을 직접 정의
@Entity
public class Member {
		
		@Id
		@GeneratedValue
		private Long id;

		private String name;

		@Embedded
		Address homeAddress;
		
		...
}
@Embeddable
public class Address {

		@Column (name = "city")
		private String city;

		private String street;

		private String zipcode;

		...
}

엔티티를 더욱 의미있고 응집력 있게 변화시킬 수 있다 ! 재사용도 가능하다 !

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시
  • 둘 중 하나만 명시해도 되지만 둘 다 적는 것을 권장 !

임베디드를 포함한 모든 값 타입은 엔티티 생명주기에 의존 !

엔티티와 임베디드 타입( = 컴포넌트)의 관계를 UML로 표현하면 컴포지션 관계가 된다.

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

  • 값 타입 / 임베디드 타입에 상관없이 DB 회원 테이블의 형태는 동일
  • 용어 / 코드를 공통으로 관리할 수 있게 됨
  • 잘 설계한 ORM 어플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다
  • 임베디드 타입 사용 전 후에 매핑하는 테이블은 같다 (임베디드 타입은 엔티티의 값일 뿐)

임베디드 타입과 연관관계

  • 임베디드 타입은 임베디드 타입을 가질 수 있다
  • 임베디드 타입은 엔티티또한 가질 수 있다 ! FK만 가지면 된다
❓ 한 엔티티 안에서 같은 타입을 2개 이상 가진다면?

컬럼명이 중복되어 MappingException : Repeated column 에러 발생 !
아래와 같이 @AttributeOverrides@AttributeOverride 를 통해 속성을 재정의하면 된다

@Entity
public class Member {
		
		...

		@Embedded
		@AttributeOverrides({
			@AttributeOverride(name = "city", column = @Column(name = "WORK_CITY"),
			...
		private Address address;

		...
}

임베디드 타입과 null

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

3. 값 타입과 불변 객체


  • 값 타입은 복잡한 객체 세상을 조금이라도 단순화하려 만든 개념
  • 값 타입은 단순하고 안전하게 다룰 수 있어야 함

값 타입 공유 참조

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

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

address.setCity("NewCity"); // 회원 1의 address 값을 공유해서 사용
member2.setHomeAddress(address);
  • 결과 회원 2의 city만 NewCity로 변경되길 원했지만 member1의 address를 영속받아 사용하였기에 회원 1과 회원 2 모두 NewCity로 변경되었다.

이와같은 공유 참조로 인해 발생하는 Side Effect는 찾기 어렵다 ! 이를 막기위해서는 값 타입을 복사해서 사용해야 한다

값 타입 복사

자바는 대입하려는 것이 값 타입인지 아닌지는 신경쓰지 않고 자바 기본 타입이면 값을 복사해서 넘기고 객체면 참조해서 넘긴다. 복사하지 않고 원본의 참조 값을 직접넘기는 것을 막을 방법이 없다.

Address a = new Address("OldCity");
Address b = a.clone(); // 항상 복사해서 넘겨야 공유되지 않는다 !!
b.setCity("New");

result > a.city = "OldCity" , b.city = "New"

객체의 공유참조는 피할 수 없다. 가장 근본적인 해결책은 set 같은 수정자 메소드를 모두 제거하는것

불변 객체

객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 완전 차단할 수 있다.

값 타입은 될 수 있으면 불변 객체로 설계해야 한다.

setter를 쓰지 않고 객체를 통으로 변경해야 한다.

4. 값 타입 비교


  • 동일성 (identity) : 실제 인스턴스가 같다. 따라서 참조 값을 비교하는 == 비교의 값이 같다.
  • 동등성 (equality) : 실제 인스턴스는 다를 수 있지만 인스턴스가 가지고 있는 값이 같다. 자바에서 동등성 비교는 equals() 메소드를 구현해야 한다.
🔊 자바에서 `equals()` 를 재정의 하면 `hashCode()`도 재정의하는 것이 안전! 그렇지 않으면 해시를 사용하는 컬렉션 (`HashSet, HashMap`) 이 정상 동작하지 않음 자바 IDE에는 대부분 `equals`, `hashCode` 메소드를 자동으로 생성해주는 기능이 있다.

5. 값 타입 컬렉션


값 타입을 하나 이상 저장하려면 컬렉션에 보관하고

@ElementCollection , @CollectionTable 어노테이션을 사용하면 된다.

@Entity
public class Member {

		@Id
		@GeneratedValue
		private Long id;

		@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>();
		
		...
}
🔊 `@CollectionTable` 을 생략하면 기본 값을 사용해서 매핑 **기본 값** : {엔티티 이름}_{컬렉션 속성 이름}.

값 타입 컬렉션 사용

Member member = new Member();

//임베디드 값 타입
member.setHomeAddress(new Address("통영", "몽돌해수욕장","660-123");

//기본값 타입 컬렉션
member.getFavoriteFoods().add("짬뽕");
member.getFavoriteFoods().add("짜장");
member.getFavoriteFoods().add("탕수육");

//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("서울","강남", "123-123"));
member.getAddressHistory().add(new Address("서울","강북","000-000"));

em.persist(member);

실제 DB에 실행되는 Insert SQL은 다음과 같다.

  • member : Insert SQL 1번
  • member.homeAddress : 컬렉션이 아닌 임베디드 값 타입이므로 회원테이블을 저장하는 SQL에 포함
  • member.favoriteFoods : Insert SQL 3번
  • member.addressHistory : Insert SQL 2번
🔊 값 타입 컬렉션은 영속성 전이(Cascade) + 고아객체 제거(Orphan Remove) 기능을 필수로 가진다.

값 타입 컬렉션도 조회할 때 페치 전략을 선택가능, default = LAZY

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

//1. 임베디드 값 타입 수정
member.setHomeAddress(new Address("새로운도시", "신도시1","123456");

//2. 기본값 타입 컬렉션 수정
Set<String> favoriteFoods = member.getFavoriteFoods();
member.getFavoriteFoods().remove("탕수육");
member.getFavoriteFoods().add("치킨");

//3. 임베디드 값 타입 컬렉션 수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("서울","기존주소", "123-123"));
addressHistory.add(new Address("새로운도시","새로운주소","000-000"));
  1. 임베디드 값 타입 수정 : homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE 한다. MEMBER 엔티티를 수정하는 것과 같음
  2. 기본 값 타입 컬렉션 수정 : 탕수육을 치킨으로 변경하려면 탕수육을 제거하고 치킨을 추가해야 한다. 자바의 String 타입은 수정할 수 없다.
  3. 임베디드 값 타입 컬렉션 수정 : 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고 새로운 주소 등록.

값 타입 컬렉션의 제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다
  • 값은 변경하면 추적이 어렵다.
  • 값타입 컬렉션에 보관된 값 타입들은 별도의 테이블에 보관된다.
    • 따라서 여기에 보관된 값 타입의 값이 변경되면 데이터베이스에 있는 원본 데이터를 찾기 어렵다는 문제가 있다.
    • 이로인해 값타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다 : NULL 입력x, 중복저장 x

값 타입 컬렉션 대안

  • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아객체제거를 사용해서 값 타입 컬렉션처럼사용!
profile
제어할 수 없는 것에 의지하지 말자

0개의 댓글