본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 중요한 내용이 판단된 부분은 강의자료의 사진자료를 사용하였습니다. 

 

기본값 타입

 

엔티티 타입

@Entity로 정의하는 객체이다. 데이터가 변해도 식별자로 지속해서 추적이 가능하다. 예를 들어 회원 엔티티의 키나 나이값을 변경해도 식별자로 인식 가능하다.

 

값 타입

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

 

 

 

값 타입의 분류

 

1. 기본값 타입

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

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

 


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

- 새로운 값 타입을 직접 정의할 수 있다. 
- JPA는 임베디드 타입(embedded type)이라고 한다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
- int, String과 같은 값타입이다. 추적도 되지 않고 변경하면 다른값으로 대체된다.
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
- 기본 생성자가 필수로 필요하다.

Member 클래스에 Period, Address 임베디드 타입 추가

 

Member 클래스에 Period, Address 라는 이름의 임베디드 타입을 추가해보자.

 

 

Address와 Peroid 클래스를 만들 때 @Embeddable 애노테이션을 붙여준다. (Getter/Setter는 생략)

 

 

Member 클래스에서 @Embedded 애노테이션을 사용하여 값 타입을 사용하면 된다. @Embedded 애노테이션은 생략이 가능하지만 명시해주는 것이 좋다.

 

 

임베디드 타입은 엔티티의 값일 뿐이다. 임베디드 타입을 사용하기 전과 후의 매핑하는 테이블은 같다. 하지만 Member 엔티티의 확장성이 생기면서 객체지향적인 코드가 된다. 예를 들어, Period클래스에 현재시간과 비교하여 기간을 산출하거나 총 기간을 계산하는 등 기간과 관련된 의미있는 메서드를 만들어서 활용할 수도 있게 된다.

임베디드 타입을 사용하면 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능해진다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많다.

 


<장점>
- 재사용이 가능하다.
- 높은 응집도를 가진다.
- 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있다.
- 임베디드 타입을 포함한 모든 값 타입은 값 타입을 소유한 엔티티에 생명주기를 의존한다.


- @AttributeOverride : 속성 재정의
한 엔티티에서 같은 값 타입을 사용하면? 컬럼명이 중복된다. @AttributeOverride를 사용해서 컬러명 속성을 재정의하면 된다. 

@AttributeOverride의 사용방법

 

 

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

 

 



값 타입과 불변 객체

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

 

의도는 member1의 city를 고치고 싶었겠지만, 임베디드 타입은 값 타입이기 때문에 member1의 city와 member2의 city가 모두 "daejeon"으로 바뀌게 된다.

 

 



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

이렇게 사용하거나 혹은 임베디드 타입의 장점을 이용해서 Address클래스에 인스턴스를 복사하는 메서드를 만들어서 사용할 수도 있겠다.



이렇게 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다. 그런데 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입이 아니라 객체 타입이다.
객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다. 객체의 공유 참조를 피할 수 없다. (타입만 맞으면 다 대입이 된다)

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

 

 

 

 


값 타입의 비교

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


값 타입은 a.equals(b)를 사용해서 동등성을 비교해야 한다. 
값 타입의 equals()메소드를 적절하게 재정의 해야한다.(주로 모든 필드 사용)

 

 

 



값 타입 컬렉션 

값 타입을 하나 이상 저장할 때 사용한다. 
@ElementCollection, @CollectionTable 사용해서 매핑하면 된다. 
데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다. 컬렉션을 저장하기 위한 별도의 테이블이 필요하다.

값 타입 컬렉션을 추가한 테이블 구조

 

