프로젝트 진행 중 현직자로부터 아키텍처 최적화에 관한 피드백을 받을 기회가 있었는데, 아쉽게도 해커톤이 종료되면서 서버를 반납해 실제로 적용해보지는 못했다. 하지만 현직자의 조언을 직접 받아볼 수 있었던 경험은 흔치 않다고 생각했고 기록과 공유를 위해 글을 작성하게 되었다.

당시 서버 아키텍처는 Nginx 기반 웹 서비스를 사용하는 Spring Boot 및 FastAPI 애플리케이션을 AWS ECS에서 구성하였다. AI 서버는 초기 단계라 잦은 프롬프트 수정과 장애가 예상되어 격리를 고려할 필요가 있었기에 API 서버와 별도 컨테이너로 분리하여 독립적으로 운영했다. 또한, 각 컨테이너 애플리케이션을 손쉽게 배포·관리할 수 있고, 클러스터 기반 로드 밸런싱과 자동 스케일링을 지원하여 서비스를 유연하게 운영할 수 있는 ECS를 선택하였다.
배포 과정에서 특히 어려웠던 점은, 환경 변수·볼륨·네트워크 모드 등을 올바르게 지정하지 않으면 컨테이너가 정상적으로 기동하지 않아 설정을 반복적으로 수정해야 했다는 점이다. 또한, 각 컨테이너의 로그를 통합적으로 확인할 방법이 마련되어 있지 않아 문제 발생 시 원인 파악이 어려웠다. 이를 개선하기 위해 CloudWatch와 연동해 로그를 중앙화하는 방법을 학습하게 되었다. 그 외에도 VPC, 서브넷, 보안 그룹, 로드 밸런서 설정 등 인프라를 직접 다루면서, 네트워크 정책과 포트 개방/차단에 대한 이해가 필수적임을 깨달았다. (AWS 공인 솔루션스 아키텍트 자격증 공부가 도움이 될 것 같아 관련 정보를 찾아보고 있는중이다.)
ECS 배포에 대한 자세한 내용은 다음 글에서 확인할 수 있다.
해커톤 내에서 기술 심사가 끝난 이후 현직자에게 받은 피드백 내용은 다음과 같았다.
좋았던 점
- 아키텍처에 대해 깊이 고민한 흔적이 보입니다.
- 인프라 아키텍처를 명확하게 그려 이해하기 쉬웠습니다.
- 필요한 의존성만 사용했으며, 코드 작성 수준이 생각보다 높았습니다.
개선 필요 사항
- 현재 아키텍처는 트래픽이 두 번 흐르는 구조로, 예전 현업자들이 사용하던 방식이라 더 나은 방안에 대한 고민이 필요해 보입니다.
- 각 서버의 부하를 줄이고 네트워크 통신 비효율을 막기 위해, API 서버가 JWKS를 제공하고 LLM 서버가 직접 요청을 받아 JWT를 파싱하는 구조를 제안합니다.
- 위 구조를 이해하고 적용하기 위해 OAuth2, OIDC, JWKS를 통한 JWT 검증 방법 등을 학습할 것을 추천합니다. (참고 오픈소스: Keycloak, Zitadel)
- Java 클래스 생성 시 메모리 효율성과 관심 지향 프로그래밍(AOP) 측면에서 개선할 여지가 있습니다.
- “서버를 분리한 것은 좋았으나, 더 좋은 방안이 있지 않을까?“라는 질문은 면접에서도 나올 가능성이 높으니 미리 대비하면 좋습니다.
교내 팀 프로젝트나 해커톤에서 OAuth2 관련 학습은 진행한 바 있으나, OIDC와 JWKS에 대해서는 이번에 처음 접하게 되었다. 이번 기회에 관련 개념과 활용 방법을 학습하려고 한다. 관련 오픈소스는 글 하단에 정리할 예정이다.
추가적으로, 기존에 내가 설계한 아키텍처에서 통신 비효율 문제를 고려하여 실제 실무 환경이라고 가정하고 각 서버의 부하를 최소화하는 구조를 적용하는 단계를 남기고자 한다. 구체적으로는 API 서버가 JWKS를 제공하고, LLM 서버가 직접 요청을 받아 JWT를 파싱하도록 설계하여 트래픽을 줄이고 인증 검증 과정을 효율화하는 방식이다.
OIDC란?
OAuth2.0 위에서 동작하는 인증(Identity) 프로토콜이다. 쉽게 말하면 OAuth2가 권한 부여(Authorization)에 초점을 맞춘 반면, OIDC는 사용자 인증(Authentication)을 추가한 구조라고 생각하면 된다.
- OAuth2.0: 리소스 접근 권한을 발급하는 프로토콜
- “앱 A가 내 구글 드라이브 파일을 읽어도 되나요?” → 허용/거부
- OIDC(OpenID Connect): OAuth2를 기반으로 사용자 신원을 검증하는 프로토콜
- “앱 A가 내 이메일과 이름을 알고 싶어요” → ID 토큰(ID Token)을 통해 제공
OIDC는 주로 JWT(JSON Web Token) 형식의 ID 토큰을 발급하며, 이 토큰 안에 사용자의 신원 정보가 담겨 있어 서버에서 검증 후 서비스 로그인/인증에 활용할 수 있다.
정리하면, OAuth2가 “권한”이라면, OIDC는 “권한 + 사용자 인증”
OIDC가 필요한 이유
OAuth 2.0의 제한점
OAuth2는 원래 “권한 부여(Authorization)”를 위한 프로토콜이다. 즉, 앱이 대신해서 사용자 자원(Resource)에 접근할 권한만을 얻는 구조이기 때문에 토큰(Access Token)을 발급해 접근은 허용하지만, 그 토큰이 누구(User ID)를 위한 것인지, 로그인한 사용자가 누구인지는 알 수 없다. 즉, “인증(Authentication)”이 불가능하다.
Ex. “이 앱이 내 구글 드라이브 파일을 읽을 수 있도록 허용할래?” → 접근은 허용하지만, 인증 불가
OIDC의 역할
OIDC가 추가되면,
- 사용자가 누구인지 식별 가능
- 프로필 정보, 이메일 등 기본 신원 정보 확인 가능
- “Access Token” 외에 “ID Token(JWT)”을 발급받아 신원을 검증 가능
Google 로그인, Kakao 로그인, Apple 로그인 이런 것들이 전부 OIDC 기반 인증이다.
OIDC는 OAuth2 위에 인증(Authentication) 계층을 추가한 국제 표준(OpenID Foundation 표준)이기 때문에 Google, Apple, Kakao 등 주요 로그인 시스템이 모두 동일한 인증 구조를 따른다.
OAuth2와 OIDC의 차이 (with. Kakao Developers 공식문서)
카카오는 OIDC를 지원하여 로그인 과정에서 사용자 인증(Authentication) 을 안전하게 처리할 수 있도록 돕는다.
OAuth2와 OIDC 개념이 혼동될 수 있다.
일반적으로는 카카오 로그인 후에 Access Token이 발급되며, 이를 이용해 다음과 같은 API를 호출할 수 있다.
GET https://kapi.kakao.com/v2/user/me
Authorization: Bearer {ACCESS_TOKEN}
이 요청을 통해 사용자의 닉네임, 프로필 이미지 등 자원(Resource) 에 접근할 수 있지만,
이 토큰이 누구(User) 에게 발급된 것인지, 즉 사용자가 누구인지는 보장하지 않는다.
따라서 Access Token으로 사용자 정보를 가져오는 것은 단순히 OAuth2의 확장 과정일 뿐이며, 이는 OIDC의 핵심 기능이 아니다.
그렇다면 카카오에서 제공하는 OpenID Connect(OIDC)는 무엇을 추가로 제공할까?

