첫 공식 업무와 JPA 쿼리 튜닝

첫 공식 업무와 JPA 쿼리 튜닝

개발일기

 

회사에서 신입으로 맡은 프로젝트는 백오피스 유지보수, 개발이고 첫 공식 업무는 백오피스에 Dynamic ACL을 도입하는 것이었다.

 

이 기능에 대한 요구사항은 예를 들자면 회사에서 데이터를 관리하는 아르바이트생을 한 명 뽑는다고 한다면 이 알바생에게 관리해야 할 데이터에 관련된 허락된 기능만 접근할 수 있게끔 권한을 제어할 수 있는 시스템을 개발하면 되는 업무였다.

그리고 이 권한을 생성하고 제어하는 기능이 런타임에 가능해야 했다.

 

내가 포트폴리오를 만들 때 작성한 시큐리티 설정 파일의 인가처리 부분은

 

image

 

이런 식으로 모든 URI와 그 URI에 접근할 수 있는 권한이 모두 하드코딩돼있는 방식이었고, 여태 이게 당연한 건 줄 알았다.

실제로 회사에서 내가 맡은 백오피스도 저런 식으로 돼있었기도 했고…

근데 팀장님이 이 하드 코딩돼있는 정보들을 모두 DB로 옮기고, 런타임에 시스템 관리자가 권한을 동적으로 생성, 제어하고 그 권한에 여러 URI를 인가할 수 있게 해 보라는 말씀을 하셨다.

 

그날부터 스프링 시큐리티 문서를 줄곧 뜯어보기 시작했는데

보면 볼수록 “이건 진짜 천재가 만든 건가?” 라는 생각이 물씬 드는 프레임워크였다.

여태까지 나는 스프링 시큐리티를 겉핥기 식으로 사용하고 있었다는 생각밖에 들지 않았다.

 

아무튼 결론적으로 기능 구현 자체는 공식 문서를 뜯어보면서 해내긴 했다.

대략적인 구조는 이렇다.

 

  1. FilterSecurityInterceptor의 앞단에 커스텀 필터를 만들어 붙인다

  2. DB에 권한과 자원(URI)을 표시하는 테이블 두 개와 이 두개 테이블을 매핑해주는 매핑 테이블을 생성한다

  3. 매핑 테이블에서 정보들을 읽어와(이를 인가 정보라 하겠다) 만들어낸 커스텀 필터에 인가 정보를 입력한다

  4. 이 인가 정보는 런타임에 업데이트될 수 있으며 업데이트 시 커스텀 필터와 동기화한다.

  5. 커스텀 필터에서 모든 요청에 대해 인가 처리를 한다

 

여기서 FilterSecurityInterceptorFilterChainProxy의 종단에 위치한 인가 처리 필터인데 이 필터의 앞단에 내가 작성한 커스텀 필터를 추가하여 인가처리를 동적으로 해낼 수 있는 기반을 만들었다.

 

현 상태에서 권한과 자원 테이블에 새로운 데이터를 추가할 경우 커스텀 필터를 reload 하여 인가 정보를 실시간으로 업데이트해주는 방식이다.

 

이때, 우선 기능 구현에 목적을 두고 개발을 하다 보니 큰 문제가 하나 있었음을 알았다.

WAS가 초기화되거나 필터가 reload 될 경우 모든 URI와 권한을 매번 읽어와야만 하는 구조이고 이렇게 매번 읽어오는 것은 구조상 어쩔 수 없는 일이긴 했는데 문제는 코드레벨에 있었다.

 

대략 7개의 권한이 있고, 총 300여 개의 URI가 있었으며, 각 권한당 약 100~300개 사이의 URI에 접근이 가능한 상태에서 이를 2중 루프를 돌려 읽다 보니 약 1,600회의 루프가 발생하고 있었고 정말 큰 문제는 이 2중 루프 안에서 select 쿼리를 건 바이 건으로 날리고 있었다는 것이다.

한마디로 1,600회의 루프가 돈다면 1,600회의 select가 발생하고 있었다는 말과 일맥상통한다.

 

for(List<SecurityAuthorization> authorizations : result) {
    ...
    for(SecurityAuthorization authorization : authorizations) {
            ...
        backofficeAuthorityRepository.findByAdmins();
            ...
    }
}

 

그래서 WAS가 초기화되거나 필터가 reload 될 경우마다 약 1,600회의 select 쿼리가 발생하고 그 시점마다 약 2초 정도의 로딩 시간이 발생했다.

 

이를 해결하는 방법은 생각보다 간단했지만, 약 2~3시간여의 고민이 필요했다.

바로 네이티브 쿼리를 사용할 것인가 말 것인가였다.

이 인가 정보를 매핑하기 위해서 스프링 시큐리티의 AntPathRequestMatcher를 이용해야 하는데 이 객체의 구조가 생각보다 복잡성이 커서 한방 쿼리로 해결하자니 쿼리가 통계성 쿼리처럼 무지막지하게 복잡해졌다.

이를 Querydsl로 구현하자니 도저히 안되겠어서 네이티브 쿼리밖에 생각이 나질 않았다.

(mybatis는 쓰기 싫었다.)

그렇다고 네이티브 쿼리를 사용하지 않으려 하니 쿼리를 쪼개 여러 번 보내야 했다.

 

결과적으로 네이티브 쿼리를 사용하려고 마음을 먹었는데, 이 근거는 다음과 같다.


첫째, 이 시스템이 앞으로 더 손댈 일이 없을 거라는 판단을 했다.

사실상 실무에서 스프링 시큐리티로 만들 수 있는 인가(Authorization) 아키텍처로 과연 이 이상의 시스템이 더 필요할까?라는 생각이 들었고, 아니라는 생각이 들었다.

따라서, 네이티브 쿼리를 쓰더라도 유지보수 관련 리스크가 매우 적다는 판단이 섰다.

 

둘째, 성능차이가 너무 압도적이었다.

쿼리를 여러번 쪼개 날리는 것과 네이티브 쿼리 단 한방으로 모든 처리를 끝내버리는 것에서 압도적인 성능차이가 발생했다.

아무튼 Spring Data JPAProjections네이티브 쿼리를 사용했고 약 1,600회의 쿼리로 처리했을 일이 단 한방에 끝나는 압도적인 퍼포먼스를 보여줬다.

실 체감 로딩 시간은 2초에서 클릭 시 즉시 수준으로 변했음은 당연지사다.

이 외에 2중 루프에서 select를 날리는 부분을 모두 찾아내어 JPA의 fetch join을 활용해 모두 최적화했다.

 

아마 이날 줄인 쿼리발생 수가 클릭당 약 2,000회 정도가 아닐까 싶었다.

 

fetch join네이티브 쿼리는 JPA를 공부하면서도 실제로 사용해 볼 일이 많지 않았는데 이번에 제대로 적용해보면서 체화하듯이 학습한 게 너무 큰 도움이 됐던 것 같다.

덤으로 내가 담당하는 프로젝트의 성능이 눈에 띄게 향상됐다는 게 가장 큰 기쁨이었다.

 

그동안 시간이 더 오래 걸리더라도 원리와 구조에 대한 이해에 큰 비중을 두면서 공부했는데, 이러한 공부 방식들이 이번에 정말 큰 도움이 됐던 것 같다.

문제에 맞닥트렸을 때 알맞은 솔루션을 찾아낼 수 있는 단단한 기반이 되어줬던 것 같다.

 


© 2022. All rights reserved.