올리브영 테크블로그 포스팅 올리브영 대규모 트래픽 레거시 시스템의 무중단 OAuth2 전환기
Tech

올리브영 대규모 트래픽 레거시 시스템의 무중단 OAuth2 전환기

Feature Flag, Jitter, Circuit Breaker로 완성한 Fail-Safe 마이그레이션 아키텍처 전략

2026.01.30

안녕하세요, 올리브영에서 인증 시스템을 개발하는 망고입니다.


커머스 플랫폼에서 사용자의 진짜 여정은 로그인에서 시작됩니다. 로그인에 성공하면 사용자는 "인증"된 상태로 사이트를 이용할 수 있게 되며, 특히 올리브영과 같은 커머스 플랫폼에서 인증은 주문, 결제, 배송 조회 등 모든 핵심 기능의 전제 조건입니다. 그렇다 보니 인증 시스템이 평소에는 조용히 제 역할을 하다가, 문제가 생기면 모든 서비스 담당자의 이목이 집중되는 가장 중요하고도 영향력있는 인프라입니다. 로그인이 풀리거나 세션이 끊기면 장바구니가 비워지고, 결제에 실패합니다. 이 때 사용자는 당황스러운 경험을 하게 됩니다. 그래서 인증 시스템을 건드리는 것은 언제나 조심스러운 일입니다.


2025년 3월부터 10월까지, 저희 스쿼드는 올리브영 온라인몰의 레거시 세션 기반 인증을 OAuth2로 전환하는 프로젝트를 진행했습니다. 전체 배포 이후 2주 뒤에 평소 대비 10배의 대규모 트래픽을 처리해야 하는 정기 플래그십 이벤트 '올영세일'을 앞두고 있었기 때문에 모든 기능을 포괄하는 OAuth2 표준을 완벽히 구현하기보다, 기존 서비스의 '핵심 기능'을 보호하고 무중단 전환을 보장하는 '안전 구조'를 최우선 목표로 삼아 '중단 없이, 장애 없이' 배포했습니다. 이 과정에 겪은 기술적 도전과 해결 과정을 공유합니다.


1. 들어가며: 대규모 트래픽 환경에서의 인증 체계 교체


1.1 왜 인증 체계를 바꿨나

기존 시스템은 Spring Session 기반이었고, 자동 로그인을 위한 별도 쿠키를 사용했습니다. 이 방식은 다음 세 가지 측면에서 명확한 한계와 비즈니스/기술적 제약을 가져왔습니다.

  • 확장성 및 복잡성 문제: 세션-쿠키의 혼재 구조로 인해 인증 로직이 복잡함, 새로운 마이크로서비스들이 중앙의 세션 Redis에 의존해야 했기 때문에, 마이크로서비스 환경으로의 전환과 독립적으로 인증할 표준 필요
  • 보안 및 제어의 한계: 로그아웃 시에도 자동 로그인 쿠키가 유효하여, 도난/분실 시 보안 사고에 즉시 대응할 수 있는 메커니즘이 부재하였고, 특정 기기에서만 로그아웃하는 기기별 세션 관리가 불가능하여 짧은 수명의 토큰을 통해 노출 시간을 최소화할 필요 있었음
  • 비즈니스 확장성 제약: 복잡한 레거시 인증 로직으로 인해 외부 서비스 연동이나 신규 서비스 론칭 시 인증 연동에 큰 공수가 소요되어 비즈니스 확장 속도 저해

OAuth2를 선택한 이유는 단순 JWT 대신 검증된 표준 프레임워크가 필요했기 때문입니다.

  • Refresh Token 무효화 가능 (RFC 7009)
  • 짧은 Access Token + 긴 Refresh Token 조합
  • 멀티 토큰 관리 표준 제공
  • 향후 OIDC(OpenID Connect, OAuth2 기반의 사용자 신원 확인 계층 표준)로 자연스럽게 진화 가능

1.2 핵심 도전

