JPA란? (+ORM이란?)
JPA는 ORM 기술이기 때문에 ORM에 대해 먼저 알아야 합니다.
ORM이란?
Object-Relational Mapping의 약자로 객체와 관계형 데이터베이스를 연관 짓는다는 뜻입니다.
객체와 관계형 데이터베이스를 연관 짓는 이유
객체와 관계형 데이터베이스 사이에서 일어나는 불일치 때문입니다.
자바는 객체 지향형 언어입니다.
관계형 데이터베이스는 관계형 테이블을 가집니다.
탄생 배경부터가 다른 목적으로 만들어졌기 때문에 두 소프트웨어는 어쩔 수 없는 패러다임의 불일치가 발생합니다.
객체-관계 간의 패러다임 불일치는 어떤것들이 있을까요?
- 세분성
경우에 따라서 데이터베이스에 있는 테이블 수보다 더 많은 클래스를 가진 모델이 생길 수 있습니다.
예를 들어, 어떤 사용자의 세부 사항에 대해 데이터를 저장한다고 해보겠습니다.
객체지향 프로그래밍에서는 코드 재사용과 유지보수를 위해 Person과 Address라는 두 개의 클래스로 나누어 관리할 수 있습니다.
하지만 데이터베이스에는 person이라는 하나의 테이블에 사용자의 세부사항을 모두 저장할 수 있습니다.
이 상황에서 Object는 2개, Table은 1개가 됩니다.
- 상속성
RDBMS는 객체지향 프로그래밍 언어의 특징인 상속 개념이 없습니다.
RDBMS에는 Table 슈퍼 타입, 서브타입 관계를 사용할 수 있지만 Select를 하기 위해서는 Join이 불가피합니다.
따라서 객체답게 모델링할수록 매핑 작업만 늘어납니다.
- 일치
RDBMS는 기본키를 이용하여 동일성의 정의합니다.
그러나 자바는 객체 식별(a==b)과 객체 동일성(a.equals(b))를 모두 정의합니다.
- 연관성
객체지향 언어는 객체 참조를 사용하여 연관성을 나타냅니다. (단방향성)
RDBMS는 연관성을 외래키로 나타냅니다.(양방향성)
- 탐색
자바와 RDBMS에서 객체를 접근하는 방법이 근본적으로 다릅니다.
자바는 그래프 형태로 하나의 연결에서 다른 연결로 이동합니다. ( Member.getTeam().getTeamId() )
만약 여기서 Member와 Team을 join 해서 값을 불러온다면 member.getTeam()을 하면 정상적으로 동작할 수 있습니다.
하지만 member.getOrder()를 한다면 여기에는 Order에 대한 join은 없으므로 null값이 들어가게 됩니다.
RDBMS는 join을 통해 여러 엔티티를 로드하고 원하는 대상 엔티티를 선택(Select)하는 방식으로 탐색합니다.
ORM의 등장 배경
대부분이 RDB를 사용하며 SQL 중심적인 개발을 하게 되는데 여기서 문제점들이 발생했습니다.
- 무한 반복, 지루한 코드 (CRUD) , (자바 객체를 SQL로 , SQL을 자바 객채로)
- 유지보수 측면에서 불편했습니다.
- 만약에 회원 객체에 전화번호를 추가해야 한다면 관련된 SQL 구문에도 전화번호를 추가해주어야 합니다. 이러한 과정 속에서 개발자의 실수가 발생할 가능성도 높습니다
- Entity 신뢰 문제(DAO객체에서. getXX가 진짜 XX를 가져오는 코드인지 내부를 보지 않고는 알 수 없음)
- 객체-관계의 패러다임 불일치( 개발자가 일일이 패러다임을 일치시켜야 했음)
- 객체를 자바 컬렉션에 저장하듯이 DB에 저장할 수 없을까?
ORM을 통해 얻는 이점
- 직관적이며 비즈니스 로직에 집중할 수 있습니다.
- SQL문이 아닌 클래스의 메서드를 통해 데이터베이스를 조작할 수 있으므로 개발자가 객체 모델만 이용해서 프로그래밍하는데 집중할 수 있습니다.
- 선언문, 할당, 종료 같은 부수적인 코드가 없거나 줄어듭니다.
- 재사용 및 유지보수의 편리성이 증가합니다.
- 객체는 객체처럼 관계형 DB는 관계형 DB처럼 설계하고 ORM 프레임워크가 중간에서 연결시켜 줌
- DBMS에 대한 종속성이 줄어듭니다.
- ANSI-SQL을 사용하는 경우에는 덜하겠지만 극단적으로 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요됩니다.
ORM과 SQL Mapper
SQL Mapper란?
Object와 SQL의 필드를 매핑하여 데이터를 객체화하는 기술
ORM처럼 객체와 테이블 간의 관계를 매핑하는 것이 아니라, SQL문을 직접 작성하고 쿼리 수행 결과를 어떠한 객체에 매핑하여 줄지 바인딩하는 방법
즉, SQL에 의존적인 방법 ex) JdbcTemplate, Mybatis
여전히 쿼리를 작성해야 함.
JPA란?
Java Persistence API의 약자로 자바 진영의 ORM 기술 표준입니다.
Persistence란 "영속성"이라는 의미를 가지며 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 말합니다.
즉, 데이터베이스와 관련이 있습니다.
다음 그림은 JPA가 자바와 JDBC사이에서 동작하는 것을 표현한 그림입니다.
다음그림은 JPA속에서 일어나는 과정들입니다.
JPA의 등장 배경
EJB에 엔티티 빈이라는 자바 표준 ORM이 존재했는데 너무 복잡했습니다.
Gavin King이라는 개발자는 이러한 불편함을 해결하고자 하이버네이트(Hibernate)라는 오픈 소스를 만들었습니다.
많은 사람들이 EJB 엔티티 빈에서 하이버네이트로 넘어가게 되었으며 자바 진영에서 JPA라는 자바 표준을 Gavin King을 데려와서 만들게 되었습니다. (그래서 하이버네이트와 거의 유사합니다.)
JPA는 인터페이스 실질적 구현체는 하이버네이트라는 오픈소스로 이해하면 됩니다.
JPA의 구현체로는 Hibernate, EclipseLink, DataNucleus이 있습니다.
또한 JPA와는 별개로 EJB에는 컨테이너라는 기능도 제공했는데 로드 존슨이라는 개발자가 이 기술도 개편해서 Spring Framework를 만들게 됩니다.
JPA의 내부구조
영속성 컨텍스트
영속성 컨텍스트는 JPA를 이해하는데 가장 중요한 용어입니다.
영속성 컨텍스트란?
"엔티티를 영구 저장하는 환경"
영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념
엔티티 매니저를 통하여 영속성 컨텍스트에 접근할 수 있음
영속성 컨텍스트의 특징
영속성 컨텍스트는 엔티티를 식별자 값으로 구분합니다. (밑에 1차 캐시 그림을 보시면 조금 더 이해하기 쉽습니다)
엔티티 매니저의 선언
EntityManager em = emf.createEntityManager();
엔티티 매니저를 통해 회원 엔티티를 영속성 컨텍스트에 저장
em.persist(member);
엔티티 컨텍스트의 특징
엔티티 매니저를 생성할 때 하나 만들어진다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있다.
엔티티란?
쉽게 말해서 객체라고 생각할 수 있습니다.
철학 또는 전산학에서의 Entity는 인간의 개념 또는 정보의 세게에서 의미 있는 하나의 정보 단위입니다.
엔티티의 생성 주기
- 비영속 : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속 : 영속성 컨텍스트에 저장된 상태
- 준영속 : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제 : 삭제된 상태
비영속
엔티티 객체를 생성했지만 아직 영속성 컨텍스트에 저장하지 않은 상태를 비영속이라 합니다.
Member member = new Member(); //비영속 상태
영속
엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태를 말하며 영속성 컨텍스트에 의해 관리됩니다.
em.persist(member); //영속 상태
준영속
영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 더 이상 관리하지 않으면 준영속 상태가 됩니다.
특정 엔티티를 준영속 상태로 만들려면 em.datch()를 호출하면 됩니다.
// 엔티티를 영속성 컨텍스트에서 분리해 준영속 상태로 만든다.
em.detach(member);
// 영속성 콘텍스트를 비워도 관리되던 엔티티는 준영속 상태가 된다.
em.claer();
// 영속성 콘텍스트를 종료해도 관리되던 엔티티는 준영속 상태가 된다.
em.close();
삭제
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제합니다.
em.remove(member); //삭제 상태
바로 DB에 넣지 않고 영속성 컨텍스트를 사용하는 이유는 무엇일까요? (영속성 컨텍스트의 존재 이유)
- 1차 캐시(영속 엔티티의 동일성을 보장해줌)
영속성 컨텍스트에는 트랜잭션의 범위 안에서만 사용되는 굉장히 짧은 캐시 레이어가 존재합니다.
find()가 일어나는 순간 엔티티 매니저가 내부의 1차 캐시를 탐색합니다.
1차 캐시에 엔티티가 존재하면 바로 반환합니다.
만약 1차 캐시에 엔티티가 존재하지 않으면 DB에서 꺼내와서 1차 캐시에 저장합니다.
이러한 1차 캐시 때문에 동일한 member1을 조회하면 같은 주소를 가집니다.
Memeber a = em.find(Member.class, "member1");
Memeber b = em.find(Member.class, "member1");
System.out.println( a== b) ; // true
- 트랜잭션을 지원하는 쓰기 지연
트랜잭션 내부에서 persist()가 일어날 때 엔티티들을 1차 캐시에 저장하고 쓰기 지연 SQL 저장소라는 곳에 INSERT 쿼리들을 생성해서 쌓아 놓습니다.
이후에 트랜잭션을 commit()하는 시점에서 DB에 쿼리들을 동시에 보냅니다.
트랜잭션을 commit 하게 되면 flush()와 commit()이 발생하게 됩니다.
flush()는 1차 캐시는 지우지 않고 쿼리들을 DB에 날려서 DB와 싱크를 맞추는 역할을 합니다.
즉, 영속성 컨텍스트는 버퍼의 기능을 제공합니다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 이때까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 트랜잭션 커밋
- 변경 감지 : Dirty Checking (엔티티 수정)
엔티티를 수정하기 위해서 데이터만 수정하면 끝입니다.
데이터만 set 하고 트랜잭션을 commit 하면 자동으로 업데이트 쿼리가 나갑니다.
어떻게 이게 가능할까요?
1차 캐시에 저장할 때 동시에 스냅샷 필드도 저장됩니다.
이후에 commit(), flush()가 발생할 때 엔티티와 스냅샷을 비교해서 변경사항이 있으면 UPDATE SQL을 알아서 만들어 DB에 저장합니다.
em.update()를 만들지 않은 이유는?
우리가 보통 자바 컬렉션을 사용할 때 리스트에서 값을 변경하고 다시 그 값을 담지 않습니다.
이것과 같은 컨셉으로 동작시키기 위해서 update를 만들지 않고 처리합니다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 수정
memberA.setUsername("nj");
memberA.setAge(27);
//em.update(member) 또는 em.persist(member)로 다시 저장해야 하지 않을까?
transaction.commit(); // 트랜잭션 커밋
삭제 또한 위의 메커니즘과 동일합니다.
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA); // 엔티티 삭제
JPA CURD 사용방법
사용하는 어노테이션들
@Entity
해당 클래스가 테이블과 매핑되는 JPA의 엔티티 클래스임을 의미합니다.
@Id
해당 멤버가 엔티티의 PK임을 의미합니다.
보통 MySQL DB는 PK를 bigint 타입으로, 엔티티에서는 Long타입으로 선언합니다.
@GeneratedValue(strategy = GenerationType.IDENTITY)
PK 생성 전략을 설정하는 어노테이션입니다.
MySQL은 자동 증가를 지원하는 DB이며, PK 자동 증가를 지원하는 DB는 해당 어노테이션을 선언해야 합니다.
@Data
Lombok이라는 코드를 효율적으로 작성할 수 있도록 도와주는 자바 라이브러리를 사용하여 class명 위에 어노테이션을 명시해줌으로써 getter, setter, equals와 같은 method를 따로 작성해야 하는 번거로움을 덜어줍니다.
@Data 어노테이션은 @Getter/@Setter, @ToString, @EqualsAndHashCode @RequiredArgsConstructor를 합쳐놓은 종합 선물 세트라고 할 수 있습니다.
@AllArgsConstructor
해당 어노테이션은 모든 필드 값을 파라미터로 받은 생성자를 만들어줍니다.
@NoArgsConstructor
해당 어노테이션은 파라미터가 없는 기본 생성자를 만들어줍니다.
Entity 클래스 생성
import lombok.*;
import javax.persistence.*;
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "member")
public class MemberVo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mbrNo;
private String id;
private String name;
@Builder
public MemberVo(String id, String name) {
this.id = id;
this.name = name;
}
}
MariaDB Sample 테이블
CREATE TABLE IF NOT EXISTS TEST.MEMBER (
MBR_NO BIGINT NOT NULL AUTO_INCREMENT,
ID VARCHAR(200),
NAME VARCHAR(200),
PRIMARY KEY(MBR_NO) /*AUTO_INCREMENT 컬럼 단일 PK */
);
Repository 클래스 생성
JPA에서는 단순히 Repository 인터페이스를 생성한 후 JpaRepository<Entity, 기본키 타입>을 상속받으면(extends 하면) 기본적인 Create, Read, Update, Delete가 자동으로 생성됩니다.
따라서 단순히 인터페이스를 만들고 상속만 잘해주면 기본적인 동작 테스트가 가능합니다.
JPA 처리를 담당하는 Repository는 기본적으로 4가지가 있습니다. (T : Entity의 클래스 Type, ID : P.K 값의 Type)
1) Repository<T, ID>
2) CrudRepository<T, ID>
3)PagindgAndSortingRepository<T, ID>
4) JpaRepository<T, ID>
public interface MemberRepository extends JpaRepository<MemberVo, Long> {
//비워져 있어도 잘 작동함.
// long 이 아니라 Long으로 작성. ex) int => Integer 같이 primitive형식 사용못함
// findBy뒤에 컬럼명을 붙여주면 이를 이용한 검색이 가능하다
public List<MemberVo> findById(String id);
public List<MemberVo> findByName(String name);
}
위에서 작성한 것처럼 메서드 이름을 잘 조합하여 쿼리를 작성할 수 있습니다.
Service 클래스의 일부 코드
private MemberRepository memberRepository;
public List<MemberVo> findAll() {
List<MemberVo> members = new ArrayList<>();
memberRepository.findAll().forEach(e -> members.add(e));
return members;
}
public Optional<MemberVo> findById(Long mbrNo) {
Optional<MemberVo> member = memberRepository.findById(mbrNo);
return member;
}
JPA와 성능 최적화
물론 중간에 JPA가 들어가기 때문에 느려질 수 있습니다.
반면에 중간에 JPA가 들어가기 때문에 다음과 같은 성능 최적화도 가능합니다.
- 1차 캐시와 동일성 보장
- 같은 트랜잭션 안이라면 두 번째 접근부터는 캐시에서 저장된 걸 가져옴(첫 번째는 쿼리가 한번 나감)
- 트랜잭션을 커밋할 때까지 INSERT SQL을 모음 (JDBC BATCH SQL 기능을 사용하여 한번에 SQL 전송)
- 지연 로딩과 즉시 로딩(설정으로 지연 로딩과 즉시 로딩을 조절할 수 있음 - 지연 로딩으로 세팅을 해놓고 성능 테스트 때 쿼리가 너무 많이 나가면 즉시 로딩으로 변경 가능)
- 지연 로딩 : 객체가 실제 사용될 때 로딩
- 즉시 로딩 : JOIN SQL로 한 번에 연관된 객체 끼리 미리 조회
그러면 JPA를 사용하면 SQL을 몰라도 되나?
아니다. 객체와 RDBMS 둘 다 모두 잘 다루어야 합니다.
ORM으로만 서비스를 구현하기 어렵고 사용하기는 편하지만 잘못 구현된 경우에 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있습니다.
JPA와 JPQL
JPA는 엔티티 객체를 중심으로 개발하기 때문에 SQL을 사용하지 않습니다.
하지만 검색 쿼리를 사용할 때는 SQL을 사용해야 합니다.
JPA는 엔티티 객체를 중심으로 개발하므로 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 합니다.
즉, 필요한 데이터만 데이터베이스에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요합니다.
따라서 JPA는 JPQL(Java Persistence Query Language)을 제공합니다.
JPQL이란 SQL을 추상화한 객체지향 쿼리 언어입니다.
JPQL vs SQL
JPQL은 엔티티 객체를 대상으로 쿼리 합니다.
SQL은 데이터베이스 테이블 대상으로 쿼리 합니다.
TypedQuery<Member> query = em.createQuery("select m from Member m", Memeber.class);
List<Member> memberList = query.getResultList();
위의 코드인 select m from Member m 은 JPQL입니다.
여기서 from Member는 회원 엔티티 객체를 말하며 Member 테이블을 말하지 않습니다.
JPQL은 데이터베이스 테이블을 전혀 알지 못합니다.
JPA와 JPQL과 QueryDSL
QueryDSL은 HQL(Hibernate Query Language) 쿼리를 타입에 안전하게 생성 및 관리할 수 있게 해주는 프레임워크입니다.
QueryDSL은 결국 jpql로 변환됩니다.
잘 와닿지 않는다면 "QueryDSL은 복잡한 쿼리의 경우 한 줄로 작성하는 JPQL을 사용하기보다 가독성이 좋고 실수하지 않도록 해주는 프레임워크"라고 생각해도 좋을 것 같습니다.
정리
JPA만으로 해결하지 못하는 검색 쿼리를 사용하기 위하여 엔티티 객체를 대상으로 쿼리 하는 JPQL을 사용합니다.
이러한 JPQL의 실수를 줄여 잠재적인 버그를 방지하고 가독성이 좋도록 하는 것이 QueryDSL입니다.
JPA의 한계점/단점
통계처리와 같은 복잡한 쿼리보다는 실시간 처리용 쿼리에 더 최적화되어있습니다.
물론 JPA에서 제공하는 Native query기능을 사용할 수 있지만 복잡한 쿼리 작업이 필요하다면 Mybatis와 같은 Mapper 방식을 사용하는 것이 더 효율적일 수 있습니다.
N+1 문제점
N+1 문제점이 무엇인가요?
조회 시 1개의 쿼리를 생각하고 설계를 했으나 나오지 않아도 되는 조회의 쿼리가 N개 더 발생하는 문제
DBMS 툴을 이용해 직접 쿼리문을 만들어 조회할 때는 하나의 쿼리가 발생하겠지만 mybatis, 넘어서는 JPA가 등장함에 따라 자동화된 쿼리문들이 생겨나면서 발생하는 문제입니다.
JPA의 경우에는 객체에 대해서 조회한다고 해도 다양한 연관관계들의 매핑에 의해서 관계가 맺어진 다른 객체가 함께 조회되는 경우 N+1이 발생하게 됩니다.
예를 들어, 유저 한 명이 쓴 게시글을 조회할 때 유저-게시글을 join 한 형태의 쿼리문을 원했지만 N개의 게시글을 또 조회하는 쿼리가 날아가는 경우가 발생할 수 있습니다.
만약 1번 조회할 시에 10만, 100만 개의 게시글이 같이 조회된다면 어마어마하게 성능이 느려질 것입니다.
예제로 사용할 연관관계
가장 흔하게 볼 수 있는 다대일 관계입니다.
한 명의 User가 여러 개의 Article을 가질 수 있는 (User : 1 , Article : N) 관계입니다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 10, nullable = false)
private String name;
@OneToMany(mappedBy = "user")
private Set<Article> articles = emptySet();
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String title;
@Lob
private String content;
@ManyToOne
private User user;
즉시 로딩(EAGER)
객체 A를 조회할 때 A와 연관된 객체를 한 번에 가져오는 것입니다.
실무에서 가장 쓰지 말아야 할 모든 문제의 첫 번째 원인이 되는 즉시 로딩입니다.
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Article> articles = emptySet();
// Article.java
@ManyToOne(fetch = FetchType.EAGER)
private User user;
사용자(user)의 입장에서 즉시 로딩을 사용한다고 하였을 때, Article의 모든 List를 다 같이 조회하고 싶을 상황이 생길 수 있는데 왜 즉시 로딩이 문제가 될까요?
사실 일반적으로 findById에 대한 메서드는 EntitiyManager에서 PK 값을 찍어서 사용하기 때문에 JPA가 내부적으로 join문을 사용해서 최적화를 다음처럼 진행해줍니다.
@Test
@DisplayName("Eager type은 User를 단일 조회할 때 join문이 날아간다.")
void userSingleFindTest() {
System.out.println("== start ==");
User user = userRepository.findById(1L)
.orElseThrow(RuntimeException::new);
System.out.println("== end ==");
System.out.println(user.name());
}
내부적으로 inner join문 하나가 날아가서 User가 조회됨과 동시에 Article까지 즉시 로딩되는 것을 확인할 수 있습니다.
findById, 즉 EntityManager에서 entityManager.find() 같은 경우 jpa가 내부적으로 join문에 대한 쿼리를 만들어서 반환을 하기 때문에 즉시 로딩으로는 문제가 없어 보입니다.
하지만 문제는 jpql에 있습니다.
우리는 JPA 이외에도 직접 jpql을 짜서 전달하기도 하고 data jpa에서 findBy~의 쿼리 메서드 같은 경우에도 data jpa 내부에서 jpql이 만들어져서 나갑니다.
jpql은 sql 그대로 번역됩니다.
만약 user의 findAll()을 요청하는 것이라면 select u from User u ;라는 쿼리가 발생하게 됩니다.
User를 찾는 것은 문제가 없지만 우리는 "즉시 로딩"을 Article column에 걸어두었습니다.
즉, 모든 User에 대해서 검색하고 싶어서 select 쿼리를 하나 날렸지만(1), 즉시 로딩이 걸려있기 때문에 각각의 User가 가진 Article을 모두 검색한다(N)이라는 N+1 문제가 발생하는 것입니다.
즉시 로딩은 Jpql로 전달되는 과정에서 Jpql 후 Eager 감지로 인한 N쿼리가 추가로 발생하는 경우가 있기 때문에 사용해서는 안된다.
지연 로딩(LAZY)
연관된 객체를 "사용"하는 시점에 로딩해주는 방법입니다.
그러면 즉시 로딩 -> 지연 로딩을 사용하면 과연 N+1문제가 발생하지 않을까요?
결국 사용할 때 User의 Article을 사용한다면 근본적인 문제점이 해결되지 않았기 때문에 여전히 N+1문제가 발생됩니다.
지연 로딩에서의 해결책 - fetch join
fetch join을 걸어주면 해결할 수 있습니다.
fetch join이란?
- 기존의 SQL join과 다릅니다
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티를 SQL 한 번에 함께 조회하는 기능
fetch join 사용
fetch join을 사용하면 N+1문제가 해결됩니다.
사실 N+1문제에 대해서는 간단한 fetch join 말고도 @EntityGraph를 사용합니다.
2개 이상의 oneToMany 자식 테이블에 Fetch join을 했을 때 MultipleBagFetchException 등 다양한 것들이 존재하는데 "현재 JPA를 학습하지 않은 시점에서는 JPA에서 N+1문제라는 것이 발생할 수 있다"라는 것만 알고 추후에 자세히 다루어 보겠습니다.
출처
https://www.youtube.com/watch?v=WfrSN9Z7MiA&list=PL9mhQYIlKEhfpMVndI23RwWTL9-VL-B7U&index=2(토크ON세미나 JPA 프로그래밍 기본기 다지기 1강 - JPA 소개 | T아카데미)
https://eun-jeong.tistory.com/31(ORM 사용 이유 장단점)
https://dundung.tistory.com/216(Tacademy JPA 강의 정리)
https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80(JPA 영속성 컨텍스트란)
https://goddaehee.tistory.com/209(JPA CRUD 해보기)
https://cornswrold.tistory.com/332(JPQL이란?)
https://www.inflearn.com/questions/38771(JPQL와 Querydsl 언제 사용해야 할까?)
https://madplay.github.io/post/introduction-to-querydsl(Querydsl : 소개와 사용법)
https://brunch.co.kr/@jinyoungchoi95/2(JPA N+1 발생 케이스와 해결책)
https://jojoldu.tistory.com/165(JPA N+1 문제 및 해결방안)