Java

[JPA(Jakarta Persistence API)] 값 타입

ride-dev 2024. 3. 13. 22:05

JPA(Jakarta Persistence API)의 데이터 타입은 크게 두 가지로 분류됩니다.

엔티티 타입과 값 타입,

엔티티 타입은 @Entity로 정의하는 클래스 객체립니다.

식별자가 있기 때문에 데이터가 변해도 식별자를 통해 추적할 수 있습니다.

 

값타입은 int, Integer, String 등 으로 사용하는 자바 기본 자료형이나 객체(실제 사용되는 값, 내용물)를 말합니다.

식별자가 없고 값만 있으므로 변경 사항을 추적할 수 없습니다.

생명 주기를 주인 엔티티에 의존합니다.

사이드 이펙트을 예방하기 위해 불변 객체로 만드는 것을 권장합니다.

값 타입

값타입은 복잡한 객체를 단순화하기 위해 만든 개념입니다.

값 타입이라고 판단될 때만 사용해야 합니다.

값 타입에는 기본 값 타입, 임베디드 타입(복합 값  타입), 컬렉션 값 타입이 있습니다.

기본 값 타입

기본 값 타입은 int, double과 같은 자바 기본 타입, Integer, Long과 같은 래퍼 클래스, String이 있습니다.

String name, int age 등으로 사용할 수 있습니다.

생명 주기를 엔티티에 의존하기 때문에 엔티티객체를 삭제하면 필드(기본 값) 또한 삭제됩니다.

엔티티와 달리 변경사항을 공유하지 않도해야 합니다.

엔티티가 변경되면 DB 테이블이 변경될 수 있으며 데이터 전체에 변경사항이 적용되지만,

일반적으로 기본 값이 변경되면, DB 테이블의 특정 row만 변경하며 다른 모든 row에 적용하면 안됩니다.

(한 명이 비밀번호를 변경하면 모든 회원의 비밀번호 또한 변경하지 않도록 해야합니다)

 

임베디드 타입, embedded type, 복합 값 타입

임베디드 타입은 새로운 값 타입을 직접 정의한 것입니다.

주로 기본 값  타입을 모아 만들기 때문에 복합 값 타입이라고도 합니다.

임베디드 타입 또한 int, String과 같은 값 타입으로 이루어져 있습니다.

값 타입을 정의하는 곳에 @Embeddable을 사용하고,

값 타입을 사용하는 곳에 @Embedded를 사용합니다.

기본 생성자가 필수적입니다.

@Entity
public class Member {
    private long id;
    private String name;
    
    @Embedded
    private Address;
    @Embedded
    private Period;
}

@Embeddable
public class Address {
    private String city;
    private String postalcode;
    //기본 생성자
}

@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    //기본 생성자
}

한 엔티티에서 특정 값 타입을 2개 이상 사용한다면,

아래와 같이 @AttributeOverride를 사용하여 컬럼 명 속성을 재정의할 수 있습니다.

@Entity
public class Member {
    private long id;
    private String name;
    
    @Embedded
    private Address;
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(
            name = "city",
            column = @Column(name = "WORK_CITY")),
        @AttributeOverride(
            name = "postalcode",
            column = @Column(name = "WORK_POSTALCODE")),
    private Address;
    @Embedded
    private Period;
}

 

높은 재사용성, 높은 응집도가 장점입니다.

해당 값 타입만 사용하는 의미 있는 메서드를 만들 수 있습니다.

임베디드 타입을 포함한 모든 값 타입은 해당 값 타입을 소유한 엔티티의 생명주기에 의존합니다.

 

임베디드 타입은 엔티티의 값일 뿐이며, 임베디드 타입을 사용해도 테이블 구조가 변경되지 않습니다.

객체와 테이블을 아주 세밀하게 (find-grained) 매핑하는 것이 가능하며,

잘 설계한 ORM 애플리케이션은 매핑한 테이블 수보다 클래스의 수가 더 많습니다.

 

여러 엔티티 객체에 하나의 임베디드 값 타입을 사용하면, 사이트 이펙트가 발생할 수 있습니다.

Address address = new Address("city", "000000"); // 임베디드 값 타입을 생성

Member member1 = new Member();
member.setName("m1");
member.setAddress(address); // 공유1
em.persist(member1);

Member member2 = new Member();
member.setName("m2");
member.setAddress(address); // 공유2
em.persist(member2);

member1.getAddress().setCity("sameCity");
// 동일한 임베디드 값 타입을 사용하기 때문에
// 두 엔티티의 값이 모두 변경됩니다.
Address address1 = new Address("city", "000000"); // 임베디드 값 타입을 생성

Member member1 = new Member();
member.setName("m1");
member.setAddress(address1);
em.persist(member1);

Address address2 = new Address(address.getCity(), address.getPostalcode());
// 기존 임베디드 값 타입을 복사해서 사용
Member member2 = new Member();
member.setName("m2");
member.setAddress(address2);
em.persist(member2);

