BackEnd/JPA

[JPA] 값 타입 - 1

샤아이인 2022. 4. 7.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 기본값 타입

JPA의 데이터 타입은 크게 2가지로 분류 가능하다. (엔티티 타입, 값 타입)

 

1. 엔티티 타입

- @Entity로 정의하는 객체이다.

- 데이터가 변해도 식별자로 지속적인 추적이 가능하다.

=> 예를 들어 회원 엔티티의 키나 나이값 을 변경해도 식별자로 인식 가능하다.

 

2. 값 타입

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

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

=> 예를 들어 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체된다.

 

값 타입은 다시 3가지 종류로 분류할수가 있다.

● 값 타입의 분류

1. 기본값 타입

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

- 래퍼 클래스(Integer, Long)

- String

 

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

=> 예를 들면 우편번호 , 또는 좌표(x, y)와 같은 복합 값을 Position클래스로 만들어 쓰려고하는 것을 임베디드 타입이라고 한다.

 

3. 컬렉션 값 타입(collection value type)

- Java collection(Array, Map, Set)에 값을 넣을수 있는 것을 컬렉션 값 타입이라 한다.


● 기본값 타입

예): String name, int age

 

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

=> 에를 들면, 회원을 삭제하면 회원의 이름, 나이 필드도 함께 값이 삭제 된다.

 

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

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

 

(참고. 자바의 기본 타입은 절대 공유되지 않는다)

 

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

JPA에서는 새로운 값 타입을 직접 정의할수 있습니다. 이를 임베디드 타입(embedded type)이라고 부릅니다.

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

int, String과 같은 값 타입이다. 엔티티가 아니다!

 

예시를 통해서 살펴보자.

 

1. 회원 엔티티에 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편번호를 갖고있다고 해보자.

출처 - 인프런 김영한 JPA

필드가 너무 복잡하지 않은가? (startDate + endDate) 를 한덩어리로 만들고, (city + street + zipcode)를 한 덩어리로 만들면 좀더 편하지 않을까?

 

2. 회원 엔티티가 이름, 근무 기간(workPeriod), 집 주소(homeAddress) 를 갖도록 변경

출처 - 인프런 김영한 JPA

Period 와 Address 라는 타입을 만들어낸 것 이다. 따라서 다음과 같이 된다.

출처 - 인프런 김영한 JPA

id, name, workPeriod, homeAddress 총 4가지 필드를 갖게 된다.

우선 코드를 보기전에 인베디드 타입을 사용하는 방법을 살펴보자.

 

● 입베디드 타입 사용법

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

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

- 기본 생성자 필수

 

● 코드 예시

우선 임베디드 타입을 사용하지 않았을때의 코드부터 살펴보자.

 

- Before

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    // Date
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    //Address
    private String city;
    private String street;
    private String zipcode;
}
 

Date 와 Address 필드 부분에 값이 많은것을 확인할수가 있다.

이것을 내장타입으로 변경해 보면 다음과 같다.

 

- After

Period, Address (값 타입 정의하기)

@Embeddable // 값 타입이 정의되는 곳에 @Embeddable 사용
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;
}

@Embeddable // 값 타입이 정의되는 곳에 @Embeddable 사용
public class Address {
    private String city;
    private String street;
    private String zipcode;
}
 

Member

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded // 값 타입이 사용되는 곳에 @Embedded 사용
    private Period workPeriod;

    @Embedded // 값 타입이 사용되는 곳에 @Embedded 사용
    private Address homeAddress;
}
 

결과는 다음과 같다.

테이블이 그대로 유지되고 있다.

 

● 임베디드 타입의 장점

- 재사용이 가능하다

- 높은 응집도를 갖는다.

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

@Embeddable // 값 타입이 정의되는 곳에 @Embeddable 사용
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private boolean isWork(){
	    ...
    }
}
 

 

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

=> 임베디드 타입도 값 타입이다. 따라서 엔티티가 죽으면 함께 사라지는 값이다.

 

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

DB 입장에서는 바뀔점이 없다. 임베디드 타입을 사용하든 안하든 회원 테이블은 똑같다.

출처 - 인프런 김영한 JPA

테이블은 DB가 데이터를 잘 관리하는것이 목적이기 때문에 위와 같이 구현되는것이 좋다.

하지만 객체는 데이터 뿐만 아니라 메서드(행위) 까지 갖고있기 때문에 묶어서 사용하면 이득을 얻을수가 있다.

 

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

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

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

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

 

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

출처 - 인프런 김영한 JPA

 

