그순간

다중 OAuth 제공자 환경에서 DX와 UX 동시에 달성하기

김태은·2026년 7월 29일·23 조회
다중 OAuth 제공자 환경에서 DX와 UX 동시에 달성하기

사용자 경험을 향한 여정

2025년, Hello, GSM(광주소프트웨어마이스터고 입학지원시스템)의 2026학년도 신입생 모집을 준비하며 개발을 진행했습니다. 변경된 입학 요강을 반영하는 작업이 큰 축이었지만, 더 나은 사용자 경험도 목표로 삼았습니다. 서버 애플리케이션을 개발하던 중 오래전 등록된 GitHub Issue를 발견했습니다.

깃허브 이슈 스크린샷

Issue #48: 사용자가 볼 수 없는 오류

당시 OAuth 인증은 Spring Security의 관례를 따랐습니다. 클라이언트가 서버 애플리케이션의 인증 시작 엔드포인트로 페이지를 리다이렉트하면, 인증이 끝난 뒤 서버가 사전에 약속한 URI로 사용자를 되돌려보내는 구조였습니다. 쉽게 말해 '구글로 로그인' 버튼을 눌러도 클라이언트가 구글 서버와 직접 통신하지 않습니다. 클라이언트는 페이지 주도권을 서버 애플리케이션에 넘기고, 나중에 결과만 받습니다.

대부분의 상황에서는 정상으로 동작했습니다. 문제는 전년도 운영 중에 드러났습니다. 전체 지원자 300여 명 가운데 2~3명이 인증에 실패했고, 이때 서버 애플리케이션은 500 응답을 반환했습니다. 클라이언트가 HTML 형식의 응답을 요구했기 때문에 Spring Boot의 기본 오류 화면인 Whitelabel Error Page가 그대로 노출됐습니다.

원인은 하나가 아니었습니다. 순간적인 Redis 연결 실패, 서버 배포 중 발생한 짧은 다운타임처럼 모두 일시적인 문제였습니다. 각각의 발생 확률은 매우 낮았지만, 하필 사용자가 인증을 진행하던 그 짧은 순간과 겹쳤습니다. 대부분의 실패가 이런 식으로 일어났습니다.

500 응답 자체도 좋은 상황은 아니지만, 더 큰 문제는 위치였습니다. 오류가 발생한 지점이 클라이언트의 통제 밖이라 사용자에게 아무런 안내도 줄 수 없었습니다. 사용자가 엔지니어가 아니라면 할 수 있는 일이 없었습니다. 원서 작성도 아닌 로그인 단계에서 막혀 학교 행정실로 전화 문의를 하는 경우까지 나왔습니다.

인증 과정에서 발생한 오류를 클라이언트가 직접 처리할 수 있게 만드는 것, 이것이 Issue #48의 요지였습니다.

기존 코드베이스

당시 사용하던 OAuth 제공자는 구글(Google)과 카카오(Kakao) 두 종류였습니다. 코드베이스는 Spring Security의 기본 구현을 충실히 따랐고, 제공되는 클래스들을 잘 활용하고 있었습니다.

그러나 Spring Security의 기본 구현은 높은 추상화를 제공하는 대신 서버에 의존하는 시스템 아키텍처를 강요했습니다. 클라이언트는 서버 애플리케이션에 종속되는 계약을 맺고, 오류 처리의 책임은 전부 서버로 넘어갑니다. 서버가 오류를 우아하게 처리할 수 있었다면 문제가 없었겠지만, 그러지 못한다는 것이 문제였습니다.

개발자 경험(Developer Experience, DX) 측면에도 문제가 있었습니다. OAuth 인증·인가 코드는 자주 바뀌지 않지만, 바꿔야 할 때는 카카오와 구글의 인증 코드가 if 분기로만 갈라져 있었습니다.

if (oAuthProvider == OAuthType.GOOGLE) {
    loginFromGoogle(userObject);
} else {
    loginFromKakao(userObject);
}

이 구조는 직관적이지 않습니다. OAuth 제공자가 바뀌면 분기를 하나씩 제거하거나 수정하거나 추가해야 합니다. 호출부가 각 함수의 내부 구현에 의존하고, 입력값과 반환값을 강제하는 계약도 없습니다. 최악은 아니었지만 좋다고 말하기도 어려운 상태였습니다.

기존 인증 플로우 다이어그램

