[JPA-09] 값 타입

이가희·2024년 12월 15일
1

JPA

목록 보기
9/16
post-thumbnail

JPA 값 타입에 대해 알아보겠다.
String, int 같은 것들 말이다.

크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있는데,
다음의 사항을 유의하며 읽어보면 좋을 듯 하다.

엔티티 타입 : 식별자가 있어, 지속적으로 추적 가능
값 타입 : 식별자가 없어, 지속적으로 추적 불가능
-> 간단한 것이지만 이 차이 때문에 값을 공유할 때 전혀 다른 결과가 초래될 수 있음

Chapter

  1. 기본값 타입
  2. 임베디드 타입 (복합 값 타입)
  3. 값 타입과 불변 객체
  4. 컬렉션 값 타입

1. 기본값 타입

정말 우리에게 익숙한 String, int, Double 등을 말한다.
그런데 이 기본값 타입은 식별자도 않고 생명주기도 엔티티에 의존한다.
(예를 들어 회원 엔티티에 String name; 이 있다면 이 값은 회원 엔티티에 생명주기를 의존함)
따라서 해당 엔티티 인스턴스를 제거하면 값 타입도 사라지게 된다.

간단하지만 여기서 중요하게 알아야 할 것은,
값 타입은 공유를 해서는 안 된다.
아래의 글들을 모두 정독하면 이해되겠지만, 값 타입을 공유하게 되면
내 이름을 변경할 때 다른 회원의 이름까지 변경될 수 있는 부작용이 초래될 수도 있기 때문이다.


2. 임베디드 타입 (복합 값 타입)

JPA에서는 임베디드 타입이라고 하는 새로운 값 타입을 직접 정의해서 사용할 수 있다.
그런데 이것도 결국 값 타입의 일종이다. (그러니까 공유해서는 안 된다.)

이 임베디드 타입은 다음과 같은 상황들에서 쓰면 좋을 것이다.

회원 엔티티에 근무 시작 일, 근무 종료일과 같이 비즈니스적으로 의미가 있는 집합이 있을 이 둘을 묶은 임베디드 타입으로 표현한다면, 응집력을 높이고 코드도 더 명확히 표현할 수 있을 것이다.


@Entity
public class Member {
	@Id
    private Long id;
    
    @Embedded Period workPeriod; //근무 기간을 나타내는 임베디드 타입
    //이를 사용하지 않았다면
    // Date startDate, Date endDate 따위를 모두 나열했어야 한다.
}

//기간 임베디드 타입
@Embeddable
public class Period {
	@Column (name = "startDate") //매핑할 컬럼 정의도 가능
	@Temporal (TemporalType.Date)
    Date startDate;
    
    @Temporal (TemporalType.Date)
    Date endDate;
    
    public boolean isWork(Date date){
    //... 값 타입을 위한 메소드를 정의할 수도 있다.
    }
}

이렇게 한 번 값 타입을 정의하면, 재사용할 수도 있고 isWork()처럼 의미 있는 메소드도 만들 수도 있다.

임베디드 타입에는 기본 생성자가 필수이며
@Embaddable : 값 타입을 정의하는 곳에 표시
@Embedded : 값 타입을 사용하는 곳에 표시
를 붙여야 한다. (둘 중 하나는 생략 가능)

이러한 임베디드 타입은 필드로 값 타입 혹은 엔티티를 선언할 수도 있다.
(= 값 타입을 포함하거나, 엔티티를 참조할 수 있다.)

그리고 @AttributeOverride를 통해 매핑 정보를 재정의 할 수도 있다.

예를 들어 회원 엔티티에 Adress homeAddress; 라는 임베디드 타입이 있었는데,
또 Adress companyAddress; 라는 것을 추가한다면 테이블에 매핑하는 컬럼명이 중복되어서, 이 어노테이션을 통해 매핑정보를 재정의 해야 한다.

//여기서 Address 값 타임 안에는 private String city, private String street가 있다.

@Entity
public class Member {
	@Id
    private Long id;
    
    @Embedded Address homeAddress;
    //새로 추가된 것이 아니니, 그냥 냅두면 된다.
    