가장 큰 과제는 무중단 마이그레이션이었습니다. 특히 8월 말 올영세일 기간 중에는 평소 대비 약 10배의 트래픽이 몰리는 상황에서도 안정적으로 전환을 진행해야 했습니다. 이에 이 글에서는 "왜 바꿨는지"보다 "어떻게 안전하게 바꿨는지"에 초점을 맞춥니다. 특히 다음 기술들에 대해 상세히 다루고 있으니 참고하며 읽어주세요.

  • Feature Flag 위임 패턴 - 세일 중에도 점진적 전환
  • 점진적 롤아웃 전략 - 10% → 100% 단계적 확대
  • Jitter 도입 - Peak TPS 40% 감소
  • Resilience4j 장애 격리 - Circuit Breaker로 안정성 확보

[참고로 OAuth2나 JWT의 기본 개념은 다루지 않습니다. 대신 대규모 트래픽 환경에서 실제로 동작하는 구현 패턴과 최적화 기법에 집중합니다.]


2. Fail-Safe Architecture: 안전한 전환을 위한 4가지 전략


무중단 마이그레이션의 핵심은 언제든 되돌릴 수 있어야 한다는 원칙이었습니다. 코드 배포 없이 런타임에 인증 방식을 전환하고, 문제 발생 시 즉시 롤백할 수 있는 구조가 필요했습니다.

2.1 Feature Flag 위임 패턴 (런타임 전환)

Feature Flag 기반 위임 패턴을 적용했습니다. 하나의 진입점(FeatureFlagDelegatingInterceptor)에서 사용자별로 다른 인증 방식으로 요청을 위임하는 구조입니다.

Feature Flag 위임 패턴 아키텍처
그림 1. Feature Flag 기반 위임 패턴 - 런타임에 인증 방식을 동적으로 전환하는 구조

2.2 구현 상세: Strategy 패턴으로 안전한 전환

Feature Flag 위임 패턴을 실제로 구현하기 위해 FeatureFlagDelegatingInterceptor를 중심으로 Strategy 패턴을 적용했습니다. 이 인터셉터는 Feature Flag 값에 따라 적절한 인터셉터 체인을 선택하는 역할을 담당합니다.


먼저 전체 아키텍처를 살펴보겠습니다. 아래 다이어그램은 HTTP 요청이 들어왔을 때 어떻게 처리되는지 보여줍니다.


Feature Flag 위임 패턴 상세 플로우
그림 2. Feature Flag 기반 위임 패턴의 전체 요청 처리 흐름

HTTP 요청이 들어오면 FeatureFlagDelegatingInterceptor가 모든 요청을 가로챕니다. 그 다음 FeatureFlagService를 통해 Database에서 기능플래그를 조회하고, 기능플래그 확인 분기점에서 Yes/No를 결정합니다. 이 결정에 따라 JWT 활성화 또는 JWT 활성화되지 않음 경로로 분기되며, 각각 JWT AuthenticationInterceptor 또는 Legacy AuthenticationInterceptor가 실행됩니다. 마지막으로 Request attribute에 Phase 정보를 저장한 후 최종적으로 Spring Controller로 전달됩니다.


이러한 흐름을 구현한 핵심 코드는 다음과 같습니다.

public class FeatureFlagDelegatingInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws Exception {
        // 1. Feature Flag 확인 (Database 조회)
        boolean isPhase2Enabled = loginTokenFlagService.activatePhase2(request, response);

        // 2. Phase 정보 저장 (모니터링용)
        request.setAttribute("oauth.phase.version", isPhase2Enabled ? "2.0" : "1.0");

        // 3. Strategy 패턴: 동적으로 인터셉터 선택
        if (isPhase2Enabled) {
            return executePhase2Chain(request, response, handler);  // JWT
        } else {
            return executeLegacyChain(request, response, handler);   // Legacy
        }
    }
}

이 구현은 네 가지 핵심 설계 원칙을 따릅니다.
첫째, 단일 진입점을 통해 모든 요청이 하나의 인터셉터를 거치도록 했습니다.
둘째, 런타임 분기를 통해 Database 설정만으로 즉시 전환할 수 있게 했습니다.
셋째, 안전한 Fallback으로 오류 시 자동으로 Legacy 모드로 전환됩니다.
넷째, 완전한 격리를 통해 Phase 2와 Phase 1 로직이 독립적으로 동작하도록 했습니다.


그렇다면 Feature Flag는 어떻게 Phase를 결정할까요? 우리는 임직원을 대상으로 선정하되, 비로그인 사용자는 제외하는 것으로 기준을 설정하고 단계를 나눴습니다.


