RE-Heat 개발자 일지

[JPA] [9] 값 타입 (상편) 본문

백엔드/JPA

[JPA] [9] 값 타입 (상편)

RE-Heat 2023. 8. 13. 22:32

https://www.inflearn.com/course/ORM-JPA-Basic

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

초급자를 위해 준비한 [웹 개발, 백엔드] 강의입니다. JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자

www.inflearn.com

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

■ JPA 데이터 타입 분류

① 엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자로 계속해 추적 가능
  • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능

② 값 타입

  • int, Integer, String처럼 단순한 값으로 사용하는 자바 기본 타입 또는 객체
  • 식별자가 없고 값만 있으므로 변경 시 추적 불가
  • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체됨

■ 값 타입 분류

① 기본값 타입

  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String

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

  • 예) x, y 좌표 묶어서 값으로 쓰고 싶을 때 쓰는 타입

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

  • 자바 컬렉션에 기본 값이나 임베디드 타입을 넣은 형태다

[1] 기본값 타입

■ 종류

  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String

■ 특징

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

 

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

int a = 20;
int b = a; //a객체의 주소값이 아닌 20이라는 값을 복사
b = 10;
  • int, double 같은 기본 타입(primitive type)은 절대 공유되지 않는다.
  • 기본 타입은 항상 값을 복사한다.
  • 따라서 값을 공유한다고 해도 부작용이 일어나지 않는다.
    • 주소값(=참조값)은 Stack영역이 가지고 있는 heap 영역의 객체 주소
Integer a = new Integer(10);
Integer b = a; // a의 참조를 복사 
a.setValue(20); // 부작용 발생
  • Integer 같은 래퍼 클래스나 String 같은 특수한 클래스는 공유 가능한 객체이지만 변경 X
  • setValue()가 가능하면 a, b 모든 값이 바뀌는 불상사가 발생하나, 래퍼 클래스는 애초에 set을 못하게 막아 놓음

 

[2] 임베디드 타입

새로운 값 타입을 직접 정의하는 걸 JPA에선 임베디드 타입이라 부른다.
  • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
  • int, String과 같은 값 타입

■ 예시

  • 기존 Member 테이블은 근무 시작·종료일, 주소의 도시·번지·우편 번호를 가지고 있음
  • 비슷한 필드끼리 묶기로 결정. 근무 시작·종료일은 근무 기간으로 도시·번지·우편 번호는 집 주소로 변경
  • 이런 식으로 묶는 것을 임베디드 타입이라고 부른다.

 

■ 임베디드 타입의 장점

  • 재사용이 가능하다.
  • 클래스 내 응집도가 높다.
  •  Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존한다.

 

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

  • 임베디드 타입은 엔티티의 값일 뿐이다.
  • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
    • 테이블 입장에선 임베디드 타입을 써도 바뀌는 게 없다.
    • 단, 객체는 데이터 외에 메서드(period.isWork() 등)까지 들고 있으므로 이득이 많다.
  • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
  • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음

 

■ 기본 사용법

  • @Embeddable : 값 타입을 정의하는 곳에 표시한다.
  • @Embedded : 값 타입을 사용하는 곳에 표시한다.
  • 기본 생성자는 필수이다.

참고] @Embeddable, @Embedded는 둘 중 하나만 명시해도 되나 둘 다 적는 게 낫다.

 

  • 값 타입을 사용하는 MemberEmb에 @Embedded를 명시한다.
  • 값 타입을 정의하는 Period, Address에 @Embeddable를 넣는다.

참고] 기존 Member 객체는 연관된 게 많아서 MemberEmb 객체를 따로 만듦

 

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

  • 임베디드 타입이 임베디드 타입을 가질 수 있다. (Address → Zipcode)
  • 임베디드 타입이 엔티티 가질 수 있다. (PhoneNumber → PhoneEntity)
    • FK만 가지면 되기 때문이다.

 

■ @AttributeOverride: 속성 재정의

한 엔티티에서 같은 값 타입을 쓰면 컬럼명이 중복된다. 이때 쓰는 애노테이션이다. 
@Entity
public class Member {
    @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;
}
  • homeAdress는 기존 컬럼명이 그대로 저장되고, workAddress는 새로 정의된 컬럼명을 사용한다.
  • @AttribteOverrides 안에 @AttributeOverride를 지정해 주는 걸 헷갈리지 말자(es 복수형 잊지 말자)

결과 : city, street, zipcode 외에 WORK_CITY, WORK_STREET, WORK_ZIPCODE도 MemberEmb 테이블에 삽입

 

 

@AttributeOverride로 매핑 정보를 재정의 하지 않으면?

컬럼명이 중복됐다는 오류가 뜬다

 

 

[3] 값 타입과 불변 객체

값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다 룰 수 있어야 한다.

 

■ 값 타입 공유 참조의 부작용

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

 

▶ 예시

  • member와 member2는 address를 공유하고 있다.
  • member.getHomeAddress().setCity("newCity")로 첫 번째 member의 city값만 바꾸는 게 의도
  • 하지만 실제 결과는 member와 member2의 city값이 newCity로 변경된다.(update 쿼리문 두 번 날아감)

첫 번째 값만 바꾸고 싶었는데, member, member2 모두 City->newCity로 변해버렸다.

 

■ 값 타입 공유 참조 부작용을 막는 해결책

값 타입의 실제 인스턴스인 값을 공유하는 건 위험하므로 대신 값을 복사해서 사용하면 된다.

 

▶ 예시

  • Address를 복사해서 member2 값에 넣고 setCity()로 첫 번째 것만 바꾼다.

  • member2는 복사한 인스턴스(주소값이 다름)를 들고 있으므로 영향이 없다.

 

■ 객체 타입의 한계

  • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
  • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
  • 자바 기본 타입에 값을 대입하면 값을 복사한다.

  • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.

  • 객체의 공유 참조는 피할 수 없다.

 

한계를 벗어나는 해결책은 바로 불변객체를 만드는 것이다.

 

■ 불변 객체(Immutable Object)

객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단할 수 있다
  • 값 타입은 불변 객체(immutable object)로 설계해야 한다.
  • 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
  • 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체

▶ 예시

  • Address에 값을 수정할 수 있는 setter를 쓰지 않거나 private으로 만들어버리면 된다.

@Setter를 제거. setCity()자체가 불가능하게 만듦.

     

=> 그렇다면 값을 바꾸는 방법은?

  • Address를 통째로 갈아엎으면 된다.
Address address = new Address("city", "street", "10000");
    
Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(address);
em.persist(member);
    
Address newAddress = new Address("NewCity", address.getStreet(), address.getZipcode());
member.setHomeAddress(newAddress); // 통째로 변경 
    
tx.commit();