member1.getAddress().setCity("newCity");
// 다른 임베디드 값 타입을 사용하기 때문에
// 두 엔티티의 값이 모두 변경되지 않습니다.

기본 타입은 값을 복사할 수 있기 때문에 위와 같이 값을 복사해서 사용하면,

공유 참조로 인해 발생하는 부작용을 피할 수 있습니다.

int a = 10;
int b = a;
b = 4; // b는 a의 값을 복사한 것이므로 a는 그대로 10입니다.

하지만 객체 타입은 참조 값을 전달하기 때문에,

객체의 공유 참조를 피할 수 없습니다.

Address a = new Address("Old");
Address b = a; // 이때, b는 객체a의 참조값을 가지고 있으므로
b.setCity("Same"); // a 또한 변경됩니다.

따라서 객체 타입을 수정할 수 없도록 불변 객체로 만들면 부작용을 예방할 수 있습니다.

값 타입과 불변 객체

불변 객체는 생성 시점 이후 값을 변경할 수 없는 객체입니다.

생성자로만 값을 설정하면 됩니다.

String, Integer 등이 자바가 제공하는 대표적인 불변객체입니다.

String str = "";
str + "a";
str + "b";
// str을 출력하면 ab가 출력됩니다.

위와 같은 결과가 나오는 이유는, 변경 사항이 적용될 때마다 새로운 객체를 만들기 때문입니다.

만약 String 객체의 값을 변경하는 로직을 작성한다면,

불변 객체가 아닌(예컨대  StringBuilder) 객체를 사용하는 것에 비해 성능 문제가 발생할 수 있습니다.

(String 으로 20초가 걸려도 StringBuilder로 0.2초 안에 해결될 수 있습니다)

값  타입의 비교

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

int a = 10;
int b = 10;

Address a = new Address("a");
Address b = new Address("a");

 

따라서 값 타입은 인스턴스 참조 값 비교에 사용하는 ==, 동일성(indentity) 비교가 아닌,

인스턴스의 값을 비교하도록 .equals()를 사용하는 동등성(equivalence) 비교를 해야합니다.

(값 타입의 equals()메서드를 재정의하여 사용합니다)

컬렉션 값  타입, collection value type

값 타입을 하나 이상 저장할 때 사용합니다.

@ElementCollection, @CollectionType을 사용합니다.

관계형 데이터베이스는 컬렉션을 같은 테이블에 저장하기 어렵습니다.

컬렉션을 저장하기 위한 별도의 테이블이 필요합니다.

 

컬렉션 값 타입은 일대다 개념에 가깝지만,

일대다 연관관계를 구성하는 것과 달리 엔티티를 여러 개 만들지 않습니다.

@Entity
public class Member {
    @Id
    private long id;
    private String name;
    
    @ElementCollection
    @CollectionTable(
        name = "FAVORITE_COLOR",
        joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name = "COLOR_NAME")
    private Set<String> favoriteColors = new HashSet<>();
    @ElementCollection
    @CollectionTable(
        name = "ADDRESS",
        joinColumns = @JoinColumn(name = "MEMBER_ID")))
    private List<Address> addressHistory = new ArrayList<>();
}
Member member = new Member();
member.setNAme("member1");

member.getFavoriteColors().add("black");
member.getFavoriteColors().add("white");
member.getFavoriteColors().add("green");

member.setAddressHistory().add(new Address("city1", "000000"));
member.setAddressHistory().add(new Address("city2", "000001"));

em.persist(member);

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

Member findMember = em.find(Member.class, member.getId());

// 불변 객체이기 때문에, 수정 할 때 update 하지 않습니다.

findMember.getFavoriteColors().remove("black");
findMember.getFavoriteColors().add("gray");

findMember.getAddressHistory().remove(new Address("city1", "000000")); // equals 재정의 필요
findMember.getAddressHistory().add(new Address("newCity", "000003"));

위와 같이 사용할 수 있습니다.

여기서 알 수 있듯이 컬렉션 값 타입 또한,

컬렉션 값 타입을 소유하고 있는 엔티티의 생명주기에 의존합니다.

 

또한 조회 시 지연 로딩이 적용되며,

CASCADE + 고아 객체 제거 기능이 적용되기 때문에 주인 엔티티 제거시,

컬렉션 값 타입 데이터 또한 제거됩니다.

 

식별자 개념이 없기 때문에 변경 시 추적이 어렵습니다.

컬렉션 값 타입에 변경 사항이 발생하면,

주인 엔티티와 연관된 모든 데이터를 삭제하고, 컬렉션에 있는 현재 값을 모두 다시 저장합니다.

(성능 문제가 발생할 수 있기에, 사용한다면 데이터의 업데이트가 없다는 전제하에 사용합니다)

매핑 테이블의 컬럼은 모두 PK로 묶어야 하며, null과 중복을 허용하지 않도록 합니다.

 

컬렉션 값 타입을 사용하는 것 대신,

일대다 관계를 위한 엔티티를 만들고 값 타입을 사용한 뒤,

CASCADE + 고아 객체 제거를 사용하는 방식을 권장합니다.

728x90