임직원을 첫 배포 대상으로 선택한 이유는 실제 운영 환경에서의 검증이 필요했기 때문입니다. 개발 서버에서 아무리 테스트해도 실제 사용자 환경에서만 발견되는 문제들이 있습니다. 특히 쿠키처럼 개인 기기와 브라우저 설정에 따라 동작이 달라지는 기능의 경우, 다양한 환경에서의 실제 검증이 필수적입니다. 임직원 대상 Beta Test를 통해 피드백을 받으면서 실 사용자에게서 발생할 수 있는 오류를 사전에 체크할 수 있었습니다.


비로그인 사용자를 제외한 이유는 간단합니다. OAuth2 기반 자동 로그인은 로그인한 사용자만을 대상으로 하기 때문에, 비로그인 사용자는 애초에 전환 대상이 아닙니다.


이러한 배경을 바탕으로 Feature Flag는 3단계 체크를 통해 Phase를 결정합니다.

@Service
public class LoginTokenFlagServiceImpl implements LoginTokenFlagService {

    @Override
    public boolean activatePhase2(HttpServletRequest request,
                                 HttpServletResponse response) {
        try {
            Customer customer = getCustomer(request, response);

            // 1. All Open Flag 확인 (전체 공개)
            if (isFlagEnabled(config.get("oauth.flag.code.phase2.all"))) {
                return true;  // 모든 사용자에게 Phase 2 적용
            }

            // 2. 비로그인 사용자는 Phase 1 사용
            if (customer == null || !customer.isLoggedIn()) {
                return false;
            }

            // 3. Staff Open Flag 확인 (임직원 대상)
            if (isFlagEnabled(config.get("oauth.flag.code.phase2.staff"))) {
                return isStaff(customer);
            }

            return false;  // 기본값: Phase 1

        } catch (Exception e) {
            // 오류 시 안전하게 Phase 1으로 Fallback
            return false;
        }
    }
}

Feature Flag는 다음과 같은 단계로 구성됩니다.

단계 Flag 설명 대상
0 (모두 비활성) Phase 1만 사용 전체
1 phase2.staff = Y 임직원 대상 테스트 내부 검증
2 phase2.all = Y 전체 공개 모든 사용자

이러한 구조 덕분에 데이터베이스 설정만 변경하면 코드 배포 없이 즉시 전환할 수 있었습니다. 또한 각 요청마다 Phase 정보를 로깅해 Datadog에서 Phase별 트래픽 분포, 에러율, 응답시간을 실시간으로 추적할 수 있었습니다. 이를 통해 점진적 롤아웃 과정에서 두 Phase 간 성능을 비교하고 안정성을 검증했습니다.


결과적으로 이 위임 패턴은 Feature Flag 값 변경만으로 인증 방식을 즉시 전환할 수 있었고, 예외가 발생하면 자동으로 Legacy로 처리되는 안전한 Fallback을 제공했습니다. Phase 2와 Phase 1 코드는 완전히 격리되어 독립적으로 동작했으며, 코드 배포 없이 DB 설정만 변경하는 무중단 배포가 가능했습니다. 임직원부터 시작해 일부 사용자를 거쳐 전체로 확대하는 세밀한 제어가 가능했고, 각 Phase별 성능 메트릭을 실시간으로 추적할 수 있었습니다.


Feature Flag 위임 패턴으로 런타임 전환 구조를 만들었다면, 이제 실제로 어떻게 사용자를 단계적으로 전환했는지 살펴보겠습니다.


2.3 점진적 롤아웃 전략 (10% → 100%)

Feature Flag를 통해 사용자별로 단계적으로 확대하는 전략을 수립했습니다. 핵심은 문제 발생 시 즉시 되돌릴 수 있는 안전장치를 유지하면서 점진적으로 범위를 넓혀가는 것이었습니다.

점진적 롤아웃 일정은 다음과 같았습니다.

날짜 전환율 단계
07/28 (월) 10% 첫 배포 - 내부 검증
07/29 (화) 10% 안정성 모니터링
07/30 (수) 20% +10% 확대
08/01-11 20% 2주간 안정화
08/12 (화) 50% +30% 확대
08/13 (수) 75% +25% 확대
08/14-15 75% 대규모 검증
08/16 (토) 100% 전체 배포 완료

