[JPA/김영한] 값 타입

수영·2021년 10월 14일
1

JPA 공부!

목록 보기
9/9

이 글은 김영한님의 JPA 강의 중 9장 값타입를 듣고 정리한 내용입니다 :)
강의 : 자바 ORM 표준 JPA 프로그래밍 - 기본편
교재 : 자바 ORM 표준 JPA 프로그래밍🤷‍♀️

이 장에서 중요한 건 임베디드 타입과 값타입 컬렉션! 이다


데이터 타입 - 엔티티, 값

프리뷰로 엔티티와 값타입을 비교해보고 가자! 값타입을 알아보고 마지막에 다시 비교 정리한다.

엔티티 타입

  • @Entity 로 정의하는 객체
  • PK같은 식별자로 추적가능 (속성들이 변해도 계속 추적가능)

값 타입

  • 단순 값으로 사용하는 자바 기본 타입 (int, Integer, String)
  • 식별자가 없음 (PK가 없음)

🔎 값타입 세가지 알고가자

1. 기본값

: 자바 기본, 래퍼 클래스, String

생명주기를 엔티티한테 의존함, 속성이니까,
값타입은 공유하면 안됨(한 회원을 변경했을 때, 다른 회원의 속성(기본값)이 변경되면 안됨!

// 기본 타입
int a = 10;
int b = a;  // a의 값을 단순 복사

b = 20;

//a = 10, b = 20임! (값을 공유하지 않고 그냥 단순 복사)
// 객체 타입
Integer a = new Integer(10);
Integer b = a; //주소값이 넘어감

a.setValue = 20;

//a = 20, b = 20임! (래퍼런스 주소로 값을 공유!)

원래는 위와 같으나, JPA 엔티티(?)에서는 래퍼클래스나 String의 경우 공유는 되지만, 변경은 불가하게 만듦! (side-effect 방지)

2. 임베디드 타입

: 새로운 값 타입을 묶어서 직접 정의하는 것으로, DB와 매핑하는 테이블은 같음!

// -----------기존 Member 엔티티 파일-----------
@Entity
public class Member {
  @Id @GenetratedValue
  private Long id;
  
	// 주소 관련 데이터들이 따로따로 있음
  private String city;
	private String street;
	private String zipcode;
}
// -----------바뀐 Member 엔티티 파일-----------
@Entity
public class Member {
  @Id @GenetratedValue
  private Long id;
  
  @Embedded
	Address address;
}

// -----------Address class 파일-----------
@Embeddable 
public class Address {
  private String city;
  private String street;
  private String zipcode;
	
	... 기본 생성자
}

// -----------실제 실행 파일-----------
Member member = new Member ();
member.setAddress ("도시명", "도로명", "집코드"); // 임베디드타입에 값 넣기

이때, 같은 임베디드 타입을 여러 개 쓰고 싶은 경우 @AttributeOverrides 어노테이션을 사용하자

@Entity
public class Member {
  @Id @GenetratedValue
  private Long id;

	@Embedded 
	  @AttributeOverrides
					  ({ @AttributeOverride(name="city", colume=@colume(name="work_city")),
						   @AttributeOverride(name="zipcode", colume=@colume(name="work_zipcode")),
						   @AttributeOverride(name="zipcode", colume=@colume(name="work_zipcode"))
	  })
	 Address workAddress;
}

값 타입은 객체를 단순화하려고 만든 개념, 그래서 값타입은 단순 + 안전 해야한다! 근데 값타입의 실제 인스턴스를 여러 엔티티에서 공유하면 위험하다! → 값을 복사해서 사용하자

Address address = new Address(city = "도시명", street = "도로명");

Member member1 = new Member();
member1.setAddress(address);

Member member2 = new Member();
member2.setAddress(address);

// SIDE EFFECT : member1, member2의 도시명이 전부 변경됨!
member1.getAddress().setCity("바뀐 도시명")

위와 같을 경우, 아래 코드처럼 값을 복사해서 사용하자!

Address address = new Address(city = "도시명", street = "도로명");

Member member1 = new Member();
member1.setAddress(address);

// 아래처럼 값을 복사해서 사용하자!
Address copyAddress = new Address(address.getCity(), address.getStreet());

Member member2 = new Member();
member2.setAddress(address);

// NO SIDE EFFECT, member2의 도시명은 바뀌지 않음!
member1.getAddress().setCity("바뀐 도시명")

이때 객체 타입은 Setter를 만들지 말자! (객체는 수정 금지!)

Primitive 타입(기본타입)은 무조건 값이 복사되나, 객체 타입은 참조값을 직접 대입한다. (하나의 인스턴스를 공유) 따라서, 객체의 공유 참조는 예방할 수가 없다! (한 객체를 수정할 때, 의도치 않게 다른 객체도 함께 수정되는 부작용이 생길 수 있음!)

아예 객체 타입을 수정할 수 없게 만들어(immutable), 부작용을 사전에 차단하자
즉, 생성자(Getter)로만 값을 설정하고, 수정자(Setter)는 애초에 만들지 말 것!

잠깐, 그럼 자기 자신의 값을 진짜 '수정'하고 싶을 때는 ?
그냥 아예 새로 만들자-! SIDE EFFECT(추적하기 어려움..)를 방지하는 게 더 중요함!

값 타입 비교하기-!

: 값타입의 비교는 인스턴스가 달라도 그 내부 값이 같으면 같다고 본다

자바에서 기본 타입은 == 비교를 한다. 그 나머지들은 모두 equals를 사용해야 한다. → 오버라이드! (참고로 해시코드도 오버라이드 해주어야 함!)

  • 동일성 비교(identity) : 인스턴스의 참조값 비교, == 비교
  • 동등성 비교(equivalence) : 인스턴스의 값 비교, equals() 비교 (equals메소드 재정의(오버라이드) 해주어야함!)

3. 컬렉션 값 타입

: 자바 컬렉션 안에 기본값 타입이나 임베디드 타입을 넣는 것 (값 타입을 하나 이상 저장 시에 사용한다)

생명주기는 엔티티에 의존함, 그니까 컬렉션을 별도로 em.persist()할 필요 없음!

@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @ElementCollection //기본값이 fetch = LAZY
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns =
        @JoinColumn(name = "MEMBER_ID")
    )
    @Column(name = "FOOD_NAME") // <String>이니까 특수하게 하나만 들어가니까
    private Set<String> favoriteFoods = new HashSet<>();

    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns =
        @JoinColumn(name = "MEMBER_ID")
    )
    private List<Address> addressHistory = new ArrayList<>();

}

