[김영한님의 자바 ORM 표준 JPA 프로그래밍을 학습 후 정리한 내용입니다.]
순서
- 기본값 타입
- 임베디드 타입
- 값 타입과 불변 객체
- 값 타입의 비교
- 값 타입 컬렉션
🥹서론
JPA에서 데이터 타입은 크게 두가지로 나뉘어진다.
1) 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능 ex) 학생 엔티티의 나이 값을 변경해도 식별자로 인식 가능
2) 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가 ex) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
- 값 타입은 기본값 타입(int, double), 임베디드 타입, 컬렉션값 타입으로 나뉘어진다.
이 중 우리는 2)값 타입에 대해서 알아볼 것이다.
1. 기본값 타입
@Entity
public class Student {
@Id
private Long id;
@Column(name = "name")
private String name;
@Column(name = "age")
private int age;
}
위와 같이 학생 엔티티의 값 타입인 name, age 속성은 식별자 값도 없고, 생명 주기도 학생 엔티티에 의존한다. 따라서 학생 엔티티 인스턴스를 제거하면, name과 age값은 제거된다.
또한, 값 타입은 공유하면 안된다. 예를 들어 학생 이름 변경 시 다른 학생 이름도 함께 변경되면 안되기 때문이다.
※참고 - 자바의 기본 타입은 절대 공유하지 못한다. 기본 타입은 항상 값을 복사하기 때문에 int, double과 같은 기본 타입은 공유할 수 없다. Integer와 같은 래퍼클래스나 String 같은 특수한 클래스는 공유가 가능하지만 변경할 수 없다.
2. 임베디드 타입
- 새로운 값 타입을 직접 정의할 수 있는데 이를 임베디드 타입이라고 한다.
- 주로 기본 값 타입을 모아서 만들기 때문에 복합 값 타입이라고도 한다.
예를 들면, 회원 엔티티에서 시간을 나타내는 STARTDATE와 ENDDATE 필드를 묶어서 Peroid라는 embedded 클래스로 활용할 수 있으며, 주소를 나타내는 CITY, STREET, ZIPCODE 필드를 Address라는 embedded 클래스로 하나로 묶어서 활용이 가능하다.
*임베디드 타입의 특징
- 임베디드 타입으로 만들면, 보다 객체 지향적으로 모델링했기 때문에 재사용성이 좋으며 높은 응집도를 보여준다.
- Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
- 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.
[사용 예시]
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Embedded
private Period period;
@Embedded
private Address address;
}
@Embeddable
public class Period{
private LocalDateTime startDate;
private LocalDateTime endDate;
}
@Embeddable
public class Address{
private String city;
private String street;
private String zipcode;
}
3. 값 타입과 불변 객체
임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험할 수 있다. 부작용(side effect)이 발생한다.
아래 코드와 같이 동일한 address 인스턴스를 공유하고 있는 member객체가 있다고 할때, member2의 주소를 변경하면 어떻게 될까?
Address address = new Address("oldCity","street","100");
Member member = new Member();
member.setHomeAddress(address);
Member member2 = new Member();
member2.setHomeAddress(address);
member2.getHomeAddress().setCity("newCity"); //member2의 주소만 변경
우리가 원하는 바와 달리 member의 주소 역시 newCity로 변경된다. 임베디드 타입은 자바의 기본 타입이 아닌 객체 타입이기 때문이다. 아래는기본 타입과 객체 타입을 비교한 그림이다. 객체 타입은 참조를 전달하기 때문에 값이 변경된다.
따라서 객체 타입을 수정할 수 없게 만들려면 부작용을 원천 차단해야 한다. → 값 타입은 불변 객체로 설계해야 한다.
불변 객체는 생성 이후 절대 값을 변경할 수 없는 객체로, 생성자로만 값을 설정한 뒤, setter를 만들지 않는 방법으로 만들 수 있다.
따라서 불변 객체를 활용해서 위 예제의 member2의 주소만을 변경하기 위해서는 1) Address를 불변 객체로 만듦 2)공유하지 않고 새로운 객체를 만들어서 대입하는 방법으로 해결할 수 있다.
1) Address를 불변 객체로 만듦
@Embeddable
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;
}
//setter 삭제
}
2)공유하지 않고 새로운 객체를 만들어서 대입
Address address = new Address("oldCity","street","100");
Member member = new Member();
member.setHomeAddress(address);
Address newAddress = new Address("newCity","street","100"); //새로운 객체 생성
Member member2 = new Member();
member2.setHomeAddress(newAddress);
4. 값 타입의 비교
자바가 제공하는 객체 비교는 2가지 방법이 있다.
- 동일성 비교 : 인스턴스의 참조 값을 비교 ==사용
- 동등성 비교 : 인스턴스의 값을 비교 equals() 사용
동일성은 서로 다른 인스턴스이므로 결과는 거짓이지만, 동등성의 경우 참이라는 결과를 반환하기 위해 equals()메서드를 제정의 해야 한다.
※참고 - equals()와 hashCode()를 재정의 할 때는 자바에서 기본적으로 제공하는 코드로 충분하다. 또한, equals()에 더불어서 해쉬를 사용한 컬렉션이 정상적으로 조회할 수 있게 하려면 hashCode()도 재정의가 필요하다.
5. 값 타입 컬렉션
값 타이블 하나 이상 저장할 때 사용한다. 우리가 이전에 만들었던 임베디드 값 타입인 Address를 여러개 저장할 때도 사용할 수 있고, 기본 값 타입으로 예를 들어 Set<String>으로 저장할 수 있다.
하지만 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없기 때문에, 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.
[값 타입 컬렉션 사용 예시]
Member member = new Member();
//임베디드 값 타입
member.setHomeAddress(new Address(”oldCity”, "street”' "100"));
//기본값 타입 컬렉션
member.getFavoriteFoods().add(”쌈뽕”);
member.getFavoriteFoods().add("짜장”);
member.getFavoriteFoods().add("탕수육,');
//임베디드 값 타입 컬렉션
member.getAddressHistory().add(new Address("seoul”, "street1","101"));
member.getAddressHistory().add(new Address("seoul", "street2", "102"》);
em.persist(member);
- 마지막에 member를 영속화했기 때문에, em.persist(member)호출로 총 6번의 INSERT SQL을 실행한다.
- 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거(ORPHAN REMOVE) 기능을 필수로 가진다.
[값 타입 컬렉션 수정 예시]
Member member = em.find(Member. class, IL);
// 1. 임베디드값 타입수정
member.setHomeAddress(new Address("newCity", "street", "100"));
// 2. 기본값 타입컬렉션수정
Set<String> favoriteFoods = member.getFavoriteFoods();
favoriteFoods.remove("탕수육");
favoriteFoods.add("치킨");
// 3. 임베디드값 타입컬렉션수정
List<Address> addressHistory = member.getAddressHistory();
addressHistory.remove(new Address("seoul", "street1", "101"));
addressHistory.add(new Address("newCity", "street1", "101"));ㅎ
- 임베디드 값 타입 수정의 경우 homeAddress 임베디드 값 타입은 MEMBER 테이블과 매핑했으므로 MEMBER 테이블만 UPDATE한다. (MEMBER 엔티티를 수정하는 것과 같다)
- 기본값 타입 컬렉션 수정의 경우 탕수육을 치킨으로 변경하려면 탕수육을 제거하고, 치킨을 추가해야 한다. String 타입은 수정할 수 없다.
- 임베디드 값 타입 컬렉션 수정의 경우 값 타입은 불변해야 한다. 따라서 컬렉션에서 기존 주소를 삭제하고, 새로운 주소를 등록했다. (Address의 eqauls(), hashCode()를 꼭 오버라이드 해야한다!)
값 타입 컬렉션의 제약사항
- 값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 값을 변경하면 추적이 힘들다.
- 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. 따라서 데이터베이스 기본 키 제약 조건으로 인해 컬럼에 null을 입력할 수 없고, 같은 값을 중복해서 저장할 수 없는 제약도 있다.
따라서 실무에서는 상황에 따라 값 타입 컬렉션 대신 일대다 관계를 고려해야 한다. 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용해야 한다. 이 경우, 영속성 전이(CASCADE) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용한다. ex)AddressEntity
정리
엔티티 타입의 특징
- 식별자 O
- 생명 주기 관리
- 공유
값 타입의 특징
- 식별자 X
- 생명 주기를 엔티티에 의존
- 공유하지 않는 것이 안전(복사해서 사용)
- 불변 객체로 만드는 것이 안전
'ORM > JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 (1) | 2023.04.24 |
---|---|
[JPA] 다양한 연관관계 매핑 (1) | 2023.04.24 |
[JPA] 연관관계 매핑 기초 (0) | 2023.04.24 |
[JPA] 엔티티 매핑 (0) | 2023.04.24 |
[JPA] OSIV 성능 최적화 정리 (0) | 2023.04.18 |
댓글