이와 같이 @ElementCollection과 @CollectionTable 애노테이션을 써서 매핑해주면 된다. @CollectionTable의 속성으로 joinColumns = @JoinColumn(name="MEMBER_ID" 를 지정해줌으로써 MEMBER_ID를 FK로 가지는 컬렉션 테이블을 생성해주게 된다.

 

 

 


값 타입의 저장예제

 

 

컬렉션 값타입은 다른 테이블임에도 불구하고 라이프 사이클이 같이 돌아 갔다. 왜냐면 이것은 값타입 이기 때문이다. 그렇기 때문에 별도로 persist를 하거나 update를 하거나 그럴 필요가 없다
마치 값 타입 컬렉션은 영속성 전이(cascade = all)와 고아 객체 제거기능(orphanRemoval = true) 속성을 모두 써준것과 같이 동작한다.

 

 

 

 

 


값 타입의 조회예제

 


Member를 조회하면 Member만 Select한다. 그 말은 즉슨, 값타입 컬렉션도 지연로딩 전략을 default로 사용한다는 의미이다. 실제로 사용하는 시점이 와야 그제서야 컬렉션 값 타입의 데이터를 불러와서 가져올 것이다.
(참고로 Embedded타입은 Member에 소속된 값 타입이기 때문에 조회 시 같이 불러와진다.)

 

 



값 타입의 수정예제

 

 

컬렉션 값 타입의 예를 보기전에 임베디드 값 타입의 수정을 잠깐 살펴보자.

잘못된 예


컬렉션 값 타입도 결국 값 타입 이기 때문에 setter를 이용해서 수정하면 안된다! side effect 발생
인스턴스 자체를 갈아끼워야 한다.

올바른 예

이렇게!

 

 

 

 

그러면 값 타입 컬렉션의 수정은 어떻게 하면 될까?

 

 

값 타입 컬렉션일 경우, 방법이 따로 없다. remove()를 이용해서 해당 컬렉션의 데이터를 삭제 한 후, 다시 새로운 데이터를 add해서 넣어주어야 한다.

이것만 봐도 효율이 떨어지는 것 같기는 한데 더 큰 문제는 한개가 더 있다.

 

분명 한개의 데이터만 삭제하고 추가로 한개의 데이터만을 add 했는데 SQL은 delete한개와 insert 2개가 나간다. 값 타입 컬렉션에 변경사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.

 

 


값 타입 컬렉션의 제약사항

- 값 타입은 엔티티와 다르게 식별자 개념이 없다.
- 값은 변경하면 추적이 어렵다.
- 값 타입 컬렉션에 변경사항이 발생하면 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
- 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 한다. (null 입력x, 중복저장x)

모든 컬럼을 묶어서 PK값으로 사용하는 테이블을 생성했었음.

 

 

 

 

값 타입 컬렉션 대안

- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려해야 한다.(값 타입에서 엔티티로 승격)
- 일대다 관계를 위한 엔티티를 만들고, 여기에 값 타입을 사용하는 방법으로 해야한다.
- 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용한다.

 

 

이처럼 AddressEntity라는 엔티티를 한개 만들어서 Address값 타입을 한번 래핑해주는 것이다.

 

 

 

List<Address>가 아닌 방금 만든 List<AddressEntity>를 사용하고 일대다 매핑을 해준다! 그리고 '영속성 전이(Cascade) + 고아 객체 제거'를 사용하여 값 타입 컬렉션처럼 이용한다.

 

 


- 값 타입 걸렉션은 만약 멀티 체크박스에서 내가 좋아하는 메뉴 여러개 선택가능하게 하는 기능이 있다. 그렇게 값이 단순하고 추적할 필요도 없고, 값이 바뀌어도 update할 필요가 없을 때 사용하는 것이다!
예를 들어 주소 이력 과 같은 정보는 무조건 엔티티로 사용해야 한다.

- 값 타입은 정말 값 타입이라고 판단 될때만 사용해야 한다. 엔티티와 값 타입을 혼동해서 엔티티를 값 타입으로 만들면 안된다. 식별자가 필요하고, 지속해서 값을 추적 및 변경해야 한다면 그것은 값 타입이 아니라 엔티티로 만들어야 한다.

 


정리

엔티티 타입의 특징

- 식별자가 있음
- 생명 주기가 관리 됨
- 공유를 할 수 있음


값 타입의 특징

- 식별자가 없음
- 생명주기를 관리하지 못하여 엔티티에 의존함
- 공유하지 않는 것이 안전
- 만약 공유해야 한다면 불변객체로 만드는 것이 안전

본 정리 내용은 김영한님의 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 중요한 내용이 판단된 부분은 강의자료의 사진자료를 사용하였습니다. 

JPA의 프록시

em.find() : 데이터베이스를 통해서 실제 엔티티 객체를 조회한다.                       

em.getReference() : 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.
getReference()를 호출하는 시점에는 데이터베이스에 select 쿼리를 하지 않는다. 그런데 getReference()가 실제 사용되는 시점에 쿼리가 나간다.

 

 

프록시 객체의 target

 

프록시는 실제 클래스를 상속 받아서 만들어진다. 실제 클래스와 겉 모양이 같다. 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다. (이론상으로는)
프록시 객체는 실제 객체의 참조(target)를 보관한다. 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

 

 

 

프록시 객체의 초기화

 

실제 사용되는 시점(사진에서는 1.getName())에서 프록시 객체는 getName()의 값을 아직 알지 못한다. 그래서 영속성 컨텍스트에 초기화를 요청한다. 그러면 영속성 컨텍스트는 DB에 들러서 실제 Entity를 생성해준다. 그리고 참조값을 가지고 있던 target에다가 실제 생성한 Entity를 연결해준다.

 

 

 


프록시의 특징

  • 프록시 객체는 처음 사용할 때 한번만 초기화한다.
  • 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아니다! 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한것이다.
  • 프록시 객체는 원본 엔티티를 상속받는다. 따라서 타입 체크시 주의해야한다. (== 비교 실패, 대신 instance of 사용) (아래 사진 예시참고)

  • 위와 같이 하면 첫번째 println은 "findMember1==findMember2 : false" 가 출력되고, 두번째와 세번째 println은 각각 true가 출력된다.

 

  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환한다. (아래 예제)

2개의 println의 결과는?
결과 : 실제 엔티티 객체

 

 

  • 그 반대도 마찬가지이다. 처음에 em.getReference()로 조회하면 em.find()해도 프록시 객체를 반환한다. (아래 예제)

2개의 println의 결과는?
결과 : 프록시 객체

 

왜 이런 결과를 얻는 것일까?

 

1. 1차 캐시에 있기 때문에
2. 마치 컬렉션에 있는 값을 비교하는 것처럼 한개의 트랜젝션 안에서 같은 pk값을 갖는 엔티티는 같은 엔티티임을 보장해준다. 즉, ==비교를 true로 만들기 위해서 영속성 컨텍스트 안에 있으면 프록시가 아니라 실제 엔티티를 반환하게 된다. (이 개념은 매우 중요하다.) 

 

 

  • 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제가 발생한다.
    (하이버네이트는 org.hibernate.LazyInitializationException예외를 터뜨린다.)

 

  • 하이버네이트 5.4.0.Final 버전까지는 단순히 세션(엔티티매니저)이 끝나면 예외가 터졌었는데, 5.4.1.Final  이상버전부터는 트랜젝션이 유지가 되면 Lazy로딩을 사용할 수 있도록 최적화 되었다. 버전이 5.4.1.Final 이상이라면 위의 예제와 같은 경우, 트랜잭션을 종료하지 않은 상태에서 세션(엔티티메니저)를 닫았기 때문에 예외가 터지지 않는다.

 

 

 

 

 

프록시 확인을 도와주는 유틸리티 메서드

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)
  • 프록시 클래스 확인 방법
    entity.getClass().getName()출력 
  • 프록시 강제 초기화
    org.hibernate.Hibernate.intialize(entity);
    -> 참고 : 이건 하이버네이트가 지원하는 초기화 방법이고, JPA 표준에는 강제 초기화가 없음. 위에 예제들에서 했던 것처럼 member.getUserName(); 처럼 강제 호출 해야함.


 

 

 


즉시 로딩과 지연 로딩

Member클래스와 Team클래스

Member와 Team이 다대일(@ManyToOne)로 연관관계 매핑이 되어있다고 가정해보자. 우리가 만든 프로그램에서 Member와 Team정보가 모두 필요한 경우보다 Member만 조회할 경우가 더 많다고 한다면 Member 엔티티를 조회할 때 항상 Team을 Join해서 조회해야할까? 단순히 Member정보만 사용하는 비즈니스 로직에서는 매우 손해일 것이다.


이럴때 FetchType을 LAZY로 하면 Member를 로딩할 때 Team은 프록시객체를 이용해서 지연로딩을 한다.
그 후 실제 Team의 속성을 실제로 사용하는 시점(Team을 불러오는 시점이 아니라 사용하는 시점이라는 것을 유의)에 프록시 객체가 초기화 되면서 값을 가지고 온다.

쉽게 말하자면 지연로딩일 때 Member를 로딩하면 Team에 프록시 객체를 넣어둔 상태로 Join하지 않고 Member만 가지고 오는 Select쿼리가 나가고, 실제 사용한다고 하면 그 때서야 Team을 가져오기 위한 Select 쿼리가 나간다.


반대로 Member와 Team을 거의 항상 같이 사용한다면? FetchType을 EAGER로 하여 Member와 Team을 Join하는 즉시로딩을 할 수 있다. 이 때 team을 조회하면 프록시가 필요없기 때문에 실제 객체가 나온다.

 

 

