싱글톤 패턴을 이용한 캐시 관리

싱글톤 패턴을 이용한 캐시 관리

1. 들어가며

기업용 서비스에서는 사용자별 권한 관리가 매우 중요한 기능 중 하나입니다. 사용자가 로그인한 이후 어떤 메뉴를 조회할 수 있는지, 어떤 기능을 사용할 수 있는지, 어떤 데이터에 접근할 수 있는지는 모두 권한 정보에 의해 결정됩니다.
진행 중인 서비스 역시 사용자 권한에 따라 노출되는 메뉴가 달라지는 구조를 가지고 있었습니다. 관리자는 별도의 관리자 페이지를 통해 사용자 또는 역할(Role)별 메뉴 권한을 설정할 수 있습니다. 사용자가 로그인하면 해당 권한 정보를 기반으로 접근 가능한 메뉴를 구성하여 화면에 제공합니다.
이러한 권한 관리 기능은 서비스 운영에 있어 필수적이지만 구현 방식에 따라 시스템 성능과 유지보수성에 상당한 영향을 미칠 수 있습니다. 특히 사용자 수가 증가하고 로그인 요청이 많아질수록 권한 조회 방식에 대한 고민이 필요합니다.
본 글에서는 메뉴 권한 정보를 관리하는 과정에서 발생했던 문제점과 이를 해결하기 위해 싱글톤 패턴을 활용한 인메모리 캐시를 도입하게 된 배경, 구현 방법, 그리고 도입 이후 얻은 효과에 대해 공유하고자 합니다.

2. 기존 구조와 문제점

초기 시스템은 비교적 단순한 구조로 설계되어 있었습니다.
사용자가 로그인하면 권한 조회 서비스를 호출합니다. 서비스는 데이터베이스(DB)에서 해당 사용자의 메뉴 권한 정보를 조회합니다. 이후 조회된 데이터를 기반으로 메뉴 목록을 생성하여 사용자에게 반환하는 방식이었습니다.
구조를 간단하게 표현하면 다음과 같습니다.
1. 사용자 로그인
2. 권한 조회 서비스 호출
3. 데이터베이스 조회
4. 메뉴 권한 정보 생성
5. 사용자 화면 반환
개발 초기에는 사용자 수가 많지 않았기 때문에 이러한 방식으로도 충분히 운영이 가능했습니다. 데이터 규모 역시 크지 않았고 로그인 요청도 많지 않았기 때문에 데이터베이스 부하가 눈에 띄게 발생하지 않았습니다.
그러나 서비스 운영 기간이 길어지고 사용자 수가 증가하면서 몇 가지 문제점이 나타나기 시작했습니다.

반복되는 동일 데이터 조회

가장 먼저 발견한 문제는 동일한 권한 정보를 반복적으로 조회하고 있다는 점이었습니다.
예를 들어 일반 사용자 그룹에 속한 사용자가 1,000명 있다고 가정합니다. 이들은 모두 동일한 메뉴 권한 정보를 사용하게 됩니다. 하지만 로그인 시마다 데이터베이스를 조회하는 구조에서는 동일한 데이터를 1,000번 반복해서 읽어오게 됩니다.
권한 정보는 대부분의 경우 변경되지 않는데도 불구하고 매번 데이터베이스 접근이 발생하고 있었던 것입니다.

데이터베이스 부하 증가

권한 정보 조회는 로그인 과정에서 반드시 수행되는 작업입니다.
따라서 로그인 요청이 증가할수록 데이터베이스 조회 횟수 역시 비례하여 증가하게 됩니다. 특히 강의 시작 시간이나 시스템 점검 이후 사용자가 동시에 로그인하는 경우 권한 조회 SQL이 집중적으로 실행되었습니다. 이는 데이터베이스 부하 증가의 원인이 될 수 있었습니다.
물론 단일 조회 자체는 큰 비용이 아닙니다. 하지만 수백 명 또는 수천 명의 사용자가 동일한 시점에 로그인할 경우 불필요한 조회가 누적되어 시스템 전체 성능에 영향을 줄 수 있습니다.

응답 시간 증가

사용자가 로그인한 이후 첫 화면이 표시되기 위해서는 권한 정보 조회가 완료되어야 합니다.
데이터베이스 응답 속도가 느려지거나 일시적인 부하가 발생할 경우 로그인 응답 시간에도 직접적인 영향을 미치게 됩니다. 권한 조회는 서비스 로직상 필수 과정이기 때문에 이 구간의 성능 개선은 사용자 경험 향상에도 중요한 요소였습니다.

확장성 한계

현재는 문제가 크지 않더라도 서비스 규모가 커질수록 로그인 요청 역시 증가하게 됩니다.
모든 요청이 데이터베이스를 직접 조회하는 구조는 장기적으로 확장성 측면에서 한계를 가질 수 있습니다. 따라서 조회 성능을 개선하면서도 유지보수성을 확보할 수 있는 새로운 구조가 필요했습니다.