    @Embedded
    @AttributeOverrides ({
  	  @AttributeOverride (name="city", column @Column (name = 
      "COMPANY_CITY")),
      @AtrributeOverride (name="street" , column =@Column(name=
      "COMPANY_STREET"))
      })
    Address companyAddress;
    //city 필드는 COMPANY_CITY 컬럼에, street 필드는 COMPANY_STREET 
    //컬럼에 매핑하겠다는 의미이다.
}
      
   

여기서 만약 member.setAddress(null) 후
entityManager.persist(member)을 하게 되면
임베디드 안에 매핑된 모든 컬럼들은 모두 null이 된다.


3. 값 타입과 불변 객체

앞서 계속 값 타입은 공유해서는 안 된다고 강조하였다.
그 이유를 지금 살펴보겠다.

값 타입 공유 참조

member1.setAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

address.setCity("NewCity"); //회원1의 address 값을 공유해서 사용 (=같은 주소값을 가진 address를 둘이서 사용하고 있음)
member2.setHomeAddress(address);

위의 상황에서 코드를 실행하면, 회원 1의 주소도 "NewCity"로 변경되어 버린다. 😱

address.setCity를 했을 때, 같은 address를 참조하기 때문에 당연히 회원1의 address의 값도 변화되기 때문이다. (생각해보면 너무 당연한말..!)
그래서 영속성 컨텍스트는 회원1, 회원2 모두 주소가 변경된 것으로 판단해서 각각 UPDATE SQL을 실행하여, 둘 다 주소가 변경되어 버린다.

이렇게 무언가를 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것을 부작용 이라고 한다.

부작용을 예방하려면 , 값을 복사해서 사용하면 된다.

값 타입 복사

member1.setAddress(new Address("OldCity"));
Address address = member1.getHomeAddress();

//회원1의 address 값을 복사해서 새로운 newAddress 를 생성 
Address newAddress = address.clone(); 
address.setCity("NewCity"); //이제 회원1과 다른 address를 가지게 됨.
member2.setHomeAddress(address);

이렇게 한다면 공유 참조를 피할 수 있다.

여기서 알아가기
1️⃣ int a =10; 2️⃣ int b = a; 3️⃣ b= 4;
여기서 a의 최종 값은 10이다. 객체와 달리 자바는 기본 타입을 대입하는 경우, 값을 복사해서 전달하기 때문이다. 객체의 경우 참조값(=주소값)을 전달하기에 부작용이 발생하게 된다.

하지만 직접적으로 원본의 참조 값을 넘기는 것을 막을 방법은 없다.
(아무래도 다른 개발자가 복사해서 넘겨주는 것이 아닌, 그대로 공유해서 사용하는 것을 계속 감시하는건 무리니까..)

그래서 값을 수정하지 못하게 setCity() 같은 수정자 메소드를 모두 제거하면,
공유 참조를 해도 값을 변경하지는 못하니까 부작용의 발생을 막을 수는 있다.
그래서 값 타입은 부작용 걱정 없이 사용하기 위해, 불변 객체 로 설계해야 한다.

@Embeddable 
public class Address {
	private String city;
    
    protected Address () {} //JPA에서 기본 생성자는 필수이다.
    
    //생성자로 초기 값을 설정한다.
    public Address (String city) {this.city = city}
    
    public String getCity(){
    	return city;
    }
    //접근자는 노출시키고, 수정자 (setter)는 생성하지 않는다.

위와 같이 하면 불변 객체로 만들 수 있다.

값 타입을 비교하기 위해서는 , equals() 를 재정의 해야한다.
그런데 이때, 해시를 사용하는 컬렉션에서도 정상 작동하도록, hashCode()도 재정의하는 것이 좋다.


4. 컬렉션 값 타입

값 타입을 하나 이상 저장하려면, 컬렉션을 이용하고 @ElementCollection, 혹은
@CollectionTable 어노테이션을 사용하면 된다.

@Entity
public class Member {
	@Id
    private Long id;
    
    @ElementCollection
    @CollectionTable (name = "FOODS",
     joinColumns = @JoinColumn (name = "MEMBER_ID"))
     @Column(name ="FOOD_NAME")
    private Set<String> favoriteFoods = new HashSet<String>();
    
