danuri
오늘의 기록
danuri
전체 방문자
오늘
어제
  • 오늘의 기록 (307)
    • java (150)
      • java (33)
      • spring (63)
      • jpa (36)
      • querydsl (7)
      • intelliJ (9)
    • kotlin (8)
    • python (24)
      • python (10)
      • data analysis (13)
      • crawling (1)
    • ddd (2)
    • chatgpt (2)
    • algorithm (33)
      • theory (9)
      • problems (23)
    • http (8)
    • git (8)
    • database (5)
    • aws (12)
    • devops (10)
      • docker (6)
      • cicd (4)
    • book (44)
      • clean code (9)
      • 도메인 주도 개발 시작하기 (10)
      • 자바 최적화 (11)
      • 마이크로서비스 패턴 (0)
      • 스프링으로 시작하는 리액티브 프로그래밍 (14)
    • tistory (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • Java
  • reactive
  • Jackson
  • S3
  • 등가속도 운동
  • Kotlin
  • PostgreSQL
  • 자바 최적화
  • connection
  • SWAGGER
  • 도메인 주도 설계
  • 마이크로서비스패턴
  • Database
  • ChatGPT
  • Bitmask
  • nuribank
  • CICD
  • Thymeleaf
  • docker
  • JPA
  • AWS
  • 트랜잭션
  • DDD
  • gitlab
  • Spring
  • Security
  • mockito
  • Saving Plans
  • RDS
  • POSTGIS

최근 댓글

최근 글

hELLO · Designed By 정상우.
danuri

오늘의 기록

java/jpa

[JPA] 값 타입

2021. 2. 2. 12:03

자바 ORM 표준 JPA 프로그래밍 - 기본편

 

스프링 핵심 원리 - 기본편 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Spring 객체지향 온

www.inflearn.com

강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고

 

JPA 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있다. 엔티티 타입은 @Entity로 정의하는 객체이다. 데이터가 변해도 식별자로 지속해서 추적이 가능하다. 값 타입은 int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말한다. 식별자가 없고 값만 있기 때문에 변경시 추적이 불가하다.

 

값 타입은 다음 3가지로 나눌 수 있다.

  • 기본값 타입
    • 자바 기본 타입(int, double)
    • 래퍼 클래스(Integer, Long)
    • String
  • 임베디드 타입(embedded type, 복합 값 타입)
  • 컬렉션 값 타입(collection value type)

 

기본값 타입


String name, int age에서 String, int와 같은 값 타입을 기본값 타입이라고 한다. 생명주기를 엔티티에 의존하기 때문에 엔티티가 사라지면 같이 사라진다.

 

또한 기본값 타입은 값을 공유하지 않아야 한다.

primitive 타입을 보면 int, double과 같은 타입은 값을 공유하지 않는다.

int a=10;
int b=a;        
b=20;

예를 들어, 다음과 같이 b=a 연산을 해도 a의 값을 복사해서 b에 대입하기 때문에 b를 변경해도 후에 a가 같이 변경되지 않는다.

 

래퍼 클래스의 경우도 마찬가지이다.

Integer a=10;
Integer b=a;
b=20;

사실 Integer, String과 같은 래퍼 클래스는 객체지만 자바언어에서 기본 타입처럼 사용할 수 있게 지원하므로 기본값 타입으로 정의했다.

 

임베디드 타입


새로운 값 타입을 직접 정의해서 사용할 수 있는데, JPA에서는 이것을 임베디드 타입이라 한다. 회원이 주소에 관련된 데이터를 가지고 있을 때, 도시, 거리, 우편번호 등 주소에 관한 세부적인 필드로 세분화된다. 이렇게 상세한 데이터를 그대로 가지고 있는 것은 객체지향적이지 않으며 응집력이 떨어진다. 명확히 '주소'와 같은 타입이 있으면 코드가 더 명확해질 것이다.

 

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}

위와 같이 @Embeddable 애노테이션으로 엠베디드 타입을 정의할 수 있다. 임베디드 타입에는 생성자, getter/setter, 메서드 등을 포함시킬 수 있다. 단, 기본 생성자는 필수이다.

 

임베디드 타입은 엔티티의 값일 뿐이기 때문에 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다. 즉, 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능하다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래 스의 수가 더 많다.

 

+) 임베디드 타입은 다른 임베디드 타입을 포함하거나 다른 엔티티를 참조할 수도 있다.

 

+) 한 엔티티에 임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @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;

그러나 @AttributeOverride를 너무 많이 사용하면 엔티티 코드가 지저분해진다. 다행히 한 엔티티에 같은 임베디드 타입을 중복해서 사용하는 일은 많지 않다.

 

추가로, 임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null이 된다.

ex) member.setAddress(null)

 

값 타입과 불변 객체


임베디드 타입과 같은 값 타입을 여러 엔티티에서 공유하면 위험하다. 임베디드 타입도 결국 객체이기 때문에 기본값 타입과 달리 참조를 전달하게 되고, 한쪽에서 그 값을 바꾼다면 해당 객체를 공유하고 있는 다른 엔티티에도 영향이 있다. 현실적으로 객체의 공유 참조는 피할 수 없고 이러한 부작용은 잡기 매우 어렵다.

 

-> 그래서 값 타입을 불변 객체로 만든다.

 

