BACK/JPA

[JPA] JPA의 값 타입 (기본 값, 임베디드, 컬렉션 타입)

연듀 2024. 4. 2. 17:09

JPA의 데이터 타입 분류

  1. 엔티티 타입
    1. 데이터가 변해도 식별자로 지속해서 추적 가능 ex) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능 
    2. @Entity로 정의하는 객체 
  2. 값 타입
    1. int, Integer, String 처럼 값으로 사용하는 자바 기본 타입이나 객체
    2. 식별자 없고 값만 있어 변경 시 추적 불가 ex) 숫자 100 -> 200 변경 시 완전히 다른 값으로 대체 

 

값 타입 분류

 

기본 값 타입, 임베디드 타입, 컬렉션 타입으로 나눌 수 있다.

 

 

기본값 타입

 

엔티티에 생명 주기를 의존한다 (회원 엔티티를 삭제하면 필드도 함께 삭제)

값 타입은 공유하면 안된다 (회원 이름 변경시 다른 회원 이름이 변경되면 안됨)

자바의 기본 값은 항상 값을 복사한다.

Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 참조값 공유는 가능하지만 변경자체가 불가능하다.

 

 

 

임베디드 타입

 

 

새로운 값 타입을 직접 정의할 수 있는 클래스를 JPA에서는 임베디드 타입이라고 한다.

기본 값 타입을 모아 복합 값 타입이라고도 한다.

int 나 String 같은 값타입이라고 볼 수 있다. 

 

-장점으로는 재사용성, 높은 응집도를 가진다는 점이다. 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.

임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존한다.

 

 

 

-임베디드 타입은 엔티티의 값일 뿐이다. 임베디드 타입 사용 전후에 매핑하는 테이블은 같다.

객체와 테이블을 세밀하게 매핑할 수 있다.

임베디드 타입 클래스 안에 엔티티가 들어올 수도 있다.

이런식으로 잘설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다. 

 

 

-한 엔티티에서 같은 값 타입을 여러개 사용할 수도 있다.

이 때 컬럼명이 중복되기 때문에 @AttributeOverrides, @AttributeOverride를 사용해 컬럼명 속성을 재정의할 수 있다. 

 

-임베디드 타입의 값이 null 이면, 매핑한 컬럼 값은 모두 null이 된다. 

 

 

 

@Embeddable: 값 타입을 정의하는 곳에 표시 

@Embedded 사용: 값 타입을 사용하는 곳에 표시 

 

-기본 생성자 필수

 

 

Address

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
Member member = new Member();
member.setUsername("hello");
member.setHomeAddress(new Address("city", "street", "100"));

em.persist(member);

 

값 타입과 불변 객체

 

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 부작용이 생길 수 있다.

한 객체에서 값을 변경했는데 다른 곳에서도 변경 된게 반영이 되버린다.

 

그래서 값(인스턴스)를 복사해서 사용해야 한다.

 

이렇게 복사해서 사용하면 공유 참조로 인한 부작용을 피할 수 있는데,

문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이라는 것이다.

 

기본 타입은 값을 대입하면 값을 복사하지만, 객체 타입은 참조 값을 직접 대입하는 것을 막을 수 없다.

즉 객체의 공유 참조를 피할 수 없다.

 

 

객체 타입을 수정할 수 없게 만들도록 값 타입은 불변 객체로 설계해야 한다.

 

*불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체

* Integer, String은 자바가 제공하는 대표적인 불변 객체 

 

생성자로만 값을 설정하고, setter를 안만들거나 private으로 설정한다.

그런데 꼭 값을 바꿔야 한다면, 다음과 같이 완전히 새로운 객체를 만들어 세팅한다.

 

Address address = new Address("city", "street", "10000");
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);

em.persist(member);

Address newAddress = new Address("newCity", address.getStreet(), address.getZipcode()); // 새로운 객체 생성
member.setHomeAddress(newAddress);

em.persist(member);

 

 


 

값 타입 비교

 

값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야 함

 

동일성 비교: 인스턴스의 참조 값을 비교, == 사용

동등성 비교: 인스턴스의 값을 비교, equals() 사용

 

값 타입들의 비교는 항상 equals를 사용해 동등성 비교를 해야 한다. 

