JPA 스터디 - 7

한상우·2023년 1월 26일
0

JPA

목록 보기
7/7

목표: JPA 가 지원하는 컬렉션의 종류와 부가 기능

컬렉션


JPA는 자바에서 기본으로 제공하는 Collection, List, Set, Map 컬렉션을 지원한다.

하이버네이트 구현체 기준

@Entity
public class Team {
	
	@OneToMany
	@JoinColumn
	private Collection<Member> members = new ArrayList<Member>();
}

Team 이 members 컬렉션을 필드로 가지고 있는 상태

Team을 영속상태로 만들기 전에 컬렉션 타입은 ArrayList 이지만 영속 상태가 되면 하이버네이트가 제공하는 PersistentBag 타입으로 변경 (PersistentBag 를 레퍼 컬렉션으로도 부름)

  • 하이버네이트가 컬렉션을 효율적으로 관리하기 위해
  • 때문에 즉시 초기화해서 사용하는 것을 권장 Collection<Member> members = new ArrayList<Member>();

레퍼 컬렉션

  1. Collection, List

중복을 허용하는 컬렉션

PersistentBag 로 사용

@Entity
public class Team {
	
	@OneToMany
	@JoinColumn
	private Collection<Member> members = new ArrayList<Member>();
}
  • add(), equals() 메소드를 사용할 수 있는데, 중복을 허용하므로 엔티티를 추가할 때 단순히 저장만 하면 된다. 따라서 지연 로딩된 컬렉션을 초기화 하지 않는다. → 값을 비교할 필요가 없으므로
  1. Set

중복을 허용하지 않는 컬렉션

PersistentSet 으로 사용

@Entity
public class Team {
	
	@OneToMany
	@JoinColumn
	private Set<Member> set = new HashSet<Member>();
}
  • 중복을 허용하지 않으므로 객체를 추가할 때마다, equals() 메소드로 같은 객체가 있는지 비교, HashSet 은 hashcode(같은 객체인지) + equals(내용이 같은지) 비교
  • 엔티티를 추가할 때 지연 로딩된 컬렉션을 초기화 한다.
  1. List + @OrderColumn

List 인터페이스에 @OrderColumn을 추가하여 순서가 있는 특수한 컬렉션 인식 가능

@OneToMany(mappedBy = "team")
@OrderColumn(name = "POSITION")
private List<Member> members = new ArrayList<Member>();

단점

  • 일대다 관계에서 다 쪽에 테이블에 @OrderColumn에서 지정한 POSITION 컬럼 매핑되지만, 실제 다쪽에 있는 엔티티는 POSITION 값을 알 수 없음. → Insert 할 때 POSITION 값이 저장되지 않음 나중에 UPDATE SQL 추가 발생
  • 하나의 데이터를 삭제할 때 순서가 보장되어야 하므로 삭제된 데이터 이후 데이터의 POSITION 값들이 하나씩 감소

@OrderBy 를 사용해 컬렉션을 정렬할 수 있음

@OneToMany(mappedBy = "team")
@OrderBy("username desc, id asc")
private List<Member> members = new ArrayList<Member>();

초기화를 실행할 때 SQL 문에서 ORDER BY 가 사용된다!!

컨버터


엔티티의 데이터를 변환해서 DB 에 저장할 수 있다.

@Entity
public class Member {
	
	@Converter(converter=BooleanToYNConverter.class)
	private boolean vip;
}

데이터베이스에 저장되기 직전에 BooleanToYNConverter 컨버터가 동작됨

@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	
	@Override
	public String convertToDatabaseColumn(Boolean attribute) {
		// 엔티티의 데이터를 DB 컬럼에 저장할 데이터로
	}

	@Override
	public String convertToEntityAttribute(String dbData) {
		// DB 조회한 데이터를 엔티티의 데이터로
	}
}
  • 제네릭에 <현재 타입, 변환할 타입>

클래스 레벨에도 설정 가능

@Entity
@Converter(converter=BooleanToYNConverter.class, attributeName = "vip")
public class Member {
	
	private boolean vip;
}

autoApply true 옵션을 적용하면 엔티티에 @Converter 를 지정하지 않아도 모든 Boolean 타입에 자동으로 컨버터 적용할 수 있음


@Converter(autoApply = true)
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
	
	@Override
	public String convertToDatabaseColumn(Boolean attribute) {
		// 엔티티의 데이터를 DB 컬럼에 저장할 데이터로
	}

	@Override
	public String convertToEntityAttribute(String dbData) {
		// DB 조회한 데이터를 엔티티의 데이터로
	}
}

리스너


  1. 엔티티가 영속성 컨텍스트에 조회된 직후
  2. 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출
  3. flush 나 commit 을 호출해서 엔티티를 DB 에 수정하기 직전에 호출
  4. remove 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출
  5. flush 나 commit 을 호출해서 엔티티를 DB 에 저장한 직후 호출
  6. flush 나 commit 을 호출해서 엔티티를 DB 에 수정한 직후 호출
  7. flush 나 commit 을 호출해서 엔티티를 DB 에 삭제한 직후 호출

이벤트 적용 위치

  • 엔티티에 직접
  • 별도의 리스너 적용
  • 기본 리스너 사용
@Entity
public class Member {
	
