반복영역 건너뛰기
주메뉴 바로가기
본문 바로가기
제품/서비스
EMS Solution
Features
클라우드 관리
서버관리
데이터베이스 관리
네트워크 관리
트래픽 관리
설비 IoT 관리
무선 AP 관리
교환기 관리
운영자동화
실시간 관리
백업 관리
스토리지 관리
예방 점검
APM Solution
애플리케이션 관리
URL 관리
브라우저 관리
ITSM Solution
서비스데스크
IT 서비스 관리
Big Data Solution
SIEM
AI 인공지능
Dashboard
대시보드
Consulting Service
컨설팅 서비스
고객
레퍼런스
고객FAQ
문의하기
가격
자료실
카탈로그
회사소개
비전·미션
연혁
2016~현재
2000~2015
인증서·수상
투자정보
재무정보
전자공고
IR자료
새소식
공고
보도자료
오시는 길
채용
피플
컬처
공고
FAQ
블로그
열기
메인 페이지로 이동
블로그
기술이야기
블로그
최신이야기
사람이야기
회사이야기
기술이야기
다양한이야기
[브레인저가 알려주는 IT#1] 네트워크 관리, SNMP가 뭔가요?
카프카를 통한 로그 관리 방법
김채욱
2023.09.19
페이스북 공유하기
트위터 공유하기
링크드인 공유하기
블로그 공유하기
메모리 누수 위험있는 FinalReference 참조 분석하기
안녕하세요! 저는 개발4그룹에서 제니우스(Zenius) SIEM의 로그 관리 기능 개발을 담당하고 있는 김채욱 입니다. 제가 하고 있는 일은 실시간으로 대용량 로그 데이터를 수집하여 분석 후, 사용자에게 가치 있는 정보를 시각화하여 보여주는 일입니다.
이번 글에서 다룰 내용은
1) 그동안 로그(Log)에 대해 조사한 것과 2) 최근에 CCDAK 카프카 자격증을 딴 기념으로, 카프카(Kafka)를 이용하여 어떻게 로그 관리를 하는지
에 대해 이야기해 보겠습니다.
PART1. 로그
1. 로그의 표면적 형태
로그(Log)는 기본적으로 시스템의 일련된 동작이나 사건의 기록입니다. 시스템의 일기장과도 같죠. 로그를 통해 특정 시간에 시스템에서 ‘어떤 일’이 일어났는지 파악할 수도 있습니다. 이렇게 로그는 시간에 따른 시스템의 동작을 기록하고, 정보는 순차적으로 저장됩니다.
이처럼
로그의 핵심 개념은 ‘시간’
입니다. 순차적으로 발생된 로그를 통해 시스템의 동작을 이해하며, 일종의 생활기록부 역할을 하죠. 시스템 내에서 어떤 행동이 발생하였고, 어떤 문제가 일어났으며, 유저와의 어떤 교류가 일어났는지 모두 알 수 있습니다.
만약 시간의 개념이 없다면 어떻게 될까요? 발생한 모든 일들이 뒤섞이며, 로그 해석을 하는데 어려움이 생기겠죠.
이처럼 로그를 통해 시스템은 과거의 변화를 추적합니다. 똑같은 상황이 주어지면 항상 같은 결과를 내놓는 ‘결정론적’인 동작을 보장할 수 있죠. 로그의 중요성, 이제 조금 이해가 되실까요?
2. 로그와 카프카의 관계
자, 그렇다면! 로그(Log)와 카프카(Kafka)는 어떤 관계일까요? 우선 카프카는 분산 스트리밍 플랫폼으로서, 실시간으로 대용량의 데이터를 처리하고 전송하는데 탁월한 성능을 자랑합니다. 그 중심에는 바로 ‘로그’라는 개념이 있는데요. 좀 더 자세히 짚고 넘어가 보겠습니다.
3. 카프카에서의 로그 시스템
카프카에서의 로그 시스템은, 단순히 시스템의 에러나 이벤트를 기록하는 것만이 아닙니다. 연속된 데이터 레코드들의 스트림을 의미하며, 이를 ‘토픽(Topic)’이라는 카테고리로 구분하죠. 각 토픽은 다시 *파티션(Partition)으로 나누어, 단일 혹은 여러 서버에 분산 저장됩니다. 이렇게 분산 저장되는 로그 데이터는, 높은 내구성과 가용성을 보장합니다.
*파티션(Partition): 하드디스크를 논리적으로 나눈 구역
4. 카프카가 로그를 사용하는 이유
로그의 순차적인 특성은 카프카의 ‘핵심 아키텍처’와 깊게 연결되어 있습니다. 로그를 사용하면,
데이터의 순서를 보장할 수 있어 대용량의 데이터 스트림을 효율적
으로 처리할 수 있기 때문이죠. 데이터를 ‘영구적’으로 저장할 수 있어,
데이터 손실 위험 또한 크게 줄어
듭니다.
로그를 사용하는 또 다른 이유는 ‘장애 복구’
입니다. 서버가 장애로 인해 중단되었다가 다시 시작되면, 저장된 로그를 이용하여 이전 상태로 복구할 수 있게 되죠. 이는 ‘카프카가 높은 가용성’을 보장하는 데 중요한 요소입니다.
∴
로그 요약
로그는 단순한 시스템 메시지를 넘어 ‘데이터 스트림’의 핵심 요소로 활용됩니다. 카프카와 같은 현대의 데이터 처리 시스템은
로그의 이러한 특성을 극대화하여, 대용량의 실시간 데이터 스트림을 효율적으로 처리
할 수 있는 거죠. 로그의 중요성을 다시 한번 깨닫게 되는 순간이네요!
PART2. 카프카
로그에 이어 에 대해 설명하겠습니다. 들어가기에 앞서 가볍게 ‘구조’부터 알아가 볼까요?
1. 카프카 구조
· 브로커(Broker)
브로커는 *클러스터(Cluster) 안에 구성된 여러 서버 중 각 서버를 의미합니다. 이러한 브로커들은, 레코드 형태인 메시지 데이터의 저장과 검색 및 컨슈머에게 전달하고 관리합니다.
*클러스터(Cluster): 여러 대의 컴퓨터들이 연결되어 하나의 시스템처럼 동작하는 컴퓨터들의 집합
데이터 분배와 중복성도 촉진합니다. 브로커에 문제가 발생하면, 데이터가 여러 브로커에 데이터가 복제되어 데이터 손실이 되지 않죠.
·
프로듀서(Producer)
프로듀서는 토픽에 레코드를 전송 또는 생성하는 *엔터티(Entity)입니다. 카프카 생태계에서 ‘데이터의 진입점’ 역할도 함께 하고 있죠. 레코드가 전송될 토픽 및 파티션도 결정할 수 있습니다.
*엔터티(Entity): 업무에 필요한 정보를 저장하고 관리하는 집합적인 것
·
컨슈머(Consumer)
컨슈머는 토픽에서 레코드를 읽습니다. 하나 이상의 토픽을 구독하고, 브로커로부터 레코드를 소비합니다. 데이터의 출구점을 나타내기도 하며, 프로듀서에 의해 전송된 메시지를 최종적으로 읽히고 처리되도록 합니다.
·
토픽(Topic)
토픽은 프로듀서로부터 전송된 레코드 카테고리입니다. 각 토픽은 파티션으로 나뉘며, 이 파티션은 브로커 간에 복제됩니다.
카프카로 들어오는 데이터를 조직화하고, 분류하는 방법을 제공하기도 합니다. 파티션으로 나눔으로써 카프카는 ‘수평 확장성과 장애 허용성’을 보장합니다.
·
주키퍼(ZooKeeper)
주키퍼는 브로커를 관리하고 조정하는 데 도움을 주는 ‘중앙 관리소’입니다. 클러스터 노드의 상태, 토픽 *메타데이터(Metadata) 등의 상태를 추적합니다.
*메타데이터(Metadata): 데이터에 관한 구조화된 데이터로, 다른 데이터를 설명해 주는 데이터
카프카는 분산 조정을 위해 주키퍼에 의존합니다. 주키퍼는 브로커에 문제가 발생하면, 다른 브로커에 알리고 클러스터 전체에 일관된 데이터를 보장하죠.
∴
카프카 구조 요약
요약한다면 카프카는
1) 복잡하지만 견고한 아키텍처 2) 대규모 스트림 데이터를 실시간으로 처리하는 데 있어 안정적이고 장애 허용성이 있음 3) 고도로 확장 가능한 플랫폼을 제공
으로 정리할 수 있습니다.
이처럼 카프카가 큰 데이터 환경에서 ‘어떻게’ 정보 흐름을 관리하고 최적화하는지 5가지의 구조를 통해 살펴보았습니다. 이제 카프카에 대해 조금 더 명확한 그림이 그려지지 않나요?
2. 컨슈머 그룹과 성능을 위한 탐색
카프카의 가장 주목할 만한 특징 중 하나는
‘컨슈머 그룹의 구현’
입니다. 이는 카프카의 확장성과 성능 잠재력을 이해하는 데 중심적인 개념이죠.
컨슈머 그룹 이해하기
카프카의 핵심은
‘메시지를 생산하고 소비’
하는 것입니다. 그런데 수백만, 심지어 수십억의 메시지가 흐르고 있을 때 어떻게 효율적으로 소비될까요?
여기서 컨슈머 그룹(Consumer Group)이 등장합니다. 컨슈머 그룹은, 하나 또는 그 이상의 컨슈머로 구성되어 하나 또는 여러 토픽에서 메시지를 소비하는데 협력합니다. 그렇다면 왜 효율적인지 알아보겠습니다.
·
로드 밸런싱:
하나의 컨슈머가 모든 메시지를 처리하는 대신, 그룹이 부하를 분산할 수 있습니다. 토픽의 각 파티션은 그룹 내에서 정확히 하나의 컨슈머에 의해 소비됩니다. 이는 메시지가 더 빠르고 효율적으로 처리된다는 것을 보장합니다.
·
장애 허용성:
컨슈머에 문제가 발생하면, 그룹 내의 다른 컨슈머가 그 파티션을 인수하여 메시지 처리에 차질이 없도록 합니다.
·
유연성:
데이터 흐름이 변함에 따라 그룹에서 컨슈머를 쉽게 추가하거나 제거합니다. 이에 따라 증가하거나 감소하는 부하를 처리할 수 있습니다.
여기까지는 최적의 성능을 위한 ‘카프카 튜닝 컨슈머 그룹의 기본 사항’을 다루었으니, 이와 관련된 ‘성능 튜닝 전략’에 대해 알아볼까요?
성능 튜닝 전략
·
파티션 전략:
토픽의 파티션 수는, 얼마나 많은 컨슈머가 활성화되어 메시지를 소비할 수 있는지 영향을 줍니다. 더 많은 파티션은 더 많은 컨슈머가 병렬로 작동할 수 있음을 의미하는 거죠. 그러나 너무 많은 파티션은 *오버헤드를 야기할 수 있습니다.
*오버헤드: 어떤 처리를 하기 위해 간접적인 처리 시간
·
컨슈머 구성:
*fetch.min.bytes 및 *fetch.max.wait.ms와 같은 매개변수를 조정합니다. 그다음 한 번에 얼마나 많은 데이터를 컨슈머가 가져오는지 제어합니다. 이러한 최적화를 통해 브로커에게 요청하는 횟수를 줄이고, 처리량을 높입니다.
*fetch.min.bytes: 한 번에 가져올 수 있는 최소 데이터 사이즈 *fetch.max.wait.ms: 데이터가 최소 크기가 될 때까지 기다릴 시간
·
메시지 배치:
프로듀서는 메시지를 함께 배치하여 처리량을 높일 수 있게 구성됩니다. *batch.size 및 *linger.ms와 같은 매개변수를 조정하여, 대기 시간과 처리량 사이의 균형을 찾을 수 있게 되죠.
*batch.size: 한 번에 모델이 학습하는 데이터 샘플의 개수 *linger.ms: 전송 대기 시간
·
압축:
카프카는 메시지 압축을 지원하여 전송 및 저장되는 데이터의 양을 줄입니다. 이로 인해 전송 속도가 빨라지고 전체 성능이 향상될 수 있습니다.
·
로그 정리 정책:
카프카 토픽은, 설정된 기간 또는 크기 동안 메시지를 유지할 수 있습니다. 보존 정책을 조정하면, 브로커가 저장 공간이 부족해지는 점과 성능이 저하되는 점을 방지할 수 있습니다.
3. 컨슈머 그룹과 성능을 위한 실제 코드 예시
다음 그림과 같은 코드를 보며 조금 더 자세히 살펴보겠습니다. NodeJS 코드 중 일부를 발췌했습니다. 카프카 설치 시에 사용되는 설정 파일 *server.properties에서 파티션의 개수를 CPU 코어 수와 같게 설정하는 코드입니다. 이에 대한 장점들을 쭉 살펴볼까요?
*server.properties: 마인크래프트 서버 옵션을 설정할 수 있는 파일
CPU 코어 수에 파티션 수를 맞추었을 때의 장점
·
최적화된 리소스 활용:
카프카에서는 각 파티션이 읽기와 쓰기를 위한 자체 *I/O(입출력) 스레드를 종종 운영합니다. 사용 가능한 CPU 코어 수와 파티션 수를 일치시키면, 각 코어가 특정 파티션의 I/O 작업을 처리합니다. 이 동시성은 리소스에서 최대의 성능을 추출하는 데 도움 됩니다.
·
최대 병렬 처리:
카프카의 설계 철학은 ‘병렬 데이터 처리’를 중심으로 합니다. 코어 수와 파티션 수 사이의 일치는, 동시에 처리되어 처리량을 높일 수 있습니다.
·
간소화된 용량 계획:
이 접근 방식은, 리소스 계획에 대한 명확한 기준을 제공합니다. 성능 병목이 발생하면 CPU에 *바인딩(Binding)되어 있는지 명확하게 알 수 있습니다. 인프라를 정확하게 조정할 수도 있게 되죠.
*바인딩(Binding): 두 프로그래밍 언어를 이어주는 래퍼 라이브러리
·
오버헤드 감소:
병렬 처리와 오버헤드 사이의 균형은 미묘합니다. 파티션 증가는 병렬 처리를 촉진할 수 있습니다. 하지만 더 많은 주키퍼 부하, 브로커 시작 시간 연장, 리더 선거 빈도 증가와 같은 오버헤드도 가져올 수도 있습니다. 파티션을 CPU 코어에 맞추는 것은 균형을 이룰 수 있게 합니다.
다음은 프로세스 수를 CPU 코어 수만큼 생성하여, 토픽의 파티션 개수와 일치시킨 코드에 대한 장점입니다.
파티션 수와 컨슈머 프로세스 수 일치의 장점
·
최적의 병렬 처리:
카프카 파티션의 각각은 동시에 처리될 수 있습니다. 컨슈머 수가 파티션 수와 일치하면, 각 컨슈머는 특정 파티션에서 메시지를 독립적으로 소비할 수 있게 되죠. 따라서 병렬 처리가 향상됩니다.
·
리소스 효율성:
파티션 수와 컨슈머 수가 일치하면, 각 컨슈머가 처리하는 데이터의 양이 균등하게 분배됩니다. 이로 인해 전체 시스템의 리소스 사용이 균형을 이루게 되죠.
·
탄력성과 확장성:
트래픽이 증가하면, 추가적인 컨슈머를 컨슈머 그룹에 추가하여 처리 능력을 증가시킵니다. 동일한 방식으로 트래픽이 감소하면 컨슈머를 줄여 리소스를 절약할 수 있습니다.
·
고가용성과 오류 회복:
컨슈머 중 하나가 실패하면, 해당 컨슈머가 처리하던 파티션은 다른 컨슈머에게 자동 재분배됩니다. 이를 통해 시스템 내의 다른 컨슈머가 실패한 컨슈머의 작업을 빠르게 인수하여, 메시지 처리가 중단되지 않습니다.
마지막으로 각 프로세스별 컨슈머를 생성해서 토픽에 구독 후, 소비하는 과정을 나타낸 소스코드입니다.
∴
컨슈머 그룹 요약
컨슈머 그룹은 높은 처리량과 장애 허용성 있는 메시지 소비를 제공하는 능력이 핵심입니다. 카프카가 어떤 식으로 운영되는지에 대한 상세한 부분을 이해하고 다양한 매개변수를 신중하게 조정한다면, 어떠한 상황에서도 카프카의 최대 성능을 이끌어낼 수 있습니다!
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
©
참고 자료
· Jay Kreps, “I Hearts Logs”, Confluent
· 위키피디아, “Logging(computing)”
· Confluent, “https://docs.confluent.io/kafka/overview.html”
· Neha Narkhede, Gwen Shapira, Todd Palino, “Kafka: The Definitive Guide”
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
#LOG
#로그
#카프카
#컨슈머
#KAFKA
#SIEM
#제니우스
김채욱
개발4그룹
실시간 대용량 로그 데이터의 수집 및 가공에 관심을 가지고 있습니다. 함께 발전해 나가는 개발을 추구합니다.
필진 글 더보기
목록으로
추천 콘텐츠
이전 슬라이드 보기
Spring Security로 로그인부터 사용자 전환까지 구현하기
Spring Security로 로그인부터 사용자 전환까지 구현하기
ID/PW 로그인으로 시작한 인증 기능도 제품의 접점이 늘어나면 다양한 요구사항을 만나게 됩니다. 외부 시스템에는 API Key를 이용한 호출이 필요하고, 개발 과정에서는 사용자 ID만으로 권한별 화면을 확인해야 할 때가 있습니다. 운영 지원을 위해 관리자가 하위 사용자의 실행 환경으로 전환하는 기능도 필요합니다. 이러한 요구사항을 구현하려면 인증 정보를 검증하는 방식뿐 아니라 요청을 구분하고 인증 상태를 유지하는 구조도 함께 이해해야 합니다. 이 글에서는 먼저 Spring Security의 공식 구조를 바탕으로 로그인 처리 흐름을 살펴보겠습니다. 이어서 프로젝트에서 세 가지 인증 방식을 분리한 방법을 자세히 알아보고, 마지막으로 원래 로그인한 사용자를 보존하면서 실행 사용자를 전환하는 구현을 살펴보겠습니다. 1장: Servlet 필터 체인부터 인증 관리자·제공자·토큰·세션까지 2장: ID/PW, API Key, 개발용 ID 인증의 커스텀 구현 3장: 상위 사용자의 하위 사용자 전환과 원래 계정 복귀 ※ 프로젝트 코드는 설명에 필요한 부분만 발췌했으며, 암호화·복호화·비밀번호 검증의 세부 구현과 실제 인증값은 주석으로 생략했습니다. 1. 공식 문서로 이해하는 로그인 처리 구조 로그인을 구현할 때는 요청을 받는 코드와 사용자를 검증하는 코드의 역할을 먼저 구분해야 합니다. Spring Security는 이를 필터, 인증 관리자, 인증 제공자, 인증 결과 객체로 나누어 처리합니다. 요청이 이 구성 요소들을 거쳐 인증되고 저장되는 흐름을 이해하면, 이후에 살펴볼 여러 로그인 방식과 사용자 전환도 같은 구조 안에서 파악할 수 있습니다. ※설명 기준: 프로젝트는 Spring Boot 3.0.4를 사용하며, Spring Security 버전은 6.0.2입니다. 이 장은 Spring Security 6.x의 Servlet 구조를 설명하며, 그림과 설명은 공식 6.5 레퍼런스를 사용했습니다. 아래 설정은 학습용 예시로, 다음 장의 프로젝트 설정과는 구분됩니다. 1.1. Servlet 컨테이너의 FilterChain에 들어가는 보안 필터 먼저 요청이 들어오는 지점부터 살펴보겠습니다. Spring Security의 보안 필터가 연결되는 대상은 Apache Tomcat 같은 Servlet 컨테이너가 실행하는 Servlet Filter 체계입니다. 간혹 이를 “Apache 필터”라고 표현하기도 하지만, Apache HTTP Server의 모듈이나 필터 체계와는 구분해야 합니다. 그림 1. 컨테이너의 필터들과 Servlet. 요청은 여러 필터를 거쳐 Spring MVC의 DispatcherServlet에 도착합니다. 각 필터는 요청을 다음 단계로 넘기거나 직접 응답하고 처리를 종료할 수 있습니다. Spring Security도 이 구조를 이용해 인증이 필요한 요청을 컨트롤러에 도착하기 전에 차단할 수 있습니다. 이 과정에서 컨테이너의 필터와 Spring Bean을 연결하는 클래스가 DelegatingFilterProxy입니다. 이 프록시는 Spring에서 관리하는 보안 필터 Bean으로 처리를 위임합니다. 일반적으로 위임 대상인 springSecurityFilterChain Bean의 실제 구현은 FilterChainProxy이며, 이름에 Chain이 포함되어 있지만 Servlet 관점에서는 하나의 Filter로 동작합니다. 그림 2. Servlet 컨테이너와 Spring ApplicationContext를 연결하는 프록시. 이 구조를 요청의 이동 순서에 따라 살펴보겠습니다. 컨테이너의 필터 체인에서 보안 처리 지점에 도착한 요청은 DelegatingFilterProxy를 통해 FilterChainProxy로 전달됩니다. FilterChainProxy는 요청에 맞는 SecurityFilterChain을 선택하고, 그 안의 보안 필터들을 순서대로 실행합니다. 보안 처리를 통과한 요청은 컨테이너의 필터 체인으로 돌아가 Servlet까지 진행합니다. 공식 문서의 Architecture에서도 이러한 계층을 각각의 그림으로 설명합니다. 1.2. 필터 매니저와 패턴 매처: 어떤 보안 체인을 적용할까 그렇다면 여러 보안 체인이 등록되어 있을 때는 어떤 체인을 선택할까요? 앞서 살펴본 FilterChainProxy가 이 선택을 담당하며, 이 글에서 말하는 “필터 매니저” 역할에 해당합니다. 우선순위가 높은 체인부터 RequestMatcher로 요청을 검사하고, 처음 일치한 체인 하나만 실행합니다. 일치하는 체인을 모두 이어서 실행하는 방식은 아닙니다. 그림 3. 요청에 처음 일치한 SecurityFilterChain 선택. 이 선택 구조를 설정하려면 요청 매처의 역할을 구분해야 합니다. 먼저 securityMatcher와 requestMatchers가 어떤 단계에서 사용되는지 알아보겠습니다. 두 설정 모두 요청을 비교하지만, 체인을 선택하는 단계와 선택된 체인 안에서 접근 권한을 확인하는 단계로 역할이 나뉩니다. 로그인 필터에도 별도의 요청 매처가 있으며, 각 역할은 다음과 같습니다. 예를 들어 /api/** 전용 체인이 먼저 선택되면 뒤쪽 웹 체인의 formLogin 설정은 해당 요청에 적용되지 않습니다. 같은 이유로 /secured/**만 담당하는 체인에 기본 /login 처리를 설정하면 로그인 요청이 그 체인에 도달하지 못합니다. 또한 어느 체인에도 일치하지 않는 요청은 Spring Security의 보호를 받지 않습니다. 마지막에 전체 요청을 담당하는 체인을 두는 이유도 이러한 선택 구조 때문입니다. 1.3. 인증 관리자, 인증 제공자, 토큰의 역할 요청에 적용할 체인이 정해지면 그 안에서 실제 인증이 진행됩니다. 이때 흔히 “authenticator”라고 부르는 역할은 하나의 클래스에 모여 있지 않습니다. 인증 필터가 입력을 해석하고, AuthenticationManager가 인증 요청을 받으며, AuthenticationProvider가 해당 인증 방식을 검증합니다. AuthenticationManager의 대표 구현인 ProviderManager는 등록된 인증 제공자에게 검증을 위임합니다. 각 제공자는 supports를 통해 자신이 처리할 수 있는 Authentication 타입을 알립니다. 이는 해당 토큰의 처리 가능 여부를 나타내며, 로그인 성공을 의미하지는 않습니다. 이처럼 역할을 분리하면 요청에서 자격 증명을 추출하는 방식과 사용자 검증 정책을 각각 확장할 수 있습니다. 필터와 인증 제공자 사이에서 전달되는 입력과 결과는 Authentication 객체로 표현합니다. ID/PW 인증에서는 UsernamePasswordAuthenticationToken을 사용할 수 있습니다. 검증 전에는 ID와 비밀번호를 담고, 검증 후에는 사용자 객체와 부여된 권한을 가진 인증 결과를 받습니다. 여기서 Token은 Spring Security 내부의 인증 객체를 뜻하며, 이 객체를 사용한다고 해서 JWT가 발급되거나 브라우저에 문자열 토큰이 반환되는 것은 아닙니다. 이제 인증 요청과 결과가 코드에서 어떻게 연결되는지 살펴보겠습니다. 공식 인증 아키텍처 예제에서 인증 요청과 결과를 받는 부분만 축약했으며, HTTP 응답과 세션 저장 처리는 생략했습니다. 위 코드의 request는 아직 검증되지 않은 인증 요청입니다. authenticate 호출을 통해 제공자의 검증을 마친 뒤에야 인증에 성공한 결과를 사용할 수 있습니다. 저장된 사용자 정보로 인증하는 기본 구현인 DaoAuthenticationProvider도 이 흐름을 따릅니다. UserDetailsService로 사용자를 조회하고, PasswordEncoder로 입력한 비밀번호와 저장된 값을 검증합니다. 사용자 조회와 비밀번호 검증은 별개의 책임이므로, 사용자 정보를 조회했다는 사실만으로 로그인이 성공하지는 않습니다. 검증을 마치면 사용자와 권한이 담긴 Authentication을 반환합니다. 비밀번호 보관·검증에 사용하는 구체적인 알고리즘은 이 글의 범위에서 생략합니다. 1.4. 표준 ID/PW 로그인 한 번 따라가기 이제 앞에서 살펴본 구성 요소들이 표준 로그인에서 어떻게 연결되는지 예제를 통해 알아보겠습니다. 공식 Form Login 예제의 핵심 설정은 formLogin입니다. 아래 코드는 해당 예제에 접근 규칙과 반환문을 추가한 학습용 예시이며, 사용자 저장소와 인증 제공자는 별도로 준비되어 있다고 가정합니다. 이 설정의 기본 폼은 POST /login으로 username, password 파라미터를 보냅니다. CSRF 보호가 켜져 있다면 폼에 CSRF 토큰도 포함해야 합니다. JSON 요청 본문으로 로그인 정보를 전달하는 API는 기본 폼 파라미터 처리와 별도로 입력 처리를 구성해야 합니다. 로그인 요청이 들어온 이후에는 앞서 설명한 인증 구성 요소들이 다음 순서로 동작합니다. 1. UsernamePasswordAuthenticationFilter가 로그인 요청에서 ID/PW를 추출합니다. 2. 미인증 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에 전달합니다. 3. 인증 제공자가 사용자와 자격 증명을 검증하고 인증 결과를 반환합니다. 4. 인증에 성공하면 세션 인증 전략을 실행하고 인증 결과를 SecurityContext에 설정합니다. 5. 성공 핸들러가 후속 응답을 생성합니다. 인증에 실패하면 실패 핸들러가 응답합니다. 이처럼 표준 로그인에서는 자격 증명 검증을 마친 뒤에도 세션 처리와 응답 생성이 이어집니다. 프로젝트의 커스텀 로그인 역시 이 후속 흐름까지 연결해야 합니다. 특히 로그인 상태를 다음 요청에서도 사용하려면 인증 결과를 어디에 저장하는지 확인해야 합니다. 1.5. 이번 요청의 인증을 다음 요청까지 유지하기 현재 사용자 정보는 SecurityContextHolder가 보관하는 SecurityContext의 Authentication으로 표현됩니다. 기본적인 Servlet 환경에서는 이 Holder가 요청 처리 스레드와 연결됩니다. 따라서 현재 요청에서 사용할 인증 정보를 설정하는 작업과, 다음 요청에서 다시 읽을 수 있도록 저장하는 작업은 구분해야 합니다. 요청 간 인증 상태를 연결하는 역할은 SecurityContextRepository가 담당합니다. 세션 기반 로그인에서는 브라우저가 다음 요청에 세션 쿠키를 보내면 서버가 저장된 컨텍스트를 복원합니다. Spring Security 6의 기본 SecurityContextHolderFilter는 컨텍스트를 읽는 역할을 담당하므로, 커스텀 인증 코드에서는 저장소에 명시적으로 저장해야 합니다. 기본 폼 로그인 필터가 처리하던 인증 흐름을 직접 구현한다면 이 저장 과정도 함께 연결해야 합니다. 인증 정보를 저장하는 것과 별개로, 새 인증에 어떤 세션 정책을 적용할지도 결정해야 합니다. 이를 담당하는 구성 요소가 SessionAuthenticationStrategy입니다. 동시 로그인 제한을 예로 들어 세션 정책을 어떻게 설정하는지 살펴보겠습니다. 아래는 공식 예제의 핵심 부분이며, 프로젝트가 같은 정책을 적용했다는 의미는 아닙니다. 이 설정은 기존 로그인을 유지하면서 두 번째 로그인을 거절합니다. maxSessionsPreventsLogin(true)를 생략하면 기본적으로 기존 세션을 만료시키는 방향으로 동작합니다. 세션 종료 이벤트를 추적하려면 HttpSessionEventPublisher도 연결해야 합니다. 또한 직접 만든 인증 필터에는 동시 세션 제어를 명시적으로 구성해야 합니다. 여기까지의 흐름은 요청에 맞는 체인 선택, 인증 객체 생성, 제공자의 검증, 인증 결과 저장으로 정리할 수 있습니다. 이 순서에 맞춰 코드를 읽으면 여러 인증 방식이 공통으로 사용하는 부분과 서로 다르게 구현한 부분을 구분하기 쉽습니다. 다음 장에서는 이 구조를 ID/PW, API Key, 개발용 ID 인증에 각각 적용한 방식을 살펴보겠습니다. 2. 세 가지 인증 요구사항을 Spring Security 확장 지점에 연결하기 프로젝트에는 인증이 필요한 세 가지 접점이 있습니다. 사용자는 화면에서 ID와 비밀번호로 로그인하고, 외부 연동 프로그램은 API Key로 API를 호출합니다. 개발자는 화면을 구현하면서 사용자 ID만으로 특정 사용자의 권한을 확인해야 합니다. 이러한 요구사항을 처리하기 위해 요청을 선택하는 SecurityFilterChain, 자격 증명을 해석하는 필터, 실제 검증을 수행하는 AuthenticationProvider의 역할을 나누었습니다. 여기서 ID는 DB의 숫자형 사용자 식별자가 아니라 사용자가 입력하는 loginName을 뜻합니다. ID/PW 인증과 개발용 인증에는 UsernamePasswordAuthenticationToken을 사용하고, API Key 인증에는 별도의 ZngApiKeyAuthenticationToken을 사용합니다. 인증 요청에 사용하는 Token은 다르지만, 인증 결과의 principal은 MyUserDetails로 통일했습니다. 따라서 이후 서비스는 동일한 사용자 모델과 메서드 권한 검사 체계를 사용할 수 있습니다. MyUserDetails는 Spring Security의 UserDetails 인터페이스를 구현한 프로젝트의 사용자 클래스입니다. Kotlin에서는 클래스 선언에 : UserDetails를 붙여 구현합니다. UserDetails는 사용자명, 권한 목록, 계정 잠금·활성 상태 등 인증에 필요한 정보를 제공하는 인터페이스입니다. 프로젝트에서는 여기에 사용자 ID와 계층, 소속 관리자, 접근 가능한 대상 그룹, 원래 로그인한 사용자 정보 등의 업무 속성을 추가했습니다. 인증 성공 후 MyUserDetails를 Authentication.principal에 넣으면 Spring Security의 권한 검사와 업무 코드의 현재 사용자 조회가 같은 객체를 기준으로 동작합니다. 다만 사용자 객체를 제공하는 것과 자격 증명을 검증하는 것은 별개의 역할입니다. UserDetails 구현만으로 비밀번호나 계정 상태가 자동 검증되지는 않으며, 이 프로젝트에서는 커스텀 Provider가 필요한 검사를 직접 호출합니다. getPassword()도 일반적인 저장 비밀번호 제공 경로로 사용하지 않습니다. 뒤에서 살펴볼 ID/PW Provider와 개발용 Provider 역시 목적에 따라 검증 범위가 다릅니다. 이러한 공통 사용자 모델을 바탕으로 각 인증 방식의 요청 구분과 결과 저장 범위를 다음과 같이 구성했습니다. 이제 요청 분류부터 시작해 각 인증 방식의 구현을 차례로 살펴보겠습니다. 이 장의 코드는 현재 저장소 구현에서 발췌했으며, 패키지와 import는 생략했습니다. 자격 증명 처리 부분은 주석으로 대체했고, 암호화·복호화·해시 알고리즘과 파라미터, 실제 인증값은 포함하지 않았습니다. 2.1 먼저 인증 방식이 충돌하지 않도록 요청을 분류합니다 세 가지 인증 방식을 함께 사용하려면 먼저 각 요청이 어느 체인으로 들어갈지 정해야 합니다. 앞 장에서 설명한 것처럼 여러 체인이 선언되어 있어도 처음 일치한 체인 하나만 요청을 처리합니다. 따라서 @Order와 securityMatcher의 조합은 단순한 실행 순서 설정을 넘어 인증 정책의 일부가 됩니다. 프로젝트에서는 세 가지 인증 진입점보다 앞에 인증 방식의 개수를 검사하는 체인을 두었습니다. 여기서 확인하는 항목은 로그인 요청 여부, 세션에 저장된 SecurityContext, Authorization 헤더입니다. 세션 쿠키의 존재만으로 판단하지 않고, HttpSessionSecurityContextRepository.containsContext(request)로 실제 저장된 컨텍스트를 확인합니다. 위 함수의 결과는 Order 40 체인의 matcher에서 사용합니다. 값이 1이 아닌 요청은 이 체인에서 선택해 401로 응답합니다. 예를 들어 세션으로 로그인한 요청에 API Key 헤더를 추가하거나, POST /api/login에 Authorization 헤더를 함께 보내면 두 방식이 겹칩니다. 이 경우 뒤쪽의 API Key 체인이나 세션 인증 체인으로 넘어가지 않습니다. 다만 이 단계에서는 Authorization 헤더의 형식이 아니라 존재 여부만 확인합니다. 이 검사의 적용 범위는 앞서 선택되는 체인까지 함께 보아야 이해할 수 있습니다. OPTIONS(Order 10), 정적 리소스(Order 20), 공개키 조회 GET /api/user/pk(Order 30), 비정상 로그아웃 처리(Order 35), 특정 보고서 생성 요청(Order 37)은 Order 40보다 먼저 선택될 수 있습니다. 이후에는 별도의 보고서용 인증(Order 50), 개발용 Basic(Order 60), API Key(Order 70), 세션 인증(Order 80)이 이어집니다. 따라서 모든 URL이 동일한 검사를 거치는 것은 아니며, @Order와 matcher를 기준으로 흐름을 확인해야 합니다. 각 인증 체인으로 분기할 때 사용하는 헤더 matcher는 대소문자를 무시하는 startsWith(authScheme, true)로 구현되어 있습니다. 이는 scheme에 따라 요청을 구분하는 역할이며, 엄격한 헤더 문법 검증까지 수행하는 구현은 아닙니다. 이렇게 요청 경로를 나눈 뒤, 각 필터와 Provider에서 인증 방식에 맞는 처리를 진행합니다. 2.2 ID/PW 로그인: 입력 형식과 사용자 정책을 분리합니다 먼저 일반 로그인은 POST /api/login 요청의 JSON body를 읽습니다. 이를 담당하는 ZngUsernamePasswordAuthenticationFilter는 AbstractAuthenticationProcessingFilter를 상속하며, 생성자에서 ProviderManager(ZngUsernamePasswordAuthenticationProvider(userService))를 전달합니다. 필터는 입력과 HTTP 요청을 처리하고 Provider는 사용자를 검증하므로, 역할 분담은 표준 로그인과 같습니다. 표준 폼 로그인과 달라지는 부분은 입력 형식입니다. 폼 파라미터 대신 JSON 본문을 읽어야 하므로 커스텀 필터를 사용했습니다. 필터는 입력을 준비한 뒤 다음과 같이 미인증 Token을 생성합니다. 이 Token은 검증할 요청을 전달하는 객체이며, 아직 인증 성공 상태는 아닙니다. 이때 details에 추가한 HTTP 요청 정보는 이후 IP 정책 검사에 사용합니다. Token을 전달받은 Provider는 서비스에 ID/PW 검증을 위임하고, 필요한 비밀번호 저장 형식 이전 처리를 수행한 뒤 사용자 상태를 추가로 확인합니다. 서비스에서는 ID 오류와 비밀번호 오류를 구별해 알리지 않고 동일한 인증 예외로 처리합니다. 자격 증명 검증 이후에는 어떤 항목을 추가로 확인하는지 코드로 살펴보겠습니다. 검증에 성공하면 principal은 문자열 ID에서 MyUserDetails로 바뀌고, authorities에는 사용자 권한이 포함되며, credentials는 null로 반환됩니다. 이 Provider의 supports는 전달된 Token이 UsernamePasswordAuthenticationToken 계열인지 확인합니다. ProviderManager는 이 지원 여부를 기준으로 검증을 담당할 Provider를 선택합니다. 인증 결과를 받은 다음에는 세션 정책을 적용하고 로그인 상태를 저장해야 합니다. 이를 위해 로그인 필터에 다음과 같은 세션 전략과 성공 핸들러를 지정했습니다. 먼저 세션 전략의 첫 번째 구성 요소는 설정값으로 최대 동시 세션 수를 확인합니다. 한도를 넘으면 신규 로그인을 예외로 거절하거나 기존 세션을 expireNow()로 만료 표시합니다. 두 번째 전략은 새 세션을 SessionRegistry에 등록합니다. 만료 표시된 세션으로 후속 요청이 들어오면 세션 체인에 배치한 ConcurrentSessionFilter가 처리합니다. 즉, 세션에 만료 여부를 기록하는 시점과 해당 세션의 다음 요청을 차단하는 시점은 구분됩니다. 다만 현재 동시 세션 전략에는 sortedBy의 반환값을 사용하지 않는 코드가 있어, 주석의 의도만으로 가장 오래 사용하지 않은 세션이 반드시 선택된다고 설명하기는 어렵습니다. 또한 위 Composite에 직접 포함된 전략은 동시 세션 검사와 등록 두 가지이므로, 이 설정만으로 세션 ID 변경 전략까지 적용된다고 단정할 수는 없습니다. 세션 전략을 통과하면 성공 핸들러가 SecurityContext를 생성하고 HttpSessionSecurityContextRepository.saveContext로 명시적으로 저장합니다. 다음 요청에서 세션에 저장된 인증 정보를 사용할 수 있는 이유입니다. 성공 응답은 리다이렉트 대신 MyResult JSON으로 반환하며, 로그인 실패 핸들러는 401을 반환합니다. 로그아웃 요청인 POST /api/logout에는 Spring Security의 로그아웃 처리와 감사 기록을 연결하고, 성공 JSON을 응답하도록 구성했습니다. 인증 결과를 통계와 감사 처리에 반영하는 흐름은 이벤트로 연결했습니다. ProviderManager에 AuthenticationEventPublisher를 설정하고, SecurityConfig의 이벤트 리스너에서 인증 성공 시 사용자 로그인 시도 횟수와 마지막 로그인 시각을 갱신합니다. 사용자 관련 실패와 API Key 자체의 실패는 서로 다른 이벤트로 매핑해 실패 횟수를 갱신할 대상도 구분합니다. 다만 이 이벤트는 Provider의 인증 결과를 알리는 것이므로, 이후 동시 세션 전략까지 성공했다는 의미는 아닙니다. 2.3 API key 인증: 사용자 인증에 호출 범위를 더합니다 ID/PW 로그인이 브라우저의 세션을 유지하기 위한 흐름이라면, 외부 연동에서는 Authorization: ZNG <API_KEY> 형식의 헤더로 인증 정보를 전달합니다. 실제 키는 예시에 포함하지 않았습니다. Order 70 체인이 이 헤더를 가진 요청을 선택하면, BasicAuthenticationFilter 위치에 배치한 ZngApiKeyAuthenticationFilter가 처리를 담당합니다. 이 필터는 OncePerRequestFilter를 상속합니다. 헤더에서 키를 읽고 형식을 확인한 뒤, 식별자와 검증용 비밀값을 미인증 ZngApiKeyAuthenticationToken에 담습니다. 여기에 요청 details를 추가해 API Key 전용 ProviderManager로 전달합니다. 인증 결과를 받은 뒤에는 컨텍스트를 설정하고 저장합니다. 이 과정이 ID/PW 로그인과 어떻게 다른지 다음 코드와 함께 살펴보겠습니다. 앞서 본 로그인 처리와 코드 형태는 비슷하지만, 여기서 사용하는 저장소는 RequestAttributeSecurityContextRepository입니다. API Key 필터는 HttpSession에 인증 결과를 직접 저장하지 않고 요청 속성 저장소를 사용하므로, 의도한 인증 유지 범위는 현재 요청입니다. 다만 설정에 SessionCreationPolicy.STATELESS가 명시된 것은 아니어서, 이 코드만으로 애플리케이션 전체에서 어떤 이유로도 세션이 생성되지 않는다고 볼 수는 없습니다. 이 인증 결과를 만드는 ZngApiKeyAuthenticationProvider는 먼저 식별자로 키를 조회하고, 비밀값 일치 여부와 활성·만료·잠금 상태를 확인합니다. 이어서 키에 연결된 사용자도 조회합니다. 키가 유효하더라도 사용자가 휴면·잠금·자격 증명 만료·비활성 상태이거나 IP 조건을 만족하지 못하면 인증하지 않습니다. 검증에 성공하면 ZngApiKeyAuthenticationToken.authenticated(user, null, user.authorities, apiKey)를 반환합니다. 전용 Token에 apiKey를 남기는 이유는 인증 이후에도 키별 호출 범위를 확인해야 하기 때문입니다. 다음 인가 코드에서 이 정보를 사용합니다. 위 코드는 인증과 인가의 역할을 구분해 보여줍니다. Provider가 “어떤 사용자와 키인가”를 검증했다면, ZngApiKeyAuthorizationManager는 “이 키로 이 요청을 호출해도 되는가”를 판단합니다. 요청은 키에 허용된 API 경로와 HTTP 메서드 matcher에 일치해야 하며, 사용자 전환·사용자 관리·API Key 관리 등 별도로 선언한 금지 matcher에도 해당하지 않아야 합니다. 이 제한은 선언된 matcher 범위에 적용되며, 모든 관리 기능을 자동으로 차단한다는 의미는 아닙니다. 컨트롤러와 서비스의 메서드 권한 검사도 별도로 작동합니다. 실패 처리도 API Key 인증 흐름에 맞추어 구성했습니다. 필터에서 AuthenticationException이 발생하면 감사 기록을 남기고 컨텍스트를 비운 뒤 403 진입점으로 응답합니다. 키 형식이 맞지 않으면 필터 자체에서 예외를 던지는 대신 다음 필터로 넘기지만, 인증된 API Key Token이 없으면 이후 인가를 통과할 수 없습니다. API Key로 들어오는 로그아웃 요청은 세션 로그아웃과 구분되므로, 앞쪽의 비정상 로그아웃 처리 체인에서 별도로 처리합니다. 2.4 개발용 ID 인증: 기본 HTTP Basic 필터에 Provider만 교체합니다 마지막으로 개발 중 사용자별 권한과 화면 동작을 확인하기 위한 인증 방식을 살펴보겠습니다. 이때는 지정한 사용자의 컨텍스트로 API를 호출할 수 있도록, 기본 HTTP Basic 처리에 ID만 조회하는 Provider를 연결했습니다. 요청을 해석하는 기본 필터는 사용하면서 사용자 검증 부분을 별도로 구성한 것입니다. 여기서 “ID만으로 로그인”한다는 것은 HTTP body에 숫자 ID 하나를 보내는 별도 로그인 API를 뜻하지 않습니다. 요청은 Basic 헤더 형식을 따르며, 기본 필터가 이를 UsernamePasswordAuthenticationToken으로 변환합니다. 이후 커스텀 Provider가 authentication.name, 즉 loginName으로 사용자를 조회합니다. 프런트엔드의 개발 로그인 설정도 입력받은 사용자명을 Basic 형식으로 준비해 후속 요청의 Authorization 헤더에 넣습니다. 실제 인증값을 만드는 표현식은 본문에서 생략했습니다. 위 Provider는 비밀번호를 비교하지 않으며, 일반 로그인 Provider에서 수행하는 사용자 상태·IP 검사도 호출하지 않습니다. 검증을 단순화한 개발 편의 기능이므로 활성화 범위를 함께 확인해야 합니다. 현재 조건은 **`prod` 프로필이 활성화되지 않은 환경(@Profile("!prod"))**입니다. 반면 API Key 체인에는 이 프로필 조건이 없습니다. 개발용 Basic 체인이 활성화된 환경에서도 ZNG 헤더 요청은 API Key 체인으로 들어가며, 두 인증 방식은 헤더에 따라 구분됩니다. 앞서 설명한 Order 40의 인증 방식 개수 검사도 개발용 요청에 적용됩니다. 따라서 세션 컨텍스트를 유지한 채 Basic 헤더를 추가하면 인증 방식이 충돌한 요청으로 처리됩니다. 지금까지 세 가지 인증 요구사항을 필터·Provider·Token·Context·세션 전략의 역할에 맞추어 연결한 방식을 살펴봤습니다. 다음 장의 사용자 전환은 이렇게 인증된 사용자가 다른 사용자의 컨텍스트로 이동하는 기능입니다. 다음 장에서는 새 자격 증명을 검증하는 로그인과 비교하면서, 전환 권한을 판단하고 세션 상태를 유지하는 방법을 알아보겠습니다. 3. 상위 사용자에서 하위 사용자로: 로그인 상태를 유지하며 실행 주체 바꾸기 운영 지원 과정에서는 관리자가 하위 사용자의 화면과 데이터 접근 범위를 확인해야 할 때가 있습니다. 매번 상대 사용자의 비밀번호를 받아 다시 로그인하는 방식으로는 이러한 요구를 처리하기 어렵습니다. 프로젝트에서는 이미 인증된 상위 사용자의 권한으로 전환 가능 여부를 판단하고, 이후 요청에서 사용할 Authentication을 대상 사용자의 것으로 교체했습니다. 이때는 두 사용자를 구분해야 합니다. 하나는 현재 기능을 실행하는 사용자이고, 다른 하나는 처음 로그인하여 전환 권한의 근거가 된 사용자입니다. 현재 사용자는 화면과 업무 권한의 기준이며, 최초 사용자는 다른 사용자로 이동하거나 원래 계정으로 돌아갈 수 있는 권한의 근거입니다. 이 구분을 유지하기 위해 먼저 원래 로그인한 사용자 정보를 보관합니다. 3.1. 원래 로그인한 사용자는 한 번만 보관합니다 MyUserDetails에는 원래 로그인한 사용자를 보관하는 orgLoginUser 필드가 있습니다. 값이 null이면 현재 객체가 원래 로그인한 사용자를 나타냅니다. 필드 주석은 ID/PW 로그인을 기준으로 작성되어 있습니다. API Key 인증으로 들어오는 전환 요청은 별도의 인가 매니저에서 차단하므로, 여기서는 일반적인 세션 로그인 이후의 전환 흐름을 중심으로 설명하겠습니다. 예를 들어 관리자 A가 사용자 B로 전환하면 B의 orgLoginUser가 A를 가리킵니다. B 상태에서 사용자 C로 다시 이동하더라도 최초 사용자는 A로 유지됩니다. 전환할 때마다 이전 사용자를 연결하는 이력 구조를 만드는 대신, 언제나 하나의 원본 계정을 기준으로 전환 권한을 판단합니다. Spring Security에도 사용자 전환용 SwitchUserFilter가 있으며, 공식 구현은 SwitchUserGrantedAuthority에 원래 Authentication을 보관해 복귀에 활용합니다. 프로젝트에서는 이 필터를 등록하지 않고, 컨트롤러에서 업무 계층을 검증한 뒤 MyUserDetails.orgLoginUser로 원본 사용자를 보관했습니다. 따라서 다음 코드는 SwitchUserFilter를 확장한 구현이 아니라, 프로젝트의 사용자 모델을 이용한 별도 구현입니다. 3.2. 전환 권한은 현재 사용자보다 최초 사용자를 기준으로 판단합니다 원본 사용자를 보관했다면 전환 요청에서 이 정보를 권한 판단에 사용합니다. 실제 진입점은 POST /api/user/change/{userId}입니다. 컨트롤러는 현재 사용자와 원본 사용자를 구분한 뒤, 원본 사용자의 계층을 기준으로 대상에게 전환할 수 있는지 확인합니다. 코드의 계층 검사는 MyUserDetails의 level을 기준으로 합니다. 1은 SUPER, 2는 ADMIN, 3은 일반 사용자이며, isAdmin()과 isSuper()도 이 값을 검사합니다. ADMIN은 대상의 adminId가 자신의 ID와 같은 경우에만 전환할 수 있습니다. adminId는 일반 사용자에게는 소속 관리자의 ID이고, ADMIN과 SUPER에게는 본인의 ID입니다. 따라서 관리자 A는 자신의 하위 사용자로 이동할 수 있으며, 본인으로 복귀할 때도 같은 조건을 통과합니다. 이 값은 조회 SQL의 case when u.z_mylevel = 3 then u.z_parid else u.z_myid end에서 생성됩니다. SUPER의 전환 요청에는 ADMIN과 같은 대상별 소속 검사 분기가 없습니다. 사용자 계층에 맞는 선택 목록은 제공하지만, 실제 전환 API가 SUPER의 대상을 반드시 그 목록 안으로 제한한다고 설명할 수는 없습니다. 이후에 살펴볼 화면의 전환 후보 목록과 서버의 전환 허용 조건은 구분해야 합니다. 최초 사용자를 기준으로 검사하기 때문에, 이미 일반 사용자로 전환한 상태에서도 org가 관리자를 가리키면 다른 하위 사용자로 이동하거나 관리자 본인으로 돌아갈 수 있습니다. 반면 처음부터 일반 사용자로 로그인했다면 org도 일반 사용자이므로 요청이 거절됩니다. 이 권한 검사는 메서드의 @PreAuthorize가 아니라 메서드 본문에서 수행합니다. 3.3. 인증된 토큰을 새로 만들고 세션에 저장합니다 전환 권한 검사를 통과하고 대상이 확정되면, 해당 사용자를 principal로 가진 새 토큰을 만듭니다. 비밀번호를 다시 검증하는 요청이 아니므로 AuthenticationManager나 ID/PW용 AuthenticationProvider는 호출하지 않습니다. 앞에서 수행한 전환 권한 검사가 이 토큰 생성의 근거가 됩니다. 이 코드에서 권한 적용의 기준은 toBe.authorities입니다. 원래 관리자의 권한을 합치지 않고 대상 사용자의 권한으로 새 Authentication을 구성하므로, 일반 업무 API는 전환된 사용자의 권한을 기준으로 동작합니다. 원래 관리자 정보는 전환과 추적을 위한 참조로 보관되며, 새 토큰에 관리자 권한이 자동으로 승계되는 것은 아닙니다. 권한을 구성한 다음에는 바뀐 인증 상태를 현재 요청과 후속 요청에 반영합니다. 새 SecurityContext를 만들어 SecurityContextHolder에 설정하는 작업은 현재 요청의 실행 주체를 바꿉니다. 이어지는 saveContext는 다음 요청에서도 전환 결과를 사용할 수 있도록 HTTP 세션에 저장합니다. 1장에서 설명한 컨텍스트 설정과 저장의 구분이 여기에도 적용됩니다. 공식 문서에서도 SecurityContextRepository를 후속 요청까지 인증 상태를 연결하는 지점으로 설명하며, HttpSessionSecurityContextRepository는 이를 HTTP 세션에 연결합니다. 이렇게 인증 객체를 교체하면 업무 코드에서도 같은 사용자 조회 방식을 유지할 수 있습니다. 현재 사용자를 가져오는 공통 함수를 통해 이 동작을 확인해 보겠습니다. 전환 후 getCurrent()는 새 principal을 반환합니다. 따라서 이 공통 조회 방식을 사용하는 개별 업무 서비스에 전환 대상의 ID를 별도로 전달할 필요가 없습니다. 현재 사용자로부터 그룹이나 권한을 조회하는 기존 코드도 전환된 사용자 정보를 기준으로 동작합니다. 3.4. 후보 목록과 복귀도 같은 원본 계정을 활용합니다 사용자 전환을 화면에서 제공하려면 이동할 수 있는 후보를 조회하고 원래 계정으로 돌아가는 흐름도 필요합니다. 후보 목록은 GET /api/user/change-list로 조회합니다. 여기에서도 원본 계정을 먼저 구한 다음, 현재 실행 중인 사용자만 목록에서 제외합니다. ADMIN에게는 본인과 직접 하위 사용자를, SUPER에게는 본인과 하위 관리자 및 그 사용자들을 제공합니다. getChildren의 SQL은 z_parid로 직접 하위를 조회하므로, SUPER의 목록은 하위 관리자와 그 사용자에 대한 두 번의 계층 조회로 구성됩니다. 임의 깊이의 트리를 재귀적으로 조회하는 구현은 아닙니다. 목록에 포함된 원본 계정으로 복귀할 때도 같은 전환 API를 사용하며, 별도의 복귀 전용 API는 두지 않았습니다. 원본 사용자 ID를 전달하면 getUserById가 DB에서 새 MyUserDetails를 조회합니다. 대상이 원본과 같을 때는 orgLoginUser를 별도로 설정하지 않으며, 이때 equals는 사용자 ID를 기준으로 비교합니다. 따라서 원본 계정으로 돌아오면 다시 orgLoginUser == null인 상태가 됩니다. 3.5. 실행 사용자와 실제 행위자를 함께 남길 수 있게 합니다 전환과 복귀가 가능해지면, 이후 작업을 누가 수행했는지도 구분할 필요가 있습니다. 사용자 B의 권한으로 실행했다는 사실만 남기면 B가 직접 수행했는지, 관리자 A가 B로 전환해 수행했는지 파악하기 어렵습니다. 이를 위해 감사 로그 모델은 현재 사용자와 원본 사용자를 함께 저장할 수 있도록 구성했습니다. 다음은 감사 로그 생성자 인자 중 관련 부분을 발췌한 코드입니다. 감사 로그 대상 메서드의 AOP는 UserService.getCurrent()로 기록할 사용자를 설정합니다. 따라서 전환 상태에서 기록되는 작업에는 현재 사용자와 원본 사용자를 구분해 담을 수 있습니다. 감사 기록과 함께 확인할 부분은 전환 시 다시 수행하는 인증·세션 처리의 범위입니다. 현재 구현은 컨트롤러에서 인증 객체를 직접 만들기 때문에, 최초 로그인에서 수행하는 계정 상태·허용 IP 검증이나 세션 인증 전략을 다시 호출하지 않습니다. SessionRegistry 재등록, 세션 ID 변경, 로그인 성공 이벤트 발행도 전환 메서드에는 없습니다. 따라서 기존 세션의 인증 주체를 교체하는 처리와 새로운 로그인에 적용하는 동시 세션 관리 정책을 같은 동작으로 보아서는 안 됩니다. 마지막으로 어떤 인증 경로에서 사용자 전환 기능에 접근할 수 있는지 확인해 보겠습니다. ZngApiKeyAuthorizationManager는 "/api/user/change/*" to POST를 금지 목록에 넣고, API Key 자체의 접근 범위와 이 목록을 함께 검사합니다. 따라서 API Key로는 사용자 전환을 할 수 없으며, 컨트롤러뿐 아니라 앞단의 필터 체인에서 수행하는 인가까지 확인해야 전체 제한을 파악할 수 있습니다. 이 글의 사례는 기존 세션 로그인 상태에서 제공하는 사용자 전환 흐름을 중심으로 설명했습니다. 이 구현을 통해서는 원본 계정으로 전환 권한을 판단하고, 대상 계정으로 업무 권한을 구성한 뒤, 바뀐 컨텍스트를 세션에 저장합니다. 이 세 역할을 구분함으로써 일반 업무 API의 현재 사용자 조회 방식을 유지하면서 사용자 전환과 원본 계정 복귀를 구현할 수 있었습니다.
2026.11.04
다음 슬라이드 보기