본문 바로가기
개발/JPA

[JPA] 값 타입

by devhooney 2022. 7. 20.
728x90

김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 정리

 

1. JPA의 데이터 타입 분류

- 엔티티 타입

(1) @Entity로 정의하는 객체

(2) 데이터가 변해도 식별자로 지속해서 추적 가능

예) 회원 엔티티의 나이 값을 변경해도 키는 식별자로 인식

- 값 타입

(1) int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체

(2) 식별자가 없고 값만 있으므로 변경 시 추적 불가

예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체

 

2. 값 타입 분류

- 기본값 타입

(1) 자바 기본 타입(int, double)

(2) 래퍼 클래스(Integer, Long)

(3) String

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

- 컬렉션 값 타입(collection value type)

 

3. 기본값 타입

- 생명주기를 엔티티의 의존

예) 회원을 삭제하면 이름, 나이 필드도 함께 삭제

- 값 타입은 공유하면 X

예) 회원 이름 변경 시 다른 회원의 이름도 함께 변경되면 안됨

참고: 자바의 기본 타입은 절대 공유 X

- int, double 같은 기본 타입(primitive type)은 절대 공유 X
- 기본 타입은 항상 값을 복사함

 

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

- 기본 값 타입을 모아서 하나의 클래스로 만들어 놓은 것을 JPA에서는 임베디드 타입이라고 한다.

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

- JPA는 임베디드 타입(embedded type)이라 함

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

- int, String과 같은 값 타입

예) 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 가진다.

 

- 임베디드 타입 사용법

(1) @Embeddable: 값 타입을 정의하는 곳에 표시

(2) @Embedded: 값 타입을 사용하는 곳에 표시

(3) 기본 생성자 필수

@Embeddable
public class Period{
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

 

- 임베디드 타입의 장점

(1) 재사용

(2) 높은 응집도

(3) Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음

(4) 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함

 

- 임베디드 타입과 테이블 매핑

(1) 임베디드 타입은 엔티티의 값일 뿐이다.

(2) 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.

(3) 객체와 테이블을 아주 세밀하게(find-grainded)매핑하는 것이 가능하다.

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

 

- 임베디드 타입과 연관관계

(1) @AttributeOvberrid: 속성 재정의

@Embedded
private Address homeAddress;

@Embedded
@AttributeOverrides({
        @AttributeOverride(name="startDate",column = @Column("WORK_STARTDATE")),
        @AttributeOverride(name="endDate",column = @Column("WORK_ENDDATE")),
})
private Address companyAddress;

(2) 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null

(3) @MappedSuperclass vs 임베디드 타입

@MappedSuperclass와 임베디드 타입은 거의 똑같다. MappedSuperclass은 부모에 @MappedSuperclass을 붙이고 상속받아 사용하고, 임베디드 타입은 부모에 @Embeddable 애노테이션을 붙이고 사용하는 곳에 상속이 아니라 @Embedded만 붙여서 위임한다. 객체지향의 일반적인 법칙에 따르면 상속보다 위임이 좋기 때문에 위임을 보통 선택하지만 이 경우에는 편의에 따라 상속을 선택하는게 좋은 선택이 될 수도 있다.

임베디드 타입의 경우 JPQL에서 한 번 더 접근해서 사용해야 하지만 상속의 경우는 한 번만 접근하면 되므로 JPQL에서 조금 더 편리함이 있다. 어떤 것을 사용하든 상관없지만 코드를 줄이고 싶다면 상속을 사용하는게 더 좋은 선택이다.

(4) 값 타입 공유 참조

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함

부작용(side effect) 발생

값 타입의 실제 인스턴스인 값을 공유하는 것은 위험

대신 값(인스턴스)를 복사해서 사용

기본 타입(primitive type)의 경우 = 을 사용하면 값을 참조하는게 아니라 값을 그냥 복사한다. 하지만 객체 타입의 경우 = 을 사용하거나 파라미터로 객체를 넘기면 객체가 복사되는게 아니라 객체의 참조값이 넘어간다. 여기서 문제가 생긴다. 위와 같이 임베디드 타입인 Address를 member1과 memeber2가 공유하고 있게되면 member1.getHomeAddress().setCity로 값을 변경해버리면 member2에 들어가있는 address에도 영향을 주게 된다. 이런 side effect는 실무에서 정말 찾기 어렵다. 따라서 주석처리 해놓은 것처럼 같은 객체를 넣는 것이 아니라 객체를 복사해서 새로운 객체를 복사해 만들고 그것을 넣어주는 것이 방법이다.

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

Member member = new Member();
member.setName("member1");
member.setHomeAddress(address);
em.persist(member);

// Address copyAddress = new Address(address.getCity(),address.getZipcode(),address.getZipcode());
// 복사한 값을 member2의 address값 세팅에 대입

Member member2 = new Member();
member.setName("member2");
member.setHomeAddress(address);
em.persist(member2);

(5) 불변 객체

객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단

값 타입은 불변 객체(immutable object)로 설계해야함

불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체

생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨

참고: Integer, String은 자바가 제공하는 대표적인 불변 객체

-> 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있다.

 

5. 값 타입의 비교

- 값 타입: 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 봐야함

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

- 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용

- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함

- 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)

 

