읽는데 약 2분
인덱스를 활용한 조회 성능 개선
안녕하세요? 오늘은 인덱스를 활용해 성능을 개선한 경험을 공유해드리고자 합니다.
‘디클’ 프로젝트에서는 사용자 로그인을 구현하기 위해 토큰 기반 인증인 JWT를 사용하고 있습니다. 토큰 기반 인증 방식은 세션과 다르게 stateless합니다. 따라서 서버는 한 번 발급한 토큰에 대해서는 제어권을 갖고 있지 않습니다.
Access Token만을 사용해서 사용자 로그인을 구현하면 어떻게 될까요? 만료 기간을 길게 설정하면 탈취에 대한 문제가 발생하고 짧게 설정하면 보안은 강화되지만 사용자 로그인이 풀리는 문제가 발생합니다. 이는 UX 관점에서 좋지 않습니다.
따라서 AccessToken + RefreshToken
을 함께 사용했습니다. Access Token의 유효 기간은 30분 이내, Refresh Token은 2주로 설정했습니다. 하지만 Refresh Token 자체가 탈취당할 위험성은 여전히 존재합니다.
RTR (Refresh Token Rotation)
최종적으로 채택한 방법은 Refresh Token을 한 번만 사용할 수 있게 했습니다. Refresh Token을 사용해 새로운 Access Token을 받급받을 때 Refresh Token도 새롭게 발급받는 방법입니다.
재발급할 때 사용한 Refresh Token 혹은 로그아웃한 사용자가 갖고 있는 Refresh Token은 탈취되더라도 사용하지 못하게 막아야합니다. 따라서 서버에서 Blacklist
객체로 저장하여 관리합니다.
findBy 에서 인덱스를 타지 못하면
위에서 설명한 과정을 코드로 나타낸 것입니다. Blacklist
에는 만료되지 않았지만 재발급에 사용되거나 로그아웃한 토큰들이 저장되어있습니다.
문제는 1번 쿼리입니다. 사용자가 많아진다면 재발급과 로그아웃이 빈번히 발생할 것입니다. Blacklist
에는 많은 엔티티가 쌓이게 되고 인덱스를 타지 않는 단순한 findBy
는 성능의 문제가 발생할 수 있습니다.
더미 데이터를 300만개 넣고 토큰을 재발급하는 로직을 호출한 모습입니다. 사용자가 로그인을 하는 경우에도 Access Token이 만료되면 재발급합니다. 만약 로그인에 수 초가 소요된다면 불편함을 느낄 수 밖에 없습니다.
EXPLAIN ANALYZE
로 실행 계획을 살펴보면 인덱스를 아에 사용하지 못하는 것을 알 수 있습니다. 항상 인덱스를 사용해야지 효율적인 것은 아닙니다. 만약 테이블에 레코드가 100만 건이 저장돼 있는데, 그중에서 20~25%를 읽어오는 쿼리가 필요하다면 테이블을 전부 읽어서 필요한 레코드만 가려내는 방식으로 처리하는 것이 효율적일 수도 있습니다.
CREATE INDEX idx_blacklist_invalid_refresh_token
ON blacklist (invalid_refresh_token);
하지만 이 경우는 하나의 행이 존재하는지 판단하면 되므로 인덱스를 타는 것이 절대적으로 유리합니다. 인덱스를 생성한 후 걸린 시간을 보면 8710ms → 137ms
로 63.5배 가량 빨라진 것을 확인할 수 있습니다. 비교 연산이나 동등 연산이 포함된 쿼리를 작성할 경우 병목 지점을 예상하여 인덱스를 사용하는 것이 좋을 것 같습니다.