Spring/Spring Data JPA

값 타입 (+ 기본값, 임베디드, 불변객체 타입)

밍구밍구밍 2024. 5. 8. 16:08

 

** 값 타입의 분류

1) 기본 값 타입

2) 임베디드 타입

3) 컬렉션 값 타입

 

1 기본 값 타입 (String name, int age 등) = DB 이상현상

- 생명주기를 엔티티에 의존한다. (= 회원을 삭제하면 이름, 나이 필드도 함께 삭제)

- 값 타입은 공유하면 안된다. (= 회원 이름 변경 시, 다른 회원의 이름도 함께 변경되면 안된다)

 

 

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

- 새로운 값 타입을 직접 정의 할 수 있다.

- JPA는 임베디드 타입이라고 한다.

- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.

- int, String 과 같은 값 타입

 

Member 를 참조하기 위해 workPeriod 와 homeAddress 객체를 생성하여 접근한다.

- Period 와 Address 객체에 하위 인스턴스들이 Member 엔티티를 참조하기 위해 Member 엔티티에 workPeriod 와 homeAddress 를 임베디드 한다. (아래 코드 참조)

 

** 임베디드 타입 종류 : @Embeded (참조 대상 엔티티) / @Embeddable (참조 객체)

code 참조)

@Embedded
private Period workPeriod;
@Embedded
private Address Homeaddress;

 

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

    public Address() {}

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
@Embeddable
public class Period {

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public LocalDateTime getStartDate() {
        return startDate;
    }

 

 

** 하나의 엔티티에 같은 타입의 객체를 두개 이상 엠베디드(@Embeded) 할 때 컬럼 이름 중복 오류가 발생 한다.

- 해결 방법 : @AttrebuteOverrides + @AttributeOverride 사용

@Embedded
private Address Homeaddress; // Address 타입 1

@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; // Address 타입 2

(두개의 공통된 Address 의 객체를 선언 하였다.

Address2 타입은 @AttrebuteOverrides + @AttributeOverride 를 통해 별도의 컬럼 이름을 설정할 수 있다.)

 

이와 같은 코드는 DB 에 아래와 같이 저장 된다.

새롭게 추가한 WORK_관련 컬럼이 추가된 것을 확인 할 수 있다.

 

임베디드 타입 장점
  • 재사용성 ↑
  • 높은 응집도 설계
  • Period.isWork() 처럼 해당 값 타입만 사용 가능한 또 다른 의미를 가지는 메서드 생성 가능
  • 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존한다.
정리
  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게 매핑 하는 것이 가능 ( @AttrebuteOverrides + @AttributeOverride )
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음
  • 엠베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험하다 (side effect 부작용 발생 가능성 ↑) 

 

! 값 타입 사용 주의 (feat. side effect)

 

1) member1과 member2의 데이터가 존재하고 동일한 객체를 생성하여 사용 하고 있다 (Address)

try {

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

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

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

    member1.getHomeaddress().setCity("newCity");

    tx.commit();

 

2) 개발자는 member1 의 city 인스턴스 이름을 newCity 로 변경 및 생성 하려고 할때 문제가 발생한다.

member1.getHomeaddress().setCity("newCity");

 

3) 사이드 이펙트 발생

CITY 컬럼 값이 member1, 2 의 값이 모두 변경 / 생성 되었다 (오류)

 

** 해결 방법 (값을 복사)

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

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

// 객체를 복사
Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeaddress(copyAddress); // member2를 copyAddress 로 수정 (다른 값 참조) 
em.persist(member2);

member1.getHomeaddress().setCity("newCity"); // member1 의 값을 변경해도 member2 는 유지 된다.

tx.commit();

(member2의 객체를 member1 객체 값을 복사한 객체로 넣는다 copyAddress)

member1의 값이 따로 변경된 것을 확인 할 수 있다. (오류 해결)

 

3. 불변 객체

위의 member1 과 memeber2 의 값 변경의 이상현상을 방지하기 위해 copyAddress 를 통해 객체를 복사 하였지만
IDE 내에서는 member2 의 setHomeaddress() 메서드에 address 를 넣든 copyAddress를 넣든 오류는 발생 하지 않는다.

혼자 개발하는 것이 아니기 때문에 해당 값을 변경하지 못하도록 해야 할 때가 있다. 이럴 때는 불변 객체를 사용하여 사전에 값이 변경되는 것을 막아야 한다.

 

방법 1 - set 메서드를 삭제한다.

방법 2 - set 메서드를 private 로 변경하여 외부 클래스에서 호출하지 못하도록 막는다

setCity 메서드를 public -> private 로 변경 하였다.

 

정리
  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체로 설계 해야한다.
  • 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고 : Integer, String 은 자바가 제공하는 대표적인 불변 객체

 

4. 값 타입 컬렉션 (값 타입을 컬렉션에 담아서 사용)

 

1) 값 타입 조회 

try {

    Member member = new Member();
    member.setUsername("member1");
    member.setHomeaddress(new Address("homeCity", "street","10000"));

    member.getFavoriteFoods().add("치킨");
    member.getFavoriteFoods().add("족발");
    member.getFavoriteFoods().add("피자");

    member.getAddressHistory().add(new Address("old1", "street","10000"));
    member.getAddressHistory().add(new Address("old2", "street","10000"));
    em.persist(member);

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

    System.out.println("======조회를 시작합니다=====");
    Member findMember = em.find(Member.class, member.getId());

    List<Address> addressHistory = findMember.getAddressHistory();
    for (Address address : addressHistory) {
        System.out.println("address.getCity() = " + address.getCity());
    }

    Set<String> favoriteFoods = findMember.getFavoriteFoods();
    for (String favoriteFood : favoriteFoods) {
        System.out.println("favoriteFood = " + favoriteFood);
    }

    tx.commit();

 

2) 값 타입 수정

System.out.println("======수정를 시작합니다=====");
Member findMember = em.find(Member.class, member.getId());

// homeCity -> newCity
// 잘못된 수정 예 : findMember.getHomeaddress().setCity("newCity");

// 올바른 수정 예 : 새로운 객체 생성
Address a = findMember.getHomeaddress();
findMember.setHomeaddress(new Address("newCity", a.getStreet(), a.getZipcode()));

tx.commit();

 

3) 컬렉션 수정

// 치킨 -> 한식 (컬렉션 수정)
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

 

4) 값 타입 주소 수정

// 주소 변경 (equals + hashcode 반드시 생성 후 remove() 사용)
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));

(주소 수정 시 .remove() 를 통해 모든 값을 delete 한 뒤 새로운 값을 재생성 하는 메커니즘으로 동작한다. 이 때, remove() 메서드는 equals() 메서드를 사용하기 때문에 반드 시 호출 대상 클래스에 equals() 메서드를 hashcode 와 함께 오버라이딩 해줘야 한다.)

이유 : equals 매서드를 오버라이딩 하지 않으면 JPA 는 기본적으로 equals() 를 == 비교로 처리 하기 때문

값 타입 컬렉션의 제약 사항
  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함 : null 입력 X, 중복 저장 X

※ 값 타입 컬렉션은 단순한 프로그램 설계에서 사용하는 것을 권장

값 타입 컬렉션 대안

  • 값 타입 컬렉션 보다는 일대다 관계를 고려
  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용