코드 예제를 보자. 예제 코드에는 Member가 JpaMember, Team이 JpaTeam이다.

 

연관관계의 주인인 JpaMember클래스이다. 그 안에 JpaTeam jpateam이 존재하고 fetch를 LAZY로 해줌으로써 지연로딩설정을 해주었다.

 

지연로딩으로 설정한 상태로 첫번째 println에서 findMember.getJpateam().getClass()하여 JpaMember안의 JpaTeam을 가져오려고 하고 있다.

 

(=== 는 실제 이 부분에서 쿼리가 나가는지 구분하기 위한 선)

두번째 쿼리에서는 findMember.getJpateam().getName()하여 JpaTeam의 이름을 실제로 호출하고 있다. 실제 사용되는 시점이다.

 

첫번째는 프록시 객체가 나왔고,

두번째는 실제 사용되는 시점이기 때문에 Select쿼리가 나가고 JpaTeam의 이름을 가지고 왔다.

 

 

 



프록시와 즉시로딩 주의

 

 

  • 가급적 지연로딩만 사용하자
  • 즉시 로딩을 적용하면 예상하지 못한 SQL이 발생한다. join sql이 너무 많이 발생할 수도 있다.
  • 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.

JPQL은 SQL과 1:1 매칭이 되기 때문에 예를 들어, "select m from Member m" 과 같은 JPQL을 작성하면 "select * from Member"와 같은 SQL이 나간다. 그런데 Member의 Team이 즉시로딩으로 되어 있으면 값을 불러올 때 모두 채워져 있어야하기 때문에 "select * from Team"와 같은 SQL이 한번 더 나간다. N+1의 1은 최초쿼리, N은 최초쿼리로 부터 파생된 쿼리들이다. 

 

  • @ManyToOne, @OneToOne은 기본이 즉시 로딩이므로 LAZY로 설정을 해주어야 한다.( ~ToOne시리즈는 모두 LAZY로 설정해 주어야한다고 기억하자)

 

 

 

영속성 전이와 고아 객체

 

영속성 전이 : CASCADE

 

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 사용한다.
예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 것이다. 

 

 

위와 같은 관계인 Parent와 Child가 있다.

아래 예제코드에는 Getter Setter가 생략되어 있는데 있어야 한다..

 

Child 클래스
Parent 클래스

Parent클래스에 있는 addChild 메서드는 연관관계 편의 메서드이다.

연관관계의 주인인 Child에는 당연히 값이 들어가야 하고, 객체 지향적인 관점에서 Parent에도 값이 들어가야 하기 때문에 연관관계 편의 메서드를 만들어서 Child값을 집어넣을 때 양쪽에 값을 모두 넣을 수 있는 메서드를 만들어서 관리한다.

 

@OneToMany의 속성으로 cascade를 설정해주었다.

 

(아래 transaction.commit() 있다고 하자.)

테스트 코드에서 child1과 child2를 추가해주었고, em.persist(parent); 를 해주었다. casecade속성이 없었다면 parent만 영속성 컨텍스트에 영속되면서 insert쿼리가 한개 나가지만 casecade속성으로 인해서 연관된 엔티티도 함께 영속상태로 만들기 때문에 child2개도 모두 영속상태가 되어 insert쿼리가 3개 나간다.

 

 

- 주의점 -
1. 부모 엔티티와 자식 엔티티의 라이프 사이클이 거의 유사하거나 동일할 때만 사용해야 한다.
2. 소유자가 하나일때만 사용해야 한다.(부모 엔티티만 자식엔티티를 소유할 때)

 



고아객체

 

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아객체로 보고 삭제하는 기능이다. 
컬렉션에서 빠진 객체는 연관관계가 삭제된다. orphanRemoval = true 옵션으로 사용한다.

 

@OneToMany의 속성으로 orphanRemoval = true을 해주었다.

 

 

em.flush(); em.clear(); 해줌으로써 영속성 컨텍스트(정확히는 쓰기 지연 SQL저장소에 쌓여있던 쿼리)에 있는 쿼리를 모두 날리고 영속성 컨텍스트를 모두 비웠다. 

그 후 em.find() 하여 DB에 접근하여 Parent 객체를 가져왔다. 그리고 첫번째 자식 엔티티를 컬렉션에서 삭제하였다.

 

그 결과 Delete 쿼리가 나간다.

 

- 주의점 -

1. 참조하는 곳이 하나일 때, 특정 엔티티가 개인 소유일 때만 사용해야 한다.
2. @OneToOne, @OneToMany만 사용가능하다.
3. 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CasecadeType.REMOVE 처럼 동작한다.

 

 

 

 

영속성 전이 + 고아객체, 생명주기

CasecadeType.ALL + orphanRemoval = true

 

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거할 수 있다. 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.
  • 이는 도메인 주도 설계(DDD)의 Aggregate Root개념을 구연할 때 유용하다.

 

'JAVA > Spring JPA' 카테고리의 다른 글

JPA의 값 타입  (0) 2022.01.05
상속관계 매핑과 매핑 정보 상속(@MappedSuperclass)  (0) 2021.12.30
다양한 연관관계 매핑  (0) 2021.12.29
단방향 연관관계와 양방향 연관관계  (0) 2021.12.28
엔티티 매핑  (0) 2021.12.25
본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 중요한 내용이 판단된 부분은 강의자료의 사진자료를 사용하였습니다. 

1. 상속관계 매핑

관계형 데이터베이스는 상속관계가 없다. 그나마 비슷한 모델은 슈퍼타입 서브파입 관계라는 논리 모델링 기법이 객체 상속과 유사하다. 상속관계 매핑은 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.



주요 애노테이션
1. @Inheritance(strategy = InheritanceType.xxx)

 


JOIND : 조인전략 - 가장 정규화된 데이터베이스 모델. 

조인전략

장점 : 
- 가장 정규화된 데이터베이스 모델이다.
- 외래키 참조 무결성 제약조건을 활용 가능하다.
- 저장공간이 효율적이다.

 

단점 : 
- 조회시 조인을 많이 사용하여 성능이 저하된다.
- 조회 쿼리가 복잡하다.
- 데이터 저장 시 INSERT SQL을 2번 호출한다.

 

 

 


SINGLE_TABLE : 단일 테이블 전략 - 한개의 테이블에 모두 다 집어 넣고 DTYPE으로 구분한다.

단일테이블 전략

장점 : 
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.


단점 : 
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수도 있다.

 

 

 


