JPA 값 타입

송현진·2023년 7월 23일
0

Jpa

목록 보기
8/9
post-thumbnail
post-custom-banner

JPA 데이터 타입 분류

엔티티 타입

@Entity로 정의하는 객체
데이터가 변해도 식별자로 지속해서 추적 가능

값 타입

int, Integer, String 같은 단순히 값으로 사용하는 자바 기본 타입이나 객체
식별자가 없고 값만 있으므로 변경시 추적 불가

기본값 타입

자바 기본 타입(int, double)

  • 생명주기를 엔티티의 의존
  • 값 타입은 공유하면 안된다
    • int, double 같은 기본타입은 절대 공유 X
    • 항상 값을 복사함

래퍼 클래스(Integer, Long), String

  • 특수한 객체로 공유 가능한 객체이지만 변경 X

⭐임베디드 타입(복합 값 타입)

공통된 특성을 묶어낼 수 있는 것(기간, 주소 등)
@Embeddable : 값 타입 정의하는 곳에 표시
@Embedded : 값 타입 사용하는 곳에 표시
기본 생성자 필수

임베디드 타입을 사용하지 않은 경우

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String city;
    private String street;
    private String zipcode;
}    

city, street, zipcode는 주소와 관련된 값이므로 임베디드 타입을 사용해 연관된 값을 하나로 묶어줄 수 있다.

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

	// Getter, Setter, Constructor(No, All)
    ...
}    
@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    @Embedded
    private Address homeAddress;
    
    // Getter, Setter
    ...
}

이렇게 Address 클래스를 만들어 관련된 값을 묶어서 @Embeddable을 사용하고, 이 타입을 사용하는 곳에선 @Embedded를 사용해서 Address 클래스를 사용한다.

✏️ 임베디드 타입은 정말 말 그대로 단순히 "값"들을 하나로 묶은 것이라 엔티티와 달리 식별자가 없고, 생명주기를 자신을 소유하는 엔티티에 의존한다. 그렇기에 재사용성이 좋고 높은 응집도를 가진다.

하나의 엔티티 에서 같은 임베디드 타입을 여러개 사용해야한다면?
아래와 같이 하나의 값 타입을 여러개 사용할 때 단순히 변수 1개를 더 추가한다면 에러가 발생한다.

@Embedded
private Address homeAddress;
@Embedded
private Address workAddress;

// 반복되는 컬럼있다는 메세지와 함께 에러 발생
Repeated column in mapping for entity: hellojpa.Member column: 
city (should be mapped with insert="false" update="false")

그래서 @AttributueOverrides@AttributeOverride를 사용해 컬럼을 재정의해서 사용한다.

@Entity
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    @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
    ...
}    

특징

  • 임베디드 타입은 그저 엔티티의 값일 뿐이다
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블엔 변화가 없다
  • 객체와 테이블을 세밀하게 매핑하는 것이 가능
  • 도메인의 언어를 공통으로 맞출 수 있다는 장점이 있다

값 타입과 불변 객체

값 타입 : 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념
따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다

실제 인스턴스인 값을 공유하는 것은 위험해서 복사해서 사용해야 한다

Address address = new Address("city", "street","zipcode");

            Member member1 = new Member();
            member1.setUsername("111");
            member1.setHomeAddress(address);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("222");
            member2.setHomeAddress(address);
            em.persist(member2);

            member1.getHomeAddress().setCity("busan");

            tx.commit();
            System.out.println("member1 : " + member1.getHomeAddress().getCity());
            System.out.println("member2 : " + member2.getHomeAddress().getCity());
            -----------------------
            // 출력값
            member1 : busan
			member2 : busan
}            

이렇게 임베디드 타입같은 값 타입을 여러 엔티티에서 공유하면 side Effect로 인해 부작용이 발생할 수 있다.

해결하려면?
Adrress의 Setter를 구현하지 않거나 private로 구현하면 된다.

  • 객체 타입을 수정할 수 없게 만들어 부작용을 원천 차단(Integer, String)
  • 값 타입은 불면 객체로 설계
  • 생성자로만 값을 설정하고 Setter를 만들지 않는다

값 타입의 비교

인스턴스 달라도 값이 같으면 같은 것으로 봐야 함

int a = 2;
int b = 2;

System.out.println("a == b : "+ (a==b));

Address address1 = new Address("busan", "namgu", "zipcode");
Address address2 = new Address("busan", "namgu", "zipcode");

System.out.println("address1 == address2 : "+(address1==address2));
System.out.println("address1 equals address2 : "+(address1.equals(address2)));
--------------------------------------
// 출력
a == b : true
address1 == address2 : false
address1 equals address2 : true

