기본값 타입
JPA의 데이터 타입 분류
엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값 타입
- int, Integer, String처럼 단순히 값으로 사용되는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
값 타입 분류
기본 값 타입
- 자바 기본 타입(int, double)
- 래퍼 클래스(Integer, Long)
- String
임베디드 타입(embedded type, 복합 값 타입)
컬렉션 값 타입(collection value type)
절대 기본 값 타입을 공유해선 안 돼.
- int, double같은 기본 타입은 공유 자체가 되지 않는다.
- Integer나 String같은 특수 클래스는 공유 가능한 객체기 때문에 변경 불가능하게 설계해야한다.
임베디드 타입(복합 값 타입)
- 주로 기본 값 타입을 모아서 만들기 때문에 복합 값 타입이라고도 불린다.
- ex) 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다고 하자.
임베디드 타입 사용법
- @Embeddable: 값 타입을 정의하는 곳에 표시
- @Embedded: 값 타입을 사용하는 곳에 표시
- 기본 생성자 필수!
임베디드 타입 장점
- 재사용성이 높다.
- 높은 응집도.
- Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티의 생명주기에 의존한다.
임베디드 타입과 테이블 매핑
![](https://blog.kakaocdn.net/dn/ylLql/btsjPLiRaYg/YYp66LCFIyWqKcZQrOWU0K/img.png)
- 임베디드 타입은 엔티티의 값일 뿐이다. 테이블은 전혀 변하지 않는다.
- 다만, 객체와 테이블을 세밀하게 매핑하는 것이 가능하다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많다.
임베디드 타입과 연관관계
![](https://blog.kakaocdn.net/dn/dLx0HT/btsjPExc377/3KPaPSqdfS3tRI04aKm540/img.png)
- 이렇게 임베디드 타입이 Entity를 가져서 연관관계 매핑을 할 수도 있다.
- 테이블은 변하지 않으므로 FK를 갖는 것과 동일하다.
속성 재정의: @AttributeOverride
- 한 엔티티에서 같은 값 타입을 여러개 사용할 경우 컬럼 명이 중복 된다.
- 이런 경우에는
@AttributeOverrides
,@AttributeOverride
를 사용해서 컬럼명 속성을 재정의할 수 있다.
값 타입과 불변 객체
- 값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
- 따라서, 값 타입은 단순하고 안전하게 다룰 수 있어야한다.
값 타입 공유 참조
![](https://blog.kakaocdn.net/dn/tVeJE/btsjOnC7Eto/E1RhbukjRFz6mAild3jSpk/img.png)
- 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 “side effect”가 발생하기 때문에 위험하다.
![](https://blog.kakaocdn.net/dn/bQjSv7/btsjQjl6BW1/1Ipc9qt78pVABkRSaG1Xi0/img.png)
- 따라서 위와 같이 엔티티를 공유하는 것이 아니라 복사하여 새로운 객체를 만들어 주어야한다.
객체 타입의 한계
// 기본 타입(primitive type)
int a = 10;
int b = a; // 값을 복사
b = 4;
// 객체 타입
Address a = new Address("Old");
Address b = a; // 참조를 전달
b.setCity("New");
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용(side effect)를 피할 수 있다.
- 문제는, 임베디드 타입처럼 직접 정의한 값 타입은 지바의 기본 타입이 아니라 객체 타입이다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 즉, 공유 참조를 피할 수 없다.
불변 객체
- 따라서, 객체 타입을 수정할 수 없는 불변 객체(imuuitable object)로 설계해야한다.
- 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성자로만 값을 설정하고, 수정자(
setter
)를 만들지 않으면 된다.
- 만약 값을 수정하고 싶다면, 새로 생성하여 변경해야한다.
- 참고: Integer, String을 자바가 제공하는 대표적인 불변 객체.
값 타입의 비교
- 값 타입은 인스턴스가 달라도 그 값이 같으면 같은 것으로 보아야한다.
int a = 10;
int b = 10;
a == b;
// true
Address a = new Address("서울 시");
Address b = new Address("서울 시");
a == b;
// false
- 동일성 비교: 인스턴스의 참조 값을 비교(== 사용)
- 동등성 비교: 인스턴스의 값을 비교(
equals()
사용)
- 값 타입은
a.equals(b)
를 사용해서 동등성을 비교해야 한다.- 따라서, 값 타입의
equals()
메소드를 적절하게 재정의해야 한다.(주로 모든 필드 사용)
- 따라서, 값 타입의
값 타입 컬렉션
![](https://blog.kakaocdn.net/dn/c7PIjn/btsjO86JL18/KybUgAl0t4LMYHqeOIEAHK/img.png)
- 컬렉션에 엔티티가 아니라 값 타입을 넣어서 쓰는 경우를 값 타입 컬렉션이라고 한다.
- 이 경우 일대다로 별도의 테이블로 뽑아야한다.
- 뿐만 아니라, 엔티티가 아니기 때문에 별도의 PK를 사용하는 것이 아니라, 모든 컬럼을 PK로 사용해야 한다.
값 타입 컬렉션 사용
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID")
)
@Column(name = "FOOD_NAME")
private Set<String> favoritFoods = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressHistory = new ArrayList<>();
@ElementCollection
,@CollectionTable
로 사용할 수 있다.
값 타입 저장 예제
ExMember member = new ExMember();
member.setName("member1");
member.setHomeAddress(new Address("city1", "street", "10001"));
member.getFavoritFoods().add("치킨");
member.getFavoritFoods().add("족발");
member.getFavoritFoods().add("피자");
member.getAddressHistory().add(new Address("old1", "street", "10001"));
member.getAddressHistory().add(new Address("old2", "street", "10001"));
em.persist(member);
값 타입 조회 예제
ExMember findMember = em.find(ExMember.class, member.getId());
- 기본 적으로 값 타입 컬렉션도 지연 로딩이 적용되어 있다(일대다이므로).
- 값 타입 컬렉션이 아니라 임베디드 값 타입은 즉시 로딩 된다.
값 타입 수정 예제
ExMember findMember = em.find(ExMember.class, member.getId());
findMember.setHomeAddress(new Address("newcity", "street", "10002"));
/*
update
ExMember
set
city=?,
street=?,
zipcode=?,
name=?,
endDate=?,
startDate=?
where
id=?
*/
findMember.getFavoritFoods().remove("치킨");
findMember.getFavoritFoods().add("한식");
/*
delete
from
FAVORITE_FOOD
where
MEMBER_ID=?
and FOOD_NAME=?
*/
/*
insert
into
FAVORITE_FOOD
(MEMBER_ID, FOOD_NAME)
values
(?, ?)
*/
findMember.getAddressHistory().remove(new Address("old1", "street", "10001"));
findMember.getAddressHistory().add(new Address("new1", "street", "10001"));
/*
delete
from
ADDRESS
where
MEMBER_ID=?
*/
/*
insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
*/
/*
insert
into
ADDRESS
(MEMBER_ID, city, street, zipcode)
values
(?, ?, ?, ?)
*/
- 값 타입은 모두 immutable이기 때문에 위와 같이 새로운 인스턴스를 만들어서 할당해주어야 한다.
- 임베디드 값 타입은 update쿼리가 날라간다.
- 값 타입 컬렉션은 delete, insert쿼리 2개가 날라간다.
- 특히!
addressHistory
쪽에서는 컬렉션 전체를 delete한 뒤 다시 모두 insert하는 것을 볼 수 있다.
- 특히!
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없기 때문에 추적하기 어렵다.
- 특히, 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 관련된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 다시 모두 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 PK로 구성해야한다(null 입력 불가능, 중복 저장 불가능).
값 타입 컬렉션의 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려해야한다.
@Entity
@Table(name="ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
@Embedded
private Address address;
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
}
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
- 이렇게 일대다를 위한 테이블을 생성하고, “일”쪽에 FK 주인을 걸어주는 단방향 매핑을 해주면 된다.
- “다”쪽에 걸어주지 않는 이유는,
AddressEntity
가 주인이 아니라Member
가 주인이기 때문이다.
- 같은 이유로,
cascade = CascadeType.
ALL
, orphanRemoval = true
설정해준다.
- “다”쪽에 걸어주지 않는 이유는,
값 타입 컬렉션을 사용해야할 때
- 정말 정말 정말 단순할 때
- ex) select box에 “치킨”, “피자”, “한식”이 있을 때 무엇을 선택했는지 확인 하기 위해서 필요할 때
- 위 예제 처럼 추적이 필요하지도 않고, update가 된다고 해도 update쿼리가 필요없을 때 사용한다.
- enum의 대체적인 방법으로 사용하자!
정리
- 엔티티 타입의 특징
- 식별자 있음.
- 생명 주기를 관리함.
- 공유 가능함.
- 값 타입의 특징
- 식별자 없음.
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불벽 객체로 만드는 것이안전
- 값 타입은 정말 값 타입이라 판단될 때만 사용(포지션(x, y))
- 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안 됨.
- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티
Uploaded by N2T
(23.06.12 10:36)에 작성된 글 입니다.