JPA 심화 1-2

5w31892p·2023년 2월 1일
0

JPA 심화

목록 보기
2/19

ORM 의 성장 과정

릴레이션(관계형 데이터베이스)를 객체(도메인 모델)로 매핑 하려는 이유

  1. 객체 지향 프로그래밍의 장점을 활용할 수 있다.
  2. 이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함
  3. 각종 디자인 패턴 사용하여 성능 개선 가능
  4. 코드 재사용

하지만, 객체를 릴레이션에 맵핑하려니 문제들이 발생한다.
ORM 이 해결해야하는 문제점과 해결책은 아래와 같다.

:: ORM이 해결해야하는 문제점과 해결책

상속의 문제

  • 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
  • RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.

해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)

관계 문제

  • 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
  • RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)

해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)

탐색 문제

  • 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
  • RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.

해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())

밀도 문제

  • 객체 : 멤버 객체크기가 매우 클 수 있다.
  • RDB : 기본 데이터 타입만 존재한다.

해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)

식별성 문제

  • 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
  • RDB : PK 로만 식별

해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id,@GeneratedValue )


ORM이 문제점만 있던게 아닌 얻은 것도 있었다.

:: ORM 이 얻은 최적화 방법

  • 1차 캐시

    • 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
    • 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
    • 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
  • 2차 캐시

    • 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
    • 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
    • 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.
  • 2차 캐시 적용 방법

    • Entity에 @Cacheable 적용 후 설정 추가
    • sharedCache.mode 설정

:: 영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연

  • 영속성
    • 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성
    • 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 되므로 파일이나 db에 영궁 저장함으로써 영속성을 부여한다.

영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)

  1. 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태

  2. 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태

  3. 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태

  4. 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태

  • 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
  • Raw JPA 관점에서 순서대로 요약정리 해보자면
    • persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
    persist() -> save()
    merge -> update()
  • 예제
Item item = new Item();		// 1
item.setItemNm("테스트 상품");	

EntityManager em = entityManagerFactory.createEntityManager();	// 2
EntityTransaction transaction = em.getTransaction();		// 3
	
transaction.begin();		
em.persist(item);		// 4-1
em.flush(item).     // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit();		// 5

em.close();			// 6

// 1. 영속성 컨텍스트에 담을 상품 엔티티 생성
// 2. 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
// 3. 데이터 변경 시 무결성을 위해 트랜잭션 시작
// 4. 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
// 5. 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
// 6. 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환


  • 쓰기 지연이 발생하는 시점
    • flush() 동작이 발생하기 전까지 최적화
    • flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능
  • 쓰기 지연 효과
    • 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
    • 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
    • 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
    • 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
  • generationType.IDENTITY
    • 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장하므로 쓰기지연 발생 안됨
  • 쓰기지연 예제
Team teamA = new Team();
teamA.setName("TeamA");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

Member member_A = new Member();
member_A.setName("memberA");
member_A.setTeam(teamA);

em.persist(member_A);

// em.flush();

Member findMember = em.find(Member.class, member_A.getId());
Team findTeam= findMember.getTeam();

System.out.println(findTeam.getName());


// flush가 있는 경우

create member
create team
insert team      // flush로 인해 쓰기지연이 발생하지 않음
insert member    // flush로 인해 쓰기지연이 발생하지 않음
print "TeamA" (memberA.getTeam())


// flush가 없는 경우

create member
create team
print "TeamA" (memberA.getTeam()) // 쓰기 지연이 발생하더라도 영속성 컨텍스트에서 조회해옴
insert team      // 쓰기 지연이 발생한 부분
insert member    // 쓰기 지연이 발생한 부분

0개의 댓글