- Address 라는 임베디드 타입은 Zipcode라는 임베디드 타입을 갖고있다.

@Embeddable
public class Address {
    String street;
    String city;
    
    @Embedded
    Zipcode zipcode; // 임베디드 타입 포함
}

 

PhoneNumber 라는 임베디드 타입은 PhoneEntity라는 엔티티를 갖고 있다.

@Embeddable
public class PhoneNumber {
    String areaCode;
    String localNumber;
    
    @ManyToOne
    PhoneEntity provider; // 엔티티 참조
}
 

● @AttributeOverride: 속성 재정의

Member안에 동일한 임베디드 타입이 있다면 어떻게 될까?

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

    // workAddress라는 homeAddress와 동일한 타입이 추가된다면?
    @Embedded
    private Address workAddress;
	
}
 

위 코드를 보면 workAddress 와 homeAddress가 같은 Address 타입으로 중복이 된다.

실행하면 다음과 같은 결과가 나온다.

중복된 column이 있다는 경고가 나온다.

 

이럴때 @AttributeOverride를 사용하여 컬럼명 속성을 재정의 해준다.

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

실행결과는 다음과 같다.

파란 박스 안처럼 column이 추가된것을 확인할수가 있다.

 

● 임베디드 타입의 값이 null이면 매핑한 컬럼의 값은 모두 null이 된다.

 

3. 값 타입과 불변 객체

값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.

따라서 값 타입은 단순하고 안전하게 다 룰 수 있어야 한다.

 

● 값 타입 공유 참조

 

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

 

출처 - 인프런 김영한 JPA

회원1 과 회원2가 같은 주소 값타입을 참족하고 있다. 여기서 주소 값타입을 한쪽에서 변경하면 회원1, 2 양쪽에서 모두 바뀌어 버린다.

 

코드로 살펴보자.

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

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
entityManager.persist(member1);

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(address);
entityManager.persist(member2);
 

위와같이 코드를 작성한후 DB를 살펴보면 다음과 같다.

member1, 2 모두 CITY 컬럼이 city값으로 되어있다.

이제 member1에서 CITY를 변경해 보자.

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

실행하면 결과는 다음과 같다.

이런 의도하지 않은 결과가 발생할수가 있다.

 

만약 이렇게 값이 공유되어 한쪽에서 변경하면 전부 변경되도록 의도한것 이라면, 값타입이 아니라 Entity를 사용해야 한다.

 

 

● 값 타입의 복사

위와같이 값 타입의 실제 인스턴스인 값을 공유하는 것은 위험하다. 대신 값(인스턴스)를 복사해서 사용해야 한다.

출처 - 인프런 김영한 JPA

따라서 코드는 다음과 같이 변해야 한다.

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

Member member1 = new Member();
member1.setUsername("member1");
member1.setHomeAddress(address);
entityManager.persist(member1);

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

Member member2 = new Member();
member2.setUsername("member2");
member2.setHomeAddress(copyAddress); // 복사한 copyAddress가 넘어감
entityManager.persist(member2);

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

● 객체 타입의 한계

- 항상 값을 복사해서 사용하면 공유참조로 인해 발생하는 부작용을 피할 수 있다.

문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아닌 객체 타입이다.

 

자바 기본 타입에 값을 대입하면 값을 복사해서 넘기기 때문에 문제가 되지 않는다.

 

하지만 객체 타입이 참조 값을 직접 대입하는 것을 막을 방법이 없다. => 객체의 공유 참조는 피할 수 없다.

 

기본 타입(primitive type)은 '='으로 값을 복사한다.

하지만, 객체 타입에서 '='을 통한 대입은 참조를 전달한다. => 인스턴스가 하나 이기에 같이 변경된다.

 

● 불변 객체

- 객체 타입을 수정할 수 없도록 부작용을 원천 차단한다.

 

값 타입은 불변 객체(immutable object)로 설계해야 한다. => 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체

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

 

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

 

그럼 값을 변경해야하는 경우에는 어떻게 해야할까? setter가 사라졌으니 말이다!

=> 새로 만들어주면 된다.

Address newAddress = new Address(newCity, address.getStreet(), address.getZipCode())
member.setHomeAddress(newAddress); // 완전 새롭게 다시 설정해준다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어 1 (기본 문법)  (0) 2022.04.08
[JPA] 값 타입 - 2  (0) 2022.04.07
[JPA] 영속성 전이와 고아 객체  (0) 2022.04.06
[JPA] 프록시와 연관관계 관리  (0) 2022.04.06
[JPA] 고급 매핑  (0) 2022.04.05

댓글