GitHub

https://github.com/Choidongjun0830

Spring

[JPA 기본편] 값 타입

gogi masidda 2024. 6. 24. 15:37

JPA는 데이터 타입을 두가지로 분류

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능.
  • 값 타입
    • int, integer, string처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경시 추적 불가
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
    • 값 타입은 기본값 타입, 임베디드 타입, 컬렉셔 값 타입이 있다.
  • 기본 값 타입
    • 자바 기본 타입(int, double)
    • 래퍼 클래스(Integer, Long)
    • String
  • 임베디드 타입(복합 값 타입)
  • 컬렉션 값 타입

기본 값 타입

  • 생명 주기를 엔티티에 의존
    • 회원을 삭제하면 이름, 나이 필드도 함께 삭제
  • 값 타입은 공유하면 안됨
    • 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안됨.

자바의 기본 타입은 절대 공유되지 않는다. 기본 타입은 항상 값을 복사한다.

Integer같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경되지 않는다. 그래서 사이드 이펙트가 일어날 일이 없다.

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

  • 새로운 값을 직접 정의할 수 있음
  • JPA는 임베디드 타입이라 함
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입
  • 값 타입을 사용하는 곳에 @Embedded, 값 타입을 정의하는 곳에 @Embeddable
  • 기본 생성자 필수
  • 예) city, street, zipCode를 모아서 Address 타입으로 만들기.
  • 장점
    • 재사용 가능
    • 높은 응집도
    • Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있다.
  • 한 엔티티에서 같은 값 타입을 사용하면 컬럼 명이 중복되기 때문에 @AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명과 속성을 재정의한다.
@Entity
//@Table(name = "USER") 객체와 데이터베이스 테이블의 이름이 다를 때
public class Member{
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @Embedded
    private Period workPeriod;

    @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;
}

값 타입과 불변 객체

  • 값 타입 공유 참조
    • 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함
      • 부작용 발생

임베디드 타입 공유 예제

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");

위처럼 코드를 작성하면 member2의 city도 newCity로 바뀐다.

그래서 값을 복사해서 사용해야 한다. 하지만, 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다. 따라서 객체의 공유 참조는 피할 수 없다.

값 타입은 생성 시점 이후 절대 값을 변경할 수 없는 객체인 불변 객체로 설계해야 한다.

생성자로만 값을 설정하고, 수정자를 만들지 않으면 된다.

값 타입의 비교

값 타입은 인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야 한다.

  • 동일성(identity) 비교: 인스턴스의 참조 값을 비교. == 사용
  • 동등성(equivalence) 비교: 인스턴스의 값을 비교. equals() 사용
    • equals()는 오버라이드해서 만들어둬야함. 인텔리제이에서 객체 클래스에서 alt + insert로 만들기

값 타입 컬렉션

  • 값 타입을 컬렉션에 담아서 사용하는 것.
  • 값을 하나 이상 저장할 때 사용한다.
  • @ElementCollection, @CollectionTable 사용
@Entity
//@Table(name = "USER") 객체와 데이터베이스 테이블의 이름이 다를 때
public class Member{
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @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<>();
    ...

결과

<aside> 🧑🏼‍💻

create table ADDRESS ( MEMBER_ID bigint not null, city varchar(255), street varchar(255), zipCode varchar(255) )

create table FAVORITE_FOOD ( MEMBER_ID bigint not null, FOOD_NAME varchar(255) )

</aside>

값 타입 생성, 추가, 수정

        		Member member = new Member();
            member.setUsername("member1");
            member.setHomeAddress(new Address("city1", "street", "10000"));
            member.getFavoriteFoods().add("치킨");
            member.getFavoriteFoods().add("피자");

            member.getAddressHistory().add(new Address("city2", "street2", "10000"));
            member.getAddressHistory().add(new Address("city3", "street3", "10000"));

            em.persist(member);

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

            //조회
            System.out.println("=====START=====");
            Member member1 = em.find(Member.class, member.getId());
            List<Address> addressHistory = member1.getAddressHistory();
            for (Address address : addressHistory) {
                System.out.println("address = " + address.getCity());
            }

            //수정. 불변해야 해서 아예 새롭게 지정
            //homeCity -> newCity
            Address homeAddress = member1.getHomeAddress();
            member1.setHomeAddress(new Address("newCity", homeAddress.getStreet(), homeAddress.getCity()));

            //치킨 -> 한식
            member1.getFavoriteFoods().remove("치킨");
            member1.getFavoriteFoods().add("한식");
            
            member1.getAddressHistory().remove(new Address("city2", "street2", "10000"));
            member1.getAddressHistory().add(new Address("newCity1", "street3", "10000"));

            tx.commit();

값 타입 컬렉션의 제약 사항

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

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

단순하고 값이 바뀌어도 추적할 필요가 없을 때 값 타입 컬렉션을 사용한다.

728x90