    @ElementCollection
    @CollectionTable (name = "ADDRESS",
     joinColumns = @JoinColumn (name = "MEMBER_ID"))
    private List<Address> addressHistory = new ArrayLst<>();
}

위의 코드의 경우,
favoriteFoods 는 FOODS 라는 테이블과 매핑하고,
이때 회원의 Pk는 FOODS 의 MEMBER_ID (FOODS 테이블의 FK)에,
favorieFoods 들의 값들은 FOOD_NAME 컬럼에 매핑하겠다는 뜻이다.

이러한 값 타입 컬렉션은 JPA에서 어떻게 사용될까?

Member member = new Member();

//임베디드 값 타입
member.setAddress(new Address("통영","몽돌해수욕장"));

//값 타입 컬렉션
member.getFavoriteFoods().add("마카롱");
member.getFavoriteFoods().add("탕수육");

entityManager.persist(member);

이를 실행하면 총 INSERT SQL 이 3번 실행되게 된다.

임베디드 값 타입은 member table에 매핑되어 있으니, 회원이 저장될 때 1번,
값 타입 컬렉션은 member table에 있는 것이 아니니, 해당 테이블에 추가한 횟수만큼 INSER SQL이 실행되어 여기서는 2번,
이렇게 총 3번이 실행된다.

여기서 값 타입 컬렉션은 기본적으로 영속성 전이와 고아 객체 제거 기능이 필수적으로 가짐을 알 수 있다.

그리고 값 타입 컬렉션 또한 조회할 때 패치 전략을 사용할 수 있으며, LAZY가 기본이다.

값 타입 수정

Member member = entityManager.find(Member.class, 1L);

//1. 임베디드 값 타입 수정
member.setHomeAddress (new Address ("새도시","신도시"));

//2. 기본값 타입 컬렉션 수정
Set<String> foods = memger.getFavoriteFoods();
foods.remove("탕수육");
foods.remove("탕후루");

//3. 임베디드 값 타입 컬렉션 수정
List<Address> address = memger.getAddressHistory();
address.remove(new Address("서울","기존주소"));
address.add(new Address("새","도시"));
  1. 임베디드 값 의 경우 MEMBER 테이블고 매핑되어 있으니, MEMBER TABLE만 UPDATE 한다. = Member 엔티티를 수정하는 것과 같다.
  2. 기본값 타입 컬렉션 의 경우 자바의 String 타입은 수정할 수 없기에, 탕수육을 탕후루로 변경하고 싶을 때 탕수육을 제거하고, 탕후루를 추가하는 식으로 수정해야 한다.
  3. 임베디드 값 타입 컬렉션 의 경우 값 타입은 불변하기에, 컬렉션에서 기존 주소를 삭제하고 새로운 주소를 등록하였다. (이때 값 타입은 equals, hascode를 꼭 구현해야 한다.)

값 타입 컬렉션의 제약사항

위의 수정코드에서도 알 수 있었듯, 단순 임베디드 값의 경우 값이 변경되어도 자신이 소속된 엔티티를 DB에서 찾고 값을 변경하면 되어 간단하다.

그러나 값 타입 컬렉션의 경우, 별도의 테이블에 보관되기 때문에 값 타입이 변경되면 데이터베이스에서 원본 데이터를 찾기가 힘들다 .
(= 엔티티는 식별자가 있어 DB에서 조회가 편하지만, 값 타입은 식별자가 없음)

따라서 JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 해당 컬렉션이 매핑된 테이블의 연관된 데이터를 모두 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장 한다.
(예를 들어 식별자가 100 인 회원의 주소 값 타입 컬렉션을 변경하면
DELETE FROM ADDRESS = WHERE MEMBER_ID = 100
으로 연관된 데이터를 모두 삭제하고, INSERT INTO... 하여 현재 값 타입 컬렉션에 있는 값들을 저장함)

이는 성능적으로 매우 좋지 않다.
따라서 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면, 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.
이때, 영속성 전이 + 고아 객체 제거를 적용하면 값 타입처럼 사용할 수 있다.


다음 시간에는 객체지향 쿼리 언어를 알아보겠다.

참조
자바 ORM 표준 JPA 프로그래밍 : 김영한

profile
안녕하세요 개발하는 사람입니다.

0개의 댓글

관련 채용 정보