본문 바로가기

백엔드/JPA

JPA 값타입

JPA의 데이터타입

JPA에는 두가지 데이터타입이 있다. 하나는 지금까지 배운 엔티티이고 나머지 하나는 값타입이다. 

엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변하면 식별자를 가지고 변경감지가 가능

값타입

  • int, String 처럼 단순히 값으로 다루는 타입
  • 데이터가 변하면 감지 불가능

 

값타입에도 종류가 있다!

  • 기본값 타입 - int, double, Integer, Long, String 등등
  • 임베디드 타입
  • 컬렉션 값 타입

먼저 기본값 타입은 엔티티를 삭제할 경우에 같이 삭제되는 데이터들이다. 생명주기를 엔티티에 의존한다고 표현한다. 값타입은 당연히 공유가 되지 않는다. 이제 임베디드타입과 컬렉션 값 타입이라는 것에 대해서 알아보자

임베디드 타입

임베디드 타입이 무엇이고 왜 쓰이는지 예제를 통해 보면

public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    
    private String city;
    private String street;
    private String detail_address;
}

위의 Parent 엔티티는 비슷한 의미를 가진 필드들이 있다. 모두 주소를 표현하고 있는 city, street, detail_address이다. 이들을 묶어서 따로 클래스를 하나 만든다.

@Embeddable
public class Address {

    private String street;
    private String city;
    private String detail_address;
}

public class Parent {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    @Embedded
    private Address address;
}

Parent 엔티티의 필드가 보다 더 단순해졌고 의미있는 데이터끼리 모였고, Address 클래스 내에다가 해당 필드들을 사용한 메소드도 만들 수 있기 때문에 응집성도 더 좋아졌다고 볼 수 있다. 사용법은 위처럼 Embedded와 Embeddable을 붙여주면 된다. 그리고 임베디드를 사용함으로써 클래스 구성이 좀 더 깔끔해진다.

임베디드 타입 주의사항

  • 기본 생성자가 필수이다!!
  • 임베디드 타입을 포함한 모든 값 타입은 엔티티에 생명주기를 의존한다.
  • side effect 방지를 위해 두 엔티티가 같은 값이더라도 임베디드 타입은 공유하지 않고 복사해서 쓰도록한다. (ex. 같은 Address 이더라도 new Address() 로 새로 생성해서 값을 복사하도록 한다.)
  • 임베디드 타입은 불변객체이어야 한다.

만약 임베디드타입을 두개 쓰고 싶다면 아래와 같이 컬럼명을 바꿔줌으로써 컬럼명중복을 피할 수 있다.

 

임베디드 타입을 불변객체로 만들기 위해서 생성자로만 값 세팅하도록 하고 setter를 만들지 않는다. 또한 값타입의 비교를 위해서 equals 를 오버라이딩해서 새로 정의하도록 한다.

 

값타입 컬렉션

//값타입 컬렉션의 사용 예시

@Entity
class Parent {
    @ElementCollection
    @CollectionTable(name="MY_FOOD", joinColumns= @JoinColumn(name="whoLove"))
    @Column(name="좋아하는음식")
    private Set<String> favoriteFoods = new HashSet<>();
    
}

//main 함수 내
Member mem = new Member();
mem.setUsername("멤버");
Address a1 = new Address("asdf","r과천","sbfd");
mem.setAddress(a1);

mem.getFavoriteFoods().add("치킨");
mem.getFavoriteFoods().add("족발");
mem.getFavoriteFoods().add("까르보나라");

member의 insert문과 MY_FOOD 라는 테이블에 치킨, 족발, 까르보나라 3개의 엔티티가 각각 insert 된다.

그리고 String 말고도 엔티티에 대한 컬렉션 값 타입도 있다.

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

    
    
public class AddressEntity {
@Id @GeneratedValue
private Long id;

private Address address;

public AddressEntity(String fsd, String bfsd, String dsng) {
this.address = new Address(fsd, bfsd, dsng);
}
}

 

이렇게 하면 delete 문과 insert 문 하나씩 나간다. 하지만 위의 엔티티 컬렉션인 addressHistory의 경우에는 하나 삭제하고 새로 추가하면 모두 날려버리고 다시 일일이 추가하는 식으로 동작한다. 왜 이런 차이가 생겼을까?

이유는 pk가 없기때문이다. 변경이 발생했을 경우에 어떤 데이터가 어떻게 바뀐건지를 모르기 때문에 변경된 엔티티의 fk를 가진 것들을 모두 삭제하고 다시 insert문을 다 날리는 것이다.

값타입 컬렉션은 모든 컬럼을 묶어서 기본키를 구성하도록 해야한다. 실무에서는 값타입의 컬렉션 대신에 일대다 관계를 쓰도록 한다. 그래서 아래와 같이 바꾸는게 좋다.

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name="sese")
    private List<AddressEntity> addressHistory = new ArrayList<>();

 

AddressEntity는 Long타입의 id와 Address타입의 address 필드 두개만 갖는 엔티티이다. 이를 임베디드 타입인 Address 타입을 엔티티로 승급했다고 한다.


값타입 정리

값타입은 식별자가 없다
엔티티에 생명주기를 의존한다.
불변객체로 만들고, 공유하지 않는것이 안전하다.

값타입은 정말 단순할 때 쓰는게 좋다. (디비에서 쿼리로 가져와야되면 엔티티 Address처럼 주소를 갑자기 가져올 일은 없으니 이런건 값타입. position x, y좌표 클래스 이런것)

 

관련글

https://www.inflearn.com/course/ORM-JPA-Basic/dashboard 를 들으며 정리한 글입니다.

 

 

'백엔드 > JPA' 카테고리의 다른 글

JPQL 중급문법  (0) 2021.08.08
JPQL 기초  (0) 2021.08.08
JPA 프록시와 영속성 전이  (0) 2021.08.06
JPA 엔티티 매핑2  (0) 2021.08.05
JPA 엔티티 매핑1  (0) 2021.07.29