페이지 주도권을 다시 설계하기

서버 애플리케이션만 수정해서는 문제의 뿌리를 뽑기 어렵다고 판단했습니다. 인증이 시작될 때 요청이 서버의 엔드포인트로 향하고 페이지가 리다이렉트되는 한, 사용자는 여전히 서버 애플리케이션 안에 갇힙니다. 오류가 발생하면 클라이언트로 다시 리다이렉트하는 방법도 있지만 임시조치에 불과했습니다. 그 지점만 다른 곳과 오류 처리 방식이 달라지기 때문에 근본적인 해결책이 아니었습니다.

기존 플로우를 검토하던 중 두 가지를 확인했습니다. OAuth 제공자들은 인증 페이지를 응답하는 엔드포인트를 공개하고 있고, 서버가 사용자를 실질적으로 인증하는 시점은 Access Token을 받아오는 지점입니다.

그렇다면 인증 페이지를 띄우고 요청을 보내는 일은 클라이언트가 맡고, Authorization Code나 Access Token만 JSON REST API로 서버에 전달하도록 플로우를 재구성할 수 있습니다. 기존 구조에서는 페이지 주도권이 클라이언트에서 서버로 넘어가지만, 새 구조에서는 클라이언트가 OAuth 제공자에게만 잠시 주도권을 넘깁니다. 이후 서버와는 API 호출로만 통신하므로 오류 처리는 클라이언트가 담당하게 됩니다.

이 내용을 문서로 정리해 팀에 공유했습니다.

팀에 공유한 인증 플로우 재설계 문서

팀 내부 회의와 설득을 거쳐, 이슈를 발견한 지 한 달여 만에 작업에 착수했습니다. 클라이언트가 새 플로우를 구현하려면 서버 애플리케이션이 먼저 준비되어야 했으므로 서버부터 손댔습니다.

OAuth 제공자별 전략 분리하기

비즈니스 로직 관점에서 보면, OAuth 제공자에 따라 로직을 분기하는 일은 처리 방식을 다르게 하는 부가 요소이지 핵심 기능이 아닙니다. 이 분기가 핵심 서비스 로직에 섞여 있으면 세 가지 문제가 생깁니다. 단위 테스트가 어려워지고, 서비스가 드러나지 않은 책임을 하나 더 지게 되고, 예측하기 어려운 분기가 늘어납니다.

그래서 전략 패턴(Strategy Pattern)을 도입했습니다. 호출부가 OAuth 제공자를 넘기면 알맞은 로직이 자동으로 선택되도록 바꿨습니다.

전략 패턴은 상황에 맞는 전략(Strategy)을 선택하는 디자인 패턴입니다. n개의 로직을 각각 전략 클래스로 구현하고, 이를 감싸는 단일 진입점 인터페이스나 추상 클래스를 정의합니다. 호출부는 그 진입점만 바라봅니다.

전략 패턴 다이어그램

출처: https://hudi.blog/strategy-pattern/

먼저 모든 제공자가 지켜야 할 계약을 인터페이스로 정의했습니다. Authorization Code와 리다이렉트 URI를 받아 UserAuthInfo를 돌려주는 것이 전부입니다.

// global/security/auth/service/provider/OAuthProvider.java
public interface OAuthProvider {

    String TOKEN_PREFIX = "Bearer ";

    String getProviderName();

    AuthReferrerType getAuthReferrerType();

    UserAuthInfo authenticate(String authorizationCode, String redirectUri);
}

전략을 고르는 책임은 팩토리로 분리했습니다. Spring이 OAuthProvider 구현체를 모두 주입해주므로, 팩토리는 이를 제공자 이름으로 색인하는 일만 합니다. 제공자를 추가할 때 팩토리를 수정할 필요가 없습니다.

// global/security/auth/service/OAuthProviderFactory.java
@Component
public class OAuthProviderFactory {

    private final Map<String, OAuthProvider> providers;

    public OAuthProviderFactory(List<OAuthProvider> oAuthProviders) {
        this.providers = oAuthProviders.stream()
                .collect(Collectors.toMap(provider -> provider.getProviderName().toLowerCase(), Function.identity()));
    }

    public OAuthProvider getProvider(String providerName) {
        OAuthProvider provider = providers.get(providerName.toLowerCase());
        if (provider == null) {
            throw new ExpectedException("지원하지 않는 OAuth Provider입니다: " + providerName, HttpStatus.BAD_REQUEST);
        }
        return provider;
    }
}