객체를 불변하게 만들면 값을 수정할 수 없으므로 부작용을 원천 차단할 수 있다. 불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 수정자(setter)를 만들지 않으면 된다. (or 수정자를 private으로 설정한다.) 참고로, Integer, String은 자바가 제공하는 대표적인 불변 객체다. 

 

이제 값 타입을 수정하고 싶다면 수정자가 따로 없기 때문에, 새로운 값 타입을 만들어 다시 할당한다.

 

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

 

값 타입의 비교


자바가 제공하는 객체 비교는 2가지다.

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

기본값 타입의 경우 어느 비교 방식을 사용해도 상관없으나, 객체 형태의 값 타입은 동일성 비교를 하면 다른 인스턴스이기 때문에 false가 나온다. 그리고, equals도 기본은 '=='비교이기 때문에 false가 나온다.

 

그러나 값 타입은 비록 인스턴스가 달라도 그 안에 값이 같으면 같은 것으로 보기를 기대하기 때문에 객체의 equals를 오버라이드해야 한다.

 

+) 일반적으로 연관된 엔티티끼리는 동등성 비교를 하지 않는다.(무한루프 문제) -> 다른 비즈니스 키로 비교하는 것으로 충분하다.(ex. 회원 -> 주민번호)

 

값 타입 컬렉션


엔티티에서 값 타입을 하나 이상 저장하려면 컬렉션에 보관하고, @ElementCollection, @CollectionTable 어노테이션을 사용하면 된다.

@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();

데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다. 그래서 컬렉션을 저장하기 위한 별도의 테이블이 필요하다. 위와 같이 엔티티(회원)의 식별자를 외래 키 값으로 갖는 새로운 테이블을 만든다.

 

<값 타입 컬렉션 저장>

Member member = new Member();
member.setUsername("member1");
member.setHomeAddress(new Address("homeCity", "street", "10000"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));
member.getAddressHistory().add(new AddressEntity("old1", "street", "10000"));

em.persist(member);

위 코드를 돌리면 insert 쿼리가 총 6번(member, favoritefoods*3, addresshistory*2) 나간다. 회원 엔티티만 insert를 했는데 값 타입 컬렉션은 자동으로 같이 insert가 되는 것이다.(기본적으로 영속성전이 + 고아 객체 제거 기능과 유사하다.)

 

<값 타입 컬렉션 조회>

반대로 조회할 때 값 타입 컬렉션은 기본적으로 지연 로딩을 지원하기 때문에 회원 엔티티를 조회한다면 회원만 select해서 쿼리가 나간다. 후에 값 타입 컬렉션을 사용할 때 해당 컬렉션을 조회한다.

 

<값 타입 컬렉션 수정>

값 타입 컬렉션을 수정할 때도 다음과 같이 해당 값 타입을 제거하고 추가해야 한다.

findMember.getAddressHistory().remove(new AddressEntity("old1", "street", "10000"));
findMember.getAddressHistory().add(new AddressEntity("newCity1", "street", "10000"));

일반적으로 remove 메서드는 값 타입의 equals를 기반으로 제거하기 때문에 꼭 equals 메서드가 오버라이드 되어 있어야 한다.

JPA 구현체들은 값 타입 컬렉션에 변경 사항이 발생하면, 값 타입 컬렉션이 매핑된 테이블의 연관된 모든 데이터를 삭제하고, 현재 값 타입 컬렉션 객체에 있는 모든 값을 데이터베이스에 다시 저장한다. 따라서 실무에서는 값 타입 컬렉션이 매핑된 테이블에 데이터가 많다면 그만큼 insert 쿼리도 많이 나가는 것이기 때문에 컬렉션 대신에 일대다 관계를 고려해야 한다.

@OneToMany(cascade = ALL,orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();

위와 같이 값 타입을 필드로 갖는 엔티티(AddressEntity)와 일대다 매핑을 하고 영속성 전이 + 고아 객체 제거 기능을 적용하면 값 타입 컬렉션처럼 사용할 수 있다. @JoinColumn을 통해 일 쪽을 연관관계의 주인으로 설정하긴 했으나 값 타입 컬렉션의 비효율성을 고려한다면 차선책 정도는 될 수 있다.

 

실전 예제 6 - 값 타입 매핑


공부한 예제를 깃허브에 등록한다.

 

코드를 작성하며 배운 내용

 

equals를 오버라이드할 때, 프록시를 고려해서 getter로 비교하는 것이 좋다.(인텔리제이에서 옵션으로 제공한다.)

 

임베디드 값 타입 내에 의미 있는 메서드를 추가할 수 있고, 컬럼 속성도 사용할 수 있다. -> 한번 정의하면 해당 값 타입을 사용하는 필드에서 공통으로 사용한다.

'java > jpa' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어2 - 중급 문법  (0) 2021.02.08
[JPA] 객체지향 쿼리 언어1 - 기본 문법  (0) 2021.02.03
[JPA] 프록시와 연관관계 관리  (0) 2021.01.29
[JPA] 고급 매핑  (0) 2021.01.27
[JPA] 다양한 연관관계 매핑  (0) 2021.01.27
    'java/jpa' 카테고리의 다른 글
    • [JPA] 객체지향 쿼리 언어2 - 중급 문법
    • [JPA] 객체지향 쿼리 언어1 - 기본 문법
    • [JPA] 프록시와 연관관계 관리
    • [JPA] 고급 매핑
    danuri
    danuri
    IT 관련 정보(컴퓨터 지식, 개발)를 꾸준히 기록하는 블로그입니다.

    티스토리툴바