동일성(identity) 비교 : 인스턴스 참조 값을 비교( == 사용, private 타입)
동등성(equivalence) 비교 : 인스턴스 값을 비교( equals() 사용, embedded 타입 등)

  • 값 타입의 equals() 메소드를 적절하게 재정의해서 사용(왠만하면 자동으로 해주는 거 써라!)
    • 💡 재정의 하지않고 equals()를 써서 비교하면 fasle가 나온다. 그 이유는 기본이 == 비교로 되어있기 때문이다.
  • hasCode()도 같이 정의해줘야됨. 그래야 자바 Collection에서 효율적으로 사용가능

⭐값 타입 컬렉션

값 타입을 컬렉션에 담아 쓰는 것
값 타입을 하나 이상 저장할 때 사용
@ElementCollection, @CollectionTable 어노테이션을 붙여 값 타입 컬렉션 사용 가능

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String username;
    @ElementCollection
    @CollectionTable(name = "FAVORITE_FOOD", joinColumns =
        @JoinColumn(name = "MEMBER_ID")
    )
    // 값이 하나고 내가 정의한 것이 아니기 때문에 예외적으로 컬럼명 변경 허용
    @Column(name = "FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<>();
    @OrderColumn(name = "address_history_order")
    @ElementCollection
    @CollectionTable(name = "ADDRESS", joinColumns =
        @JoinColumn(name = "MEMBER_ID")
    )
    private List<Address> addressesHistory = new ArrayList<>();

    public String getUsername() {
        return username;
    }
    public Set<String> getFavoriteFoods() {
        return favoriteFoods;
    }

    public List<Address> getAddressesHistory() {
        return addressesHistory;
    }    
}

RDB에는 내부적으로 컬렉션을 담을 수 있는 구조가 없다. 그냥 값만 넣을 수 있는 구조이다

  • 컬렉션은 1:N 개념이라 DB는 컬렉션을 하나의 테이블에 저장할 수 없다
  • 이런 관계를 DB 테이블에 저장하려면 별도의 테이블(Join 가능하도록)이 필요하다

예제

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("busan", "street", "zipcode"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("헤이에그누들");
member.getFavoriteFoods().add("볶음짬뽕");

member.getAddressHistory().add(new Address("seoul", "street1", "10001"));
member.getAddressHistory().add(new Address("incheon", "street2", "10002"));

em.persist(member);

tx.commit();

Member만 저장했는데 값 타입 컬렉션에 대한 insert 쿼리가 날아갔다.
즉, 값 타입 컬렉션은 Member 객체의 라이프 사이클과 동일하게 적용된다.
값 타입은 별도로 persist나 update 할 필요 없이 Member에서 값을 변경하면 자동으로 처리해준다.
그렇기에 값 타입 컬렉션은 영속성 전이 + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

....

em.flush();
em.clear();

System.out.println("============ START ============");
Member findMember = em.find(Member.class, member.getId());

tx.commit();

영속성 컨텍스트를 비운 후 Member 조회하면 컬렉션들은 같이 조회되지 않는다.
그 이유는 컬렉션 값 타입들은 지연 로딩 전략을 취하기 때문이다.
💡 @ElementCollection의 fetch 기본값이 LAZY이다.

수정은 Member 객체에서 getter로 불러와서 remove 후 add 해주면 된다.

제약 사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
    • ADDESS엔 id가 존재하지 않는다.
    • 값이 중간에 변경되었을 때 DB가 해당 row만 찾아 변경할 수 없다.
  Hibernate: 
      create table ADDRESS (
          MEMBER_ID bigint not null,
          city varchar(255),
          street varchar(255),
          zipcode varchar(255)
      )
  • 값 타입 컬렉션에 변경사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고
    값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
    • 대안
      • @OrderColumn(name = "address_history_order) 를 사용해 update 쿼리 날아갈 수 있도록 가능(의도대로 동작하지 않을 때가 많음)
    • 결론
      • 값 타입 컬렉션을 사용하지 말자
      • 대부분이 엔티티임
      • 추적 필요없고 값이 바뀌어도 업데이트 필요없을 경우에만 사용하자
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼은 묶어서 기본키 구성
    • null X
    • 중복 저장 X

대안

  • 1:N 관계 고려하는 것이 나음
    • 1:N 관계의 엔티티 만들고, 여기에서 값 타입 사용
  • 영속성 전이 + 고아 객체 제거 사용해서 값 타입 컬렉션처럼 사용
    • @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    • 실무에서 쿼리 최적화에 유리

참고
임베디드 타입

profile
개발자가 되고 싶은 취준생
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

정리가 잘 된 글이네요. 도움이 됐습니다.

답글 달기