자바생
article thumbnail
Published 2021. 12. 12. 02:04
값 타입 Spring 강의/JPA - 기본편
728x90

JPA의 데이터 타입

JPA의 데이터 타입은 크게 엔티티 타입과 값 타입으로 나눌 수 있다.

엔티티 타입과 값 타입의 가장 큰 차이는 식별자의 유무이다.

엔티티는 식별자를 통해 구별할 수 있지만 값은 할 수 없다.

 


기본 값

기본 값은 엔티티에 생명주기를 의존한다.

Member가 삭제되면 age나 name이 삭제되기 때문이다.

 


임베디드(embedded) 

임베디드 타입은 사용자가 JPA에서 새로운 값 타입을 직접 정의한 것이다.

 

임베디드 타입은 여러 곳에 사용할 수 있는 재사용성이 좋다.

member도 주소가 필요하고, group도 주소가 필요할 때

member에 주소를 모두 쓰고, group에도 주소를 모두 쓰는 것보다

주소 임베디드 타입을 만든다면 매우 객체지향적이고 깔끔한 코드가 될 것이다.

 

임베디드 타입을 포함한 모든 값 타입은 해당 엔티티에 생명주기를 의존한다.

 

예를 들어,  주소 라는 값 타입을 사용하고 싶다.

(주소 안에는 우편번호, 거리 이름, 도시 이름이 있다)

 

해당 임베디드 클래스에서는

@Embeddable

엔티티에서는 임베디드를 나타내는

@Embedded
private Address homeAddress;

어노테이션을 사용한다.

 

하지만 주의해야할 점이 있다.

하나의 엔티티 클래스에서 같은 임베디드 타입을 사용하면 컬럼 명이 중복된다.

 

Repeated column in mapping for entity를 마주치게 된다.

 

따라서 전에 책을 통해 배웠던 @AttributeOverrides를 사용하면 된다.

 

@Embedded
@AttributeOverrides({
        @AttributeOverride(name = "city",
        column = @Column(name = "WORK_CITY")),
        @AttributeOverride(name = "street",
                column = @Column(name = "WORK_STREET")),
        @AttributeOverride(name = "zipcode",
                column = @Column(name = "WORDK_ZIPCODE")),
})
private Address workAddress;

결과가 이렇게 나옴을 알 수 있다!

만약 임베디드 타입의 값이 null이면 매핑한 컬럼도 모두 null이다.

즉, 주소가 null 이면 거리 이름, 우편번호, 도시가 모두 null이 된다!

 

 


값 타입과 불변 객체

객체를 사용하다 보면 변경에서 부작용이 발생하게 된다.

member1의 주소를 바꿨는데, member2의 주소도 같이 바뀌는 것을 알 수 있다.

 

Address address = new Address("city", "street", "zipcode");
Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);

Member member2 = new Member();
member2.setName("member2");
member2.setHomeAddress(address);
em.persist(member2);

member.getHomeAddress().setCity("newCity");

이 방법은 새로운 객체를 만들어 넣어줘야한다.

 


값 타입 컬렉션

@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<>();

해당 연관관계는 일대다 연관관계에서, 연관관계 주인이 현재 일에 있다.

 

값 컬렉션을 사용하기 위해 @ElementCollection 어노테이션을 사용한다.

 

하지만 관계형 DB는 컬렉션 같은 테이블을 저장할 수 없다.

따라서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

 

이 때, @CollectionTable을 사용하여 테이블을 만든 뒤에 매핑을 한다.

 

FAVORITE_FOOD 같은 경우에는 값이 한 개(String)이기 때문에 @Column 을 사용할 수 있다.

 

만약 Address의 이름을 바꾸고 싶다면 @AttirubteOverride를 사용해야한다.

즉, @Column 을 사용할 수 없다.

 


값 타입 컬렉션 사용

처음에 값 타입 컬렉션에 값을 저장해본다.

 

Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));

em.persist(member);

얘네는 따로따로 persist하지 않고 member만 persist해도 저장이 된다.

ADDRESS, FAVORITE_FOOD는 Member와 다른 테이블인데도 불구하고 생명주기가 같다.

왜?

값 타입 컬렉션도 값 타입의 종류로 본인 스스로 라이프 사이클이 없다.

마치 cascade.all + orphanremoval = true 기능이 있는 것이다.

 

이제 조회해본다.

 

 

조회를 하면 멤버만 가져오는 것으로 보아 컬렉션들은 모두 지연로딩인 것을 알 수 있다.

 

System.out.println("=========ADDRESS 조회==============");
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("address = " + address.getCity());
}

System.out.println("=========FOOD 조회===========");
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFood = " + favoriteFood);
}

@ElementCollection의 내부를 보니 fetch의 default 전략이 LAZY임을 알 수 있다.

 

그렇다면 수정은 어떻게 해야하까?

 

임베디드 값 타입을 수정

findMember.getHomeAddress().setCity("newCity");

앞서 배우다시피, 위와 같이 코드를 작성하게 되면 안된다.

새로운 address 객체를 생성하여 넣어줘야한다.

 

findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));

 

기본값 타입 컬렉션 수정

 

//치킨 -> 한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

 

임베디드 값 타입 컬렉션 수정

 

처음에는 Address 클래스에 equals와 hashcode를 오버라이딩하지 않은 체로 삭제를 할 때,

제대로 삭제되지 않는다.

현재 2개의 데이터가 있었는데 삭제를 하고나서도 여전히 데이터의 개수는 2개이다.

findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));

List<Address> addressHistory = findMember.getAddressHistory();
System.out.println("addressHistory.size() = " + addressHistory.size());

하지만 equals와 hashcode를 오버라이딩 하고 나서 위 코드를 실행하게 되면 개수가 줄어든다.

 

하지만 삭제를 하면서 신기한 점이 있다.

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

findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

위 쿼리를 보면 insert가 두 개 나가는 것을 알 수 있다.

왜 이렇게 나갈까?

컬렉션이 아닌 단일 값 타입은 값이 변경되어도 엔티티를 DB에서 찾고 수정하면 추적하기 쉽다.

그러나 값 타입 컬렉션은 별도의 테이블에 보관된다. 그래서 해당 테이블의 값 타입의 값이 변경되면 DB에 있는 원본 데이터를 찾기 어렵다. 즉, address는 삭제하면 id가 있는게 아니라 추적이 어렵게 된다. 또한, 중간에 city나 street, zipcode 중에 중간에 수정되면 알 수 없다.

 

그래서 AddressEntity를 새로 생성한다.

그리고 Member와 일대다 연관관계를 맺는다. 물론 연관관계의 주인은 Member이다.

이 때, cascade.all과 orphanremoval = true 옵션을 사용한다면 값 타입 컬렉션처럼 사용할 수 있다.

 


Reference

자바 ORM 표준 JPA 프로그래밍

 

728x90
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그