[자바 ORM 표준 JPA 프로그래밍 - 기본편] - 값타입

이재표·2023년 10월 2일
0

값타입

jpa에서 타입은 크게 엔티티와 값타입으로 분류된다.

엔티티 타입

  • @Entity로 정의되는 객체
  • 데이터가 변해도 식별자로 추적가능

    엔티티 안의 필드 값이 변해도, 계속하여 해당 엔티티를 추적하여 값을 확인할수 있다

값타입

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

    한 변수의 값이 변경되면 그냥 값이 변경되고 끝나므로 추적같은것 불가

값타입 분류

값타입에는 기본값 타입, 임베디드 타입, 컬렉션 값타입 3가지 종류가 존재한다.

기본값 타입

기본값 타입은 String, int와 같은 primitive 타입을 말한다.

  • 생명주기를 엔티티에 의존한다.

    회원을 생성, 삭제하면 이름, 나이같은 기본값타입도 함께 생성, 삭제된다. 즉 생명주기를 엔티티에 의존하고 있다.

  • 값 타입은 공유하면 안된다.

    특정 엔티티의 이름같은 값을 공유하면 다른 회원의 이름도 함께 변경되면 안된다.

    • 하지만 기본값타입은 복사하면 단순히 값을 복사하므로 공유될여지가 없다.
    • Integer와 같은 래퍼 클래스나 String같은 특수한 클래스는 참조때문에 다른 엔티티에 공유될수 있지만, 해당 클래스의 값을 변경할 방법이 없기때문에 공유되어도 사이드 이펙트가 발생할 여지가 없다.

임베디드 타입

개발을 진행하다보면 "시작일"과"종료일" 같이 비슷한 성격의 필드를 마주치게된다. 이때 단순히 엔티티안에 작성해주는것 보다 하나의 타입으로 정의해서 사용하면 좀 더 재사용성이나 높은 응집도를 가져 유용하다.
이것을 임베디드 타입을 이용하여 구현할수 있다.

임베디드 타입은 다음과 같이 비슷한 유형의 필드를 하나의 클래스로 묶어서 마치 타입처럼 사용하는 것을 말한다.

원래 Member클래스는 다음과 같이 생겼다.

하지만 비슷한 유형의 날짜와 주소들을 임베디드 타입으로 묶으면 다음과 같이 구조화할수 있다.

임베디드 타입은 @Embeddable, @Embedded 2가지 어노테이션을 이용하여 사용할수 있는데 코드를 통해 살펴보자

// Member객체, 주소와 기간을 임베디드타입으로 묶어 놓았다.
public class Member{
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    private String name;
    @Embedded
    private Address homeAddress;
    @Embedded
    private Period workPeriod;
}
// @Embeddable을 통해 임베디드타입으로 사용한다는것을 선언한다.
// 기본생성자는 필수이다.
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

임베디드 타입을 사용함으로써 "재사용성"이 좋아지고, "높은 응집도"를 갖게된다. 또한 각 임베디드 타입 내에서 메서드를 생성하여 의미있는 메서드(Period.isWork : 기간안에 포함되어있는지 확인하는 메서드)들을 만들수 있으며, 모든 임베디드 타입은 "엔티티의 생명주기"에 의존하게 된다.

만약 같은 임베디드 타입을 한 엔티티에서 여러번 사용해야한다면?
컴럼명이 중복되기 때문에 그냥은 사용못하고 @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의해서 사용할수 있다.

참고!

  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래
    스의 수가 더 많음
@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;
    @Embedded
    private Address homeAddress;

이때 만약 임베디드 타입을 선언할때 값이 NULL이라면 엔티티의 해당 필드도 NULL로 들어가게된다

값타입과 불변객체

만약 다른 엔티티에 값타입을 공유하면 어떻게 될까?
값타입의 경우 "클래스"이기 때문에 참조하게 되는것을 막을수 없다. 따라서 그에 따른 사이드 이펙트를 막을수 없다.

객체 타입의 한계
기본타입(Primitive type)

int a = 10;
int b = a;//기본 타입은 값을 복사
b = 4;

객체 타입

Address a = new Address(“Old”);
Address b = a; //객체 타입은 참조를 전달
b. setCity(“New”)

예시

Address address = new Address("city", "street", "10000");
Member member1 = new Member();
em.persist(member1);