Shadow Mode: 토큰 선배포 전략

본격적인 전환 전, Phase 1에서는 토큰을 "발급만" 하고 실제 인증에는 사용하지 않았습니다. 기존 세션 기반 인증은 그대로 유지하면서, 백그라운드에서 OliveToken(Access Token, Refresh Token)을 함께 발급하는 방식입니다. 즉, 기존 세션과 새로운 토큰이 공존하는 상태에서 안정성을 먼저 검증한 것입니다. 이 전략의 핵심 목적은 아래 두 가지였습니다.

  • 성능 검증: 토큰 발급 로직이 실제 트래픽에서 안정적으로 동작하는지 확인
  • 토큰 선배포: 최대한 많은 사용자에게 미리 토큰을 발급

토큰 선배포가 중요한 이유는, 나중에 세션에서 토큰 기반으로 전환할 때 토큰이 없는 사용자는 로그아웃되기 때문입니다. 미리 사용자의 "주머니"에 토큰을 넣어두면, 실제 전환 시점에 자연스럽게 로그인이 유지됩니다.


롤아웃 전략은 다음과 같습니다.

  1. 점진적 확대: 10% → 20% → 50% → 75% → 100%

    • 각 단계마다 2-14일의 안정화 기간 확보
    • 50% 이전까지는 신중하게 진행 (2주간 20% 유지)
  2. 단계별 목적

    • (임직원 대상): 내부 검증
    • 10%: 기본 동작 검증
    • 20%: 초기 사용자 피드백
    • 50%: 대규모 트래픽 검증, 성능 이슈 확인
    • 75%: 올영세일 전 최종 검증
    • 100%: 전체 전환
  3. 최종 목표: 올영세일 대비

    • 100% 전환 완료 후 2주 버퍼를 두고 올영세일 진입
    • 평시 트래픽에서 100% 안정성 확인 = 올영세일 준비 완료
    • 올영세일 = 실전 대규모 트래픽 검증
08/16 - 100% 배포 완료
  ↓
08/16~28 - 2주간 안정화
  ↓
08/29 - 올영세일 시작 (평소 대비 10배 트래픽)

올영세일 기간 중 실전 트래픽에서 새로운 인증 체계를 검증했습니다. 평소 대비 10배의 트래픽 상황에서도 안정적으로 작동했습니다.

올영세일 기간 Latency 메트릭
그림 3-1. 올영세일 기간 Latency 모니터링 (P50/P75/P95)

올영세일 기간 Request Volume 메트릭
그림 3-2. 올영세일 기간 요청량 추이

올영세일 기간 Error Rate 메트릭
그림 3-3. 올영세일 기간 에러율 (0% 유지)

실제 운영 메트릭 (2025.08.29-09.04, 올영세일 기간)입니다.

지표 측정값 비고
P50 레이턴시 약 5ms 50% 요청이 10ms 이하
P75 레이턴시 약 35ms 75% 요청이 40ms 이하
P95 레이턴시 약 50ms 95% 요청이 50ms 이하
리소스 사용률 CPU/메모리 30-35% 안정적 범위 유지
성공률 100% 전 기간 무장애 운영

이 수치들이 말해주는 핵심 성과는 명확했습니다. 세일 기간 중에도 절반의 요청이 5ms 이내에 처리되었고, 95%의 요청이 50ms 이내에 완료되는 매우 빠른 응답 속도를 유지했습니다. 리소스 사용률은 30-35% 수준을 유지하며 여유 있는 운영이 가능했고, 100% 성공률로 전 기간 무장애 운영을 달성했습니다. 약 5ms, 약 35ms, 약 50ms 수준의 레이턴시가 전 기간 동안 일관성을 유지하며 안정적인 성능을 보여주었습니다.


결국 Feature Flag 위임 패턴의 진정한 가치는 이 순간에 드러났습니다. 코드 배포 없이 런타임에서 전환할 수 있었고, 문제가 생기면 즉시 롤백할 수 있었으며, 사용자별로 세밀하게 제어할 수 있었습니다. 무엇보다 중요한 것은 평소 대비 10배의 트래픽이 발생하는 세일 기간에도 안전하게 전환을 완료할 수 있었다는 점입니다.


