JPA 트러블슈팅 교육 내용 정리

송재호·2022년 3월 23일
0

Entity 클래스의 필드에 @Column 어노테이션은 생략이 가능하다.
@Transient과 같은 어노테이션을 함께 쓰지 않는 이상 기본적으로 똑같은 컬럼명으로 매핑이 된다.


날짜 타입 매핑의 경우
낮은 버전 Java에서 사용하던 Date, Calender 같은 경우에는 @Temporal 어노테이션을 붙여주어야 한다.
자바 8이후의 date/time에는 붙일 필요 없다.


복합키를 만들 경우에는
@IdClass
@EmbeddedId / @Embeddable
둘 중 하나를 선택해서 만들어야 한다.

복합 key class 생성 시에는 다음과 같은 제약조건이 있다.
public class, public 기본 생성자, serializable 구현, equals 및 hashcode 메서드 정의


@ManyToMany는 실무에서 보통 안쓴다.
나는 쓸 일도 없었기 때문에 직접 찾아보니 중간 테이블을 자동 생성하기 때문에 응용에 어려움이 있다고 한다.
https://codeung.tistory.com/254
다대다 관계가 필요하다면 중간 테이블에 대한 Entity class를 직접 만들어 @ManyToOne과 @OneToMany의 조합을 만들어 사용하는 것을 권장하고 있다.


양뱡향 연관 관계

관계의 주인

  • 연관관계의 주인은 외래키(FK)가 있는 곳
  • 연관관계의 주인이 아닌 경우, mappedBy 속성으로 연관 관계의 주인을 지정

JPA 내부적으로도 FK참조를 기반으로 구현하기 때문에 본질적으로 참조의 방향은 단방향이다.
물리적으로 존재하지 않는 연관관계 처리를 위해 mappedBy 속성을 통해 주인을 정하는 것.

관계를 양방향으로 만들면 반대 방향으로의 객체 그래프 탐색이 가능한 장점이 있다.
따라서 우선적으로는 단방향 맵핑을 사용하되, 반대 방향 객체 그래프 탐색 기능이 필요할 때 양방향을 사용하는 것을 권장한다.

@oneToMany 처럼 일대다 관계를 맺을 때는,
@JoinColumn(name = “”) 의 name에는 관계를 맺는 쪽의 FK가 들어가며(내 컬럼명이 아님), 해당 엔티티에는 그 컬럼에 대한 필드가 없어야 한다.

cascading을 사용하는 경우 일대다 단방향 매핑은 불필요한 쿼리를 발생시킬 수 있음 (insert 후에 update문이 또 실행).
결론적으로 조회할 때는 단방향 매핑을 지향해야 하지만, update나 create시에도 엔티티를 사용할 경우는 연관관계 방향성을 재고해 보아야 한다.

트러블슈팅 - 단방향 일대다(1:N) 관계에서 영속성 전이를 양방향 일대다(1:N)로 변경해보자 !
N쪽에서 @ManyToOne이 주인쪽이기때문에 @JoinColumn name을 사용해서 1쪽의 필드를 참조
1쪽에서 @OneToMany는 주인이 아니기때문에 mappedBy =“” 속성을 이용해서 N쪽의 필드를 참조

  • 양쪽에 다 데이터를 매핑해줘야 하는 불편함이 있지만, insert 후 불필요한 update 쿼리를 날리는 현상이 해결되기를 기대 !!
  • 결과는 실패, Repeated column in mapping for entity: com.xxx.xxx.jpa.entity.MemberDetail column: member_id 두번썼다는 에러 (복합키쪽에 이미 있는걸 한번 더 썼기 때문에)
  • 그래서 @JoinColumn 대신 @MapsId(“memberId”)를 사용해본다!

@MapsId 는 컬럼을 중복으로 사용할 수 있게 해주는 듯!
pk에 쓴 것을 fk에도 쓸 수 있게 해주어 결과적으로 업데이트 문이 사라졌다.

따라서 무조건 단방향이 좋은 건 아니고, 연관관계를 이용해서 update, insert하는 경우에는 (다대일은 상관없다. fk를 가지지 않은 부분에서 연관관계 잡을때의 문제) 양방향으로 잡아주는 편이 좋다.

일대다 관계에서, @JoinColumn 쓸 때 insertable updatable을 false로 주고 조회용으로 쓰게 되면
다 쪽에서 fk 필드를 명시적으로 가질 수 있게 되며 그것을 통해서 저장해주면 된다.
다만 그렇게 되는 경우 각각 조합(set)을 해줘야 하는 것. (하지만 이것은 객체지향적이지 않고 db query와 유사하긴 한 것임, 필요에 따라 사용하는 것이 나쁘지는 않다…)