	@PrePersist
	public void prePersist() {
		// 리스너
	}
}

엔티티 그래프


JPQL 페치 조인을 통해 엔티티를 조회할 때 연관된 엔티티들을 함께 조회할 수 있는데, 이때 연관된 엔티티 어떤것을 가져올지에 따라 각기 다른 JPQL 페치 조인 작성이 필요하다

결국 하나의 엔티티에 대해 함께 조회할 엔티티에 따라 다른 JPQL 을 사용해야 하는 것인데,

그래프 기능을 통해 엔티티를 조회하는 시점에 함께 조회할 연관된 엔티티를 선택할 수 있다

JPA 15장

예외 처리


JPA 예외는 모두 언체크 예외 (RuntimeException의 자식)

JPA 표준 예외는 크게 2가지로 나눌 수 있음

  • 트랜잭션 롤백을 표시하는 예외
    • 심각한 예외 → 강제 커밋해도 안댐
  • 트랜잭션 롤백을 표시하지 않는 예외

JPA 예외 변환

서비스 계층에서 JPA 예외를 직접 사용하면 JPA에 의존하게 되는 것

따라서 스프링 프레임워크는 데이터 접근 계층에 대한 예외를 추상화해서 제공

이를 위해서는 PersistenceExceptionTranslationPostProcessor 를 스프링 빈으로 등록하면 댐

Repository 어노테이션을 사용한 곳에 예외 변환 AOP를 적용해 추상화한 예외로 변환시킴

롤백시 주의 사항

트랜잭션 롤백은 DB 반영 사항만 롤백하는 것이지, 수정한 자바 객체를 원상태로 복구해주지는 않음

객체는 수정된 상태로 영속성 컨텍스트에 남아있어, 트랜잭션이 롤백된 영속성 컨텍스트를 그대로 사용하는 것은 위험

  • 스프링의 기본 전략인 트랜잭션당 영속성 컨텍스트 전략은 문제가 발생하면 트랜잭션 AOP종료 시점에 트랜잭션을 롤백하면서 영속성 컨텍스트도 함께 종료함

엔티티 비교


영속성 컨텍스트 내부에는 엔티티 인스턴스를 보관하기 위한 1차 캐시가 있다. 1차 캐시는 영속성 컨텍스트와 생명주기를 같이 한다.

영속성 컨텍스트가 같으면 엔티티를 비교할 때

  • 동일성
  • 동등성
  • 데이터베이스 동등성

위 3가지를 만족한다.

프록시 심화


Member 객체를 먼저 em.getReference() 메소드를 통해 프록시로 조회하고 em.find로 한 번더 조회하였다면 영속성 컨텍스트는 프록시로 조회된 엔티티에 대해서 같은 엔티티를 찾는 요청이 오면 원본 엔티티가 아닌 처음 조회된 프록시를 반환한다.

Member refMember = em.getReference(Member.class, "member1"); // 프록시 반환
Member findMember = em.find(Member.class, "member1"); // 프록시 반환

만약 순서를 반대로 원본 엔티티를 먼저 조회한다면 getReference() 를 할 때 영속성 컨텍스트에 이미 있는 원본을 반환하면 되므로 프록시를 반환하지 않는다.

→ 따라서 동일성이 보장된다.

타입 비교

프록시는 원본 엔티티를 상속받아 만들어지므로 instanceof 를 통해 타입을 비교할 수 있다.

(refMember instanceof Member)

이후 내용은 그냥 넘어갔다…

성능 최적화


N+1 문제

  • 페치 조인 사용
  • 하이버네이트 @BatchSize 연관된 엔티티를 조회할 때 지정한 size만큼 SQL의 IN절을 사용해 조회함 만약 조회한 회원이 10명일 때 size = 5 로 해두면 SQL은 2번만 추가로 실행
    @org.hibernate.annotations.BatchSize(size = 5)
    private List<Order> orders = new ArrayList<Order>();
  • 하이버네이트 @Fetch(FetchMode.SUBSELECT) 연관된 데이터를 조회할 때 서브 쿼리를 사용해서 N+1 문제를 해결

읽기 전용 쿼리 성능 최적화

영속성 컨텍스트에 엔티티가 관리될 때 영속성 컨텍스트는 변경 감지를 위해 스냅샷 인스턴스를 보관하므로 많은 메모리를 사용한다. 읽기 전용으로 엔티티를 조회하면 메모리 사용량을 최적화할 수 있다.

  • 스칼라 타입으로 조회
  • 읽기 전용 쿼리 힌트 사용
  • 읽기 전용 트랜잭션 사용 @Transactional(readOnly = true) → 강제로 플러시를 호출하지 않는 한 플러시가 일어나지 않음 (플러시할 때 스냅샷 비교와 같은 무거운 로직들이 수행됨)

배치 처리

JPA 등록 배치

많은 양의 엔티티를 한 번에 등록할 때 영속성 컨텍스트에 엔티티가 계속 쌓이지 않도록 일정 단위마다 영속성 컨텍스트의 엔티티를 DB에 플러시하고 초기화 해야 한다. 계속 쌓이면 메모리 부족 오류가 발생할 수 있다.

  1. 페이징 처리
  2. 커서

자세한 건 정리 안했당.. 읽어봐~

profile
안녕하세요 ^^

0개의 댓글