JPA의 데이터 타입 분류
엔티티 타입
@Entity
로 정의하는 객체- 데이터가 변해도 식별자로 지속해서 추적 가능
- ex. 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
값 타입
int
,Integer
,String
처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체- 식별자가 없고 값만 있으므로 변경 시 추적 불가
- ex. 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
기본값 타입
종류
- 자바 기본 타입 (
int
,double
..) - 래퍼 클래스 (
Integer
,Long
...) String
특징
- 생명주기를 엔티티의 의존 (ex. 회원을 삭제하면 이름, 나이 필드도 함께 삭제)
- 값 타입은 공유하면 안된다. (ex. 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안된다. 부작용(=side effect))
※ 자바의 기본 타입int
,double
같은 기본 타입(primitive type
)은 항상 값을 복사하며 절대 공유되지 않고,Integer
같은 래퍼 클래스나String
같은 특수한 클래스도 공유 가능한 객체(참조값을 공유)이지만 변경할 수 없다.
임베디드 타입
특징
- 새로운 값 타입을 직접 정의할 수 있다.
- JPA는 임베디드 타입(
embedded type
)이라 한다. - 주로 기본값 타입을 모아 만들어서 복합값 타입이라고 한다.
int
,String
과 같은 값 타입
장점
- 재사용이 용이하다.
- 높은 응집도를 가진다.
- 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존한다.
테이블 매핑
- 임베디드 타입은 엔티티의 값일 뿐이다. (값이
NULL
이면 매핑한 컬럼 값 모두NULL
) - 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
- 객체와 테이블을 아주 세밀하게(
find - grained
) 매핑하는 것이 가능하다. - 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
연관관계
임베디드 타입 클래스 내에 엔티티가 속할 수 있다.
사용 방법
@Embeddable
: 값 타입을 정의하는 곳에 해당 어노테이션 사용@Embedded
: 값 타입을 사용하는 곳에 해당 어노테이션 사용- 임베디드 타입을 선언한 클래스에는 기본 생성자를 필수로 가져야 한다.
예제
회원 엔티티는 이름, 근무기간, 집주소를 가진다.
구조
테이블 매핑
소스
- Member.java
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// 기간
@Embedded // 임베디드 타입 : 값 타입 사용 어노테이션 (생략 가능)
private Period workPeriod;
// 주소
@Embedded // 임베디드 타입 : 값 타입 사용 어노테이션 (생략 가능)
private Address homeAddress;
// getter, setter
}
- Period.java
@Embeddable // 임베디드 타입 : 값 타입 정의 어노테이션
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
// getter, setter
}
- Address.java
@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;
}
// getter, setter
}
- JpaMain.java
// 임베디드 타입 예시
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(new Address("city", "street", "100-1"));
member.setWorkPeriod(new Period());
em.persist(member);
동작 결과
(1) Table DDL 결과
(2) Insert SQL 및 결과
@AttributeOverride (속성 재정의)
- 한 엔티티에서 같은 값 타입을 사용하기 위한 어노테이션
- 그냥 같은 값 타입을 사용하게 되면 컬럼명 중복 문제 발생
@AttributeOverrides
,@AttributeOverride
를 사용하여 컬럼명 재정의
속성 재정의 예제
소스
- Member.java
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
// 기간
@Embedded // 임베디드 타입 : 값 타입 사용 어노테이션 (생략 가능)
private Period workPeriod;
// 주소
@Embedded // 임베디드 타입 : 값 타입 사용 어노테이션 (생략 가능)
private Address homeAddress;
// 주소
@Embedded // 임베디드 타입 : 값 타입 사용 어노테이션 (생략 가능)
// 속성 재정의
@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;
// getter, setter
}
동작 결과
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념으로 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
값 타입 공유 참조
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. (side effect 발생)
예시
// JpaMain.java
Address address = new Address("city", "street", "100-1");
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(address);
em.persist(member);
Member member2 = new Member();
member2.setUsername("userB");
member2.setHomeAddress(address);
em.persist(member2);
// 참조 예시. member, member2 전부 new City로 변경된다.
member.getHomeAddress().setCity("new City");
값 타입 복사
값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하기 때문에 값(인스턴스)를 복사하여 사용한다.
예시
// JpaMain.java
Address address = new Address("city", "street", "100-1");
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(address);
em.persist(member);
// 값 타입 복사 예시(Address 복사)
Address copyAddress = new Address("city", "street", "100-1");
Member member2 = new Member();
member2.setUsername("userB");
member2.setHomeAddress(copyAddress); // 복사한 인스턴스로 변경
em.persist(member2);
// member의 Address 정보만 변경된다.
member.getHomeAddress().setCity("new City");
※ 기존 인스턴스 값 변경 시에도 복사하여 사용한다.
// 기존 Address
Address address = new Address("city", "street", "100-1");
// address 내 값을 바꾸고 싶을때도
// 해당 인스턴스를 복사하여
// 필요한 부분의 값만 바꿔 생성자를 통해 새 인스턴스를 만들어 바꿔야 한다.
Address newAddress = new Address("New City", address.getStreet(), address.getZipcode());
객체 타입의 한계
- 임베디드 타입처럼 직접 정의한 값 타입은 자바 기본 타입이 아닌 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 복사를 하지만 객체 타입은 참조값을 전달하기 때문에 값을 공유하게 되어 추후 값 변경 시 영향을 받게 된다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없어 객체의 공유 참조는 피할 수 없다.
불변 객체
- 생성 시점 이후 절대 값을 변경할 수 없는 객체로 생성자로만 값을 설정하고 수정자(
setter
)를 만들지 않는 객체 - 객체 타입을 수정할 수 없도록 만들어 부작용(side effect)을 원천 차단하기 때문에 값 타입은 불변 객체(
immutable object
)로 설계해야 한다. - ex.
Integer
,String
값 타입 비교
값 타입은 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야한다.
동일성 비교 vs 동등성 비교
- 동일성(
identity
) 비교 : 인스턴스의 참조값을 비교 ('=='
사용) - 동등성(
equivalence
) 비교 : 인스턴스의 값을 비교 (equals()
사용) - 값 타입은
equals()
메소드를 사용해서 동등성 비교를 해야하며equals()
메소드를 적절하게 재정의해서 사용해야 한다. (주로 모든 필드 사용)
예제
/** ValueMain.java */
// 자바 기본 타입 비교 (== 사용)
int a = 10;
int b = 10;
System.out.println("a == b : " + (a==b)); // true
// 값 타입 비교 (equals() 사용)
Address address1 = new Address("city", "street", "zipcode");
Address address2 = new Address("city", "street", "zipcode");
// equals() 기본 동작이 == 비교이기 때문에 false
// Address 클래스 내에서 equals() 상속받아 재정의하여 동작시 true
System.out.println("address1 == address2 : " + (address1.equals(address2)));
/** Address.java */
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;
}
// equals() 재정의
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Address address = (Address) obj;
return (Objects.equals(city, address.city)
&& Objects.equals(street, address.street)
&& Objects.equals(zipcode, address.zipcode));
}
// equals() 재정의 시 hashCode()도 재정의 필요
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
값 타입 컬렉션
- 관계형 데이터베이스는 테이블 내에 컬렉션을 담을 수 있는 구조가 없기 때문에 별도의 테이블이 필요하다.
- 값 타입을 하나 이상 저장할때 사용한다.
@ElementCollection
,@CollectionTable
어노테이션을 사용하여 구현한다.
값 타입 컬렉션 사용 예제
구조
저장 예제
- Member.java
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Embedded
private Period workPeriod;
// 주소
@Embedded
private Address homeAddress;
@ElementCollection // 컬렉션 지정
@CollectionTable(name = "FAVORITE_FOOD", joinColumns = @JoinColumn(name = "MEMBER_ID")) // 컬렉션 테이블
@Column(name = "FOOD_NAME") // 테이블 내 외래키 제외 1개의 컬럼일 경우 예외적으로 사용
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection // 컬렉션 지정
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID")) // 컬렉션 테이블
private List<Address> addressHistory = new ArrayList<>();
// getter, setter
}
- JpaMain.java
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("햄버거");
member.getFavoriteFoods().add("아이스크림");
member.getAddressHistory().add(new Address("oldCity1", "street", "zipcode"));
member.getAddressHistory().add(new Address("oldCity2", "street", "zipcode"));
em.persist(member); // 값 타입은 생명주기가 없기 때문에 Member의 생명주기를 따라간다.
- Table DDL SQL
- Insert SQL 및 결과
조회 예제
- Member.java
는 이전 예제와 동일
- JpaMain.java
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("햄버거");
member.getFavoriteFoods().add("아이스크림");
member.getAddressHistory().add(new Address("oldCity1", "street", "zipcode"));
member.getAddressHistory().add(new Address("oldCity2", "street", "zipcode"));
em.persist(member); // 값 타입은 생명주기가 없기 때문에 Member의 생명주기를 따라간다.
em.flush();
em.clear();
System.out.println("===== find Member Start =====");
Member findMember = em.find(Member.class, member.getId()); // Member 정보만 조회한다. (지연로딩)
System.out.println("===== find AddressHistory Start =====");
List<Address> addressHistory = findMember.getAddressHistory(); // 강제 호출, DB 조회
for(Address address : addressHistory) {
System.out.println("address.city : " + address.getCity());
}
System.out.println("===== find FavoriteFoods Start =====");
Set<String> favoriteFoods = findMember.getFavoriteFoods(); // 강제 호출, DB 조회
for(String food : favoriteFoods) {
System.out.println("favorite food : " + food);
}
- Select SQL 및 결과
※ 값 타입 컬렉션도 지연로딩 전략 사용
수정 예제
- Member.java
는 이전 예제와 동일
- JpaMain.java
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("햄버거");
member.getFavoriteFoods().add("아이스크림");
member.getAddressHistory().add(new Address("oldCity1", "street", "zipcode"));
member.getAddressHistory().add(new Address("oldCity2", "street", "zipcode"));
em.persist(member); // 값 타입은 생명주기가 없기 때문에 Member의 생명주기를 따라간다.
em.flush();
em.clear();
System.out.println("===== find Member Start =====");
Member findMember = em.find(Member.class, member.getId());
// (1) homeCity -> newCity (homeAddress)
// 값 타입 수정은 단순 값 수정이 아닌 새로운 인스턴스로 변경해야 한다.
// (단순 값 수정은 side effect 발생)
Address oldAddress = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", oldAddress.getStreet(), oldAddress.getZipcode()));
// (2) 치킨 -> 한식
// 값 타입 컬렉션 수정은 기존 값을 지우고 새로 추가해야 한다.
// 컬렉션의 값만 변경해도 JPA가 알아서 insert, update, delete 쿼리를 실행한다.
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");
// (3) oldCity1 -> newCity1 (addressHistory)
// 값 타입 컬렉션이 수정되면 주인 엔티티와 관련된 모든 데이터를 삭제하고
// 값 타입 컬렉션에 현재 남은 값을 다시 insert 한다.
findMember.getAddressHistory().remove(new Address("oldCity1", "street", "zipcode"));
findMember.getAddressHistory().add(new Address("oldCity1", "street", "zipcode"));
- Update SQL
※ 값 타입 컬렉션은 영속성 전이 + 고아객체 제거 기능을 필수로 가진다고 볼 수 있다.
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없어 값 변경시 추적이 어렵다.
- 값 타입 컬렉션에 변경 사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재값을 모두 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. (
NULL
허용 X, 중복 저장 X)
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 더 좋다.
- 일대다 관계를 위한 엔티티를 만들고 여기에서 값 타입을 사용한다.
- 영속성 전이 + 고아객체 제거를 사용해서 값 타입 컬렉션 처럼 사용한다.
예제
- Member.java
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@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<>();
// 값 타입 컬렉션 대안
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
// getter, setter
}
- AddressEntity.java
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
@Id @GeneratedValue
private Long id;
private Address address;
public AddressEntity() {}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city, street, zipcode);
}
// getter, setter
}
- JpaMain.java
Member member = new Member();
member.setUsername("userA");
member.setHomeAddress(new Address("homeCity", "street", "zipcode"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("햄버거");
member.getFavoriteFoods().add("아이스크림");
// 값 타입 컬렉션 대안
member.getAddressHistory().add(new AddressEntity("oldCity1", "street", "zipcode"));
member.getAddressHistory().add(new AddressEntity("oldCity2", "street", "zipcode"));
em.persist(member); // 값 타입은 생명주기가 없기 때문에 Member의 생명주기를 따라간다.
em.flush();
em.clear();
System.out.println("===== find Member Start =====");
Member findMember = em.find(Member.class, member.getId());
// 값 타입 컬렉션 대안
findMember.getAddressHistory().remove(new AddressEntity("oldCity1", "street", "zipcode"));
findMember.getAddressHistory().add(new AddressEntity("oldCity1", "street", "zipcode"));
- 동작 결과
정리
- 엔티티 타입은 식별자가 존재하며 생명 주기를 관리할 수 있으며 공유를 할 수 있다.
- 값 타입은 식별자가 없고 생명 주기를 엔티티에 의존하기 때문에 공유하지 않는 것이 안전하지만 공유될 경우가 있어 불변 객체로 만든다.
※ 값 타입은 정말 값 타입이라 판달될 때나 정말 간단할 때만 사용하며 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다. 식별자가 필요하고 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티이다.
'Dev > JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어 - 기본 문법 (1) (0) | 2021.09.07 |
---|---|
[JPA] 실습 - 값 타입 매핑 (0) | 2021.09.02 |
[JPA] 실습 - 연관관계 관리 (0) | 2021.08.27 |
[JPA] 프록시와 연관관계 관리 (0) | 2021.08.27 |
[JPA] 실습 - 상속관계 매핑 (0) | 2021.08.25 |
댓글