각 제공자는 인터페이스를 구현하고 자기 몫의 외부 통신만 담당합니다. 카카오 구현체를 예로 들면 다음과 같습니다.

// global/security/auth/service/provider/KakaoOAuthProvider.java
@Component
@RequiredArgsConstructor
public class KakaoOAuthProvider implements OAuthProvider {

    private static final String PROVIDER_NAME = "kakao";

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final KakaoOAuth2Client kakaoOAuth2Client;
    private final KakaoUserInfoClient kakaoUserInfoClient;

    @Override
    public String getProviderName() {
        return PROVIDER_NAME;
    }

    @Override
    public AuthReferrerType getAuthReferrerType() {
        return AuthReferrerType.KAKAO;
    }

    @Override
    public UserAuthInfo authenticate(String authorizationCode, String redirectUri) {
        validateAuthorizationCode(authorizationCode);

        ClientRegistration clientRegistration = getClientRegistration();
        KakaoTokenResDto tokenResponse = exchangeCodeForToken(authorizationCode, clientRegistration, redirectUri);
        KakaoUserInfoResDto userInfo = getUserInfo(tokenResponse.accessToken());

        String providerId = extractUserEmail(userInfo);
        providerId = providerId == null ? userInfo.id().toString() : providerId;
        validateUserEmail(providerId);

        return new UserAuthInfo(providerId, PROVIDER_NAME, getAuthReferrerType());
    }

    // 이하 토큰 교환, 사용자 정보 조회, 검증 메서드 생략
}

그 결과 호출부에서 if 분기가 사라졌습니다. 서비스는 제공자 이름을 팩토리에 넘기고, 돌아온 UserAuthInfo로 인증을 마무리하는 일만 합니다.

// global/security/auth/service/OAuthAuthenticationService.java
public void execute(String provider, String code, String redirectUri, HttpServletRequest request) {
    String decodedCode = URLDecoder.decode(code, StandardCharsets.UTF_8);
    OAuthProvider oAuthProvider = oAuthProviderFactory.getProvider(provider);

    UserAuthInfo userAuthInfo = oAuthProvider.authenticate(decodedCode, redirectUri);

    completeAuthentication(userAuthInfo, request);
}

이 구조로 바꾸면서 네 가지를 얻었습니다.

첫째, OAuth 제공자를 고르는 분기가 핵심 로직에서 계층적으로 분리됐습니다. 세션 저장과 사용자 정보 upsert만으로도 이미 방대했던 인증 로직을 그만큼 줄였습니다. 특히 외부 소셜 OAuth 인증에서 반드시 발생하는 외부 네트워크 의존성을 각 전략 클래스 안에 캡슐화해 떼어낼 수 있었습니다.

둘째, 제공자별 변경이 다른 클래스로 전파되지 않습니다. 실제로 카카오 OAuth에서 파라미터 일부가 폐기(Deprecated)되어 사용할 수 없게 된 일이 있었습니다. 구글에서는 같은 필드를 아직 사용하고 있었고 제거 예정도 없었습니다. 이때 카카오 쪽 인증 코드만 수정하고 구글 쪽은 그대로 두었습니다. 호출자가 알 필요 없는 변경은 호출자에게 드러나지 않고, 다른 전략 클래스에도 영향이 없었습니다. 결합도를 효과적으로 낮췄다는 뜻입니다.

셋째, 각 전략 클래스를 독립적으로 테스트할 수 있게 됐습니다.

넷째, OAuth 제공자를 추가하거나 제거할 때 클래스를 새로 만들거나 지우기만 하면 됩니다. DX를 끌어올리고 코드베이스 품질을 확보한 지점입니다.

Spring Security 기본 구현에서 벗어나기

팀 내부에서 논쟁이 많았던 사안입니다. 리팩터링의 최종 목표가 Spring Security가 의도하는 흐름과 상당히 달라진다는 문제가 있었습니다. 잘 쓰고 있던 클래스를 대부분 활용할 수 없게 되므로, 이 방향이 맞는지에 대한 지적이 꾸준히 나왔습니다. 결국 대부분의 클래스를 직접 구현하기로 결정했습니다.