점진적 롤아웃으로 안전하게 전환을 완료했지만, 대규모 트래픽 환경에서 또 다른 최적화 포인트를 발견했습니다.


2.4 Jitter 도입 (Peak TPS 40% 감소)

Feature Flag로 점진적 전환을 준비했지만, 또 다른 문제가 남아있었습니다. 대규모 동시 접속 시 토큰 갱신 요청이 특정 시점에 집중되는 현상이었습니다. 세일 기간 중 넷퍼널(Net Funnel) 대기 상황을 예로 들어보겠습니다.

09:00:00 - 넷퍼널 오픈, 5,000명 동시 입장
         → 5,000건 로그인 요청 (초당 5,000 TPS)

09:05:00 - 정확히 5분 후, Access Token 만료
         → 5,000건 토큰 갱신 요청 (초당 5,000 TPS)

09:10:00 - 또 다시 5분 후
         → 5,000건 토큰 갱신 요청 (초당 5,000 TPS)

토큰 갱신 요청의 시간별 집중 현상
그림 4. Access Token 만료 시점에 토큰 갱신 요청이 집중되는 문제

Access Token의 만료 시간은 5분으로 고정되어 있기 때문에, 갱신 요청이 5분 간격으로 스파이크를 만들었습니다. 이 문제를 해결하기 위해 Jitter(무작위 지연)를 적용했습니다. 토큰 만료 시간에 ±30초의 랜덤 값을 추가하는 방식입니다.


핵심 아이디어는 간단합니다. 각 토큰마다 독립적인 랜덤 만료 시간을 부여하되, 설정을 통해 활성화/비활성화할 수 있게 했습니다. 사용자 경험에는 영향이 없습니다. ±30초는 사용자가 인지할 수 없는 수준이기 때문입니다.


그렇다면 Jitter 적용의 효과는 어땠을까요? 아래 그래프에서 확인할 수 있습니다. 막대 그래프는 Access Token이 고정 5분일 때 Refresh API에 가해지는 부하이고, 선 그래프는 Jitter(±30초)가 적용되었을 때의 부하입니다.


Jitter 적용 전후 TPS 비교
그림 5. Jitter 도입으로 Peak TPS가 40% 감소한 모습

결과를 정리하면 다음과 같습니다.

지표 Jitter 없음 Jitter 적용 개선
Peak TPS 5,000 3,000 -40%
평균 TPS 1,000 1,000 동일
요청 분산 5분 간격 집중 1분 간격 분산
시스템 안정성 주기적 부하 평준화된 부하

Jitter 도입의 가치는 분명했습니다. 순간 최대 부하가 5,000 TPS에서 3,000 TPS로 40% 감소했고, 급격한 스파이크가 제거되어 부하가 평준화되었습니다. Auto Scaling이 대응할 시간을 확보함으로써 시스템 안정성이 크게 향상되었으며, ±30초의 차이는 사용자가 전혀 인지할 수 없는 수준이어서 사용자 경험에는 영향을 주지 않았습니다. 이러한 Jitter 적용을 통해 세일 기간과 같은 고부하 상황에서도 안정적인 서비스를 제공할 수 있었습니다.


2.5 Resilience4j 장애 격리 (Circuit Breaker)

OAuth2 전환 과정에서 또 하나의 중요한 고려사항이 있었습니다. 부하 최적화와 함께, 외부 서비스 장애에 대한 대비도 필요했던 거죠. 실제로 Authorization Server에 장애가 발생하면 어떻게 될까요? 토큰 발급과 갱신이 모두 외부 서버에 의존하는 구조에서, 해당 서버의 장애가 전체 서비스 중단으로 이어질 수 있었습니다.


OAuth2 인증 흐름은 로그인할 때 Token 발급을 요청하고, 토큰을 갱신할 때 Refresh를 요청하며, 로그아웃 시 Token 폐기를 요청하고, 로그인 기기를 조회할 때 Active Device 조회를 요청합니다. 모든 단계에서 Authorization Server를 호출합니다. 만약 이 서버가 느려지거나 응답하지 않으면 어떻게 될까요? 요청이 무한정 블로킹되어 스레드가 고갈되고, 하나의 장애가 전체 시스템으로 전파되며, 결국 로그인이 불가능해져 전체 커머스 기능이 마비됩니다.