3. 데이터 특성 분석

성능 개선 방안을 검토하기 위해 먼저 메뉴 권한 데이터의 특성을 분석하였습니다. 분석 결과 메뉴 권한 데이터는 다음과 같은 특징을 가지고 있었습니다.
첫째, 데이터 변경 빈도가 매우 낮습니다. 권한 정보는 관리자 페이지를 통해서만 수정할 수 있습니다. 일반 사용자가 서비스를 이용하는 과정에서는 거의 변경되지 않습니다.
둘째, 조회 빈도가 매우 높습니다. 사용자가 로그인할 때마다 권한 정보를 조회해야 합니다. 일부 기능에서는 추가적인 권한 검증도 수행합니다.
셋째, 동일한 데이터를 여러 사용자가 공유합니다. 같은 역할(Role)에 속한 사용자들은 대부분 동일한 권한 정보를 사용합니다.
즉, 메뉴 권한 데이터는 "변경은 적고 조회는 많은(Read-Heavy) 데이터"라는 특징을 가지고 있었습니다. 이러한 데이터는 캐시 적용 효과가 가장 큰 대표적인 사례입니다. 따라서 권한 정보를 데이터베이스가 아닌 메모리에서 관리하는 방안을 검토하게 되었습니다.

4. 왜 싱글톤 패턴을 선택했는가

캐시를 적용하기로 결정한 이후 또 하나의 고민이 있었습니다. 바로 캐시 객체를 어떻게 관리할 것인가였습니다.
권한 정보는 로그인 서비스뿐만 아니라 메뉴 생성 서비스, 권한 검증 서비스 등 다양한 컴포넌트에서 사용됩니다. 만약 서비스마다 별도의 캐시 객체를 생성한다면 동일한 데이터가 여러 번 메모리에 적재될 수 있습니다. 또한 특정 서비스에서 캐시를 갱신하더라도 다른 서비스의 캐시에는 반영되지 않아 데이터 불일치 문제가 발생할 가능성이 있습니다.
이를 해결하기 위해 애플리케이션 전체에서 하나의 캐시 객체만 사용하도록 설계하였습니다. 이때 적합한 패턴이 바로 싱글톤(Singleton) 패턴입니다. 싱글톤 패턴은 애플리케이션 내에서 특정 객체를 단 하나만 생성하고 모든 컴포넌트가 해당 객체를 공유하도록 하는 디자인 패턴입니다. 싱글톤 패턴을 적용하면 다음과 같은 장점을 얻을 수 있습니다.

데이터 일관성 확보

모든 서비스가 동일한 캐시 객체를 사용하므로 권한 데이터의 일관성을 유지할 수 있습니다.

메모리 사용량 최소화

권한 데이터를 한 번만 메모리에 적재하므로 중복 저장을 방지할 수 있습니다.

빠른 데이터 접근

데이터베이스 대신 메모리에서 데이터를 조회하므로 응답 속도가 향상됩니다.

유지보수성 향상

캐시 관련 로직이 하나의 객체에 집중되므로 관리가 쉬워집니다. Spring Framework 환경에서는 기본적으로 Bean Scope가 Singleton이므로 이러한 구조를 자연스럽게 구현할 수 있습니다.

5. 구현 방법

구현은 비교적 단순한 구조로 진행하였습니다.
애플리케이션이 시작될 때 데이터베이스에서 메뉴 권한 정보를 조회하여 캐시에 저장합니다. 이후 로그인 요청이 발생하면 데이터베이스를 조회하지 않고 캐시 데이터를 반환합니다. 관리자가 권한 정보를 수정하면 캐시를 갱신하여 최신 상태를 유지합니다.
구현 흐름은 다음과 같습니다.
1. 애플리케이션 기동
2. 메뉴 권한 정보 조회
3. 캐시에 저장
4. 로그인 요청 발생
5. 캐시 데이터 반환
6. 관리자 수정 시 캐시 갱신
아래는 개념을 단순화한 예시 코드입니다.

image1.png

위 코드에서는 권한 정보를 저장하는 캐시 객체가 단 하나만 생성됩니다.
로그인 시에는 getMenus() 메서드를 통해 캐시된 메뉴 정보를 조회하며, 관리자 페이지에서 권한 정보가 변경되면 refresh() 메서드를 호출하여 최신 데이터를 반영합니다.
실제 운영 환경에서는 서비스 계층 분리, 예외 처리, 초기 로딩 실패 대응, 모니터링 등의 기능을 추가하여 사용하였습니다.

6. 운영 중 고려했던 사항

