@JPA의 데이터 타입은
두가지로 나뉜다.
엔티티 타입
값 타입
ex) 자바 기본타입 (String , int) , 래퍼 클래스(Integer,Long), String
기본 값 타입은 생명주기를 엔티티에 의존한다.
즉 게시글 엔티티를 삭제하면 당연하게도 안에있는 String content, String title 등등의 필드도 함께 사라진다.
값타입이 중요한 특징은 공유 될 수 없다는 것인데 값타입은 참조로 저장되지 않고 복사되기 때문에 독립적인 인스턴스로 존재한다.
(유저의 이름을 바꾼다고 다른 유저의 이름도 변경되지는 않는다. )
즉 우리는 이러한 이유때문에 기본값타입을 사용했을때 안전하게 사용할 수 있었던 것이다.
값타입을 모아서 직접 정의한것을 임베디드 타입이라고 한다.
다만 기본 값타입을 모아서 만들기 떄문에 복합 값타입으로 불린다.
예시를 보자면

<김영한님의 jpa 기본편 강의자료>
Member 객체에 저장되어 있던 기본 값타입
String city, String street, String zipcode 모두 주소에 관련된 기본 값타입이었기 때문에 이를 모아서 임베디드 타입으로 만들어준다.
사용방법은
@Embeddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
코드로 보자면
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address(){
}
}
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {} // 기본 생성자 필수
}
@Entity
@Getter @Setter
public class Member {
// ...
@Embedded
private Period workPeriod;
@Embedded
private Address homeAddress;
}
그리고 임베디드 타입은 꼭 기본생성자를 필수로 가져야한다.
그럼 임베디드 타입을 왜 쓸까?
=> 재사용성, 응집도, 그리고 필요한 매서드를 정의하여 사용할 수 있다는 것이다. 예를 들어
pubic String totalAddress(String city,String street,String zipcode){
return "city +" " + street + " " + " zipcode"
}
뭐 이런식으로 필요한 매서드를 정의해서 사용할 수 있다는점 ..
Entity에도 정의하는게 가능하겠지만 주소에 관련된 부분만 따로 임베디드 타입으로 정리해두었으니 코드 가독성이나 위에서말한 응집도의 측면에서 더 낫다는 생각이 든다.
아 그리고 DB에서 Member 테이블은 임베디드 값 타입으로 바꿔도 이전처럼 그대로 private String city, private String street,private String zipcode를 컬럼으로 가진다.
<<아래 사진 참조 >>
Address address = new Address("city", "street", "zipcode");
Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
Member member2 = new Member();
member2.setUsername("member1");
member2.setHomeAddress(address);
em.persist(member2);
// member1의 주소를 변경했지만 member2도 변경됨
member1.getHomeAddress().setCity("newCity");
member1과 member2를 생성하고 member1의 임베디드 타입에 접근해서 City부분을 바꾸어 주었다. 원래 코드의 의도는 member1의 city를 바꾸는것이었지만 주소를 참조하고 있어 member2의 주소도 변경이된다. 때문에 이런식으로 값을 변경하면 안되고 항상 값을 복사해서 사용해야한다.
Address address = new Address("city", "street", "zipcode");
MemberR member1 = new MemberR();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
// 값을 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
MemberR member2 = new MemberR();
member2.setUsername("member1");
member2.setHomeAddress(copyAddress); // 복사한 값을 사용
em.persist(member2);
생각해보면 당연한 이야기다...
하지만 어떤 일이 벌어질지는 모르기때문에 무조건 setter를 닫아두고 생성자로만 변경기 가능하게 하는 방법등으로 수정할 수 없느 ㄴ불변객체로 만들어야한다.
Address address = new Address("city", "street", "zipcode");
MemberR member1 = new MemberR();
member1.setUsername("member1");
member1.setHomeAddress(address);
em.persist(member1);
// member1.getHomeAddress().setCity("newCity"); 이제 setter는 못쓴다.
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member1.setHomeAddress(newAddress); // 값을 변경하려면 객체를 새로 생성한다음 넣어주는 방식을 사용
번거로운 작업이지만 사이드 이펙트를 막을 수 있으므로 값타입은 무조건 불변 객체로 만들도록 하자.
- 값타입의 동일성 비교
@Embeddable
@Getter
public class Address {
// ...
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Address other = (Address) obj;
return Objects.equals(city, other.city) &&
Objects.equals(street, other.street) &&
Objects.equals(zipcode, other.zipcode);
}
}
Address address1 = new Address("city", "street", "zipcode");
Address address2 = new Address("city", "street", "zipcode");
System.out.println(address1.equals(address2)); //true
분명히 각각 생성된 객체이므로 java에서는 해당 객체끼리의 동일성비교를 false를 리턴할것이다. 하지만 값 타입은 값이 같다면 같은것으로 봐야한다.
때문에 위 처럼 equals 매서드를 재 정의해서 사용해야한다.
임베디드 타입이나 기본 값 타입을 컬렉션에 넣어 사용하는 것으로 값 타입을 하나 이상 저장할 때 사용한다.
데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다. 컬렉션을 저장하기 위해서는 별도의 테이블이 필요하다.
=> 어떤 테이블에 대한 컬렉션인지 알아야하기때문에 생성된 테이블에는 해당 테이블의 pk값을 가진다.

설정하는 방법을 코드로 보면
@Entity
public class Member {
// .....
@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<>();
}
db에는 위와같이 저장이 된다.
**
<값 타입 수정 방법>
//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
findMember.getAddressHistory().remove(new Address("old1", "street", "482-123"));
값 타입은 엔티티와 다르게 식별자 개념이 없기 떄문에 값을 변경하면 추적이 어렵다.
또, 값 타입이 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야하기 때문에 null 입력이 안되고 중복 저장도 안된다.
값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된
모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두
다시 저장한다.
결론은 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는게 낫다.
일대다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용해서 영속성 전이(Cascade) + 고아 객체 제거로 값 타입 컬렉션처럼 사용할 수 있다.
값타입을 ENTITY로 감싼다.
@Entity
public class AddressEntity {
@Id
@GeneratedValue
private Long id;
private Address address;
}
@Entity
public class Member {
// ...
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "member_id")
private List<AddressEntity> addressHistory = new ArrayList<>();
}