앞서 말했듯이 OIDC는 주로 JWT 형식의 ID 토큰을 발급한다고 했다.
카카오 역시 OIDC를 활성화할 경우 로그인 과정에서 Access Token과 함께 ID Token(JWT) 을 발급한다.
이 ID Token이 바로 OIDC의 핵심이며, 로그인한 사용자의 고유 ID(sub), 발급자(iss), 이메일 등의 정보가 담기게 된다.
{
"token_type": "bearer",
"access_token": "abcd1234...",
"expires_in": 21599,
"refresh_token": "efgh5678...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5..."
}
→ OIDC를 서비스에 적용하면 사용자의 로그인을 더 안전하게 처리할 수 있다. (“이 토큰은 카카오가 발급했고, 사용자 A가 로그인한 게 맞다” 를 신뢰할 수 있어서)
카카오가 OIDC를 권장하는 이유는, OAuth2가 제공하지 못하는 “사용자 신원 검증”과 “토큰 위변조 방지”를 JWT + JWKS 기반의 안전한 인증 구조로 보완하기 때문
JWKS란?
JWKS는 JSON Web Key Set의 약자로, JWT(JSON Web Token)를 검증할 때 사용하는 공개 키(Public Key) 정보를 JSON 형태로 모아놓은 표준 형식을 말한다.
JWT와 JWK 역할을 구분해놓고 시작하자.
- JWT(JSON Web Token)는 사용자 인증이나 정보 교환을 위해 서버가 발급하는 토큰으로, 로그인 상태 유지나 API 요청 시 사용자 인증에 사용된다.
- JWK(JSON Web Key)는 JWT의 서명을 검증하기 위한 키를 JSON 형태로 표현한 것이며, JWT가 변조되지 않았는지 확인하는 데 사용한다.
JWK vs JWKS
- JWK (JSON Web Key)
- JSON 형식으로 표현된 단일 키
- 비대칭 키 쌍의 공개 키 또는 대칭 키일 수 있음
- 키 식별자(kid), 키 타입(kty), 알고리즘(alg) 등 메타데이터 포함
- JWKS (JSON Web Key Set)
- 여러 개의 JWK를 모아둔 키 집합
- 일반적으로 HTTPS 엔드포인트를 통해 제공
- 클라이언트는 이 집합을 참고하여 JWT 검증에 사용
# JWK
{
"kty": "RSA",
"kid": "1234abcd",
"use": "sig",
"n": "...",
"e": "AQAB"
}
#JWKS
{
"keys": [
{ "kty": "RSA", "kid": "1234abcd", ... },
{ "kty": "RSA", "kid": "5678efgh", ... }
]
}
쉽게 말하면, JWK는 ‘한 장의 열쇠’, JWKS는 ‘열쇠 꾸러미’ 라고 생각하면 된다.
JWT 검증을 위해 클라이언트가 서버에서 열쇠를 받아서 확인하는 구조를 지원하는 게 JWKS이다.
JWKS Endpoint
JWKS는 보통 HTTPS를 통해 제공되며, 클라이언트나 서버가 이 공개 키 집합을 이용해 JWT의 서명을 검증할 수 있다.
OpenID Connect 표준에서는 일반적으로 아래와 같은 URL 경로를 통해 접근한다.
https://<server_domain>/.well-known/jwks.json
- <server_domain>: 인증 서버나 OAuth2/OIDC 제공자의 도메인
- /.well-known/jwks.json: 공개 키 집합(JWKS)을 제공하는 표준 경로
- 클라이언트는 이 URL을 호출해 공개 키를 가져와 JWT의 서명을 검증
JWKS 생성
정확하게는 '생성'이 아니라, JWT를 검증하기 위해 필요한 공개 키 집합(JWK Set)을 JSON 형태로 준비해 제공하는 것
Java에서는 JWKSet (MicahParks 라이브러리) 를 사용하면 RSA 키 쌍을 만들고, 공개키를 JWKS(JSON Web Key Set) 형태로 변환할 수 있다.
실제 서버에서 API 엔드포인트로 제공하지 않고 테스트나 예제용으로 다른 서버가 JWT 검증에 사용할 공개키를 JWKS 형식으로 쉽게 확인할 수 있다는 상황을 가정하여, unit test 형식으로 구현해보자.
+ 웹 상에서 JWKS를 직접 생성하고 테스트해볼 수 있는 사이트도 존재한다. → https://jwkset.com
public class JWKSUnitTest {
@Test
void createJWKSExample() throws Exception {
// 1. RSA Key Pair 생성: JWT 서명 검증에 사용할 공개키와 비공개키 쌍을 생성
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
KeyPair keyPair = keyGen.generateKeyPair();
// 2. JWK 생성: 공개키만 JWK로 변환하고 keyID를 부여
RSAKey jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.keyID("test-key-id")
.build();
// 3. JWKSet 생성: 단일 키를 포함하는 JWKS 객체 생성
JWKSet jwkSet = new JWKSet(Collections.singletonList(jwk));
// 4. JSON 확인: JWKS 형식(JSON)으로 변환하여 출력 및 테스트
String jwksJson = jwkSet.toJSONObject().toJSONString();
System.out.println("JWKS JSON:\n" + jwksJson);
// 검증
assertNotNull(jwksJson);
}
}
unit test 코드를 실행했을 때 출력되는 JWKS JSON 예시
{
"keys":[
{
"kty":"RSA",
"n":"yYVh9F5t0W0r0Tx-lqz1D58x6FQy6X2q0P9Q7zN4F9pZlRb5P4JQ4Xw6dW6dXyR7...",
"e":"AQAB",
"kid":"test-key-id"
}
]
}
- keys: 공개키 목록 (JWKS는 여러 키를 담을 수 있음)
- kty: 키 타입 (RSA)
- n: 공개키의 모듈러스(base64url)
- e: 공개키의 지수(base64url)
- kid: 키 식별자, JWT 검증 시 어떤 키를 사용할지 구분하는 용도
→ 다른 서버나 클라이언트가 이 JSON을 받아서 JWT 서명을 검증할 수 있게 된다.
JWKS 기반 아키텍처 최적화 단계 (SimHae 프로젝트 버전)
1. 상황
- API 서버: 클라이언트에게 JWT 발급
- LLM(AI) 서버: 클라이언트 요청을 받아 JWT 검증 후 처리
2. 문제
- 기존 구조: 클라이언트 요청 → API 서버를 거쳐서 JWT 검증 → LLM 서버 처리
- 이렇게 되면 트래픽이 두 번 흐르고, LLM 서버가 매번 API 서버를 호출해야 해서 서버 부하 증가와 네트워크 비효율 발생
3. 개선
- API 서버가 JWKS 제공: JWT를 검증하는데 필요한 공개키 집합을 API 서버가 엔드포인트로 제공
- LLM 서버가 직접 JWT 검증: LLM 서버가 클라이언트 요청에서 JWT를 파싱하고, JWKS에서 공개키를 가져와 서명을 검증
4. 장점
- LLM 서버가 JWT 검증을 직접 하기 때문에 API 서버 부하 감소
- 트래픽이 API 서버를 거치지 않고 바로 LLM 서버에서 검증 → 네트워크 효율 향상
- JWT 구조와 JWKS를 활용하면 보안적으로도 안전하게 검증 가능
5. 정리
- 지금 구조에서 API 서버는 JWT 발급과 JWKS 제공만 하고, LLM 서버가 “누가 보낸 JWT인지” 검증까지 직접 처리하면 전체 시스템이 더 효율적이고 안정적임
학습 이후에는 현직자 피드백을 받아들이는데에 있어서 어떤 변화가 있었는지?
JWKS란 다른 서버나 서비스가 JWT를 검증하고 싶을 때, “이 공개키들을 참고해서 서명이 맞는지 확인”하도록 제공되는 공개키 집합이였다.
→ LLM 서버가 직접 요청을 받아 JWT를 파싱하는 구조를 언급한 현직자 피드백을 통해, 여기서 말하는 ‘다른 서버’가 실제 API 요청을 처리하는 서버가 아니라 AI 요청을 받아 처리하는 LLM 서버(FastAPI)를 말씀하신 것임을 알 수 있었다.
뿐만 아니라, 내 AI 서버는 현재 JWT 검증 과정을 거치지 않도록 설계되어 있어 URL만 알면 누구나 요청하고 응답을 받을 수 있다. (AI 서버가 클라이언트 요청을 직접 받기보다는, 중간 서버(Spring Boot)를 통해 요청을 위임받아 처리하도록 설계되어 있기 때문이다.) JWKS를 사용하면 AI 서버나 다른 서비스가 공개키를 참고해 JWT 서명을 검증할 수 있어, 인증된 요청만 처리하도록 보장할 수 있다.
현직자분이 JWKS 도입을 제안하신 이유를 깨달았다...
지금까지 아키텍처 최적화 방안을 이해하고 적용하기 위해 학습한 내용을 정리했다. 공부할수록 이러한 조언이 쉽게 얻을 수 없는 값진 경험임을 느꼈고, 지금까지 몰랐거나 놓쳤던 부분이 많다는 것도 깨달았다. 앞으로도 학습을 이어가며, 실제 프로젝트에 적용할 기회가 올 때까지 꾸준히 배워나가자 💪