Address address1 = new Address(address.getCity(),address.getStreet(),address.getZipcode());

Member member2 = new Member();
em.persist(member2);

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

다음과 같은 경우 member객체가 같은 Adress 객체를 공유하기 때문에 member객체중 하나만 변경되어도 다른 객체도 변경되어 update쿼리가 나가는 것을 볼수 있다.

그렇다면 객체의 값을 전달하고자 할때는 어떻게 해야할까?? 위의 코드를 예시로 하면 새로운 Address객체를 만들어서 다른 객체에 넣어줘야한다.

그렇다면 여러 개발자가 협업하다 새로운 값 타입을 만들어 넣는것을 까먹는다면? 추적할 방법이 없다... 따라서 값타입을 불변객체로 만들어 사용해야한다. 여러 방법이 있지만 가장 간편한 방법은 @Setter와 같이 수정자를 지워주는 방법이다!!(eg. String과 Integer와 같은 래퍼 객체는 대표적인 불변객체이다!!)

값타입 비교

값타입 : 인스턴스가 달라도 그 안의 값이 같으면 같은것으로 봐야한다.
기본타입의 경우 바로 값을 비교하기 때문에 == 비교가 가능

int a = 10;
int b = 10;
a==b // true

하지만 값 타입의 경우 참조가 들어가기 때문에 서로 같은 값이라도 다른 인스턴스라면 false가 나옴

Address address1 = new Address("city");
Address address2 = new Address("city");
address2 == address1; // false

때문에 값타입을 비교하기 위해 equals 와 hashCode를 통해 값을 일일히 비교해줘야한다.

  • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
  • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals()
    사용

값타입 컬렉션

다음과 같이 특정 값타입을 컬렉션으로 받는 상황이 있을수 있다.

다음과 같은 경우 값타입 컬렉션을 사용한다.

  • 값타입을 하나 이상 저장할때 사용
  • @ElementCollecion, @CollectionTable 사용
    하지만 데이터베이스는 컬렉션과 같이 한 필드에 여러 값을 저장하는 방법이 없다. 따라서 컬렉션을 저장하기 위해 별도의 테이블을 생성하는 방법밖에 없다.

값타입 컬렉션의 특징

  • 값타입 컬렉션은 지연로딩을 사용
  • 영속성 전이와 고아객체 제거기능을 필수로 가진다 -> 생명주기를 엔티티에 의존한다.

다음과 같이 그냥 사용하면 되겠지라 생각할수 있지만 치명적인 단점
값 타입 컬렉션에 변경사항이 발생하면, 엔티티의 해당 컬렉션의 모든 정보를 삭제하고, 값타입 컬렉션에 있는 현재값을 다시 저장한다!
매우 치명적이다.

따라서 해당 방법은 실무에 사용하면 안된다(만약 사용하려면 정말 단순한 작업일때만, 엔티티에 의존적이어도 되는 경우에만 사용한다(메뉴 선택 체크박스 같은 경우))

값타입 컬렉션의 대안책으로는 일대다 관계를 통해 엔티티로 연관관계를 맺어주는 방법이다.

@Entity
public class AddressEntity {
    @Id
    @GeneratedValue
    private Long id;
    private Address address;
    public AddressEntity(String city,String street,String zipcode){
        this.address = new Address(city, street, zipcode);
    }
}

영속성 전이와 고아 객체제거를 사용하여 위와 같이 값타입을 엔티티로 wrapping하여 사용하면, 엔티티가 삭제되더라도 값타입 엔티티를 통해 추적가능하다.

이때 쿼리에서 update쿼리가 나가게 되는데, 일대다 단방향 매핑과 같은 형태이기 때문에 어떨수 없다.

엔티티 타입의 특징

  • 식별자가 있어야한다.(pk)
  • 생명 주기 관리가 영속성 컨텍스트에 의존
  • 공유될수 있다.
    값 타입의 특징
    • 식별자 없다(값타입의 필드와 의존 엔티티의 fk를 모두 식별자로써 사용)
    • 생명 주기를 엔티티에 의존
    • 공유하지 않는 것이 안전(복사해서 사용)
    • 불변 객체로 만드는 것이 안전

즉 값타입은 정말 값타입이라 판단될때만 사용하고, 엔티티와 값타입을 혼동하여 사용하면 안된다. 값의 추적이 필요하다면 엔티티를 사용해야한다.

0개의 댓글