데이터베이스에는 자바의 컬렉션을 저장할 수 있는 메커니즘이 아니다. 자바의 컬렉션 데이터를 저장하려면 별도의 테이블에 하나하나 저장해야한다.
사용 방법 예시 코드
Address 임베디드 값 타입 :
@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;
}
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());
}
}
위를 값 타입 컬렉션으로 사용한 Member :
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY) // EAGER은 즉시 가져오는 것
@JoinColumn
private Team team;
@Embedded
private Period workPeriod;
@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<>();
... Getter
}
@ElementCollection, @CollectionTable 어노테이션을 사용해서 자바 컬렉션으로 줄 수 있다. FK로 Member id를 가져 가야하므로(DB상으로는 테이블이므로 FK가 있어야 된다.) JoinColumn을 해줘야한다.
예외적으로 String과 같이 임베디드 값 타입과 같이 다중 컬럼이 들어가는것이 아니라 단일 값 타입이면 위의 코드처럼 name을 줄 수 있다. 하지만 그외에 경우는 당연히 @Column을 줄 수 없고 임베디드 타입 클래스 안에 정의가 된다.
값 타입 컬렉션 저장 방법
member1.getFavoriteFoods().add("치킨");
member1.getFavoriteFoods().add("족발");
member1.getFavoriteFoods().add("피자");
member1.getAddressHistory().add(new Address("old1", "street", "10000"));
member1.getAddressHistory().add(new Address("old2", "street", "10000"));
예시 코드와 같이 get으로 가져와서 .add하고 넣고 싶은 값을 넣으면 된다.
값 타입 컬렉션 조회 방법
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);
}
간단하다. 그냥 가져와서 뿌리면 된다.
값 타입 컬렉션 수정 방법
먼저 아래의 코드는 값 타입 컬렉션은 아니고 단일 임베디드 타입이다. 잘못된 예시인데 한번 보자.
findMember.getHomeAddress().setCity("newCity"); // 안된다 값타입은 이뮤터블 해야함
왜 안될까? getHomeAddress자체가 래퍼런스를 반환한다. setCity하면 같은 레퍼런스를 참조하고 있는 엔티티가 존재할 때, 그 엔티티의 homeAddress도 값이 바뀌어버리는 대참사가 발생할 수 있다. 그냥 Setter를 만들지 말거나 더 좋은 방법은 private으로 외부에서 아예 사용하지 못하게 막는 것이다.
그럼 좋은 예시는?
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity!", a.getStreet(), a.getZipcode()));
이렇게 아예 새로 만들어버려야한다. 그냥 같은 레퍼런스 안에서 바꿀 생각을 하지말자.
그럼 이제 값 타입 컬렉션을 보자. 어떻게 수정해야 할까?
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity!!@~", "street", "10000"));
아예 삭제를 하고 add를 해줘야한다. 밑의 getAddressHistory()에서 삭제할 때 같은 값을 가진 Address를 생성해서 해시비교를 통해 삭제하고 add를 통해서 원하는 값을 다시 삽입해야 한다.
근데 문제는 ...
컬렉션 자체를 아예 지워버리고 각각을 (위의 코드에서는 2개를 추가했던 상태임) 다시 insert하는 모습을 보이고 있다.
느낌이 온다.. 쓰면 안된다라는 느낌이...!
값 타입 컬렉션의 한계
1. 값 타입은 식별자 개념이 없고 모든 컬럼을 묶어서 기본 키를 구성한다.
2. 값이 변경되면 추적이 어렵다.
3. 값 타입 컬렉션에 변경이 발생하면 모든 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 존재하는 현재 값을 모두 다시 insert한다.
헉 그러면 어쩌쥬..?
이런 경우에는 가능하면 사용하지말고 일대다 단방향 매핑의 엔티티 기법을 쓰는 게 좋다. 기본적으로 임베디드 타입은 생명주기가 엔티티에 결정되기 때문에 이런 속성을 일대다 단방향 매핑에 주려면 cascade.ALL 과 orphanRemoval = true를 주면 활용면에서 달라지지 않는다.
정리
엔티티 타입은 식별자가 존재하고, 생명 주기를 관리해야 하며, 공유되는 특성이 있다.
이와 다르게 값 타입은 식별자가 존재하지 않고 생명 주기가 엔티티에 의존되며 절대 레퍼런스를 공유해서는 안된다! 불변 객체로 만드는 것이 좋다. (Setter를 private으로 닫아두고 생성자만 열어두기)
정말 필요할때만 쓰는 것이 좋다. 식별자가 있어야하고, 변경이 잦다면 엔티티로 만들자.
'JPA' 카테고리의 다른 글
[JPA] JPQL 기본 (0) | 2020.10.27 |
---|---|
[JPA] 임베디드 타입 (0) | 2020.10.23 |
[JPA] orphanRemoval (0) | 2020.10.22 |
[JPA] CASCADE (0) | 2020.10.22 |
[JPA] 지연로딩 (0) | 2020.10.22 |