JPA 데이터 타입 분류
1. 엔티티 타입이란?
@Entity 로 정의 하는 객체를 의미하며 데이터가 변하더라도 식별자를 통해 지속적으로 추적할 수 있다.
2. 값 타입이란?
int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본타입( 래퍼클래스, String 등 .. ) 이나 객체를 의미한다. 식별자가 없고 값만 존재하므로 변경시 추적이 불가능하다.
기본적으로 자반의 기본타입은 절대 공유 되지 않는다. 공유하지 않는다는 것의 의미를 코드로 설명하면 다음과 같다.
- 기본타입이 공유되지 않을 경우
int a = 10;
int b = a;
a = 20;
// a 는 20, b 는 10 으로 출력
위 코드에서 int b = a 로 선언했음에도 기본타입이 공유 되지 않고 각각 출력되는 것을 볼 수 있다.
- 공유 될 경우
Integer a = new Integer(10);
Integer b = a;
a.setValue(20);
// b 와 a 는 모두 20으로 출력
Integer 라는 매퍼 클래스를 사용하 경우 공유 되어 동일하게 20 이 출력되는 것을 볼 수 있다.
3. 임베디드 타입 이란?
새로운 값 타입을 직접 정의할 수 있는 타입을 의미한다. 그런 의미에서, JPA 는 임베디드 타입이라고 말할 수 있다.
기본 값 타입을 모아서 만들기 때문에 복합 값 타입이라고도 한다.
간단한 예시를 들어보자.
회원이라는 엔티티는 회원식별자, 이름, 근무시작일, 근무종료일, 주소도시, 주소번지, 우편번호를 가진다.
이 때 근무시작일/종료일을 Period 로, 주소도시/주소번지/우편번호를 Address 라는 각각의 임베디드 타입으로 선언할 수 있다.
실제 코드로 작성할 경우 @Embeddable 과 @Embedded 를 사용할 수 있다.
- @Embeddable 애노테이션을 넣어줌 ( 값을 정의하는 클래스 위에 )
- @Embedded : 값을 사용하는 곳에 넣어 줌
코드 예시는 다음과 같다.
@Embeddable
@Getter
public class Address {
private String city;
private String street;
private String zipcode;
protected Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
}
@Entity
@Getter @Setter
public class Member {
...
@Embedded
private Address address;
...
}
장점
- 재사용, 높은 응집도, Period.isWork() 처럼 해당 값 타임만 사용하는 의미 있는 메소드를 만들 수 있음.
- 임베디드 타입을 포함한 모든 값 타입은 값타임을 소유한 엔티티에 생명주기를 의존함
주의 해야할 점은, 임베디드 타입은 엔티티의 값을 뿐이라는 것이다. 임베디드 타입을 사용한다고해서 Member 에 매핑되는 테이블의 속성이 달라지는 것은 아니다. 그렇기 때문에 잘 설계한 orm 애플리케이션은, 테이블 수보다 엔티티 클래스 수가 더 많다.
4. @AttributeOverrides, @AttributeOverride
한 엔티티안에서 동일한 값을 사용하고 싶다면 @AttributeOverrides 와 @AttributeOverrid 를 사용하면 된다.
예를 들어 Member 안에 동일한 Address 타입을 두번 사용하고 싶을 때 다음과 같이 사용할 수 있다.
@Embedded
@AttributedOverride(name = "city", column = @Column(name = "work_city"))
private Address adress;
@Embedded
private Address adress2;
5. 값 타입과 불변객체
기본적으로 값 타입은 복잡한 객체 세상을 조금이라도 단순화 하려고 만든 개념이므로, 단순하고 안전하게 다룰 수 있어야 한다.
임베디드 타입과 같은 값 타입을 공유하게 되면 위험하므로 값을 복사해서 사용한다.
아래 코드는 두 객체 member1, member2 에 대하여 동일한 address 값 타입을 공유하는 경우에 해당한다.
Address address = new Address( "city","street", "zipcode");
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.getHomeAddress().setCity("newCity");
tx.commit();
값 타입을 공유하게 되면 member 에 있는 city 값만 newCity 로 바뀌는게 아니라, member2 의 city 도 newCity 로 바뀐다는것을 알 수 있다. 이렇게 member 에 대한 newcity 만 바꾸길 원했지만 원하지 않는 결과 ( member2 의 city 도 newCity 로 바뀜 ) 가 나올 수 있기 때문에 값 타입의 공유는 처음부터 하지 않는게 좋다.
값타입을 공유하지 않고 복사를 사용하면 어떻게 될까? 다음 예제에서는 address 를 복사해서 작성했다.
Address address = new Address( "city","street", "zipcode");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());
Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress);
em.persist(member2);
member.getHomeAddress().setCity("newCity");
tx.commit();
copyAddress 를 통해 address 를 복사 했다. 이렇게 사용하면 member 의 city 값에 대해서만 newCity 로 바꿀 수 있다.
그런데 이렇게 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입에 해당하므로, 근본적으로 공유 참조가 해결 된 것은 아니다. ( 객체는 참조가 공유 되는 것을 막을 수는 없다. )
그렇다면 객체타입을 수정 할 수 없게 만들순 없을까 ? 그럼 부작용을 원천 차단 할 수 있으니까! 위에서 공유 참조가 발생한 원인이라고하면, newCity 라는 값을 변경하려고 한 것 부터 시작했기 때문에, 값 변경(수정) 자체를 막아 버리면 해결 될 것 처럼 보인다.
맞다. 값 타입은 불변 객체로 설계 해야한다. 같은 맥락에서 생성자로만 값을 설정하고 setter 를 따로 두지 않아야 한다.
참고로 Integer 과 String 은 자바가 제공하는 가장 대표적인 불변 객체 이다.
- 불변 객체란? 생성 시점 이후 절대 값을 변경할 수 없는 객체를 의미한다.
즉 member2.setHomeAddress(copyAddress); 에서 setHomeAddress 자체를 제거하거나 private 로 선언해서 사용하도록 하면 불변객체로 설계하게 되는 것이다. 이를 통해 공유참조 문제를 해결 할 수 있다.
번외로, 불변객체로 선언했는데 값을 바꾸고 싶을때는 다음 코드 처럼 생성자를 이용해서 바꿀 수 있다.
Address address = new Address( "city","street", "zipcode");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress);
5. 값 타입의 비교
- 동일성 : == 을 사용함 ( 인스턴스 참조값을 비교 )
- 동등성 : equals 를 사용함 ( 인스턴스 내의 실제 값을 비교 )
값 타입의 비교는 인스턴스의 실제 값을 비교해야 한다. 따라서 equals 를 사용 해야한다. equals 구현시 hashcode() 도 함께 구현하도록 하자.
@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(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
6. 컬렉션 값 타입과 @ElementCollection, @CollectionTable
들어가기 전에, 실무에서는 값 타입 컬랙션 대신에 일대다 관계를 고려하는게 낫다. 엄청 단순한걸 구현할 때나 사용한다고 보면 된다.
본론으로 돌아가서, 컬렉션 값 타입은 자바 컬렉션에 임베디드나 기본타입을 넣는것을 의미한다. 즉 값 타입을 하나이상 저장할 때 사용한다.
실제 코드 작성시에는 @ElementCollection, @CollectionTable 을 사용할 수 있다.
@Embeded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "favorite_foods", 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<>();
값 타임 컬렉션은 영속성전에 cascade + 고아 객체 제거기능을 필수로 가진다 . 또한 지연 로딩에 해당한다.
값 타입 컬렉션 제약 사항
- 값 타임 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터 값을 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. ( null 입력과 중복저장을 하지않는다.)