TABLE_PER_CLASS : 구현 클래스마다 테이블 전략 - 슈퍼타입 테이블을 없애고 슈퍼타입에 속성들이 서브타입의 속성들로 중복되어서 들어간다.

장점 :

- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
- not null 제약조건을 사용가능하다.


단점 : 
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다.(UNION SQL이 필요)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
- 최대한 사용을 하지 말자.

 

 

@DiscriminatorColumn(name="DTYPE") 

DTYPE이라는 컬럼을 추가하여 어떤 서브타입 테이블의 데이터가 들어왔는지 쉽게 파악이 가능하다.

@DiscriminatorValue("xxx")

서브타입 객체의 DTYPE 필드 value를 바꿀 수 있다.





2. @MappedSuperclass

공통 매핑 정보가 필요할 때 사용한다. 공통 속성만 부모클래스에 두고 상속하여 사용하고 싶을 때 사용한다. DB 입장에서는 똑같은데 객체 입장에서 속성만 상속받아서 사용하는 것이다.

테이블과 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할이며, 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.


참고 : @Entity 클래스는 엔티티나 @MappedSuperClass로 지정한 클래스만 상속가능

 


특징

- 상속관계 매핑이 아니다.
- 엔티티가 아니므로 테이블과 매핑이 되는것이 아니다.
- 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만을 제공하는 것이다.
- 조회, 검색이 불가하다.(em.find()불가) 
- 직접 생성해서 사용할 일이 없으므로 추상 클래스를 권장한다.

 

사용예시1
사용예시2

위와 같이 @MappedSuperclass 애노테이션을 붙인 클래스를 만들고 extends를 이용하여 상속받아서 사용하면 된다. BaseEntity는 @MappedSuperclass이기 때문에 테이블이 생성되지는 않는다.

'JAVA > Spring JPA' 카테고리의 다른 글

JPA의 값 타입  (0) 2022.01.05
JPA의 프록시와 연관관계 관리  (0) 2022.01.03
다양한 연관관계 매핑  (0) 2021.12.29
단방향 연관관계와 양방향 연관관계  (0) 2021.12.28
엔티티 매핑  (0) 2021.12.25
본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 중요한 내용이 판단된 부분은 강의자료의 사진자료를 사용하였습니다. 

연관관계 매핑시 고려상항 3가지

1. 다중성

다대일 (@ManyToOne)

- DB입장에서 생각해보면 MEMBER가 N, TEAM이 1이다. 그러면 MEMBER에 외래키가 있어야 한다. 
- 외래 키가 있는 쪽이 연관관계의 주인이고, 양쪽을 서로 참조하도록 개발해야 한다.

 


일대다 (@OneToMany)

- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인인 방식이다.
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있다.
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조이다..
- @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용한다.(중간에 테이블을 하나 추가하는 방식) 

- 엔티티가 관리하는 외래 키가 다른 테이블에 있다는게 최대 단점이다.
- 연관관례 관리를 위해 추가로 UPDATE SQL을 실행한다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.

- 일대다 양방향 매핑도 할 수 있다. Member에 Team team을 만들어주고 @JoinColumn(insertable=false, updatable=false)를 해주어서 강제로 읽기 전용으로 매핑을 해주는 것이다. 
이런 매핑은 공식적으로 존재하지는 않는다. 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다.

 


일대일 (@OneToOne)

- 일대일 관계는 그 반대도 일대일이다.
- 주 테이블이나 대상 테이블 중에 외래키를 선택하는게 가능하다. 
- 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가가 되어야 일대일 관계가 된다.
- 단방향은 @ManyToOne관계의 단방향과 마찬가지로 @OneToOne 애노테이션을 붙여주고 @JoinColumn하여 외래키를 지정해주면 되고, 양방향은 역방향에 @OneToOne(mappedBy = "key")를 적용해주면 된다.
- 일대일에서 대상 테이블에 외래 키 단방향 관계는 JPA에서 지원을 하지 않는다. 양방향 관계는 지원한다.

주 테이블에 외래키가 있는 경우
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자가 선호하는 방법이다.
- JPA 매핑이 편리하다.
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
- 단점 : 값이 없으면 외래 키에 null을 허용한다.

대상 테이블에 외래키가 있는 경우
- 전통적인 테이터베이스 개발자가 선호하는 방법이다.
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지한다.
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시로딩 된다.


다대다 (@ManyToMany)

- 관계형 데이터베이스는 정규환된 테이블 2개로 다대다 관계로 표현할 수 없다. 그래서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
하지만 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
- 연결 테이블이 단순이 연결만 하고 끝나지 않는다. 
- 주문시간, 수량 같은 데이터가 들어올 수 있다.
- @ManyToMany -> @OneToMany, @ManyToOne으로 바꾼다.
- 실무에서는 절대 쓰지말자

 




2. 단방향, 양방향

테이블 

- 외래 키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음

 

객체

- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향

 

 


3. 연관관계의 주인

- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 A->B, B->A처럼 참조가 2군데
- 객체 양방향 관계는 참조가 2군데 있음. 둘중 테이블의 외래키를 관리할 곳을 지정해야한
- 연관관계의 주인 : 외래 키를 관리하는 참조
- 주인의 반대편 : 외래키에 영향을 주지 않음, 단순 조회만 가능

본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 설명에 필요한 사진 중 중요한 사진이라고 생각한 사진을 강의자료에서 가져왔음을 출처로서 밝힙니다.

 

 

단방향 연관관계와 양방향 연관관계를 예제 코드를 작성하면서 정리해보았다. 예제의 시나리오는 다음과 같다.

1. 회원과 팀이 있다.

2. 회원은 하나의 팀에만 소속될 수 있다.

3. 회원과 팀은 다대일 관계이다.

 

 

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다. 
테이블은 외래키로 조인을 사용해서 연관 테이블을 찾는다. 하지만 객체는 참조를 사용해서 연관된 객체를 찾는다. 테이블과 객체 사이에는 이런 큰 간격이 있다. 이를 어떻게 해결 할 수 있을까? 

 

 

 

 

단방향 연관 관계

객체 연관관계를 사용한 설계

 

 

테이블 연관관계를 보면 MEMBER에서 TEAM_ID를 외래키로 가짐으로써 멤버가 어떤 팀에 소속되어 있는지 알 수 있다. 이러한 테이블 구조를 보고 객체의 연관관계를 생각해본다면 Member클래스에서 team을 다대일로 매핑해줌으로써 멤버가 어떤 팀에 소속되는지 알 수 있다.

 

 

 