캐시를 적용하면서 가장 중요하게 고려했던 부분은 캐시 무효화(Cache Invalidation)였습니다. 캐시를 사용하는 가장 큰 위험 요소는 실제 데이터와 캐시 데이터가 서로 달라지는 상황입니다. 권한 정보는 자주 변경되지 않지만 변경 가능성이 완전히 없는 것은 아닙니다. 관리자가 특정 메뉴 권한을 수정했는데 캐시가 갱신되지 않는다면 사용자는 변경 이전의 권한을 계속 사용하게 됩니다. 이를 방지하기 위해 관리자 페이지에서 권한 수정이 완료되면 즉시 캐시를 다시 적재하도록 구현하였습니다.

또한 동시성 문제 역시 고려해야 했습니다. 권한 조회는 다수의 사용자가 동시에 수행할 수 있기 때문에 일반 HashMap 대신 ConcurrentHashMap을 사용하여 스레드 안전성을 확보하였습니다.

현재 구조는 단일 서버 환경을 기준으로 설계되었습니다. 단일 인스턴스에서는 애플리케이션 메모리에 저장된 캐시만 관리하면 되므로 구조가 비교적 단순합니다. 하지만 향후 서비스 규모가 증가하여 다중 인스턴스 환경으로 확장될 경우에는 각 서버가 서로 다른 캐시 데이터를 보유할 수 있는 문제가 발생할 수 있습니다.

예를 들어 관리자가 특정 메뉴 권한을 수정했을 때 한 서버의 캐시만 갱신되고 다른 서버의 캐시는 갱신되지 않는다면 사용자마다 서로 다른 권한 정보가 조회될 수 있습니다. 이러한 문제를 방지하기 위해서는 Redis와 같은 분산 캐시를 도입하거나 캐시 갱신 이벤트를 각 서버에 전파하는 구조를 고려할 수 있습니다. 현재는 단일 서버 환경이므로 적용하지 않았지만 향후 확장 시 검토할 수 있는 부분으로 정리하였습니다.

애플리케이션 재시작 상황도 고려하였습니다. 서버가 재시작되면 메모리에 저장된 캐시 데이터는 모두 사라지게 됩니다. 따라서 애플리케이션 시작 시 권한 정보를 자동으로 로딩하는 초기화 로직을 구현하여 서비스 가용성을 유지하였습니다.

이러한 경험을 통해 캐시는 단순히 저장하는 것보다 "언제 갱신할 것인가"를 설계하는 것이 훨씬 중요하다는 점을 확인할 수 있었습니다.

7. 도입 결과

싱글톤 패턴 기반 인메모리 캐시를 적용한 이후 여러 긍정적인 효과를 얻을 수 있었습니다.
첫째, 로그인 과정에서 발생하는 권한 조회 SQL 실행 횟수가 크게 감소하였습니다. 기존에는 로그인할 때마다 데이터베이스를 조회하였지만 캐시 적용 이후에는 대부분의 요청이 메모리에서 처리되었습니다.
둘째, 데이터베이스 부하가 감소하였습니다. 권한 조회를 위해 사용되던 커넥션 사용량이 줄어들면서 데이터베이스 자원을 보다 효율적으로 사용할 수 있게 되었습니다.
셋째, 응답 속도가 개선되었습니다. 메모리 조회는 데이터베이스 조회보다 훨씬 빠르기 때문에 로그인 처리 과정의 성능이 향상되었습니다.
넷째, 유지보수성이 향상되었습니다. 권한 정보가 하나의 객체에서 관리되므로 구조를 이해하기 쉬워졌습니다. 장애 분석이나 기능 개선 시에도 관련 로직을 빠르게 파악할 수 있게 되었습니다.
다섯째, 향후 확장 기반을 마련하였습니다. 현재는 인메모리 캐시를 사용하고 있지만 향후 시스템 규모가 확대될 경우 Redis와 같은 분산 캐시 솔루션으로 전환할 수 있는 구조적 기반을 확보하였습니다.

8. 마치며

메뉴 권한 정보와 같이 변경 빈도는 낮고 조회 빈도는 높은 데이터는 캐시 적용 효과가 매우 큰 영역입니다. 사용자 로그인 이후 필요한 메뉴 권한 정보를 싱글톤 패턴 기반 인메모리 캐시로 관리함으로써 데이터베이스 부하를 줄이고 응답 성능을 개선할 수 있었습니다.
특히 이번 적용을 통해 단순히 캐시를 사용하는 것보다 데이터의 특성을 분석하고 적절한 설계 패턴을 선택하는 것이 중요하다는 점을 다시 한번 확인할 수 있었습니다. 싱글톤 패턴은 비교적 단순한 구조이지만 전역적으로 공유되는 데이터를 관리해야 하는 상황에서 매우 효과적인 선택지가 될 수 있습니다.
앞으로도 서비스 특성에 맞는 캐시 전략과 아키텍처 개선을 지속적으로 검토하여 보다 안정적이고 효율적인 시스템을 구축해 나갈 계획입니다.

dwmoon

Site footer