JPA의 데이터 타입을 크게 분류하면 엔티티 타입과 값 타입으로 나눈다.
엔티티 타입은 @Entity
로 정의하는 객체이고, 값 타입은 int
, Integer
, String
처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
값 타입은 3가지로 나눌 수 있다.
기본 값 타입은 말그대로 자바가 제공하는 기본 데이터 타입이고, 임베디드 타입은 JPA에서 사용자가 직접 정의한 값이다. 컬렉션 타입은 하나 이상의 값 타입을 저장할 때 사용한다.
기본은 다뤘으므로 생략하도록 하겠다.
직접 정의한 임베디드 타입도 int, String 처럼 값 타입이다.
공통적으로 쓰는 어떤것(ex - 시간, 주소)들을 엔티티 클래스 마다 그대로 가지고 있으면 객체지향적이지 않으며 응집력이 떨어진다. 이런 공통 타입이 생기면 더 명확해진다.
임베디드 타입을 사용하려면 2가지 어노테이션이 필요하다.
둘 중 하나는 생략해도 된다.
@Embeddable
: 값 타입을 정의하는 곳에 표시@Embedded
: 값 타입을 사용하는 곳에 표시임베디드 타입은 엔티티의 값일 뿐이다. 따라서 값이 속한 엔티티의 테이블에 매핑한다.
이 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는것이 가능하기 때문에 잘 설계된 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많다.
Mybatis로 개발을 한다면 테이블, 객체 1:1매핑을 한다. 그렇기에 객체지향으로 개발하려고 해도 이미 SQL에 너무나 의존적인 개발을 진행했기에 여러 클래스를 매핑하는 작업이 수월하지 않았다.
ORM인 JPA를 사용하면 귀찮은 반복 작업은 JPA에게 할당하고 모델을 설계하는데 집중할 수 있다.
이 기능으로 연관된 테이블은 모조리 @Embedded
로 묶어 사용하는 그림이 그려진다❗️
만약 주소가 집주소 그리고 회사 주소가 있다고 가정할때 클래스는 똑같은데 컬럼 값을 다르게 줘야한다면 속성을 재정의해서도 값을 줄수가 있다.
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Address {
@Column(name = "city")
private String city;
private String street;
private String zipcode;
}
@Embedded
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "COMPANY_CITY")),
@AttributeOverride(name = "street", column = @Column(name = "COMPANY_STREET")),
@AttributeOverride(name = "zipcode", column = @Column(name = "COMPANY_ZIPCODE")),
})
private Address companyAddress;
이렇게해서 재정의를 해주면 companyAddress
에는 override한 @Column
들이 매핑되게 된다.
그래서 SQL 쿼리문은 아래와 같이 출력된다.
여기서 column이 소문자로 나온 이유는 내가 JPA Buddy로 column설정을 무조건 언더바에 lowerCase로 나오게해서 그렇다.
설정이 없다면 대문자로 나오게 될 것이다.
이런식으로 공통적으로 쓰는것은 저번 강의에서 봤던 @MappedSuperClass
와 같이 사용한다면 시너지가 극대화 될 것이라고 생각한다❗️
임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.
member.setAddress(null); //null
em.persist(member);
멤버 테이블의 주소와 관련된 값은 모두 null이 된다.
값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다.
member1.setHomeAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();
address.setCity("NewCity"); //멤버1의 address 공유
member2.setHomeAddress(address);
이렇게 update를 하게되면 멤버2만 NewCity로 변경이 되는 것이 아니라 멤버1의 주소도 NewCity로 변경된다. 이것은 영속성 컨텍스트가 멤버1, 2 둘 다 city 속성값이 변경된 것으로 생각하기 때문에 둘다 update 쿼리를 날리게 된다.
이런식으로 예상치 못한 곳에서 문제가 발생하는 것은 부작용이라고 한다. 이 부작용을 막기 위해선 값을 복사해서 사용하면 된다.
값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
자바 객체는 CallByReference 이기 때문에 참조값을 전달한다.
clone()
이 아니라 예를 들어
Address a = new Address("주소1");
Address b = a;
b.setCity("주소테스트");
이렇게 b Address에 a가 참조하는 인스턴스의 참조값 자체를 b에 넘기면 둘은 같은 인스턴스를 공유참조 한다. 이렇게되면 a의 city값도 변하게 되는 것이다.
인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있는데 복사하지 않고 원본 참조 값을 직접 넘기는 실수를 완전하게 막을 수는 없다. 그래서 객체의 공유 참조는 피할 수 없다.
책에서 해결책은 setter메소드를 모두 제거하면 된다고한다. 제거하면 부작용의 발생을 막을 수 있다.
객체를 불변하게 만들면 값을 수정할 수 없다. 그렇기에 부작용 원천 차단이 가능하다.
따라서 값 타입은 될 수 있으면 불변 객체로 설계해야 한다.
불변 객체의 값은 조회할 수 있지만 수정할 수 없다. 이 불변 객체도 객체기에 참조값 공유를 피할 수는 없지만 수정이 불가능하므로 부작용이 발생할 우려는 없다.
이런데에서 깨달은 것이 바로 생성자에서 값을 할당하는 것이다.
@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Address {
private String city;
public Address(String city) {
this.city = city;
}
}
멤버1의 주소값을 조회하여 새로운 주소 생성
Address address = member1.getHomeAddress();
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);
이렇게 해서 불변이라는 작은 제약조건으로 부작용이라는 에러를 막을 수 있다.
이것은 너무나 잘 알고들 있을거라고 생각한다.
==
사용equals()
사용엔티티 타입과 값 타입의 특징은 다음과 같다.
생성 → 영속화 → 소멸
의 주기가 있다.em.persist(entity)
로 영속화em.remove(entity)
로 제거엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만드는 실수는 범하지 말자!!!