teamId를 단순하게 Long형의 아이디값으로 갖는 것이 아니라 @ManyToOne 을 해줌으로써 다대일 매핑을 해줄 수 있다. 테이블 구조로 생각해보면 FK키가 있는 쪽이 항상 N, 즉 '다' 가 된다. 객체 연관관계에서도 N인 쪽을 @ManyToOne해주고, @JoinColumn(name = "TEAM_ID) 해주어서 외래키로 사용할 컬럼과 그 컬럼의 이름을 정해주는 것이다.

 

 

이와 같이 사용하면 된다.

TeamA를 하나 만들어서 영속성 컨텍스트에 영속을 시켜주고, memberA을 만들어서 영속성 컨텍스트에 영속을 시켜주는데 이 때, member.setJpaTeam(team); 해서 멤버를 팀에다가 넣어주면 된다. 

 

em.persist(team); 을 하면 JpaTeam의 id가 @GeneratedValue이기 때문에 먼저 DB에 들러서 PK값을 얻어온 후 영속성 컨텍스트에 저장을 한다. 

그 후에 member를 만들어서 member.setJpaTeam(team); 하게 되면 JPA가 위에서 걸어준 @ManyToOne 애노테이션과 @JoinColumn을 보고 연관관계를 파악하여 자동으로 PK값을 FK로 사용을 한다.

 

 

 

조회 할때에는 em.find(Member.class, member.getId()); 하여 findMember를 만들고, findMember.getTeam(); 하여 바로 사용할 수 있다.

이 때, DB로 날아가는 쿼리를 보면 Member와 Team을 Join해서 select하는 것을 볼 수 있다. 

 

 

 

 

 

 

 

 

 

양방향 연관관계

양방향 매핑

 

 

위의 단방향 연관관계와 비교해보면 테이블 연관관계는 똑같다. 

양방향 연관관계를 매핑해주는 작업을 해도 테이블에는 영향이 없는 것이다. 가장 좋은 설계는 단방향 연관관계로 설계하는 것이고, 필요시에 양방향 연관관계를 걸어주는 것이 바람직하다.

 

테이블 연관관계에서 보면 TEAM_ID를 FK로 가짐으로서 멤버가 어떤 팀에 소속되었는지 알 수 있었고, 또 반대로 팀에 어떤 멤버들이 소속되어 있는지도 알 수 있다. 테이블은 사실 방향의 개념이 없는 것이다. 

 

단방향 연관관계를 생각해보면, Member클래스에는 TEAM_ID를 JoinColumn으로 하는 필드를 만들어줌으로써 멤버가 어떤 팀에 속했는지 알 수 있지만, 반대로 Team에서는 Member로 바로 접근이 불가능했었다.
이것이 바로 테이블과 객체 사이의 차이점이다.

 

 

양방향 매핑에서는 Team 엔티티에 컬렉션을 추가해 주어야 한다. Team 엔티티에 List를 추가해줌으로써 이 팀의 어떤 멤버가 있는지 담을 수 있는 것이다.

 

Team은 Member와 일대다 관계이다. 즉 '1' 에 해당됨으로 @OneToMany 애노테이션을 붙여주면 된다. 여기서 중요한 것은 mappedBy 속성이다. mappedBy는 말 그대로 '나는 누구에게 매핑되어져 있다'를 표시해주는 것이다. 여기에서 "team"은 내가 매핑당하고 있는 FK값이 되는 변수명이다.

 

 

 

위와 같이 양방향 연관관계 매핑이 된것을 조회해볼 수 있다.

em.find(Member.class, member.getId()); 하여 findMember를 만들고, 이 findMember가 속한 팀을 getJpaTeam() 하여 가져오고 그 팀의 getMembers() 하여 컬렉션을 얻어올 수 있는 것이다.

Team에 컬렉션을 만들어서 서로 양방향 매핑을 해줌으로써 이런 조회가 가능해 진 것이다.

 

 

 

그런데, 중요한것이 있다.

아까 mappedBy를 Team에다가 써주었었는데, 그러면 mappedBy는 대체 어느 방향에 써주어야 하는 것일까?

 

 

우선 왜 mappedBy와 같은 것을 써줘야 하는지 이해를 해보자.

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다. 객체를 양방향으로 참조하려면 위의 사진처럼 단방향인 연관관계를 2개 만들어야 하는 것이다.

 

 

 

반면에 테이블을 생각해보면 테이블은 외래키 하나로 두개의 테이블의 연관관계를 관리할 수 있다. MEMBER.TEAM_ID 외래키 하나로 양방향 연관관계를 가지게 되는것이다. 

 

 

그렇다면 결국, 객체는 Team team든지 List members 든지, 둘중에 하나로 외래 키를 관리해야하는 것이다.

그래서 가장 중요한 것이 바로 연관관계의 주인(Owner)를 정하는 것이다!

 

 

연관관계의 주인

- 양방향 매핑 규칙이다.
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정해야 한다.
- 연관관계의 주인만이 외래키를 관리(등록, 수정)한다.
- 주인이 아닌쪽은 읽기만 가능하다.
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
외래 키가 있는 곳을 주인으로 정해라. (중요!!!)
ManyToOne(1:N에서 N)인 곳이 주인이 된다. (중요!!!)
- 자동차와 자동차 바퀴가 있으면, 물론 자동차가 비즈니스적으로는 중요하지만, 자동차의 주인은 바퀴! 가 된다고 기억하자.

 

 

 

 

 

 

 

양방향 매핑시 가장 많이 하는 실수

 

주인이 아닌 방향에 연관관계 설정한 예시 - 잘못됨

 

역방향(주인이 아닌 방향)만 연관관계 설정을 하면 외래키 값이 null이 된다.  왜?!
Team에 있는 mappedBy된 것은 읽기 전용이다. JPA에서 update할때나 insert할때는 이 객체를 보지 않는다. 즉, JpaMember에 있는 jpateam이 주인인데, 주인이 아닌곳에 값을 넣었기 때문에 null 이 나오는 것이다.
양방향 매핑 시 연관관계의 주인에 값을 입력해야 한다.

 

 

주인에 올바르게 값을 넣은 예시 - 올바름

 

위의 코드에서 member.setJpaTeam(team)까지 하면 사실 JPA입장에선 맞는 코드이다. Team만들고 영속성 컨텍스트에 올리고, 디비에서 가져온 키값을 (1차캐시에 있는) 외래키로 가져와서 setTeam해주면 문제가 없기 때문이다. 그 이후에 getMembers(); 하여 호출하게 되면 JPA는 실제 사용하는 시점이라고 판단하여 Member에 대한 select 쿼리를 또 한번 날려준다.

그런데, 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다. 왜 그럴까?

 

 

 

 

객체 지향적으로 문제가 있음을 보여주는 예시

 

문제 1 . flush(), clear()가 없으면?

 

-> 1차 캐시에 있는것을 find해준다.  그러면 컬렉션에 값이 없다.. 객체지향적으로 생각했을 때 team, member에 모두 값을 넣어주는게 맞다.

 


문제2. 테스트 케이스 작성할 때도 같은 문제

 

-> 위의 상황과 비슷하게 member는 조회가 되는데 team은 없는 상황이 나올수도 있다.

 

 

 

 

 

 

양방향 연관관계가 있으면 양쪽의 값을 모두 넣어주어야 하는 번거로움을 조금이라도 줄이고자 연관관계 편의 메서드를 사용해도 좋다.

 

 

예제에서 member.setJpateam(team); 하면 set할때 team참조를 저장하여 연관관계를 설정해주었다. 이 때, 나 자신의 인스턴스(this, 여기서는 member객체)를 mappedBy쪽의 컬렉션에 add를 해주는 것이다. 이렇게 하면 set과 동시에 양쪽의 값을 모두 넣어줄 수 있다.

 

 

 

반대로 team에 member를 추가할 때 연관관계 편의 메서드를 사용할 수도 있다.

임의로 addMember 메서드를 만들어서 member.setJpateam(this);로 현재 team객체를 set에 넣고, 컬렉션에 member를 add해주면 양쪽의 값을 모두 넣어줄 수 있다.

 

두 방법중 정답은 없다. 이 예제에서 보면 팀을 만들 때 그 팀에 속한 멤버를 넣어줄 수도 있는 것이고, 멤버를 만들 때 그 멤버가 속할 팀을 정해줄 수도 있는 것이다. 개발할 비즈니스 모델에 맞추어서 만들면 된다.

단, 2가지를 한번에 사용하면 충돌이 일어날 수 있기 때문에 한가지 방법만 사용하자.

 

 

 

 

추가적으로 주의할 것!

- toString(), lombok, JSON생성 라이브러리 를 사용할 때 무한루프를 조심하자.

  -> 양방향 관계인 양쪽 엔티티에 모두 toString()을 Override하여 사용하면 컬렉션이 있는 쪽의 엔티티 안의 값들을 호출하게 되고 서로 무한으로 왔다갔다 호출이 된다. 롬복 라이브러리의 @Data 애노테이션을 사용하면 @ToString 을 포함하고 있어서 자동으로 toString()을 오버라이딩하기 때문에 주의해야 한다.


- JSON생성 라이브러리로 인한 문제 : 컨트롤러에서 entity를 반환하지 말자! 엔티티를 변경하면 API 스펙이 바뀐다. dto로 변환해서 반환하는것을 추천.

'JAVA > Spring JPA' 카테고리의 다른 글

JPA의 프록시와 연관관계 관리  (0) 2022.01.03
상속관계 매핑과 매핑 정보 상속(@MappedSuperclass)  (0) 2021.12.30
다양한 연관관계 매핑  (0) 2021.12.29
엔티티 매핑  (0) 2021.12.25
영속성 컨텍스트  (0) 2021.12.24

- 객체와 테이블 매핑 : @Entity, @Table
- 필드와 컬럼 매핑 : @Column
- 기본키 매핑 : @Id, @GeneratedValue

 

 

객체와 테이블 매핑, 필드와 컬럼 매핑, 기본키 매핑의 방법을 간단하게 알아보고 각 애노테이션의 중요한 옵션들과 기능들에 대해서 알아보자.

 

 

 

 

객체와 테이블 매핑

 

 

 

@Entity가 붙은 클래스는 JPA가 관리하는 엔티티라고 한다.
- 기본 생성자가 필수이다.(파라메터가 없은 public또는 protected생성자) JPA기본 스펙이 이렇게 되어 있다.
- final 클래스, enum, interface, inner 클래스에 사용할수 없다.
- 저장할 필드에 final을 사용할 수 없다. 
- name 속성 : 같은 클래스 이름이 없으면 가급적이면 기본값을 사용하자.

- @Table : 아래와 같은 기능이 있는 속성들을 가진다.

 

 

 

 

 

 

데이터베이스 스키마 자동 생성

 

spring.jpa.hibernate.ddl-auto=create

- properties나 yml은 위과 같은 형식으로 옵션을 설정할 수 있다.

 

<property name="hibernate.hbm2ddl.auto" value="create" />

- xml은 위와 같은 형식으로 옵션을 설정할 수 있다.

 

 

 

- DDL을 애플리케이션 실행 시점에 자동 생성해줄 수 있다. 
- 테이블 중심의 개발에서 객체 중심의 개발을 할 수 있다.
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL을 생성할 수 있다.

 

<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

- 데이터베이스 방언? : dialect를 사용하여 value에 원하는 데이터베이스를 설정하면 그에 맞게 하이버네이트에서 자동으로 쿼리를 변경해준다. 예를들어 MySQL에서 VARCHAR인 타입은 Oracle에서는 VARCHAR2인데, 이러한 차이를 자동으로 변경해준다.

- 실무에서는 사용하지 말고 개발단계에서만 사용하자. (테이블의 변경에서 문제가 생기면 큰 장애가 발생...)

 

 

 

 

 

 

 

필드와 컬럼 매핑

 

필드 매핑 애노테이션의 종류

 

@Column 애노테이션의 속성들

 

 

- @Temporal(TemperalType.DATE) 애노테이션은 Java8 이상부터 적용되는 LocalDate, LocalDateTime을 사용하게 되면, LocalDate는 Date타입으로(년,월,일) , LocalDateTime은 Timestamp타입으로 (년,월,일,시간) 인식된다.

 

- @Enumerated(EnumType.STRING) 애노테이션을 사용할 때, 옵션을 주의해야 한다. 디폴트옵션은 @Enumerated(EnumType.ORDINAL) 이다. 이는 Enum타입의 순서(index)를 사용한다. 이 방식에서의 문제점은 중간에 Enum타입에 값이 추가,변경 되었을 때 index값이 바뀐다는 것이다. 그러면 모든 데이터가 틀어지는 상황이 발생할 수도 있다. Enum타입을 사용할 때는 그냥 @Enumerated(EnumType.STRING)을 사용하자.

 

 

 

 

 

 

 

기본키 매핑

 

- @Id는 직접할당 방식으로 이 애노테이션을 사용하면 직접 PK값으로 등록한 것이다.

- @GeneratedValue는 자동할당 방식으로 전략에 따라 데이터베이스에 기본키 매핑방식을 위임한다.

- @Id나 @GeneratedValue로 등록하면 데이터베이스에 위임한 것이기 때문에 값을 등록하면 안된다. (코드에서 set하여 넣으면 안된다는 뜻)

 

 

@GeneratedValue의 전략

1. @GeneratedValue(strategy = GenerationType.IDENTITY)

기본 키 생성을 데이터베이스에 위임한다.
- Mysql을 사용하면 Auto Increase를 사용하여 만든다.

- 특징 : 이 전략을 사용하면 데이터베이스에 기본키 생성을 위임하기 때문에 영속성 컨텍스트에 영속(em.persist();)할 때, 기본키 값을 모른다! 1차 캐시에 담을 때 기본키를 key값으로 영속성 컨텍스트에 등록하기 때문에 발생하는 문제이다.  그래서 이 전략을 사용할 때에만 예외적으로 em.persist(); 하는 시점에 DB에 insert 쿼리가 날아가버린다.

 

 

 

2. @GeneratedValue(strategy = GenerationType.SEQUENCE)

 

 

- 시퀀스 오브젝트를 만들어 낸다.
- @SequenceGenerator(name = "MEMBER_SEQ_GENERATOR", sequenceName = "MEMBER_SEQ")
- 이와 같이 시퀀스 이름을 설정할 수도 있다.

- 특징 : 이 전략을 사용하면 영속성 컨텍스트에 영속(em.persist();)할 때, 매핑된 데이터베이스 시퀀스에서 시퀀스 값을 알아야 영속성 컨텍스트에 등록할 수 있다. (시퀀스 오브젝트는 DB가 관리하는 것이기 때문에 DB에 가봐야 알 수 있는 것이다.) 


그래서 em.persist(); 할 때 DB에서 매핑된 데이터베이스 시퀀스에서 다음 시퀀스값을 먼저 얻어오고(call next value for SEQUENCE 하여 다음 시퀀스를 얻는 로그를 볼 수 있다.) 그 다음에 영속성 컨텍스트에 등록한다. 
영속성 컨텍스트에 등록만을 한 것이기 때문에 트랜젝션을 커밋하기 전까지는 실제 쿼리가 날아가지 않는다.

 


* 의문점 : 그러면 영속성 컨텍스트에 영속을 할때마다 DB에 들러야하면 네트웤을 계속 타서 성능이 저하되는 것이 아닐까?

 

-> allocationSize 옵션을 이용한다.
allocationSize = 50 으로 지정해놓으면 call next value을 한번 하면 미리 50개의 사이즈를 디비에 올려놓고(DB는 시퀀스가 51번 부터 된다.) 메모리에서 1씩 사용하는 것이다. 50개 다쓰면 다시 50개 가져와서 사용한다. (DB는 시퀀스가 101번 부터 된다.) 

 

이 때, 로그를 보면 call next value가 2번 호출된 것을 볼 수 있다. 이유는 첫 번째 call은 처음 DB에 들렀을 때 DB내부적으로 시퀀스 값을 1증가시켜서 가져온 값이고, 두 번째 call은 allocationSize가 50이기 때문에 DB시퀀스를 50까지 증가시켜 놓는 작업 위한 call이라고 이해하면 된다.

 

 

3. @GeneratedValue(strategy = GenerationType.TABLE)

 

 

- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
- 장점 : 모든 데이터베이스에 적용 가능하다.
- 단점 : 성능이 떨어진다.
- 특징 : SEQUENCE전략과 비슷하게 allocationSize옵션을 동일한 방식으로 사용할 수 있다. 

 

 

 

4. 권장하는 식별자 전략

- 기본키 제약 조건 : null 아니고 유일, 변하면 안된다. 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
- 권장 : Long형 + 대체키 + 키 생성전략 사용

 

 

JPA에서 가장 중요한 2가지가 있다. 

1. 객체와 관계형 데이터베이스 매핑하기 (Object Relational Mapping)

2. 영속성 컨텍스트

영속성 컨텍스트에 대해서 공부한 내용을 정리해보자.

 

 

 

본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 설명에 필요한 사진 중 중요한 사진이라고 생각한 사진을 강의자료에서 가져왔음을 출처로서 밝힙니다.

 

 

 

영속성 컨텍스트란?

- 번역을 하자면, "엔티티를 영구 저장하는 환경" 이라는 뜻이다.
- 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다. 엔티티 매니저(Entity Manager)를 통해서 영속성 컨텍스트에 접근한다.
- 엔티티 매니저와 영속성 컨텍스트가 1:1 생성된다.

 

 

 

 

 

엔티티의 생명주기

 

1. 비영속(new/transient)

- 영속성 컨텍스트와의 전혀 관계과 없는 새로운 상태를 뜻한다.

- 그냥 객체를 생성해서 세팅만 한 상태, 이거는 JPA와 전혀 관계가 없는 상태이다.

 

 

 

 

2. 영속(managed) 

- 영속성 컨텍스트에 관리되는 상태를 뜻한다. 

 

- 객체를 생성해서 엔티티매니저를 얻어와서 em.persist(memebr); 하면 객체를 영속 컨텍스트에 member가 들어가면서 영속 상태가 된다. 

- 중요한점은 em.persist(memebr); 한다고 DB에 쿼리가 날라가는 것이 아니다. ts.commit(); 하는 순간 날라간다.

 

 

 

3. 준영속(detached) 

- 영속성 컨텍스트에 저장되었다가 분리된 상태이다.

 

 

 

 

 

 

4. 삭제(removed) 

- 객체가 삭제되는 상태이다.

 

 

 

 

 

 

 

 

영속성 컨텍스트의 이점

 

1. 1차 캐시

 

 

- em.persist(memebr); 하여 영속 상태가 되었다고 가정하자. 영속성 컨텍스트(EntityManager)에는 1차 캐시가 있다. 1차 캐시에는 @Id로 우리가 매핑한 pk값이 key값으로, member가 value값으로 저장이 된다. (Map형태로 저장이 된다.)

 

 

 

 

 

- 이후 조회를 한다고 가정하여 em.find(Member.class, "member1"); 하면 JPA는 DB를 먼저 찾는게 아니라 우선 영속성 컨텍스트에서 1차캐시를 뒤져서 캐시값을 조회하여 일치하면 바로 Entity값을 꺼내온다.

- 그런데 만약 em.find(Member.class, "member2"); 하여 1차 캐시에 없는 것을 조회하면, 1차적으로 1차 캐시에서 찾고 없으면 DB를 조회한다. 그 후 DB에서 조회한 member2를 1차 캐시에 다시 저장한다. 그 이 후 member2를 반환한다.

 

- EntityManager는 트랜젝션단위로 생성하고 지우기 때문에 1차 캐시로 인해 엄청난 이득을 보는것은 아니고, 찰나의 순간에만 이득이다.

 

 

 

2. 영속 엔티티의 동일성 보장

- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다. (마치 자바 컬렉션에서 같은 레퍼런스를 바라보는 객체를 == 비교하면 같은 것처럼) 

 

- 출력의 결과는 true가 나온다. 이유는 1차 캐시를 사용하기 때문이다. em.find(Member.class, "member100");을 하면 먼저 1차 캐시를 뒤지지만 1차 캐시에는 해당 엔티티가 없으니 DB를 조회한다. DB에서 가져온 객체를 1차 캐시에 저장한 후 return 한다. (member1)

다시한번 em.find();를 했을 때에도 1차 캐시를 검색하는데 아까 위에서 member1에서 가져온 객체와 같은 객체가 캐시에 담겨있다. 그러므로 member2는 1차 캐시에서 꺼내온다. 같은 객체를 == 비교하면 당연히 참조값이 같기 때문에 출력결과는 true가 나온다. 

여기에서 알 수 있었던 점은 em.persist(); 해야만 무조건 영속성 컨텍스트에 저장되는 것이 아니라, 영속성 컨텍스트에 없는 객체라면 em.find();했을 때에도 DB에서 가져온 객체가 1차 캐시에 들어가서 영속상태가 될 수 있다.

 

 

 

3. 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연 (Buffering)

 

 

- em.persist(memebrA); 할 때까지는 Insert sql을 데이터베이스에 보내지 않는다. transaction.commit(); 하여 커밋하는 순간 데이터베이스에 쿼리를 날린다. 

 

- 영속 컨텍스트 안에는 "쓰기 지연 SQL저장소" 라는게 있다. persist(memberA);하면 memberA가 1차 캐시에 저장이 됨과 동시에 INSERT SQL을 생성하여 쓰기 지연 SQL저장소에 쌓는다. 그 후에 또 한번 persist(memberB); 하면 1차캐시에 저장이 되면서 SQL을 쓰기 지연 SQL저장소에 쌓는다.
transaction.commit();을 하는 시점에 쓰기 지연 SQL저장소에 있던 SQL들이 날아간다. (JPA에서는 flush()라고 한다. 플러시는 아래에서 설명)

 

 

 

4. 엔티티 수정 - 변경감지

 

- 데이터를 변경하는 상황에서 Member member = em.find(Member.class, "member100"); 하여 영속 엔티티를 조회하고, member.setName("hello"); 와 같이 이름을 변경하였다. update쿼리를 날리려면 em.update(member); 와 같은 코드를 사용하거나 em.persist(member); 하여 영속성 컨텍스트에 영속을 다시 해주어야 할까? 

 

 


아니다. 우리가 자바 컬렉션 이용할 때 get하여 객체를 얻고 변경 후에 다시 add하여 집어 넣어주나? JPA에서도 그렇게 하지 않아도 된다. 어떻게 이런일이 가능할 수 있을까? 

 

 

     4.1. Dirtiy Checking

- 이유는 바로 Dirty Checking때문이다.

 

 

- transaction.commit();을 하면 flush();가 호출이 되고, 엔티티와 스냅샷을 비교한다.

스냅샷은 1차 캐시안에 있는데 값을 최초로 읽어온 시점, 영속 컨텍스트에 값을 집어 넣든 DB에서 가져오든 최초 시점의 상태를 스냅샷으로 떠놓는 것이다. 비교하여 변경을 감지하고, 변경이 있으면 UPDATE SQL을 쓰기지연 SQL저장소에 쌓는다. 그 후 쿼리를 날려서 DB에 변경값을 반영(commit)한다.

 

- 간단하게 변경감지 메커니즘은 다음과 같이 정리할 수 있다.

플러시 발생 -> 변경감지(Dirty Checking) -> 수정된 엔티티 쓰기 지연 SQL저장소에 등록 -> 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제쿼리)

 

 

 

 

 

