2023. 5. 22. 10:58ㆍBackEnd(Java)/JPA 개념
✅ 아래 내용들에 대해서 알아보자
- 기본값 타입
- 임베디드 타입
- 값타입 공유 참조
- 값타입 비교
- 컬렉션 값 타입
기본값 타입
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있
어야 한다. 값 타입을 이해하기 위해 간단한 예시 코드를 보자.
자바의 기본형 타입과 참조형 타입의 값 복사를 하는 과정인데, 기본형 타입은 각각 다른 메모리 주소공간을 가지게 되고 값을 복사하게 되므로 b값이 변하더라도 a에는 전혀 영향이 없게 된다.
반대로, 참조형 타입은 참조값을 대입(공유)하므로 둘 중 하나의 값이 변하게 되면 둘 다 변하게 되어 영향이 발생하게 된다.
(자바의 call by value, call by reference에 대해서 궁금하다면 이 글을 참조해 주세요 ㅎㅎ)
JPA에서는 테이터 타입 분류를 2가지로 구성한다
1. 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능(ex. 회원 엔티티 나이가 변해도 식별자로 인식 가능)
2. 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경 시 추적 불가(ex. 숫자 100 -> 200으로 변경)
- 값타입인 경우 equals로 값을 비교해야 한다.(동등성 비교)
- 기본값 타입
- 자바 기본타입(int, double)
- 래퍼 클래스(Integer, Long)
- String - 임베디드 타입(embedder type)
- 컬렉션 값 타입(collection value type)
임베디드 타입
JPA에서는 새로운 값 타입을 직접 정의해서 사용할 수 있는데, 이것을 임베디드 타입(복합값 타입)이라고 한다.
직접 정의한 임베디드 타입도 int, String처럼 값 타입이다. 실제 예제 코드를 같이 보자
Member 엔티티에 날짜와 관련된(startDate, endDate), 주소와 관련된(city, street, zipcode) 필드를 추가하였다.
필드 추가 후 실행시키면 DB에 반영이 잘 되는 것을 확인할 수 있다.
그런데 위의 멤버 엔티티처럼 구성하게 되면 단점이 생기는데, 다른 엔티티에도 날짜, 주소와 관련된 필드추가 하고 싶을 때
중복 코드가 발생하여 재사용성이 안 좋아지게 되고, 응집도가 낮아지며, 해당 값타입에 사용할 수 있는 의미 있는 메서드를 만들 수 없게 된다.
그렇다면, 이러한 단점들을 어떻게 해결할 수 있을까? 바로 임베디드 타입을 사용하는 것이다.
날짜와 관련된 Period, 주소와 관련된 Address 클래스를 만들고 @Embeddable을 사용해서 값 타입을 정의하는 곳이라고 표시를 하고 실제 값 타입을 사용하는 곳인 Member 엔티티에 가서 해당 값을 @Embedded를 통해 필드를 추가하면 된다.
임베디드 타입을 사용함으로써
- 재사용성이 높아지고
- 높은 응집도를 가지며
- Period.isWork()처럼 값 타입에 의미 있는 메서드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명 주기를 의존하게 되어 관리가 편해진다.
하지만, 임베디드 타입을 사용할 때 주의사항이 있다.
- 기본 생성자가 필수이다. -> JPA에서 기본생성자를 통해 객체를 생성하므로
- @Embeddable(값 타입 정의하는 곳에 표시), @Embedded(값 타입 사용하는 곳에 표시) 둘 중 하나를 명시 안 할 경우 JPA에서 커스텀 값 타입을 인식을 못하게 된다. -> 참고로 둘 중 하나는 생략해도 됨
만약 동일한 임베디드 타입을 사용해야 할 경우 어떻게 해야 할까?
아래 코드를 보면 Address 임베디드 타입에 대해서 homeAddress, workAddress 필드를 각각 구성하게 된다.
그냥 실행할 경우 Embedded 타입 중복 오류가 발생하게 된다.
중복 오류를 해결하기 위해 위 코드처럼 @AttributeOverrides를 사용하여 칼럼명 속성을 재정의해서 사용할 수 있다.
재정의 후 실행해 보면 @Column의 name 속성에 설정한 것처럼 구성되어서 DB에 반영이 된다!
만약 임베디드 타입 값을 null로 넣을 경우 임베디드 타입의 칼럼들은 모두 null로 저장된다.
저장된 값 결과
값 타임 공유 참조
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. 아래 코드를 같이 보자.
멤버 엔티티 2개를 영속화하였고 member1의 address 임베디드 타입의 city라는 필드값을 변경하였다.
예상했던 결괏값은 member1인 city필드 값만 변경인데 실제 결과를 보면 update 쿼리가 2번 실행되었고, DB값도 member1 뿐만 아니라 member2의 city 필드값도 같이 변경되는 것을 확인할 수 있다.
이러한 현상이 나온 이유는 Address을 생성해서 같이 쓰고 있다. 임베디드 타입은 Primitive 값이 아니라 객체 타입이므로 객체 타입끼리는 대입 시 주소값 복사로 인해 동일한 주소값을 공유하므로 member1의 address 임베디드 타입값을 변경하면 member2도 같이 변하게 되므로 update 쿼리가 2번 나가게 된다!
그렇다면 이러한 side effect를 어떻게 해결해야 할까?
각각 따로 객체를 구성하여 엔티티를 생성하는 것이다. (아래 코드 참고)
위 코드처럼 변경하면 실제 address, address2는 각각 메모리 공간을 할당하게 되고 변하게 되더라도 서로 영향을 끼치지 않게 된다.(deep copy) 이렇게 하면 객체 간의 공유 참조를 피할 수 있게 됨
결국, 객체 타입은 공유 참조를 피할 수가 없기 때문에 막을 방법이 없다. 이로 인해 컴파일단계에서 체크를 못하게 되고 결국 side effect가 발생하게 된다.
이러한 문제점들을 해결하기 위해 값타입은 반드시 불변객체로 만들어서 사용해야 한다. 그리고 값타입은 setter를 지양해야 하며 최대한 변경을 하지 않고 새로운 객체를 만들어서 사용하도록 지향해야 한다.
(불변 객체 : 생성 시점 이후로 절대 값을 변경할 수 없는 객체)
결론적으로, 값타입은 반드시 불변으로 만들어서 혹시 모를 sideEffect 발생을 미리 방지해야 한다.
값 타입 컬렉션
엔티티에 값 타입을 하나 이상 저장할 때 사용한다. @ElementCollection, @CollectionTable 어노테이션을 사용한다
- @ElementCollection : JPA에서 컬렉션 값을 매핑할 때 사용, 이 어노테이션 사용하면 해당 필드가 별도의 테이블에 저장되고 해당 엔티티와 연관 관계를 맺게 된다.
- @CollectionTable : @ElementCollection과 함께 사용되는 어노테이션으로, 매핑할 컬렉션 값을 저장할 테이블을 지정하기 위해 사용한다. 속성값 중 name이 테이블명이 되고, joincolumns에 FK키를 설정할 수 있다.
값타입 컬렉션도 지연 로딩 전략(FetchType.LAZY)이 가능하다. 그리고 값 타입 컬렉션은 영속성 전이(Cascade) + 고아 객체 제거 기능(orphanremoval=true)을 내부적으로 가진다. 실제 코드를 보면서 이해해 보자.
@ElementCollection와 @CollectionTable을 사용하여 값타입 컬렉션을 사용을 지정하였다. 그리고 오른쪽에 실제 저장하는 코드를 볼 수 있다.
결과를 보면 DB에 Address, FAVORITE_FOOD 테이블이 생긴 것을 알 수 있다.
값 타입 컬렉션 제약사항
1. 값 타입은 엔티티와 다르게 식별자가 없어서 값을 변경하면 추적이 어렵다
2. 값 타입의 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제 후, 값 타입 컬렉션에 있는 현재 값을 모두 저장함 -> 왜냐하면, 식별자 키가 없어서 어떤 것을 삭제해야 할지 JPA가 모름(굉장히 비효율적임)
3. 따라서 값 타입 컬렉션을 매핑하는 테이블은 모든 칼럼을 묶어서 PK로 지정해야 한다.
실무에서는 값 타입 컬랙션 대신에 일대다(OneToMany) 관계 엔티티를 고려하는 것이 관리적인 측면에서 더 좋다.
Address 엔티티 구성
Member 엔티티에 OnetoMany 연관관계 설정
정리
엔티티 타입 | 값 타입 특징 |
식별자 존재함 | 식별자 존재하지 않음 |
생명 주기 독립적 | 생명 주기를 엔티티에 의존 |
공유 가능 | 공유하지 않는 것이 안전(일괄 변경될 수 있음, 무조건 불변객체로 사용) |
참고로, 값 타입 비교할 때는 동등성 비교를 하므로 자바에서는 동등성 비교를 eqauls/hascode로 비교한다.
따라서 꼭 equals/hashcode를 재정의를 해줘야 한다.
감사합니다. 😀😀
'BackEnd(Java) > JPA 개념' 카테고리의 다른 글
OSIV (0) | 2023.06.11 |
---|---|
엔티티 매핑 (0) | 2023.05.21 |
영속성 컨텍스트 (0) | 2023.05.17 |