값 타입의 equals() 메소드를 적절하게 재정의한다. 

 

 


 

값 타입 컬렉션

 

 

값 타입을 하나 이상 저장할 때 사용한다.

@ElementCollection, @CollectionTable 을 사용한다.

데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없기 때문에 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

 

 

@Embedded
private Address homeAddress;

@ElementCollection
@CollectionTable(name="FAVORITE_FOOD", joinColumns = @JoinColumn(name="MEMBER_ID"))
@Column(name="FOOD_NAME") // 값이 하나기 때문
private Set<String> favoriteFoods = new HashSet<>();

@ElementCollection
@CollectionTable(name="ADDRESS", joinColumns = @JoinColumn(name="MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

 

 

 

 

값 타입 컬렉션들은 모두 member에 라이프 사이클이 의존된다.

따라서 값 타입들을 별도로 persist() 할 게 없다. 멤버에서 값을 바꾸면 자동으로 업데이트 된다.

 

값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다.

 

값 타입 컬렉션도 지연 로딩 전략을 사용한다. 

 

Member findMember = em.find(Member.class, member.getId());

 

를 하면, 멤버에 소속된 값 타입인 homeAddress는 select로 가져오고 컬렉션은 지연로딩이 적용되어 가져오지 않는다.

값을 실제 사용하려고 하는 시점에 가져온다.

 

 

값 타입 멤버 수정하기

Member findMember = em.find(Member.class, member.getId());

//findMember.getHomeAddress().setCity("newCity");  -> 이렇게 하면 안된다. setter를 private로 하거나 없애버려야 한다.
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("new City", a.getStreet(), a.getZipcode())); // 새로운 인스턴스로 갈아 끼워야 한다.

 

 

새로운 인스턴스를 넣어 줘야 한다.

 

// 치킨 -> 한식 수정하려 할 때
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식"); // 컬렉션의 값만 변경해도 실제 데이터베이스 update 쿼리가 날라간다.

findMember.getAddressHistory().remove(new Address("old1", "street", "10000")); // equals, hashcode 가 구현되어 있으면 동등성 비교 해 지워짐
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

 

 

값 타입 컬렉션의 제약 사항

 

값 타입은 엔티티와 다르게 식별자 개념이 없다. 따라서 값을 변경하면 추적이 어렵다. 

 

값 타입 컬렉션에 변경사항이 생기면, 주인 엔티티와 연관된 모든 데이터를 삭제하고

값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

 

값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 한다. (null 입력 x, 중복 저장x)

 

 

값 타입 컬렉션 대안 

 

실무에서는 값 타입 컬렉션 대신에 일대다 관계로 하는 것이 낫다.

일대다 관계를 위한 엔티티를 만들고, 여기서 값 타입을 사용하는 것이다.

 

영속성 전이(Cascade) + 고아 객체 제거를 사용해 값 타입 컬렉션 처럼 사용한다.

 

 

ex) AddressEntity

@Entity
@Table(name="ADDRESS")
public class AddressEntity {
    @Id @GeneratedValue
    private Long id;
    private Address address;

    public AddressEntity(Address address) {
        this.address = address;
    }

    public AddressEntity(String city, String street, String zipcode){
        this.address = new Address(city, street, zipcode);
    }

    public AddressEntity() {

    }
...

 

 

Member

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) // 값 타입 보다 더 활용할 수 있는게 많아짐
@JoinColumn(name="MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

 

정리

 

엔티티 타입의 특징

  • 식별자가 있고, 생명주기를 직접 관리하며 공유가능하다

값 타입의 특징

  • 식별자가 없고, 생명 주기를 엔티티에 의존한다.
  • 공유하지 않는 것(복사해 사용하는 것)이 안전하다.
  • 불변 객체로 만드는 것이 안전하다.

 

 

-> 값 타입은 정말 값 타입이라 판단될 때만 사용한다.

엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다.

식별자가 필요하고, 지속해서 값을 추적하고 변경해야 한다면 값 타입이 아니라 엔티티다. 

 

 

 

 

 

인프런 자바 ORM 표준 JPA 프로그래밍 - 기본편을 수강하고 정리한 글입니다.

https://www.inflearn.com/course/ORM-JPA-Basic