6. 값 타입 컬렉션 - 거의 사용 하지 않음(일대다 관계 사용)

- 값 타입을 하나 이상 저장할 때 사용

- @ElementCollection, @CollectionTable 사용

- 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.

- 컬렉션을 저장하기 위한 별도의 테이블이 필요함.

@Entity
public class Member{
    // 생략

    // joinColumn에 관한 설명은 위 링크 참고
    
    @ElementCollection // 값 컬렉션임을 인지
    @CollectionTable(name ="ADDRESSES", // 테이블만들고 이름 설정
        joinColumns = @JoinColumn(name = "MEMBER_ID")) // 현재 엔티티를 참조하는 FK 설정
    // 컬럼명 바꾸러면 AttributeOverrides 사용하면 됨
    private List<Address> addresses = new ArrayList<>(); // 값 타입을 리스트
           

    @ElementCollection // 값 컬렉션임을 인지
    // 컬렉션 테이블 만들기, 테이블 이름, 외래키 설정
    @CollectionTable(name ="FAVORITE_FOOD",
    joinColumns = @JoinColumn(name = "MEMBER_ID"))
    @Column(name="FOOD_NAME") // 위와 달리 값이 하나고 내가 정의한 것이 아니므로 테이블 만들 때 컬럼명을 이처럼 수정 가능
    private Set<String> favoriteFoods = new HashSet<>();

    
}


// String 타입인 값타입 컬렉션은 그냥 add remove로 넣고 빼면 된다.
// 임베디드 타입인 값타입 컬렉션은 아래와 같이 처리한다.
// 대부분의 컬렉션은 equals 비교가 기본이기 때문에 반드시 overrided해놨어야 정상적으로 작동한다.
// REMOVE 시점, findMember에 찾아온 멤버가 담겨져있다고 가정
findMember.getAddresses().remove(new Address("city","street","1111"));

- 값 타입 컬렉션도 지연 로딩 전략 사용

참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.

 

값 타입 컬렉션의 제약사항

(1) 값 타입은 엔티티와 다르게 식별자 개념이 없다.

(2) 값은 변경하면 추적이 어렵다.

(3) 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

(4) 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본키를 구성해야 함: null 입력 X, 중복 저장 X

 

7. 정리

엔티티 타입의 특징

(1) 식별자 O

(2) 생명 주기 관리

(3) 공유

 

- 값 타입의 특징

(1) 식별자 X

(2) 생명 주기를 엔티티에 의존

(3) 공유하지 않는 것이 안전(복사해서 사용)

(4) 불변 객체로 만드는 것이 안전

 

- 값 타입은 정말 값 타입이라 판단될 때만 사용

- 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안됨

- 식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티.

728x90