결과적으로 옳은 판단이었습니다. OAuth 제공자들이 제공하는 기술 문서는 완성도가 높고, 필요한 정보는 이미 널리 알려져 있었습니다. 직접 구현해도 표준을 충실히 지킬 수 있었습니다. 그때 만든 클래스들은 이 글을 쓰는 1년 뒤까지 쿠키 설정 일부를 제외하면 변경 없이 안정적으로 동작하고 있습니다.

다만 추상화 레이어를 버리고 직접 구현하는 과정의 진통도 만만치 않았습니다.

OAuth 제공자 통신 계층 직접 구현하기

가장 큰 난관은 OAuth 제공자와 통신하는 구조였습니다. Feign 클라이언트를 구성하고, 제공자가 돌려주는 응답을 처리하는 계층을 직접 만들어야 했습니다.

기존에도 Feign 클라이언트를 쓰고 있었지만, 입학원서가 작성되면 Discord로 알리는 정도의 기능만 담당했고 커스텀 오류 처리 핸들러는 없었습니다. 그런데 OAuth 제공자의 응답 중에는 로그에 묻혀서는 안 되는 것이 있습니다. 예를 들어 리다이렉트 URI가 올바르지 않다는 응답은 애플리케이션에 주입된 설정값에 문제가 있다는 뜻이므로 즉시 대처해야 합니다. 기존 오류 핸들러에서는 이런 응답이 로그조차 남기지 못하고 묻혔습니다.

그래서 Jackson을 주입받아 복잡한 JSON 응답을 추출하고, 로그를 남기고, 필요하면 알림을 발행하는 Decoder를 설계했습니다.

log.error("Feign 클라이언트 오류 - 메서드: {}, HTTP 메서드: {}, URL: {}, 상태: {}, 이유: {}",
        methodKey,
        httpMethod,
        url,
        status,
        response.reason());
log.error("응답 헤더: {}", headers);
log.error("응답 본문: {}", errorBody);
logRequestDetails(response, methodKey);
case 429 -> {
    userMessage = "요청이 너무 많습니다. 잠시 후 다시 시도해 주세요.";
    httpStatus = HttpStatus.TOO_MANY_REQUESTS;
}
case 500 -> {
    userMessage = "외부 서비스 내부 오류가 발생했습니다.";
    httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
}
case 502 -> {
    userMessage = "게이트웨이 오류가 발생했습니다.";
    httpStatus = HttpStatus.BAD_GATEWAY;
}
case 503 -> {
    userMessage = "서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해 주세요.";
    httpStatus = HttpStatus.SERVICE_UNAVAILABLE;
}
default -> {
    userMessage = "외부 요청 처리 중 오류가 발생했습니다.";
    httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
}

Feign 클라이언트 자체는 토큰 교환 엔드포인트를 그대로 옮긴 형태로 단순하게 유지했습니다.

// global/thirdParty/feign/client/oauth/kakao/KakaoOAuth2Client.java
@FeignClient(name = "kakao-oauth2-client", url = "https://kauth.kakao.com", configuration = FeignConfig.class)
public interface KakaoOAuth2Client {

    @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    KakaoTokenResDto exchangeCodeForToken(@RequestBody Map<String, ?> formParams);
}

간헐적인 세션 오류 해결하기

Redis에 세션을 기록하는 과정도 직접 구현했습니다. 처음에는 Spring Session의 관례에 맞춰 간단히 만들었습니다.

private void setSecurityContext(HttpServletRequest request, Authentication authentication) {
    SecurityContextHolder.getContext().setAuthentication(authentication);
    HttpSession session = request.getSession(true);
    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
    securityContext.setAuthentication(authentication);
    session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext);
}

그런데 로그인 100건당 1건 정도로 오류 알림이 발행됐습니다. 알림을 발생시킨 사용자는 인증에 아무 문제가 없었고, 영향 범위조차 가늠하기 어려웠습니다.

[http-nio-8080-exec-7] [2025-10-28 10:34:21.123] [ERROR]
[LoggingFilter.java:87] - LoggingFilter의 FilterChain에서 예외가 발생했습니다.
java.lang.IllegalStateException: creationTime key must not be null
	at org.springframework.session.data.redis.RedisSessionMapper.handleMissingKey(RedisSessionMapper.java:62) ~[spring-session-data-redis-3.5.2.jar!/:3.5.2]
	at org.springframework.session.data.redis.RedisSessionMapper.apply(RedisSessionMapper.java:72) ~[spring-session-data-redis-3.5.2.jar!/:3.5.2]
	at ...

