관련 내용
<JPA 영속성 컨텍스트부터 다양한 매핑까지 완전히 이해하기>
- JPA 기본 동작과 영속성 원리 이해하기
- JPA 엔티티 매핑 - 테이블, 컬럼, 기본키
- JPA 연관 관계 매핑과 고급 매핑
- JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기
JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기
- (현재 글)JPA 값 타입 이해하기
- JPQL, fetch join 중심으로 이해하기
JPA JPQL, fetch join 중심으로 이해하기
개요 목적
JPA 엔티티 안에는 다양한 값 필드들이 존재한다. (기본 값, 컬랙션 등)
반면에 DB의 테이블은 모든 데이터를 하나의 컬럼에 저장한다. 자바처럼 다양한 값 타입이 없는 것이다.
이런 둘의 차이 엔티티의 다양한 값 타입을 → DB 테이블의 컬럼에 잘 맞춰 변경해주는 것이 JPA 값 타입의 역할이다.
이번 시간에는 JPA 값 타입의 종류와 사용법에 대해서 알아본다.
마지막에는 테이블 생성 전략에 대해서도 간단하게 알아본다.
값 타입의 종류
- 기본값 타입
- 자바 기본 타입 (int, double)
- 래퍼 클래스 (Integer, Long)
- String
- 임베디드 타입 (복합 값 타입)
- JPA에서 정의해서 사용해야 한다.
- ex) 집의 주소를 담은 객체(Address.class)
- 컬렉션 값 타입
- 마찬가지로 JPA에서 정의해서 사용해야 한다.
- 컬렉션에 기본값 또는 임베디드 타입을 넣은 형태이다.
1. 기본 값 타입
기본 값 타입을 JPA에서 사용할 때 가장 큰 특징은, 공유되지 않아서 안전하다는 것이다.
데이터가 공유 되지 않아야 하는 이유(불변 객체여야 하는 이유) 사이드 이펙트 문제
B라는 엔티티의 이름이라는 필드에 “asdf”라는 값이 있었다. 그런데 A라는 엔티티가 이름을 설정할 때, B 엔티티 이름 필드를 참조해서 값을 작성했다. 데이터가 공유된 것이다. 그런데 여기서, B엔티티의 이름 필드를 “zzz”로 변경해서 DB에 넘겼더니, 그 값을 공유한 A DB 테이블의 이름 컬럼도 “zzz”로 변경되는 문제가 발생했다. 그래서 JPA에서 데이터는 공유되면 안 된다.
자바의 기본(primitive) 타입은 절대 공유되지 않는다.
int a = 20;
// 20이라는 값을 복사 a 데이터가 복사된 것이 아니다.
int b = a;
b = 10;
//a 값은 그대로 20이다.
Integer 같은 래퍼 클래스나 String과 같은 특수한 클래스는 공유 가능한 객체이지만 변경은 불가능하다. setter가 없다.
Integer a = new Integer(10);
// a의 참조를 복사할 수는 있지만
Integer b = a;
//이런 식으로 set할 수는 없다.
a.setValue(20);
//Java에서는 변경 자체를 불가능(Immutable)하게 만들어서 Side Effect를 막았다.
2. 값에 임베디드 타입 사용 (복합 값 타입)
기본 값을 모아서 객체 자체를 값으로 사용한다. 아래 같은 예시로 기본 값 타입으로만 구성하는 것이 아니라, 기본 값 타입들을 묶어서 만든 객체를 값으로 등록 할 수 있다.
방법
@Embeddable (값 타입을 정의 하는 곳에 표시한다.)
@Embedded (값 타입을 사용하는 곳에 표시한다.)
임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다. JPA 코드만 달라질 뿐이다.
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;
...
}
@Embeddable
@NoArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
...
}
@Embeddable
@NoArgsConstructor
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
...
}
임베디드 타입의 특징
- 임베디드 타입은 임베디드 타입을 가질 수 있다.
- e.g. Address «Value» 임베디드 타입은 Zipcode «Value» 라는 임베디드 타입을 가진다.
- 임베디드 타입은 엔티티 타입을 가질 수 있다.
- e.g. PhoneNumber «Value» 임베디드 타입이 PhoneEntity «Entity» 를 가질 수 있다. FK만 가지면 되기 때문이다.
- 한 Entity 안에서 같은 값 타입을 2개 이상 가지면 @AttributeOverrides, @AttributeOverride를 통해 속성을 재정의하면 된다.
@Entity
public class Member {
...
@Embedded
private Address homeAddress;
@Embedded
//같은 Address라서 컬럼 값이 같아지는 문제
//@AttributeOverrides 각 임베디드 타입 컬럼명을 지정할 수 있다.
@AttributeOverrides({ // 새로운 컬럼에 저장 (컬럼명 속성 재정의)
@AttributeOverride(name="city", column=@Column(name = "WORK_CITY"),
@AttributeOverride(name="street", column=@Column(name = "WORK_STREET"),
@AttributeOverride(name="zipcode", column=@Column(name = "WORK_ZIPCODE")})
private Address workAddress;
...
}
임베디드 타입(객체)은 공유 참조 사이드 이펙트 문제를 조심해야 한다.
기본 값 타입과 달리, 두 엔티티가 임베디드 타입의 값을 공유하고 한쪽에서 set으로 고치게 된다면 데이터를 공유한 다른 엔티티 DB 값도 변경되는 문제가 발생한다.
Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
em.persist(member2);
// 첫 번째 member의 Address(city) 속성만 변경하고 싶다.
//그런데 결과적으로 member2의 값도 변하게 된다.
//update query 2번 나간다.
member.getHomeAddress().setCity("new city");
tx.commit();
해결 방법
값 타입을 Wrapper 클래스 ,Stirng class 같은 불변 객체로 설계 하면된다.
생성자나 빌더를 통해서 생성을 가능하게 하지만, 값을 변경하는 메소드는 만들지 않는 것이다. 그럼 객체 특성상 공유는 가능하지만, 값 변경을 원천적으로 차단하여 예상치 못한 데이터 변경 부작용을 막을 수 있다.
객체 값 타입은 비교에서 ==가 아닌, equals를 사용하여, 내용물을 비교하게 해야 한다.
기본 값 타입은 인스턴스가 달라도 값이 같으면 같은 것으로 판단된다.
int a = 10;
int b = 10;
System.out.println("a == b: " + (a == b)); // true
반면에 객체는 값이 같아도, 주소가 다르기 때문에 == 비교시 false를 반환한다.
Address address1 = new Address("서울시");
Address address2 = new Address("서울시");
System.out.println("address1 == address2: " + (address1 == address2)); // false
해결 방법
자바에서 제공하는 eqauls() 메소드를 만들어서, 안에 있는 내용물을 비교하게 해야 한다.
System.out.println("address1 == address2: " + (address1.equals(address2)));
3. 값 컬렉션 타입 사용
자바에는 리스트같은 컬렉션 구조가 있지만, DB에는 한 컬럼에 한 개의 밸류만 넣을 수 있기 때문에, 리스트 구조를 가지는 것은 불가능하다. 그것을 가능하게 하는 방법을 그림과 코드를 통해서 알아보자.
컬렉션 타입 엔티티 적용하기
아래 그림 구조를 사용하는 엔티티 코드를 작성해보자.
엔티티 구성 코드
@Entity
@Table
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Embedded
private Address address;
//이부분을 설정해서
//컬렉션을 관리할 테이블 이름과, 외래키를 어떤 것으로할 것인지
//설정한다.
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
public Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
...
}
컬랙션 타입 값 저장
관리하는 DB 테이블이 다르다고 컬랙션들을 따로 persit해줄 필요없이, 그저 멤버에 세팅만 해주면 된다. 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다
Address address = new Address("homeCity", "street", "zipcode");
Member member = new Member();
member.setName("username");
member.setAddress(address);
//컬랙션 저장
member.getAddressHistory().add(new Address("OLDCity", "street", "zipcode"));
member.getAddressHistory().add(new Address("NEWCity", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("붕어빵");
member.getFavoriteFoods().add("국수");
em.persist(member);
tx.commit();
컬랙션 타입 값 조회
값 타입 컬랙션도 조회하려면 다른 테이블을 join해야 하기 때문에, 기본적으로 LAZY 지연 로딩이 사용된다.
Member findMember = em.find(Member.class, member.getId());
Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
//실제 값을 가져 올때 select 쿼리를 날리는 지연로딩을 사용한다.
System.out.println(favoriteFood);
}
List<Address> addressHistory = findMember.getAddressHistory();
for (Address address1 : addressHistory) {
//실제 값을 가져 올때 select 쿼리를 날리는 지연로딩을 사용한다.
System.out.println(address1.getCity());
}
컬랙션 타입 값 수정
컬랙션 테이블에는 id가 존재하지 않는다.그렇기 때문에 값이 중간에 변경되었을 때 DB가 해당 row만을 찾아서 변경할 수 없다. 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
Member findMember = em.find(Member.class, member.getId());
//@embaded 타입 수정
// homeCity -> newCity
// findMember.getHomeAddress().setCity("newCity"); // 틀린 방법
Address findAddress = findMember.getAddress();
findMember.setAddress(new Address("newCity", findAddress.getStreet(),
findAddress.getZipcode())); // 새로 생성
//컬랙션(기본 값) 타입 수정
//치킨 -> 한식
// 치킨 -> 한식
//완전히 삭제한 후에, 새롭게 값을 넣어준다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
//컬랙션(객체) 타입 수장
// old1 -> newCity1
// equals(), hashCode() 에 대한 제대로 정의가 필요하다.
// 컬랙션에서 같은 값을 가진 Address를 찾기 위해서
findMember.getAddressHistory().remove(new Address("OLDCity", "street1", "10001"));
findMember.getAddressHistory().add(new Address("newCity1", "street1", "10001"));
그런데 결론이 컬랙션대신 일대다 관계를 사용하자
실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 낫다.
일대다 관게 + 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용하자.
식별자가 필요하고, 지속해서 값을 추적하고 변경해야 한다면 그것은 값 타입이 아닌 엔티티이다.
Entity
public class Member {
...
@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<>();
// 일대다 관계를 사용하자
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
}
'Web Sever 개발과 CS 기초 > 스프링' 카테고리의 다른 글
JDBC 코드를 JPA 기술을 사용하여 변경하기(배달 서비스 음식 등록) (0) | 2023.04.28 |
---|---|
JPA JPQL, fetch join 중심으로 이해하기 (0) | 2023.04.28 |
JPA지연 로딩, 즉시 로딩 이해와 사용법 영속성전이, 고아 객체 알아보기 (0) | 2023.04.28 |
JPA 연관 관계 매핑과 고급 매핑 (0) | 2023.04.28 |
JPA 엔티티 매핑 - 테이블, 컬럼, 기본키 (0) | 2023.04.28 |