연관 필드 예시: @ManyToOne, @OneToOne, 대상이 엔티티 객체 (예: m.team)
🔽 예시 코드
//JPQL 쿼리
select o.member fromOrder o
//실행 시 발생하는 SQL 쿼리 (묵시적 조인 발생)
SELECT M.*FROM ORDERS O INNERJOINMEMBER M ON O.MEMBER_ID = M.ID
//추가 탐색 가능
select o.member.name fromOrder o
설명: 단일 값 연관 필드는 o.member와 같은 필드를 사용하여 연관된 객체의 속성에 접근할 수 있으며, o.member.name과 같은 추가 탐색이 가능하다.
직접적으로는 추가 탐색이 불가능하지만, FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
다른 엔티티 객체들의 컬렉션(list, set 등)을 참조하며, 경로 탐색을 통해 컬렉션 내의 각 객체에 접근할 수 있다.
묵시적으로 내부 조인이 발생한다.
연관 필드 예시: @OneToMany, @ManyToMany, 대상이 컬렉션 (예: m.orders)
//JPQL 쿼리
select t.members from Team t
//실행 시 발생하는 SQL 쿼리 (묵시적 조인 발생)
SELECT M.*FROM TEAM T INNERJOINMEMBER M ON M.TEAM_ID = T.ID
//명시적 조인 시 별칭을 통해 추가 탐색 가능
select m.username from Team t join t.members m
설명: 컬렉션 값 연관 필드는 t.members와 같이 컬렉션을 참조하며, 추가 탐색을 위해 명시적 조인을 통해 별칭을 사용하여 접근할 수 있다.
컬렉션 값 연관 필드 명시적 조인 동작 과정 1. 명시적 조인 시 'join t members m' 구문에서 Team 객체의 members 컬렉션과 Member 객체 간의 조인이 내부적으로 수행된다. 2. 이후 members 컬렉션 내의 각 Member 객체가 Team 객체와 연결되어, 관련된 Member의 데이터에 접근할 수 있다.
//가능, 문제는 내부 조인이 2번 발생
select o.member.team fromOrder o
//가능, 컬렉션이지만 경로 탐색의 끝이기 때문에 가능
select t.members from Team t
//불가능, 추가 탐색 불가(컬렉션 값 연관 필드)
select t.members.username from Team t
//가능, 컬렉션이지만 명시적 조인을 통해 탐색 가능
select m.username from Team t join t.members m
Stringjpql="select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for (Member member : members) {
System.out.println("username = " + member.getUsername() + ", " +
"teamname = " + member.getTeam().name());
}
설명:
패치 조인을 사용하면 회원과 팀을 지연 로딩(LAZY)으로 설정하더라도, 패치 조인에 의해 데이터가 즉시 로드되므로 지연 로딩이 발생하지 않는다.
루프 구문에서 getTeam() 메서드는 더 이상 프록시 객체가 아니다. 패치 조인으로 인해 실제 Team 엔티티가 이미 로드되어 있으므로, getTeam() 호출 시 실제 데이터에 직접 접근할 수 있다.
컬렉션 패치 조인은 엔티티를 조회할 때 연관된 컬렉션도 함께 조회하는 경우에 사용되며, 특히 일대다 관계의 컬렉션을 패치 조인할 때 사용된다.
🔽 예시 코드
Team 엔티티를 조회하면서, 연관된 Member 컬렉션(t.members)도 함께 조회하는 경우
//JPQL 쿼리
select t from Team t fetch t.members where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*, M.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
🔽 예시 상황 및 사용 코드
팀을 조회하면서, 각 팀의 회원들도 함께 조회하고 싶은 상황
1️⃣ 문제 상황: DISTINCT를 사용하지 않는 패치 조인 코드
Stringquery="select t from Team t join fetch t.members where t.name = '팀A'";
List<Team> teams = em.createQuery(query, Team.class).getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName() + ", " + "team = " + team);
for (Member member : team.getMembers()) {
System.out.println("->username = " + member.getUsername() + ", " + "member = " + member);
}
}
//출력
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
설명:
컬렉션 패치 조인을 사용하면, 일대다 관계인 Team과 Member를 조인할 때, Team 객체는 하나임에도 불구하고 Member와 조인하면서 결과가 중복되어 출력된다. 이는 Team과 관련된 Member가 각각 별도로 조인되면서 발생하는 현상이다. 즉, 하나의 Team 객체가 여러 개의 Member 객체와 조인되어, Team 객체가 중복되어 리스트에 나타나게 된다. 따라서 같은 Team이 여러 번 출력되며, 그에 따른 Member도 중복되어 나타난다.
또한, 패치 조인을 사용하면 Team과 Member를 지연 로딩(LAZY)으로 설정하더라도, 패치 조인으로 인해 관련 데이터가 즉시 로드되기 때문에 지연 로딩이 일어나지 않는다.
2️⃣ 해결 방법: DISTINCT를 사용하는 패치 조인 코드
해결 방법은 JPQL 쿼리에서 DISTINCT 키워드를 사용하여 중복 엔티티를 제거하는 것이다. 이를 통해 중복된 결과가 출력되지 않도록 할 수 있다.
//JPQL 쿼리
select t from Team t join t.members m where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
//출력
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
설명:
Team과 Member 엔티티를 조인하여 Team을 조회하지만, 연관된 Member 컬렉션(t.members)은 함께 조회되지 않는다.
JPQL 쿼리는 결과를 반환할 때 연관된 엔티티를 자동으로 로드하지 않으며, SELECT 절에 명시된 엔티티만 조회한다.
지연 로딩(LAZY)으로 설정 시, 프록시 객체나 초기화되지 않은 컬렉션 래퍼를 반환한다.
즉시 로딩(EAGER)으로 설정 시, Member 컬렉션을 즉시 로딩하기 위해 추가 쿼리가 실행될 수 있다.
🔽 패치 조인을 사용하는 경우
//JPQL 쿼리
select t from Team t joinfetch t.members where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*, M.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
별칭을 사용하면 쿼리가 복잡해지고, 패치 조인의 기본 목적인 즉시 로딩의 효율성이 떨어질 수 있다.
객체 그래프는 전체 데이터의 연관 관계를 한 번의 쿼리로 완전히 로드하는 것을 목표로 한다. 즉, 객체 그래프를 완전히 조회하는 것이 아닌 일부 데이터만 선택적으로 로드하면 연관된 데이터의 일관성이 깨질 수 있다.
Hibernate에서는 별칭 사용이 가능하지만, 이는 권장되지 않는다.
둘 이상의 컬렉션은 패치 조인할 수 없다.
데이터가 곱하기가 되어 예기치 못한 정합성 문제를 일으킬 수 있다.
컬렉션을 패치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
조인된 결과에 중복된 엔티티가 발생할 수 있는데, 이를 처리하지 않고 페이징을 하게 되면 문제가 발생할 수 있다.
DISTINCT를 사용하여 중복 제거를 시도할 수 있지만, 이도 적절한 방법은 아니다. DISTINCT를 적용하게 되면 데이터의 정렬과 중복 제거가 복합적으로 작용하여 페이지가 일관되지 않을 수 있기 때문이다.
Hibernate는 패치 조인과 페이징을 함께 사용할 때 경고 로그를 남기고 메모리에서 페이징 처리하므로 매우 위험하다. 데이터베이스에서 직접 페이징 처리를 하지 않고 모든 데이터를 메모리로 로드하면 메모리 사용량이 급격히 증가하고 성능이 저하될 수 있다.
대안 방법:
DTO로 조회: 페이징 문제를 해결하기 위해 DTO를 사용하여 필요한 데이터만 조회한다.
일대일, 다대일 관계의 페이징: 단일 값 연관 필드는 패치 조인 시에도 페이징이 가능하므로, 방향을 뒤집어서 해결할 수 있다.
배치 사이즈 설정: 배치 사이즈를 사용하면 연관된 엔티티를 한 번의 쿼리로 그룹화하여 로드할 수 있다. 페이징 API와 패치 조인을 어쩔 수 없이 함께 사용해야 하는 상황에서는 배치 사이즈를 설정이 유용하다. 이는 페이징 중 연관된 엔티티를 가져올 때 발생하는 N+1 문제를 해결할 수 있다.
@BatchSize 어노테이션: 컬렉션 필드에 @BatchSize(size = 배치사이즈)를 사용하여 배치 사이즈를 조정할 수 있다.
글로벌 배치 사이즈 설정: 배치 사이즈를 글로벌 설정으로 조정할 수 있다.
정합성(Consistency): 데이터베이스 및 정보 시스템에서 데이터의 정확성과 일관성을 유지하는 개념이다. 도메인 무결성, 참조 무결성, 엔티티 무결성 등이 있다.
//JPQL 쿼리
selectcount(m) fromMember m //엔티티를 직접 사용하여 조회
selectcount(m.id) fromMember m //엔티티의 id를 사용하여 조회
//실행 시 발생하는 SQL 쿼리 (JPQL 둘 다 같은 SQL을 실행)
SELECTCOUNT(M.ID) AS CNT FROMMEMBER M
설명: 첫 번째 JPQL 쿼리의 count(m)은 엔티티의 별칭을 직접 사용하며, JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다. 결과적으로 실행된 SQL 쿼리는 동일하다.
🔽 엔티티를 파라미터로 전달
Stringquery="select m from Member m where m = :member";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("member", memberA) //엔티티를 파라미터로 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.ID=?
설명:
'm = :member'로 엔티티를 직접 파라미터로 전달하면, SQL에서는 'M.ID=?'로 변환된다.
즉, 엔티티의 기본 키 값을 사용하여 조회를 수행한다.
🔽 엔티티의 식별자를 파라미터로 전달
Stringquery="select m from Member m where m.id = :memberId";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("memberId", memberA.getId()) //식별자를 직접 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.ID=?
설명:
'm.id = :memberId'로 식별자를 직접 전달하면, SQL에서는 'M.ID=?'로 변환된다.
즉, 엔티티의 기본 키 값을 사용하여 조회를 수행한다.
결론: 엔티티를 파라미터로 전달하는 경우와 엔티티의 식별자를 파라미터로 전달하는 경우 모두 같은 SQL 쿼리를 실행한다.
Stringquery="select m from Member m where m.team = :team";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("team", teamA) //Team 엔티티를 파라미터로 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.TEAM_ID=?
설명:
m.team은 TEAM_ID라는 외래 키와 매핑되어 있기 때문에, 'm.team = :team'으로 엔티티를 직접 파라미터로 전달하면, SQL에서는 'WHERE M.TEAM_ID=?'로 변환된다.
즉, 연관된 엔티티를 직접 파라미터로 전달하면, JPQL은 내부적으로 해당 엔티티의 외래 키 값을 추출하여 SQL 쿼리에서 조건으로 사용한다.
🔽 연관된 엔티티의 식별자를 파라미터로 전달
Stringquery="select m from Member m where m.team.id = :teamId";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("teamId", teamA.getId()) //Team 엔티티의 식별자를 직접 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.TEAM_ID=?
설명:
m.team.id는 TEAM_ID라는 외래 키 값이며, 이를 기준으로 Member 엔티티를 필터링하기 때문에, 'm.team.id = :teamId'로 식별자를 직접 파라미터로 전달하면, sQL에서는 'WHERE M.TEAM_ID=?'로 변환된다.
즉, 식별자 값을 직접 전달하면, JPQL은 해당 식별자를 기반으로 SQL 쿼리의 조건으로 사용한다.
Member와 Team 간에 묵시적 조인이 일어날 것 같으나 MEMBER 테이블이 TEAM_ID를 외래 키로 가지고 있어 묵시적 조인이 일어나지 않는다.
m.team.name 등은 호출하면 묵시적 조인이 일어난다.
결론: 연관된 엔티티를 파라미터로 전달하는 경우와 연관된 엔티티의 식별자를 파라미터로 전달하는 경우 모두 같은 SQL 쿼리를 실행한다.
애플리케이션 로딩 시점에 JPQL 문법을 검증한다. 문법에 오류가 있으면 애플리케이션 실행 시 에러가 발생한다.
🔽 예시 코드
//Named 쿼리를 어노테이션에 정의@Entity@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)publicclassMember {
@Id@GeneratedValueprivate Long id;
private String username;
...
}
//사용 코드
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "memberA")
.getResultList();
🔽Named 쿼리 환경에 따른 설정 (XML)
XML이 항상 우선권을 가진다.
애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
🔽 Spring Data JPA에서의 Named 쿼리
Spring Data JPA에서는 Named 쿼리를 인터페이스 메서드에 직접 선언할 수 있다.
publicinterfaceMemberRepositoryextendsJpaRepository<Member, Long> {
@Query("select m from Member m where m.emailAddress = ?1")
Member findByEmailAddress(String emailAddress);
}
어노테이션을 통한 쿼리 정의보다 Spring Data JPA를 사용하는 방식이 실무에서 더 깔끔하게 관리된다고 한다.
JPA에서는 엔티티를 수정할 때 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제할 때는 EntityManager의 remove() 메서드를 사용하는데, 이러한 방법으로 대량의 데이터를 처리하는 것은 효율적이지 않다.
이러한 문제를 해결하기 위해, 벌크 연산을 사용하여 한 번의 쿼리로 대량의 데이터를 수정하거나 삭제할 수 있다.
🔽 예시 코드
intresultCount= em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount); //영향 받은 엔티티 수를 반환
🔽 executeUpdate()
executeUpdate()의 결과는 영향 받은 엔티티 수를 반환한다.
이 메서드는 UPDATE, DELETE 쿼리를 지원한다.
INSERT(예: INSERT INTO ... SELECT) 쿼리는 Hibernate에서 지원한다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 실행하므로, 데이터 정합성 문제를 발생시킬 수 있다.
🔽 예시
Stringquery="select p from Product p where p.name = :name";
// 1. 상품 A 조회 (상품 A의 가격은 1000원이라고 가정)ProductproductA= em.createQuery(query, Product.class)
.setParameter("name", "productA")
.getSingleResult();
// 1000 출력
System.out.println("상품 A 수정 전 가격 = " + productA.getPrice());
// 2. 벌크 연산을 통해 모든 상품의 가격을 10% 상승
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
// 3. 1000 출력, 문제 발생!!!
System.out.println("상품 A 수정 후 가격 = " + productA.getPrice());
설명:
가격이 1000인 상품 A를 조회하여, 상품 A는 영속성 컨텍스트에서 관리된다.
벌크 연산으로 모든 상품의 가격을 10% 증가시키면, 상품 A의 가격도 1100으로 변경될 것으로 예상된다.
그러나, 출력 결과는 기대했던 1100이 아닌 1000으로 출력된다. 이는 벌크 연산이 영속성 컨텍스트의 상태를 업데이트하지 않기 때문에 발생하는 문제이다. 영속성 컨텍스트는 데이터베이스의 변경 사항을 반영하지 않아, 조회된 상품 A의 가격이 갱신되지 않는다.
벌크 연산을 수행한 직후에 em.refresh() 메서드를 사용하여 영속성 컨텍스트의 엔티티를 데이터베이스의 최신 상태로 갱신한다.
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
em.refresh(productA); //영속성 컨텍스트 새로 고침
System.out.println("상품 A 수정 후 가격 = " + productA.getPrice());
벌크 연산 후 영속성 컨텍스트 초기화 (clear) 후 다시 조회
벌크 연산을 수행한 직후에 em.clear() 메서드를 사용하여 영속성 컨텍스트를 초기화하고, 이후에 데이터를 새로 조회한다.
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
em.clear(); //영속성 컨텍스트 초기화ProductupdatedProductA= em.find(Product.class, productA.getId()); //데이터 새로 조회
System.out.println("상품 A 수정 후 가격 = " + updatedProductA.getPrice());
위 방법들 중에서 가능하다면 벌크 연산을 가장 먼저 수행하는 방법을 사용하는 것이 권장된다.
연관 필드 예시: @ManyToOne, @OneToOne, 대상이 엔티티 객체 (예: m.team)
🔽 예시 코드
//JPQL 쿼리
select o.member fromOrder o
//실행 시 발생하는 SQL 쿼리 (묵시적 조인 발생)
SELECT M.*FROM ORDERS O INNERJOINMEMBER M ON O.MEMBER_ID = M.ID
//추가 탐색 가능
select o.member.name fromOrder o
설명: 단일 값 연관 필드는 o.member와 같은 필드를 사용하여 연관된 객체의 속성에 접근할 수 있으며, o.member.name과 같은 추가 탐색이 가능하다.
직접적으로는 추가 탐색이 불가능하지만, FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.
다른 엔티티 객체들의 컬렉션(list, set 등)을 참조하며, 경로 탐색을 통해 컬렉션 내의 각 객체에 접근할 수 있다.
묵시적으로 내부 조인이 발생한다.
연관 필드 예시: @OneToMany, @ManyToMany, 대상이 컬렉션 (예: m.orders)
//JPQL 쿼리
select t.members from Team t
//실행 시 발생하는 SQL 쿼리 (묵시적 조인 발생)
SELECT M.*FROM TEAM T INNERJOINMEMBER M ON M.TEAM_ID = T.ID
//명시적 조인 시 별칭을 통해 추가 탐색 가능
select m.username from Team t join t.members m
설명: 컬렉션 값 연관 필드는 t.members와 같이 컬렉션을 참조하며, 추가 탐색을 위해 명시적 조인을 통해 별칭을 사용하여 접근할 수 있다.
컬렉션 값 연관 필드 명시적 조인 동작 과정 1. 명시적 조인 시 'join t members m' 구문에서 Team 객체의 members 컬렉션과 Member 객체 간의 조인이 내부적으로 수행된다. 2. 이후 members 컬렉션 내의 각 Member 객체가 Team 객체와 연결되어, 관련된 Member의 데이터에 접근할 수 있다.
//가능, 문제는 내부 조인이 2번 발생
select o.member.team fromOrder o
//가능, 컬렉션이지만 경로 탐색의 끝이기 때문에 가능
select t.members from Team t
//불가능, 추가 탐색 불가(컬렉션 값 연관 필드)
select t.members.username from Team t
//가능, 컬렉션이지만 명시적 조인을 통해 탐색 가능
select m.username from Team t join t.members m
Stringjpql="select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
for (Member member : members) {
System.out.println("username = " + member.getUsername() + ", " +
"teamname = " + member.getTeam().name());
}
설명:
패치 조인을 사용하면 회원과 팀을 지연 로딩(LAZY)으로 설정하더라도, 패치 조인에 의해 데이터가 즉시 로드되므로 지연 로딩이 발생하지 않는다.
루프 구문에서 getTeam() 메서드는 더 이상 프록시 객체가 아니다. 패치 조인으로 인해 실제 Team 엔티티가 이미 로드되어 있으므로, getTeam() 호출 시 실제 데이터에 직접 접근할 수 있다.
컬렉션 패치 조인은 엔티티를 조회할 때 연관된 컬렉션도 함께 조회하는 경우에 사용되며, 특히 일대다 관계의 컬렉션을 패치 조인할 때 사용된다.
🔽 예시 코드
Team 엔티티를 조회하면서, 연관된 Member 컬렉션(t.members)도 함께 조회하는 경우
//JPQL 쿼리
select t from Team t fetch t.members where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*, M.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
🔽 예시 상황 및 사용 코드
팀을 조회하면서, 각 팀의 회원들도 함께 조회하고 싶은 상황
1️⃣ 문제 상황: DISTINCT를 사용하지 않는 패치 조인 코드
Stringquery="select t from Team t join fetch t.members where t.name = '팀A'";
List<Team> teams = em.createQuery(query, Team.class).getResultList();
for (Team team : teams) {
System.out.println("teamname = " + team.getName() + ", " + "team = " + team);
for (Member member : team.getMembers()) {
System.out.println("->username = " + member.getUsername() + ", " + "member = " + member);
}
}
//출력
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
teamname = 팀A, team = hellojpa.jpql.Team@1e60b459
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
설명:
컬렉션 패치 조인을 사용하면, 일대다 관계인 Team과 Member를 조인할 때, Team 객체는 하나임에도 불구하고 Member와 조인하면서 결과가 중복되어 출력된다. 이는 Team과 관련된 Member가 각각 별도로 조인되면서 발생하는 현상이다. 즉, 하나의 Team 객체가 여러 개의 Member 객체와 조인되어, Team 객체가 중복되어 리스트에 나타나게 된다. 따라서 같은 Team이 여러 번 출력되며, 그에 따른 Member도 중복되어 나타난다.
또한, 패치 조인을 사용하면 Team과 Member를 지연 로딩(LAZY)으로 설정하더라도, 패치 조인으로 인해 관련 데이터가 즉시 로드되기 때문에 지연 로딩이 일어나지 않는다.
2️⃣ 해결 방법: DISTINCT를 사용하는 패치 조인 코드
해결 방법은 JPQL 쿼리에서 DISTINCT 키워드를 사용하여 중복 엔티티를 제거하는 것이다. 이를 통해 중복된 결과가 출력되지 않도록 할 수 있다.
//JPQL 쿼리
select t from Team t join t.members m where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
//출력
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
->username = 회원1, member = Member{id=1, username='회원1', age=10}
->username = 회원2, member = Member{id=2, username='회원2', age=10}
설명:
Team과 Member 엔티티를 조인하여 Team을 조회하지만, 연관된 Member 컬렉션(t.members)은 함께 조회되지 않는다.
JPQL 쿼리는 결과를 반환할 때 연관된 엔티티를 자동으로 로드하지 않으며, SELECT 절에 명시된 엔티티만 조회한다.
지연 로딩(LAZY)으로 설정 시, 프록시 객체나 초기화되지 않은 컬렉션 래퍼를 반환한다.
즉시 로딩(EAGER)으로 설정 시, Member 컬렉션을 즉시 로딩하기 위해 추가 쿼리가 실행될 수 있다.
🔽 패치 조인을 사용하는 경우
//JPQL 쿼리
select t from Team t joinfetch t.members where t.name ='팀A'//실행 시 발생하는 SQL 쿼리
SELECT T.*, M.*FROM TEAM T INNERJOINMEMBER M ON T.ID = M.TEAM_ID WHERE T.NAME ='팀A'
별칭을 사용하면 쿼리가 복잡해지고, 패치 조인의 기본 목적인 즉시 로딩의 효율성이 떨어질 수 있다.
객체 그래프는 전체 데이터의 연관 관계를 한 번의 쿼리로 완전히 로드하는 것을 목표로 한다. 즉, 객체 그래프를 완전히 조회하는 것이 아닌 일부 데이터만 선택적으로 로드하면 연관된 데이터의 일관성이 깨질 수 있다.
Hibernate에서는 별칭 사용이 가능하지만, 이는 권장되지 않는다.
둘 이상의 컬렉션은 패치 조인할 수 없다.
데이터가 곱하기가 되어 예기치 못한 정합성 문제를 일으킬 수 있다.
컬렉션을 패치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
조인된 결과에 중복된 엔티티가 발생할 수 있는데, 이를 처리하지 않고 페이징을 하게 되면 문제가 발생할 수 있다.
DISTINCT를 사용하여 중복 제거를 시도할 수 있지만, 이도 적절한 방법은 아니다. DISTINCT를 적용하게 되면 데이터의 정렬과 중복 제거가 복합적으로 작용하여 페이지가 일관되지 않을 수 있기 때문이다.
Hibernate는 패치 조인과 페이징을 함께 사용할 때 경고 로그를 남기고 메모리에서 페이징 처리하므로 매우 위험하다. 데이터베이스에서 직접 페이징 처리를 하지 않고 모든 데이터를 메모리로 로드하면 메모리 사용량이 급격히 증가하고 성능이 저하될 수 있다.
대안 방법:
DTO로 조회: 페이징 문제를 해결하기 위해 DTO를 사용하여 필요한 데이터만 조회한다.
일대일, 다대일 관계의 페이징: 단일 값 연관 필드는 패치 조인 시에도 페이징이 가능하므로, 방향을 뒤집어서 해결할 수 있다.
배치 사이즈 설정: 배치 사이즈를 사용하면 연관된 엔티티를 한 번의 쿼리로 그룹화하여 로드할 수 있다. 페이징 API와 패치 조인을 어쩔 수 없이 함께 사용해야 하는 상황에서는 배치 사이즈를 설정이 유용하다. 이는 페이징 중 연관된 엔티티를 가져올 때 발생하는 N+1 문제를 해결할 수 있다.
@BatchSize 어노테이션: 컬렉션 필드에 @BatchSize(size = 배치사이즈)를 사용하여 배치 사이즈를 조정할 수 있다.
글로벌 배치 사이즈 설정: 배치 사이즈를 글로벌 설정으로 조정할 수 있다.
정합성(Consistency): 데이터베이스 및 정보 시스템에서 데이터의 정확성과 일관성을 유지하는 개념이다. 도메인 무결성, 참조 무결성, 엔티티 무결성 등이 있다.
//JPQL 쿼리
selectcount(m) fromMember m //엔티티를 직접 사용하여 조회
selectcount(m.id) fromMember m //엔티티의 id를 사용하여 조회
//실행 시 발생하는 SQL 쿼리 (JPQL 둘 다 같은 SQL을 실행)
SELECTCOUNT(M.ID) AS CNT FROMMEMBER M
설명: 첫 번째 JPQL 쿼리의 count(m)은 엔티티의 별칭을 직접 사용하며, JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다. 결과적으로 실행된 SQL 쿼리는 동일하다.
🔽 엔티티를 파라미터로 전달
Stringquery="select m from Member m where m = :member";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("member", memberA) //엔티티를 파라미터로 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.ID=?
설명:
'm = :member'로 엔티티를 직접 파라미터로 전달하면, SQL에서는 'M.ID=?'로 변환된다.
즉, 엔티티의 기본 키 값을 사용하여 조회를 수행한다.
🔽 엔티티의 식별자를 파라미터로 전달
Stringquery="select m from Member m where m.id = :memberId";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("memberId", memberA.getId()) //식별자를 직접 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.ID=?
설명:
'm.id = :memberId'로 식별자를 직접 전달하면, SQL에서는 'M.ID=?'로 변환된다.
즉, 엔티티의 기본 키 값을 사용하여 조회를 수행한다.
결론: 엔티티를 파라미터로 전달하는 경우와 엔티티의 식별자를 파라미터로 전달하는 경우 모두 같은 SQL 쿼리를 실행한다.
Stringquery="select m from Member m where m.team = :team";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("team", teamA) //Team 엔티티를 파라미터로 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.TEAM_ID=?
설명:
m.team은 TEAM_ID라는 외래 키와 매핑되어 있기 때문에, 'm.team = :team'으로 엔티티를 직접 파라미터로 전달하면, SQL에서는 'WHERE M.TEAM_ID=?'로 변환된다.
즉, 연관된 엔티티를 직접 파라미터로 전달하면, JPQL은 내부적으로 해당 엔티티의 외래 키 값을 추출하여 SQL 쿼리에서 조건으로 사용한다.
🔽 연관된 엔티티의 식별자를 파라미터로 전달
Stringquery="select m from Member m where m.team.id = :teamId";
List<Member> resultList = em.createQuery(query, Member.class)
.setParameter("teamId", teamA.getId()) //Team 엔티티의 식별자를 직접 전달
.getResultList();
//실행 시 발생하는 SQL 쿼리
SELECT M.*FROMMEMBER M WHERE M.TEAM_ID=?
설명:
m.team.id는 TEAM_ID라는 외래 키 값이며, 이를 기준으로 Member 엔티티를 필터링하기 때문에, 'm.team.id = :teamId'로 식별자를 직접 파라미터로 전달하면, sQL에서는 'WHERE M.TEAM_ID=?'로 변환된다.
즉, 식별자 값을 직접 전달하면, JPQL은 해당 식별자를 기반으로 SQL 쿼리의 조건으로 사용한다.
Member와 Team 간에 묵시적 조인이 일어날 것 같으나 MEMBER 테이블이 TEAM_ID를 외래 키로 가지고 있어 묵시적 조인이 일어나지 않는다.
m.team.name 등은 호출하면 묵시적 조인이 일어난다.
결론: 연관된 엔티티를 파라미터로 전달하는 경우와 연관된 엔티티의 식별자를 파라미터로 전달하는 경우 모두 같은 SQL 쿼리를 실행한다.
애플리케이션 로딩 시점에 JPQL 문법을 검증한다. 문법에 오류가 있으면 애플리케이션 실행 시 에러가 발생한다.
🔽 예시 코드
//Named 쿼리를 어노테이션에 정의@Entity@NamedQuery(
name = "Member.findByUsername",
query = "select m from Member m where m.username = :username"
)publicclassMember {
@Id@GeneratedValueprivate Long id;
private String username;
...
}
//사용 코드
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "memberA")
.getResultList();
🔽Named 쿼리 환경에 따른 설정 (XML)
XML이 항상 우선권을 가진다.
애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
🔽 Spring Data JPA에서의 Named 쿼리
Spring Data JPA에서는 Named 쿼리를 인터페이스 메서드에 직접 선언할 수 있다.
publicinterfaceMemberRepositoryextendsJpaRepository<Member, Long> {
@Query("select m from Member m where m.emailAddress = ?1")
Member findByEmailAddress(String emailAddress);
}
어노테이션을 통한 쿼리 정의보다 Spring Data JPA를 사용하는 방식이 실무에서 더 깔끔하게 관리된다고 한다.
JPA에서는 엔티티를 수정할 때 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고, 삭제할 때는 EntityManager의 remove() 메서드를 사용하는데, 이러한 방법으로 대량의 데이터를 처리하는 것은 효율적이지 않다.
이러한 문제를 해결하기 위해, 벌크 연산을 사용하여 한 번의 쿼리로 대량의 데이터를 수정하거나 삭제할 수 있다.
🔽 예시 코드
intresultCount= em.createQuery("update Member m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount); //영향 받은 엔티티 수를 반환
🔽 executeUpdate()
executeUpdate()의 결과는 영향 받은 엔티티 수를 반환한다.
이 메서드는 UPDATE, DELETE 쿼리를 지원한다.
INSERT(예: INSERT INTO ... SELECT) 쿼리는 Hibernate에서 지원한다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리를 실행하므로, 데이터 정합성 문제를 발생시킬 수 있다.
🔽 예시
Stringquery="select p from Product p where p.name = :name";
// 1. 상품 A 조회 (상품 A의 가격은 1000원이라고 가정)ProductproductA= em.createQuery(query, Product.class)
.setParameter("name", "productA")
.getSingleResult();
// 1000 출력
System.out.println("상품 A 수정 전 가격 = " + productA.getPrice());
// 2. 벌크 연산을 통해 모든 상품의 가격을 10% 상승
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
// 3. 1000 출력, 문제 발생!!!
System.out.println("상품 A 수정 후 가격 = " + productA.getPrice());
설명:
가격이 1000인 상품 A를 조회하여, 상품 A는 영속성 컨텍스트에서 관리된다.
벌크 연산으로 모든 상품의 가격을 10% 증가시키면, 상품 A의 가격도 1100으로 변경될 것으로 예상된다.
그러나, 출력 결과는 기대했던 1100이 아닌 1000으로 출력된다. 이는 벌크 연산이 영속성 컨텍스트의 상태를 업데이트하지 않기 때문에 발생하는 문제이다. 영속성 컨텍스트는 데이터베이스의 변경 사항을 반영하지 않아, 조회된 상품 A의 가격이 갱신되지 않는다.
벌크 연산을 수행한 직후에 em.refresh() 메서드를 사용하여 영속성 컨텍스트의 엔티티를 데이터베이스의 최신 상태로 갱신한다.
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
em.refresh(productA); //영속성 컨텍스트 새로 고침
System.out.println("상품 A 수정 후 가격 = " + productA.getPrice());
벌크 연산 후 영속성 컨텍스트 초기화 (clear) 후 다시 조회
벌크 연산을 수행한 직후에 em.clear() 메서드를 사용하여 영속성 컨텍스트를 초기화하고, 이후에 데이터를 새로 조회한다.
em.createQuery("update Product p set p.price = p.price * 1.1")
.executeUpdate();
em.clear(); //영속성 컨텍스트 초기화ProductupdatedProductA= em.find(Product.class, productA.getId()); //데이터 새로 조회
System.out.println("상품 A 수정 후 가격 = " + updatedProductA.getPrice());
위 방법들 중에서 가능하다면 벌크 연산을 가장 먼저 수행하는 방법을 사용하는 것이 권장된다.