OAuth 인증과 직접 관련이 없는 곳에서 OAuth 세션 키의 creationTime 필드가 누락됐다는, 이해하기 어려운 스택 트레이스가 간헐적으로 나타났습니다. 원인을 찾기 위해 여러 가지를 시도했습니다.

먼저 도메인 모델에서 null이 될 수 있는 필드를 Optional API로 방어했습니다. 비즈니스 요구사항상 null이 허용되더라도 인증 객체가 알 수 없는 값을 만나면 NullPointerException이 발생하는 것 아닌가 하는 가정이었습니다. 해결되지 않았습니다.

Role memberRole = Optional.ofNullable(member.getRole()).orElse(Role.UNAUTHENTICATED);

다음으로 세션 인증 과정에서 문제가 있는 세션을 즉시 폐기하고 재발급하도록 바꿨습니다. 팀원에게 아이디어가 신선하다는 평은 받았지만, 세션 생성 자체에 문제가 있는 한 해결책이 되지 못했습니다.

HttpSession oldSession = request.getSession(false);
if (oldSession != null) {
    try {
        oldSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
        oldSession.getCreationTime();
        oldSession.getLastAccessedTime();
    } catch (IllegalStateException e) {
        try {
            oldSession.invalidate();
        } catch (Exception ex) {
            log.error("세션 무효화 중 예외 발생", ex);
        }
        oldSession = null;
    }
}

HttpSession session = (oldSession != null) ? oldSession : request.getSession(true);

그러던 중 팀원이 관련 포스트를 찾았습니다. 세션 관리 전략을 migrateSession()으로 지정하라는 내용이었고, 적용하자 문제가 사라졌습니다.

.sessionManagement(session -> session
        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        .sessionFixation().migrateSession()
);

알고 보니 Spring Session에 공식 보고된 이슈였습니다. 우리 쪽 문제가 아니라 프레임워크 쪽 문제였습니다.

요약하면 이렇습니다. creationTime처럼 생성 시각에 따라 값이 바뀌는 필드는 수정이 잦습니다. 같은 세션 키에 쓰기 요청이 동시에 들어오면 순서대로 반영되거나 한쪽이 유실되는 것이 아니라, 어느 스레드도 키를 수정하지 못하고 값이 깨져 null이 들어갑니다. 기본 전략인 ChangeSessionIdAuthenticationStrategy를, 세션 값을 변경할 때 완전히 새 세션을 만들고 기존 값으로 채워 마이그레이션하는 전략으로 교체함으로써 해결했습니다.

인증 요청이 두 번 발생한 이유

세션 쓰기 요청이 왜 동시에 겹치는지도 확인해야 했습니다. 사용자의 중복 클릭도 있었지만, 더 큰 원인은 클라이언트 콜백 페이지에 있었습니다.

당시 증상은 이렇습니다. 올바른 Authorization Code인데도 OAuth 제공자가 인증을 거부했습니다. 일부 사용자에게 비정기적으로 발생하는 간헐적 문제였고, 이 때문에 로그인을 여러 번 시도해야 하는 불편이 생겼습니다. 문제가 유입된 프론트엔드 저장소의 PR #222와 이를 해결한 PR #367에 기록이 남아 있습니다.

원인은 콜백 페이지의 useEffect 의존성 배열이었습니다. Authorization Code를 발급받은 사용자는 콜백 페이지로 리다이렉트되는데, 이 페이지의 의존성 배열에 googleLogin, kakaoLogin, router가 들어 있었습니다. 참조하는 객체가 바뀔 때마다 서버에 인증을 다시 요청한다는 뜻입니다.

    if (provider === 'google' || provider === 'admin') {
      googleLogin(code);
    } else if (provider === 'kakao') {
      kakaoLogin(code);
    } else {
      router.replace('/');
      toast.error('로그인에 실패했습니다.');
    }
  }, [code, provider, googleLogin, kakaoLogin, router]);