이 문제를 해결하기 위해 Resilience4j의 Circuit Breaker를 도입했습니다. Circuit Breaker는 전기 회로의 차단기처럼 작동합니다. 정상 상태에서는 모든 요청을 통과시키다가, 실패율이 50%를 초과하면 회로를 열어(OPEN) Authorization Server 호출을 차단하고 즉시 실패를 반환합니다. 30초 대기 후에는 반열림 상태(HALF_OPEN)로 전환되어 일부 요청만 허용해 서버 회복을 확인하고, 성공하면 다시 정상 상태(CLOSED)로 돌아가 정상 운영을 재개합니다.


Circuit Breaker 상태 전환 다이어그램
그림 6. Circuit Breaker의 3가지 상태(CLOSED, OPEN, HALF_OPEN)와 전환 조건

실제 설정 예시를 살펴보겠습니다.

resilience4j:
  circuitbreaker:
    instances:
      OYAuthorizationApi:
        # 실패율 임계값
        failureRateThreshold: 50              # 50% 이상 실패 시 OPEN
        slowCallRateThreshold: 50             # 50% 이상 느린 호출 시 OPEN
        slowCallDurationThreshold: 3000       # 3초 이상을 "느림"으로 간주

        # 측정 윈도우
        slidingWindowType: COUNT_BASED        # 호출 횟수 기반
        slidingWindowSize: 100                # 최근 100번 호출 기준
        minimumNumberOfCalls: 10              # 최소 10번 호출 후 판단

        # 상태 전환
        waitDurationInOpenState: 30000            # OPEN 상태 30초 유지
        permittedNumberOfCallsInHalfOpenState: 5  # HALF_OPEN에서 5번 테스트

        # 자동 상태 전환
        automaticTransitionFromOpenToHalfOpenEnabled: true

실제 코드로 구현할 때는 @CircuitBreaker 어노테이션과 함께 @TimeLimiter, @Retry를 조합하여 다층 보호 체계를 구축했습니다.

@Service
public class AuthorizationServerClient {

    @CircuitBreaker(name = "authorizationServer", fallbackMethod = "fallbackResponse")
    @TimeLimiter(name = "authorizationServer")
    @Retry(name = "authorizationServer")
    public ResponseEntity<TokenResponse> callAuthorizationServer(TokenRequest request) {
        // Authorization Server 호출 (Circuit Breaker 보호)
        return restTemplate.postForEntity(
            authServerUrl + "/oauth2/token",
            request,
            TokenResponse.class
        );
    }

    // Circuit Breaker OPEN 시 또는 예외 발생 시 실행
    private ResponseEntity<TokenResponse> fallbackResponse(
            TokenRequest request,
            Exception exception) {

        log.warn("Circuit breaker activated: {}", exception.getMessage());

        // Fallback 전략에 따라 처리

        return someFallbackMethod(request);
    }
}

이 코드가 실제로 어떻게 동작하는지 살펴보겠습니다. 정상 상황에서는 Authorization Server를 정상적으로 호출합니다. 그러다가 최근 100번의 호출 중 50번 이상이 실패하면 Circuit이 OPEN 상태로 전환되고, 이후 들어오는 모든 요청은 Authorization Server를 호출하지 않고 즉시 fallback 메서드로 처리됩니다. 30초가 지나면 자동으로 일부 요청만 허용해 서버 상태를 확인하고, 정상이면 다시 모든 트래픽을 통과시킵니다.


하지만 Circuit Breaker만으로는 충분하지 않았습니다. TimeoutRetry를 함께 적용해야 했습니다.

resilience4j:
  timelimiter:
    instances:
      OYAuthorizationApi:
        timeoutDuration: 3s        # 3초 타임아웃

  retry:
    instances:
      OYAuthorizationApi:
        maxAttempts: 2             # 최대 2회 시도 (원본 1회 + 재시도 1회)
        waitDuration: 100          # 재시도 간격 100ms
        retryExceptions:
          - java.net.SocketTimeoutException
          - java.net.ConnectException

이 세 가지 보호 장치가 어떻게 협력하는지 살펴보겠습니다.

Request
  ↓
[Timeout: 3초] ─────→ 3초 초과 시 실패
  ↓
[Retry: 최대 2회] ───→ 네트워크 오류 시 재시도
  ↓
