문제 상황
Signoz 상에서 Slow Query를 확인해 보니 아래의 두 쿼리가 상위권에 선정되었습니다.
- Token이 블랙리스트인지 확인 (7.10ms 소요)
- spring 인증객체로 두기위한 member select (8.18ms 소요)


크게 느린 속도는 아니지만 Slow Query로 선정됐으므로 개선을 진행해보고자 합니다.
해결 방법
위의 두가지 Slow Query는 인증이 필요한 모든 요청에서 사용되는데요. 아래와 같이 점진적으로 성능을 개선하고자 합니다.
- 인증객체를 Member 엔티티 → 인증 DTO 로 변경
- MySQL → Redis로 토큰 저장소 변경
인증객체를 Member에서 DTO로 변경
현재 인증 및 인가를 위해 Spring Security + JWT를 혼합하여 사용하고 있습니다.
서비스 코드에서 인증객체(PrincipalDetails)를 손쉽게 활용하기 위해 JwtAuthenticationFilter 에서 토큰 검증을 마치고 setAuthentcation()을 통해 Security Context Holder에 인증객체를 담아두게 됩니다.
그리고 인증객체는 아래와 같이 Member 엔티티 자체로 구현해 두었습니다.
때문에, 인증객체를 할당하기 위해선 member 엔티티를 직접 select 하는 과정이 필요하며, 이 과정에서 slow query로 선정된 쿼리가 실행되게 됩니다.
@RequiredArgsConstructor
public class PrincipalDetails implements UserDetails {
private final Member member;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(member.getRole().toString()));
return authorities;
}
...
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PrincipalDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
Member validMember = memberRepository.findById(Long.valueOf(id))
.orElseThrow(() -> new CustomException(CustomResponseStatus.MEMBER_NOT_EXIST));
return new PrincipalDetails(validMember);
}
}
이게 문제인 이유는 필요 없는 컬럼까지 모두 조회한다는 것입니다.
실제로 인증객체인 Member에서 사용하는 데이터는 id(pk)와 role 뿐인데 생성된 쿼리를 보면 member의 모든 컬럼(23개 컬럼..)을 select 하고 있습니다.
select m1_0.id,m1_0.additional_experiences,m1_0.additional_progress,m1_0.certification_count,m1_0.company_name,m1_0.created_at,m1_0.email,m1_0.img_url,m1_0.major_type,m1_0.modified_at,m1_0.nickname,m1_0.points,m1_0.position,m1_0.preferred_job,m1_0.progress,m1_0.project_count,m1_0.provider,m1_0.provider_id,m1_0.ps_tier,m1_0.role,m1_0.withdraw_reason,m1_0.withdrawn_at,m1_0.work_period
from member m1_0
where m1_0.id=? and (m1_0.withdrawn_at is NULL)
실제 필요한 컬럼만 select 해오는 것이 쿼리 최적화 방법 중 하나입니다.
이를 위해, MemberAuthInfo라는 DTO를 생성하고 적용해 보겠습니다.
변경 후 쿼리는 아래와 같이 필요한 2개의 컬럼만 select 하고 있습니다.
Hibernate:
select
m1_0.id,
m1_0.role
from
member m1_0
where
m1_0.id=?
과연 실제 API 콜 자체에서 큰 속도 개선이 있을까요?
- [BEFORE] 회원 정보 조회 api 테스트 3회
- 1회 → 404ms (첫 실행)
- 2회 → 66ms
- 3회 → 34ms
- [AFTER] 회원 정보 조회 api 테스트 3회
- 1회 → 355ms (첫 실행)
- 2회 → 53 ms
- 3회 → 38ms
네.. 속도 개선은 이루어지지 않았습니다. 대체 왜 그럴까요?
왜 큰 차이가 나지 않을까
23개 → 2개의 컬럼만을 조회하도록 로직을 변경하며 성능 개선을 기대했지만 이루어지지 않았습니다.
이유가 궁금했고 아래와 같은 이유로 성능 개선이 이루어지지 않았다는 것을 알 수 있었습니다.
DTO 생성을 위해 Member 조회 시 PK를 통한 단건 조회를 하고 있는데요. 쿼리상으로 WHERE id=? 가 될 것입니다.
이때, 내부 동작흐름은 아래와 같습니다.
- PK 인덱스에서 키 탐색 (해당 pk 번호의 페이지 접근)
- 한 행에 대한 모든 데이터(컬럼)가 담겨있는 Leaf Page 도달
- 해당 Page 에서 데이터 조회
이런 특징 때문에 PK 단건 조회 상황에서 SELECT id, role와 SELECT * 의 I/O 비용자체는 똑같아지게 됩니다.
즉, 조회하는 컬럼은 줄였지만 디스크/버퍼 I/O 를 줄이진 못했기에 성능 개선이 이루어지지 않은 것입니다.
회고
과거에 JOIN 쿼리에서 엔티티 조회 → 특정 컬럼만 조회하도록 쿼리 리팩토링을 했었고, 성능 개선을 했던 기억이 있었습니다.
이때에 기억을 토대로 이번 성능개선도 기대했었는데요.
PK 단건 조회 시에는 조회 컬럼수의 감소로 인한 성능개선을 기대할 수 없다는 사실을 배우게 됐습니다.
BEFORE, AFTER를 비교 테스트해 보고 성능개선이 되지 않아 처음에는 멘탈이 나갔었던 게 기억이 나는데요 ㅋㅋㅋ
그래도 새로운 지식을 습득했기에 삽질은 아니었다고 생각합니다.
다음 글로는 Token 저장소를 MySQL에서 Redis로 변경에 대해 다뤄보겠습니다!
'스프링부트' 카테고리의 다른 글
| Redis 통신 라이브러리의 비교 (feat. Lettuce VS Redisson) (5) | 2025.08.11 |
|---|---|
| 멀티 인스턴스에서 안정적인 SSE 구조 설계 (0) | 2025.07.13 |
| @Cacheable 은 무적인가? (2) | 2025.07.08 |
| 락을 활용해서 동시성 문제를 해결해보자 (3) | 2025.07.08 |
| double 타입의 위도, 경도 값 테스트시 부동소수점 오차 발생에 관하여 (1) | 2025.05.25 |