구체적으로는 Next.js의 useRouter가 안정적인 참조를 보장하지 않는 것이 문제였습니다. hydration이나 네트워크 지연 때문에 최초 인증 요청 이후 router 객체가 다시 저장되면서 참조가 바뀝니다. useEffect는 참조 변경을 감지해 googleLogin이나 kakaoLogin을 다시 호출합니다. 이미 사용된 Authorization Code로 두 번째 요청이 날아가므로 OAuth 제공자가 인증을 거부하고, 같은 세션 키에는 쓰기 요청이 겹칩니다.

의존성 배열에서 불안정한 참조를 제거해 인증 요청이 한 번만 발생하도록 바꿨습니다.

프레임워크에서 벗어난 범위

이런 시행착오를 거치며 Spring Security의 영역에서 상당히 벗어나게 됐습니다. UserInfo 객체를 직접 정의하고, 세션 생명주기를 직접 관리하고, 정보 가공도 직접 처리합니다. ClientRegistrationRepository처럼 인증·인가 플로우와 직접 관련이 없고 프로퍼티 스캔 용도로 쓸 수 있는 클래스는 그대로 유지하고 있지만, 그 외 대부분의 클래스는 프레임워크 관리 밖으로 나왔습니다. 장단점이 뚜렷한 변경이었고, 득과 실을 비교해 결정한 사안입니다. 적어도 예측 가능한 인증 플로우를 만드는 데는 기여했습니다.

인증 시작 지점으로 Authorization Code 선택하기

현재 OAuth 플로우는 PKCE(Proof Key for Code Exchange) 대신 Client Secret으로 정보를 교환합니다. 로그인 아키텍처를 구상할 때, 서버의 인증 책임이 시작되는 지점을 Access Token과 Authorization Code 중에서 골라야 했습니다. 팀은 Authorization Code를 선택했고 지금까지 유지하고 있습니다.

Access Token부터 시작하는 방법에도 장점이 있었습니다. 상용 로그인 라이브러리를 쓸 수 있고, 실질적인 인증 시작 지점부터 서버의 책임이 시작됩니다. 문제는 클라이언트가 악의적인 사용자에게 언제든 노출된다는 점입니다. Next.js 서버 액션에서 관리하는 방식으로 Authorization Code 접근을 차단할 수는 있지만, 어떤 경로로든 값이 노출되면 공격자가 공개된 토큰 교환 엔드포인트로 사용자 정보를 얻을 수 있습니다. Authorization Code 노출을 막는 데 투자하기보다 처음부터 서버로 넘기는 편이 투자 대비 이득이 크다고 판단해 이렇게 결정했습니다.

Authorization Code에서 인증이 시작되면서 서버는 아래와 같은 흐름을 갖게 됐습니다. Authorization Code로 Access Token을 교환하고, 그 토큰으로 다시 사용자 정보를 가져오는 두 번의 외부 I/O를 서버가 담당합니다. 트랜잭션 분리 같은 과정에서 애를 먹었지만, 클라이언트의 책임을 지나치게 늘리지 않으면서도 클라이언트가 일정한 주도권을 갖도록 설계했습니다.

재구성한 인증 플로우 다이어그램

결과

2026학년도 신입생 모집의 입학지원 과정은 Hello, GSM으로 문제없이 진행됐습니다. 다운타임은 없었고, OAuth 인증과 관련해서는 사용자 불편 사항이나 오류 발생이 한 건도 없었습니다. 리팩터링의 성과가 수치로 드러난 셈입니다.

개발 중 Feign 클라이언트로 통신하는 OAuth 제공자 API의 스펙이 쿼리 파라미터에서 요청 본문(Request Body)으로 변경된 일이 있었습니다. 결합도를 낮춘 설계 덕분에 간단히 수정했고, 작업 시간은 3시간이 걸리지 않았습니다.

이 OAuth 플로우 설계 경험은 팀의 인증·인가 도메인 이해도를 종합적으로 높였습니다. 이후 보안 관련 작업이 크게 늘었습니다. 보안은 실제 문제가 터지기 전까지 문제가 드러나지 않는 특성이 있지만, 지금까지는 사고 없이 안정적으로 운영되고 있고 팀 전체가 사고를 미리 막기 위해 노력하고 있습니다.

고려할 사항이 많았고 여기에 기록하지 못한 해결 과정도 많았습니다. 그만큼 당시 참여한 인원 모두가 작업을 마쳤을 때 만족했던 기억이 납니다. 하나의 도메인에 깊이 몰입해보는 경험은 해볼 만하다고 단언할 수 있습니다.

목차