값 타입 수정하려면? → 값타입은 불변해야 안전!(오류나면 추적하기 힘드니까) 수정하고 싶으면 새로 생성해야함

이때, eqauls, hashcode 함수 오버라이딩이 잘 되어있어야 잘 삭제됨!


Member findMember  = em.find(Member.class, member.getId());

// Case 1. String타입 favoriteFoods를 치킨-> 라면으로 변경, 그냥 삭제 후 새로 생성
findMember.getFavoiteFood().remove(0:"치킨");
findMember.getFavoiteFood().add("라면");

// Case 2. Address타입 addressHistory를 변경하려면 삭제후 새로 생성
findMember.getHomeAddress().remove(new Address ("이전 도시명", "도로명", "집코드");
findMember.getHomeAddress().add(new Address ("새 도시명", "도로명", "집코드");
// 이 경우 다만, 쿼리가..? 
// 쿼리를 보면 Address 테이블 전체 삭제 후 데이터 다시 저장함

Case 2처럼 값타입 컬렉션에 변경이 있으면,

  • 주인 엔티티와 연관된 모든 데이터를 삭제하고
    컬렉션에 있는 현재값을 모두 다시 저장한다
  • 그리고 null, 중복도 안됨
  • 외래키도 없음, 추적이 어려움 (오류있으면 찾아내기 어려움..)
  • 즉, 여러모로 불편하다!

⇒ 실무에서는 값타입 컬렉션 대신에 일대다 관계를 사용하자 !
⇒ 그럼 값타입 컬렉션은 언제 사용할까?

정말 단순한 옵션일때, 예를 들면 [치킨, 피자] 중에 선택할 때
다시 수정할 필요도, 추적할 필요도 없을 경우에 쓰자


엔티티랑 값타입 다시 비교정리하자

엔티티

  • 식별자 O
  • 생명주기 관리
  • 공유

값타입

  • 식별자 X → 오류나면 값 추적이 어렵다
  • 생명주기를 엔티티에 의존한다.
  • 공유하지 않는게 안전! 대신 복사해서 사용하자
  • 불변 객체로 만들자

profile
🎵🎵🎵🎶🎵

0개의 댓글