N+1 문제

@ManyToOne 조건으로 잡힌 컬럼에 대해 findOne 했을 때는, join해서 잘 가지고 온다.
그러나 findAll 하면 커스터머에 대한 N+1 쿼리가 계속 나간다. 왜? 기본패치전략이 Eager라서 다 가지고와야해서

해결방법

  • Fetch Join
    • JPQL join fetch
    • Querydsl fetchJoin()
  • Entity Graph
  • 그 외 (Hibernate에서만 사용 가능)
    • Hibernate @BatchSize : N이 천개면 천개만큼 하지말고 사이즈만큼씩 묶어서 진행해라
    • Hibernate @Fetch(FetchMode.SUBSELECT) : in 절로 넣어서 처리하게 하는 것이다.

JPA 사용하는 경우 쿼리 결과에 대해서는 entityManager가 관리를 하게 되는데,
entityManager가 dirty checking을 하고 1차캐싱을 하기 때문에 이미 가져온 것에 대해선 다시 쿼리를 날리지 않는다.

그러나 entityManager가 체크해주는건 하나의 레코드 (entity)에만 해당되고,
연관관계 맺혀져 있는 것에 대해서는 관리하지 못한다.

그런데 findAll같은 경우 여러개를 가져와야 하니까 , 우선 모두 페치조인 된 쿼리를 실행을 하고, 실행 결과로 나온 엔티티에 대해 연관관계를 추후에 따져보게 되는것이다. (메모리상에서 매핑한다는 것이고, 그래서 추가쿼리가 발생하지 않는것임)

어노테이션 프로세서를 통해 (JPAAnnotaionProcessor) q클래스 생성 후 사용 (querydsl-api, querydsl-jpa 메이븐 플러그인 추가)
큐클래스 생성시점은 메이븐 컴파일 시점이므로 generate ~~ 를 실행하지 않더라도 메이븐 컴파일 돌리면 생성이 된다.


Querydsl

서드파티 중 querydsl은 기본적으로 jpql을 만들어주는데, jooq은 네이티브 쿼리를 지향하고 있어서 DB에 종속적일 수 있다. (querydsl도 네이티브쿼리 만드는게 있긴 하다)

스프링 data jpa에서 querydsl 사용하는건 두가지 방법이 있음
Spring Data JPA + Querydsl

  • QuerydslPredicateExecutor
  • QuerydslRepositorySupport

(강의는 QuerydslRepositorySupport 방식으로 진행)

  1. @NoRepositoryBean 어노테이션이 붙은 ~RepositoryCustom 인터페이스를 생성해준다.
  2. 그 후에 이것을 구현한 ~RepositoryImpl 클래스를 만들어주면서 extends QuerydslRepositorySupport 하면 된다. (postfix를 따로 수정하지 않았다면 무조건 impl로 끝나야 한다)
    super생성자에 엔티티의 .class 전달해야 함

interface repository 에서는 extends OrderRepositoryCustom, JpaRepository<Order, Long> 와 같이 다중 상속 받아서 기본 메서드와 구현한 모든 메서드를 사용할 수 있게 만들 수 있다 (인터페이스라서 다중 상속 됨).

N+1 문제를 해결하기 위해 fetch join 하는 것은 좋으나,
이것저것 조인되어 만들어진 쿼리 자체가 실행계획이 엉망진창인 경우 더 최악의 결과를 가져올 수 있다.

따라서 무조건적인 정답은 아니라고 생각하고 한번 생각해보아야 한다.

트러블 슈팅 - Custom Repository 구현 시 흔히 하는 실수
기본 Repository interface

  • MemberRepository
    = Custom Repository interface
  • MemberRepositoryCustom >> 이것 @NoRepositoryBean 해주는거
    = Custom Repository 구현 class
  • MemberRepositoryCustomImpl (X)
  • MemberRepositoryImpl (O)

스프링부트 auto configure 빈 생성할 때 기본설정 이름이 ~Impl이고, 위에 만든 ~Custom은 그냥 메서드 구현용 껍데기니까 진짜 레파지토리 이름에 ~Impl을 붙여주는게 맞다.

참고

  • h2데이터베이스는 메모리 말고 파일시스템에 저장할 수도 있다.
    문제가 생기는 경우 해당 경로 파일을 rm 해주면(지우면) 된다.
  • logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
    위 설정을 해야 쿼리에 파라미터 바인딩이 된 값들을 볼 수 있다.
profile
식지 않는 감자

0개의 댓글