플러시 (flush)

- 플러시를 하면 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다.
- flush하면 1차 캐시나 영속성 컨텍스트가 지워질까? 아니다. 쓰기 지연 SQL 저장소에 있는 쿼리들을 DB에 반영하는 과정일 뿐이다. (영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 것이다.)

 

 

- 영속성 컨텍스트를 플러시 하는 방법

1. em.flush() - 직접호출

 

- em.flush(); 직전까지의 상황은 member객체에 값을 set해주고 em.persist(member);를 하여 영속상태로 만들어주었다. 그러면 영속성 컨텍스트 내부에서는 1차 캐시에 member를 저장하고, INSERT SQL을 만들어서 쓰기 지연 SQL 저장소에 쿼리를 쌓고 있는 상태이다. 

ts.commit();하면 DB로 쿼리가 날라가는 상황인데, 커밋명령을 하기 전에 em.flush(); 를 직접 호출하여 쿼리를 바로 DB에 날릴 수 있는 것이다.

 

 

2. 트랙잭션 커밋 - 자동 호출

 

3. JPQL 쿼리 실행 - 자동 호출

 

 

- 이 상황에서 memberA, memberB, memberC가 조회가 될까? 당연히.. 아직 flush가 된 상황이 아니라 안된다. 그래서 JPQL쿼리를 실행하면 JPA는 자동으로 flush를 해주고 쿼리를 날린다. 

 

 

 

 

 

 

준영속상태

- 영속상태의 엔티티가 영속성 컨텍스트에서 분리(detach) 한 상태이다.


- 준영속 상태로 만드는 방법

1. em.detach(entity) - 특정 엔티티만 준영속상태로 전환

 

- em.find(); 해서 1차 캐시를 먼저 검색하지만 없다. DB에서 10L을 pk값으로 가지는 객체를 가져온다. 그 후에 em.detach(member);를 해주어서 준영속상태로 만들었다. 이렇게 하면 JPA가 관리하지 않는(영속성 컨텍스트에서 분리된) 엔티티가 되기 때문에 1차 캐시에서도 삭제된다.

 


2. em.clear() - 모든 엔티티를 준영속상태로 전환

- 영속성 컨텍스트에 있는 모든 엔티티를 준영속 상태로 전환한다.

 

3. em.close() - 영속성 컨텍스트를 종료

- 영속성 컨텍스를 종료한다.

 

 

+ Recent posts