JPA 를 쓰면 사실 인덱스가 필요없다. 이건 또 무슨 헛소리일까, 7월의 무더위를 먹어버린 탓일까? 자 한번 내 얘기를 들어보시라.
이 주장은 커버링인덱스와 관련이 있다. 커버링인덱스는 어떠한 쿼리보다도 가장 빠르게 응답 할수있는 현상을 말한다. 커버링인덱스는 select 쿼리의 결과 필드가 쿼리에 쓰인 인덱스인 경우에 커버링인덱스가 발동한다. 아래의 쿼리를 보자.
select t.col1, t.col2 from tb_table as t where t.col1 = ? and t.col2 =?
이 쿼리의 대상 tb_table 에는 복합인덱스 col1 과 col2 가 있다. 이 쿼리의 반환 결과는 col1 과 col2 만 반환한다. 이 경우에 커버링인덱스가 발동한다. 커버링인덱스는 mysql explain 실행계획에서 Extra : 'Using index' 라고 표기 된다.
이번엔 다른 커리이다.
select * from tb_table as t where t.col1 = ? and t.col2 =?
이번 쿼리는 col1, col2 만 반환하는 것이 아닌 전체 필드를 반환하는 * 와일드카드를 쓴 쿼리이다, 이 쿼리는 커버링인덱스가 발동하지 못한다. 왜일까? 커버링인덱스가 실행계획에서 Using index 로 표기 되는 의미를 떠올려보면 답을 알 수 있다.
이는 알고 보면 매우 단순한데, 커버링인덱스는 검색(where 절)에 사용한 인덱스를 쿼리 결과로 사용해버리기 때문이다. 책의 목차를 펼쳐서 페이지를 찾으려 했지만, 본문에 이미 원하는 데이터가 있는 그런 느낌인 셈이다.
처음의 쿼리에서는 인덱스 필터링에 사용된 col1, col2 를 반환을 원했기 때문에, 하드디스크의 원본 레코드까지 찾아갈 필요없이 사용한 복합 인덱스 col1, col2 만을 가지고 바로 쿼리 결과를 응답 할수 있다. 반면 두번째 쿼리에서는 col1, col2 복합인덱스로 찾아낸 레코드 포인터 (pk) 를 통해 하드디스크에서 어떠한 레코드를 꺼내오는 과정이 필요하기 때문에 전자에 비해 추가 과정이 필요해진다.
그렇다면 아래 쿼리는 어떨까? order by col3 desc 정렬 구문이 붙었다. 검색에는 col1,col2 의 복합 인덱스를 사용했는데 뜬금없이 col3 이란 컬럼으로 정렬을 원하는 쿼리이다. 이미 감을 잡은 이도 있겠다, 정답은 커버링인덱스로 동작하지 않는다. 당연하게도 col3 을 정렬하려면 하드디스크에 엑세스해서 col3 을 알아내야 하기 때문에 동작할수 없다.
select t.col1, t.col2 from tb_table as t where t.col1 = ? and t.col2 =? order by col3 desc;
커버링 인덱스가 좋은 것은 잘 알겠다. 그럼 JPA 에서는 이를 어떻게 쓸수 있을까? 아쉽게도 JPA 는 기본적으로 쿼리 결과를 엔티티 객체로 로딩해야하기 때문에 * 와일드카드의 매카니즘을 사용한다. 즉 JPA에서는 커버링인덱스가 동작하지 않는다. 그래서 JPA 에서는 인덱스가 필요없다는 자극적인 제목을 썻던 까닭이다. 여기에는 JPA 의 기본 오퍼레이션은 PK 를 기반으로 필터링 한다는 것도 관련이 있다. 물론 인덱스는 꼭 써야 한다. PK 만으로 쿼리하는 단순한 서비스는 없을테니깐.
그렇다고 좌절하지 말자. JPA 에서 커버링인덱스를 사실 쓸수있다. 방법은 기본적인 엔티티 레포지토지(SimpleRepository)를 사용하지 말고, 프로젝션 Projection ( https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html ) 이라는 별개의 기능을 통해 쿼리를 실행한 결과를 담는 식으로 접근하면 커버링인덱스를 발동시킬 수 있다.
JPA에서 프로젝션(Projection)은 쿼리 결과를 원하는 형태(객체, 일부 필드 등)로 매핑해서 가져오는 기능이다. 쿼리 결과를 엔티티로 로딩하는 것이 아닌, 엔티티의 일부 속성만 로딩하고 싶을 때 사용할 수 있다. 이 기능을 사용하면 커버링인덱스를 유도할 수 있다.
프로젝션이란 이름은 RDBMS 의 Projection 과 동일한 개념이 차용된 기능이다. RDBMS 의 Projection 은 수학의 관계형 대수에서 Projection 에서 유래되었다. Projection 은 어떠한 집합에서 특정 속성의 부분집합을 추출하는 연산을 말한다. 표기는 π (projection) 연산 이라 표기한다. SQL 에서 테이블의 원하는 컬럼만 출력하는 것도 이 프로젝션의 개념으로 인해 가능한 기능이다.
JPA 프로젝션의 사용방법은 2가지 방법으로 사용할수 있다. 1) 인터페이스 기반, 2) 클래스 기반
1) 인터페이스 기반 프로젝션 (Interface-based Projection)
public interface UserNameOnly {
String getName();
}
List<UserNameOnly> findBy();
- 장점: 필요한 필드만 가져와서 퍼포먼스 절감 가능
- 주의: 인터페이스의 메서드명은 정확히 필드명과 매핑되어야 함
2) 클래스 기반 프로젝션 (DTO Projection, Constructor Expression)
public class UserDto {
private final String name;
private final int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
@Query("SELECT new com.example.UserDto(u.name, u.age) FROM User u")
List<UserDto> fetchUserDto();
- 장점: 타입 안정성 확보
- 주의: new 패키지명.클래스명(...) 반드시 전부 써야 함
자세한 것은 아래 포스트를 참고하자.
https://www.baeldung.com/spring-data-jpa-projections
JPA 프로젝션 기능을 썼을 떄의 이점은, 커버링인덱스 말고도 N+1 에 대한 문제를 완화 할수 있다는 점도 있다. 구조적으로 JPA 에서는 N+1 이 일어나는 것이 자연스럽기 때문에 언제 어디서나 발생할수 있다. 설사 N+1이 발생하더라도 그것은 큰 문제가 아니다. 다만 이 시점엔 사용하지 않는 필드인데도 N+1 이 트리거가 되어 불필요한 쿼리가 발생한다면 그것은 리소스 사용의 관점에서 낭비가 된다. 쿼리가 날라가면 당연히 RDBMS 에 부담(SQL 쿼리 파싱, 버퍼풀 캐싱 관리 등)을 주기 때문이다.
대부분 Spring 웹 개발자는 자신이 작성한 로직에서는 joinColumn 이 달린 속성을 사용하지 않아 N+1 가 발생하지 않을 것이라 생각하지만, Spring 의 내부 기능에 의해서 N+1은 트리거 된다. 대표적으로 엔티티 객체를 json 으로 직렬화하는 과정에서는 엔티티의 모든 속성을 순회하기 때문에 N+1 이 무조건 발생된다.
이런 상황이라면 JPA 프로젝션 기능을 사용해서 필요한 속성의 값만 얻어오는 것도 전략이 될 수 있다. 커버링인덱스의 목적과는 결이 다르지만 RDBMS 리소스를 절약한다는 관점에서는 성능에 도움이 되기 때문이다. JPA 프로젝션을 구글링하면 대부분 이러한 이유로 사용하는 모습을 많이들 볼 수 있었다.
프로젝션 기능은 번거롭기도 하다. 엔티티를 이미 정의했는데, 엔티티의 동일한 일부 속성을 담은 정의를 또 해야하기 때문이다. 하지만 서비스가 고도화되면 성능을 신경쓰지 않을 수 없다. 이런 의미에서 JPA의 프로젝션 기능은 단순한 유틸 기능이 아니라, 성능을 확보할 수 있는 수단으로 봐야 한다. 이는 JPA 하나만으로 모든 문제를 해결할 수는 없고, 결국은 DB 레벨의 이해와 최적화가 반드시 병행되어야 한다는 결론으로 도출이 된다. 그래서 DB 공부도 게을리 하면 안 된다.
끝.
'Tech > mysql | DB모델링 | JPA' 카테고리의 다른 글
TEXT 컬럼은 무조건 피해야할까? (0) | 2025.07.16 |
---|---|
스칼라 서브쿼리는 최악이다 (0) | 2025.07.11 |
JPA 에서는 int 를 써야하나요? integer 를 써야하나요? (1) | 2025.07.09 |
테이블 풀스캔이 인덱스 보다 성능이 더 좋다. (2) | 2025.07.04 |
레코드 식별키 가상키 vs 복합PK (0) | 2025.06.21 |