[Circuit Breaker] ───→ 반복 실패 시 차단
  ↓
Authorization Server

요청이 들어오면 먼저 Timeout이 3초 제한을 걸고, 네트워크 오류가 발생하면 Retry가 최대 2회까지 재시도하며, 그래도 계속 실패하면 Circuit Breaker가 회로를 차단합니다. 이렇게 3단계 보호 체계를 구축함으로써 단일 실패 지점을 제거하고 시스템 안정성을 확보했습니다.


이 시스템이 어떻게 작동했는지는 2025년 9월에 발생한 실제 사례를 통해 확인할 수 있었습니다. 당시 Authorization Server의 응답 시간이 정상 상태인 3초에서 10초 이상으로 급증하는 장애가 발생했습니다. Circuit Breaker는 실패율이 50%에 도달하자 즉시 OPEN 상태로 전환되었고, 이후 들어오는 모든 요청은 Authorization Server를 호출하지 않고 Fallback 전략으로 처리되었습니다. Fallback 전략은 Graceful Degradation 모드로 동작해 기존 세션 인증 방식으로 자동 대체되었고, 사용자는 서비스 중단 없이 계속 이용할 수 있었습니다. 30초 후 Circuit Breaker는 HALF_OPEN 상태로 전환되어 서버 정상화를 확인했고, 정상 응답을 받자 다시 CLOSED 상태로 돌아가 정상 운영을 재개했습니다. 전체 영향은 약 2분간 일부 요청이 Fallback으로 처리된 것뿐이었고, 실제 서비스 중단은 없었습니다.


이 사례가 보여주는 핵심 가치는 명확합니다. 외부 서버 장애가 전체 서비스로 전파되지 않았고, 자동 복구 덕분에 수동 개입이 필요하지 않았으며, 사용자는 어떤 영향도 느끼지 못했습니다.


결국 Resilience4j를 도입한 가장 큰 성과는 장애 격리였습니다. Authorization Server에 문제가 생겨도 전체 서비스로 확산되지 않았습니다. 빠른 실패 전략으로 타임아웃을 기다리지 않고 즉시 fallback으로 처리했고, 자동 회복 메커니즘 덕분에 서버가 정상화되면 자동으로 트래픽을 재개했습니다. 무엇보다 스레드 보호가 중요했습니다. 블로킹으로 인한 스레드 고갈을 방지함으로써 시스템 전체가 마비되는 최악의 상황을 막았습니다. 그리고 실시간 모니터링을 통해 장애 상황을 즉시 감지하고 대응할 수 있었습니다.


3. 마치며


6개월간의 OAuth2 전환 프로젝트를 되돌아보면, 성공의 핵심은 완벽한 기술이 아니라 안전한 접근 방식이었습니다. Feature Flag로 언제든 되돌릴 수 있는 구조를 만들고, 10%부터 시작해 점진적으로 확대하며, Circuit Breaker로 장애를 격리했습니다.


가장 중요했던 것은 완벽보다 안정성을 우선했다는 점입니다. 이론적으로 괜찮을 것이라는 가정이 아닌, 실제 트래픽으로 검증하고 작은 실패를 빠르게 경험하며 큰 실패를 예방했습니다. Authorization Server는 언제든 장애날 수 있다는 전제로 모든 안전장치를 설계했고, 그 결과 올영세일이라는 가장 중요한 순간에도 안전하게 전환을 완료할 수 있었습니다.


레거시 시스템 전환을 고민 중이라면, 안전하게 되돌릴 수 있는 구조를 먼저 만들고, 테스트 환경이 아닌 실제 사용자로 검증하며, 장애를 가정한 설계로 시작하시길 권장합니다. 올리브영의 인증 체계는 이제 OAuth2를 기반으로 OIDC 도입을 준비하며 계속 진화하고 있습니다. 이 글이 같은 고민을 하는 개발자분들께 도움이 되길 바라며 글을 마치겠습니다. 감사합니다.


참고자료

OAuth2 및 인증 표준


장애 격리 패턴

OAuth2무중단 배포Feature Flag
올리브영 테크 블로그 작성 올리브영 대규모 트래픽 레거시 시스템의 무중단 OAuth2 전환기
🚀
망고 |
Backend Developer
미래를 만들어가는 중