시니어 백엔드 면접 질문 9편 - 네트워크/보안 (5~10년차)
9편은 네트워크와 보안입니다. 면접관은 프로토콜 외우기보다 운영 중 어디서 비용이 들었고 무엇을 모니터링했는지를 봅니다.
시리즈 구성:
- 1~8편 (이전): 인프라 / 운영 / 설계 / 동시성 / 분산 / 가용성 / DB / 플랫폼
- 9편 (현재): 네트워크/보안
- 부록 (예정): 면접 마지막 5분 - 역질문 20선
1. DNS TTL
Q1. DNS TTL을 무조건 짧게 두면 좋을까요?
기대 답변: 아닙니다. 트레이드오프:
- 짧은 TTL: 장애 시 빠른 IP 전환 가능, 캐시 히트율 저하·DNS 쿼리 증가
- 긴 TTL: 캐시 효율 좋음, 장애 전파 느림
운영 패턴: 평소 TTL 5~30분, 점검·DR 직전 60초로 선제 인하 후 작업.
💡 실제 사례 보기
시나리오: 메인 리전 AWS Tokyo 장애. DR 리전(Seoul)으로 전환 시도. 평소 DNS TTL이 *3600초(1시간)*로 설정되어 있어 30분간 사용자 80%가 못 들어옴.
장애 타임라인:
| 시점 | 이벤트 |
|---|---|
| T+0 | Tokyo 리전 네트워크 장애 |
| T+2분 | 모니터링 알람, DR 결정 |
| T+5분 | Route 53 가중치 변경 (Tokyo 0%, Seoul 100%) |
| T+5분~35분 | 사용자 80%가 여전히 Tokyo IP로 접속 (DNS 캐시) |
| T+35분 | 대부분 캐시 만료, Seoul로 전환 완료 |
근본 원인:
- TTL 3600초 설정, 다양한 ISP·브라우저가 최대 1시간 캐시
- 일부 브라우저는 DNS pinning으로 추가 캐시
- 사용자 단말 OS 캐시까지 합치면 30분~1시간
개선 — DNS 운영 정책 재설계:
평소 TTL:
- 메인 도메인: 300초 (5분) — DR 대응 가능 시간
- 정적 자산(CDN): 3600초 — 캐시 효율 우선
DR 직전 선제 인하:
- 계획된 점검 24시간 전 TTL을 60초로 낮춤
DNS 외 안전망:
- L7 로드밸런서 헬스체크 + 자동 페일오버 (DNS 무관, 즉시 작동)
- AWS Global Accelerator (Anycast IP, 즉시 전환)
브라우저 캐시 회피 — Anycast:
- 같은 IP에 접속해도 BGP가 가장 가까운 리전으로 라우팅
- 리전 장애 시 BGP가 자동 전환 (DNS 변경 불필요)
결과 (다음 DR 훈련):
- 실제 DR 전환 시간: 30분 → 90초
- Global Accelerator 도입으로 DNS 의존도 자체 감소
교훈:
- DNS TTL 결정은 순수 DNS 문제가 아니라 DR 전략의 일부
- 진짜 빠른 페일오버는 DNS가 아닌 LB 또는 Anycast가 답
- DR 훈련에서 실제 전환 시간을 측정해 TTL 정책 검증
🔄 꼬리질문 1: 브라우저 DNS 핀닝의 함정은?
기대 답변:
- 일부 브라우저(Chrome 등)는 자체 캐시를 60초 이상 유지
- DNS 변경해도 사용자 화면에서 즉시 반영 안 됨
- → 장애 대응은 DNS만 의존하지 말고 L4/L7 로드밸런서 레벨에서 전환
📋 실제 사례
상황: DR 전환 시 DNS TTL을 60초로 짧게 줄였는데도, *일부 사용자(주로 Chrome)*가 변경 후 5분 넘게도 옛 IP로 접속해 장애 리전에 계속 붙음. “TTL 60초인데 왜 5분이나?”
진단 — 브라우저 DNS 핀닝:
- Chrome 등은 자체 DNS 캐시를 OS·TTL과 별개로 유지 (보통 60초+, 연결 재사용 시 더 길게)
- HTTP keep-alive 커넥션은 이미 맺은 연결을 계속 재사용 → DNS 변경 무관하게 옛 IP 유지
- TTL을 줄여도 브라우저·커넥션 레벨 캐시는 못 건드림
조치 — DNS 의존 탈피:
- 장애 전환을 DNS가 아니라 L7 LB / BGP Anycast 레벨에서 수행
- Anycast는 같은 IP라 브라우저 캐시·핀닝과 무관하게 BGP가 트래픽을 다른 리전으로 라우팅
결과:
- 브라우저 핀닝 영향 0 (IP가 안 바뀌므로)
- DR 전환이 브라우저 캐시 잔존 없이 즉시 반영
교훈: 브라우저 DNS 핀닝·keep-alive 때문에 DNS TTL을 줄여도 일부 사용자는 옛 IP에 잔존한다. 진짜 빠른 전환은 DNS 레벨이 아니라 L7 LB / BGP Anycast(같은 IP 유지) 에서 해야 브라우저 캐시를 우회한다.
🔄 꼬리질문 2: 네거티브 캐시는 뭔가요?
기대 답변:
- 응답이
NXDOMAIN이면 그 결과도 캐시됨 (SOA의 minimum TTL) - 새 도메인 추가 직후 일정 시간 못 찾는 현상이 여기서 나옴
- 회피: SOA TTL을 미리 짧게 설정
📋 실제 사례
상황: 신규 서비스 도메인(api-v2.example.com)을 추가했는데, 일부 사용자는 추가 직후 1시간 가까이 “도메인을 찾을 수 없음” 에러. 다른 사용자는 바로 접속됨. 일관성 없는 현상에 혼란.
진단 — 네거티브 캐시:
- 도메인 추가 전에 그 도메인을 조회한 resolver들이
NXDOMAIN(없음) 응답을 캐시 - 네거티브 캐시 TTL = SOA 레코드의 minimum TTL(1시간으로 설정돼 있었음)
- 그래서 도메인을 추가해도 resolver가 캐시된 NXDOMAIN을 1시간 유지 → “못 찾음”
조치:
- SOA의 minimum TTL을 미리 60초로 낮춤 (네거티브 캐시 윈도우 축소)
- 신규 도메인은 충분히 미리 추가 (홍보·배포 전에)
결과:
- 신규 도메인 추가 후 네거티브 캐시 잔존 1시간 → 1분
- “일부만 못 접속” 현상 소멸
교훈: 없는 도메인 조회 응답(NXDOMAIN)도 캐시된다 (네거티브 캐시, SOA minimum TTL 기준). 도메인을 추가 전에 조회했던 resolver는 캐시된 “없음”을 유지해 일시적으로 못 찾는다. SOA minimum TTL을 미리 짧게 두는 게 회피책.
🔄 꼬리질문 3: 운영에서 어떤 지표를 보나요?
기대 답변:
- DNS 쿼리 QPS와 캐시 hit ratio
- Resolver 응답 시간 p99
- 변경 후 새 IP로의 트래픽 비율 (CDN/LB 로그로 추적)
📋 실제 사례
상황: DR 훈련에서 DNS를 전환한 뒤 “전환이 완료됐는지” 를 체감·추정으로만 판단. 실제로는 얼마나 많은 사용자가 새 IP로 옮겨갔는지 객관적 지표가 없어 “이제 됐나?” 불확실.
조치 — 트래픽 비율로 전환 진행률 측정:
- Route 53 query log + CloudWatch 로 DNS 쿼리 추적
- L7 LB 로그에서 신규 IP(Seoul) 도달 트래픽 비율 을 실시간 계산
- 전환 후: 5분 50% → 30분 95% → “95% 도달 = 전환 사실상 완료” 객관 판정
결과:
- DR 전환 진행률을 그래프로 실시간 확인 (체감 아님)
- “언제 옛 리전을 완전히 내려도 되는지” 데이터 기반 결정
교훈: DNS 전환 효과는 체감이 아니라 트래픽 비율로 측정한다. Route 53 query log + LB 로그에서 “신규 IP 도달 트래픽 비율” 을 추적하면 전환 진행률이 객관적으로 보인다. 이게 있어야 옛 리전을 안전하게 내리는 시점을 판단할 수 있다.
2. Refresh Token Rotation
Q2. Refresh Token 탈취에 어떻게 대비하시나요?
기대 답변: Token Rotation — 갱신 요청마다 새 refresh token을 발급하고 이전 토큰은 무효화합니다.
- 이미 사용된 토큰이 다시 들어오면 탈취 의심
- 해당 사용자의 토큰 패밀리 전체 무효화 + 강제 로그아웃
- 무효화 이력은 Redis로 빠르게 조회
💡 실제 사례 보기
시나리오: 보안팀이 VPN 우회 패턴 사용자 12명 발견. Refresh token 탈취 의심.
탐지 — Token Rotation 추적:
data class RefreshTokenRecord(
val tokenId: String,
val familyId: String, // 같은 로그인 세션의 모든 토큰이 공유
val userId: Long,
val usedAt: Instant?, // 사용되면 기록
)
fun refresh(oldToken: String): TokenPair {
val record = tokenStore.get(oldToken) ?: throw InvalidTokenException()
// 🚨 이미 사용된 토큰이 다시 들어옴 → 탈취 의심
if (record.usedAt != null) {
invalidateFamily(record.familyId) // 패밀리 전체 무효화
throw TokenReusedException()
}
record.markUsed()
return TokenPair(newAccess, newRefresh)
}실제 탐지:
| 시점 | 이벤트 |
|---|---|
| T+0 | 정상 사용자 로그인, RT-1 발급 (familyId=F1) |
| T+30분 | 정상 갱신: RT-1 사용 → RT-2 발급, RT-1 markUsed |
| T+45분 | 공격자가 RT-1 재사용 → reuse 탐지 |
| T+45분 | F1 전체 무효화 — RT-2도 죽음, 강제 로그아웃 |
Race Condition 처리 (정상 사용자 여러 탭):
- 갱신 후 짧은 grace window(5초) 동안 동일 응답 반환
- BroadcastChannel API로 탭 간 새 토큰 공유
저장 위치 — XSS 방어:
- 브라우저: HttpOnly + Secure + SameSite=Strict 쿠키
- LocalStorage 절대 금지
결과:
- 탈취 의심 12건 모두 자동 차단 + 정상 사용자 강제 재로그인
- 보안 사고 보고서 0건 (3개월)
교훈:
- Token Rotation은 완벽한 탈취 방지가 아니라 피해 최소화 메커니즘
- Family 개념이 정상 사용자와 공격자 모두 차단하는 명확성의 핵심
- false positive 발생 시 grace window + 사용자 친화 UX 필요
🔄 꼬리질문 1: 정상 사용자와 탈취자를 어떻게 구분하나요?
기대 답변:
- 같은 refresh token이 두 번 사용되면 둘 중 하나는 탈취
- 어느 쪽이 정상인지 알 수 없으므로 양쪽 모두 무효화가 안전
- 디바이스 fingerprint(브라우저·IP·OS)로 보조 판단 가능하지만 결정적 증거 아님
📋 실제 사례
상황: Refresh token 재사용 탐지로 12명의 탈취 의심을 잡았는데, “둘 중 누가 진짜 사용자인지”를 판별하려 디바이스 fingerprint·IP로 정상 추정을 시도했다가 오히려 공격자를 정상으로 오판할 뻔함.
진단:
- 같은 RT가 두 번 사용 = 둘 중 하나는 탈취자인데, 어느 쪽이 진짜인지 확정할 방법이 없음
- fingerprint·IP는 위조 가능 → 보조 신호일 뿐 결정적 증거 아님
- “정상을 골라내려다” 공격자를 통과시키는 위험
조치 — 모호하면 양쪽 모두 차단:
- 누가 정상인지 판별하려 하지 않고, 토큰 패밀리 전체를 무효화 + 양쪽 모두 강제 로그아웃
- 정상 사용자는 다시 로그인하면 끝 (불편하지만 안전), 공격자는 새 인증 불가
- fingerprint는 사후 분석·알림용으로만 활용 (차단 판단엔 안 씀)
결과:
- 12건 모두 안전 처리 (공격자 통과 0)
- 정상 사용자는 재로그인 한 번의 불편만
교훈: Refresh token 재사용 시 “누가 정상인지” 판별하려 들면 공격자를 통과시킬 위험이 있다. 모호하면 보수적으로 — 양쪽 모두 무효화가 안전하다. 정상 사용자의 재로그인 불편 < 공격자 통과 위험. fingerprint는 보조 신호일 뿐 차단 근거가 못 된다.
🔄 꼬리질문 2: Rotation 도입 후 UX 영향은?
기대 답변:
- 정상 사용자도 약간의 race(여러 탭에서 동시 갱신)로 강제 로그아웃 가능
- 완화: 갱신 후 짧은 grace 윈도우 동안 구 토큰 허용
- 동일 토큰 동시 요청은 첫 응답을 다른 탭에 공유(BroadcastChannel)
📋 실제 사례
상황: Token Rotation 도입 직후, 정상 사용자의 강제 로그아웃이 일 30건 발생. 조사해보니 탈취가 아니라 여러 탭을 열어둔 사용자가 피해자였음.
진단 — 멀티 탭 동시 갱신 race:
- 사용자가 탭 3개를 열어둠 → access token이 동시에 만료
- 3개 탭이 동시에 같은 RT로 갱신 요청 → 첫 요청은 성공(RT 소비), 나머지 2개는 “이미 사용된 RT”로 탐지 → reuse로 오인 → 강제 로그아웃
- 정상 사용자를 공격자로 오판
조치 — Grace Window + 탭 동기화:
- 갱신 후 짧은 grace window(5초) 동안 구 RT 재사용을 허용 (같은 응답 반환) → 멀티 탭 race 흡수
- BroadcastChannel API로 한 탭이 갱신한 새 토큰을 다른 탭에 공유 → 애초에 중복 갱신 안 하게
결과:
- 정상 사용자 강제 로그아웃 일 30건 → 0건
- 보안(탈취 차단)은 그대로 유지 (grace window는 5초로 짧음)
교훈: Token Rotation은 멀티 탭 동시 갱신을 탈취로 오인해 정상 사용자를 쫓아낸다. Grace window(짧게) + BroadcastChannel(탭 간 토큰 공유) 로 race를 흡수해야 한다. 보안 강화는 UX false positive까지 챙겨야 사용자가 떠나지 않는다.
🔄 꼬리질문 3: Refresh Token을 어디에 저장하나요?
기대 답변:
- 브라우저: HttpOnly + Secure + SameSite=Strict 쿠키
- 모바일: Keychain(iOS) / EncryptedSharedPreferences(Android)
- LocalStorage는 XSS에 노출되므로 금지
📋 실제 사례
상황: 레거시 SPA가 Refresh Token을 LocalStorage에 저장. 어느 날 서드파티 분석 스크립트의 공급망 공격(XSS) 으로 LocalStorage가 읽혀 다수 사용자 토큰이 탈취되는 보안 사고.
진단 — LocalStorage는 XSS에 무방비:
- LocalStorage는 JavaScript로 자유롭게 읽힘 → XSS 한 번이면 토큰 전부 노출
- 서드파티 스크립트(분석·광고)가 같은 출처에서 실행되면 LocalStorage 접근 가능
- 토큰이 오래 유효한 Refresh Token이라 피해가 큼
조치 — HttpOnly 쿠키로 이전:
- Refresh Token을 HttpOnly + Secure + SameSite=Strict 쿠키로 저장 → JavaScript에서 읽기 불가(XSS 무력화)
- CSRF는 SameSite=Strict + CSRF 토큰으로 방어
- 모바일은 Keychain/EncryptedSharedPreferences (OS 보안 저장소)
- 사고 토큰 전체 로테이션 + Rotation 도입
결과:
- XSS로 토큰을 읽을 수 없게 됨 (HttpOnly)
- 이후 유사 공급망 공격에도 토큰 안전
교훈: LocalStorage 토큰은 XSS 한 번이면 전부 털린다 (JS로 읽힘). Refresh Token은 HttpOnly + Secure + SameSite 쿠키(JS 접근 불가)에 두어야 XSS를 무력화한다. 모바일은 OS 보안 저장소. “편하다고 LocalStorage”는 보안 부채.
3. JWT vs Session
Q3. JWT와 세션 중 어느 쪽이 좋은가요?
기대 답변: 도메인에 따라 다릅니다.
- JWT (stateless): 서버 확장 용이, 토큰 자체에 정보 → 즉각 권한 회수 어려움
- Session (stateful): 즉각 무효화 가능, 서버에 상태 저장 부담
JWT는 짧은 access + Refresh Rotation, Session은 Redis 클러스터로 보완하는 게 일반적입니다.
💡 실제 사례 보기
시나리오: SNS 서비스가 JWT (TTL 24시간)로 시작. 1년 후 악성 사용자 즉시 차단이 비즈니스 요구사항으로 등장. JWT 구조의 한계 직면.
JWT 구조의 한계:
- 토큰 발급 후 서버는 무효화할 수 없음 (검증만 가능)
- 24시간 TTL 만료까지 계속 유효
- 차단 요구사항: 5분 이내 무효화
옵션 검토:
옵션 1 — JWT TTL을 5분으로 단축:
- Refresh 호출 부하 24배 증가
- 단점이 너무 큼
옵션 2 — Blacklist 도입 (JWT 유지):
- 차단된 토큰 ID를 Redis Set에 보관
- 모든 API에서 blacklist 조회 → stateless 이점 사라짐
옵션 3 — Session 회귀 (선택):
- Redis 클러스터에 세션 저장
- 차단 시 세션 한 키 삭제로 즉시 무효화
Session + 로컬 캐시 최적화:
// 1) 로컬 캐시 (Caffeine, 30초)
var session = localCache.getIfPresent(sessionId)
// 2) Redis (캐시 미스)
if (session == null) {
session = redisSessionStore.get(sessionId) ?: throw UnauthorizedException()
localCache.put(sessionId, session)
}
// 차단 시 — Pub/Sub로 로컬 캐시 무효화 전파
fun banUser(userId: Long) {
redisSessionStore.findByUserId(userId).forEach { redisSessionStore.delete(it.sessionId) }
redisPubSub.publish("session.invalidated", ...)
}결과:
- 악성 사용자 차단 시간: 24시간 → 평균 32초 (로컬 캐시 30초)
- Redis QPS: 12만, p99 0.5ms
교훈:
- JWT는 완벽한 stateless가 매력이지만 권한 회수 불가가 치명적인 도메인이 있음
- Session도 로컬 캐시로 Redis QPS를 견딜 수 있음
- 선택 기준은 즉각 회수가 필요한가? — 필요하면 Session
🔄 꼬리질문 1: 강제 로그아웃을 JWT로 어떻게 구현하나요?
기대 답변:
- 짧은 access token TTL (5~15분) + Refresh로 보완
- Blacklist: 무효화한 token id를 Redis에 TTL만큼 보관
- 모든 API에서 blacklist 조회 → stateless 이점 약해짐
→ 즉각 회수가 핵심이면 Session이 더 적합합니다.
📋 실제 사례
상황: “JWT를 유지하면서 강제 로그아웃도 하자”며 Blacklist(차단 토큰 ID를 Redis에 저장, 모든 API가 조회)를 도입. 그런데 모든 요청마다 Redis blacklist 조회가 추가되어, “stateless라서 빠르다”던 JWT의 장점이 사실상 사라짐.
진단:
- JWT의 매력은 서버 상태 조회 없이 토큰 자체로 검증(stateless)
- Blacklist는 매 요청마다 Redis 조회를 강제 → Session과 Redis 의존도가 똑같아짐
- 결국 “JWT + Blacklist” = “Session인데 토큰 검증만 복잡한” 어정쩡한 구조
조치 — 요구사항 재평가 후 Session 회귀:
- 즉각 강제 로그아웃이 핵심 요구라면, JWT를 억지로 유지할 이유가 없음을 인정
- Session(Redis) + 로컬 캐시(30초)로 전환 → 즉각 무효화 + Redis 부하는 캐시로 흡수
- JWT는 권한 회수가 드문 외부 파트너 API에만 한정 유지
결과:
- 강제 로그아웃 요구를 자연스럽게 충족 (세션 키 삭제)
- “JWT + Blacklist”의 어정쩡함 해소
교훈: JWT에 대규모 Blacklist를 붙이면 매 요청 Redis 조회로 stateless 이점이 사라져 Session보다 나을 게 없다. 즉각 강제 로그아웃이 핵심 요구면 JWT를 억지로 유지하지 말고 Session(Redis + 로컬 캐시) 으로 가는 게 정직하다. 도구는 요구사항에 맞춰 고른다.
🔄 꼬리질문 2: JWT 페이로드에 뭘 넣어도 되나요?
기대 답변:
- 민감 정보 절대 금지 (signed지만 encrypted가 아님 — 누구나 디코드)
- 권한·역할 같은 정적 정보만
- 자주 변하는 정보는 매 갱신 시 다시 발급되도록
📋 실제 사례
상황: 편의를 위해 JWT 페이로드에 사용자 이메일·전화번호·생년월일을 넣었음. 보안 점검에서 “토큰만 디코드하면 PII가 그대로 노출된다” 는 심각한 지적.
진단 — JWT는 signed이지 encrypted가 아님:
- JWT(JWS)는 서명으로 위변조는 막지만, 페이로드는 Base64로 누구나 디코드 가능
- 즉 암호화가 아님 → 토큰을 가진 사람(또는 로그·프록시에 찍힌 토큰)은 PII를 그대로 봄
- GDPR/개인정보 관점에서 PII 노출 사고
조치 — 최소 정보 원칙:
- 페이로드에서 PII 전부 제거 →
userId+role(권한) 같은 비민감 정적 정보만 유지 - PII가 필요하면 userId로 서버에서 조회 (토큰에 안 담음)
- 정말 토큰에 민감 정보를 담아야 하면 JWE(암호화) 사용 (드묾)
결과:
- 토큰 디코드로 PII 노출 0
- 보안 점검 통과
교훈: JWT(JWS)는 서명이지 암호화가 아니다 — 페이로드는 누구나 디코드한다. 이메일·전화번호 등 PII를 절대 넣으면 안 된다. userId + role 같은 비민감 정적 정보만 담고, 민감 정보는 서버에서 userId로 조회한다.
🔄 꼬리질문 3: 분산 환경에서 세션 일관성은?
기대 답변:
- Sticky Session: LB가 같은 서버로 라우팅 — 확장성 저하
- Redis 클러스터 세션: 일반적인 선택. p99 지연·만료 정책 관리
- 세션 복제: 인스턴스 간 복제 — 노드 늘면 비용 폭증, 권장 안 함
📋 실제 사례
상황: Tomcat 내장 세션 + 세션 복제(인스턴스 간 세션 동기화)로 운영하다가, 트래픽 증가로 인스턴스를 3대 → 10대로 늘리자 세션 복제 트래픽이 폭증해 네트워크가 마비, 오히려 응답이 느려짐.
진단 — 세션 복제의 O(N²) 비용:
- 세션 복제는 모든 인스턴스가 서로의 세션을 동기화 → 노드 N개면 복제 트래픽이 N² 비례로 증가
- 10대가 되니 복제 트래픽이 실제 서비스 트래픽을 초과
- 확장할수록 더 느려지는 역설
조치 — 외부 세션 스토어:
- Tomcat 세션 복제 제거 → Redis Cluster에 세션 저장 (Spring Session)
- 각 인스턴스는 무상태 → 어느 인스턴스가 받아도 Redis 조회로 세션 확인
- Sticky Session도 불필요 (LB가 자유롭게 분산)
- Redis 부하는 로컬 캐시(30초) 로 흡수
결과:
- 세션 복제 트래픽 0 → 노드 확장이 선형적으로 동작
- 10대 → 30대로 확장해도 문제 없음
교훈: 세션 복제(인스턴스 간 동기화)는 노드 수에 O(N²)로 비용이 폭증해 확장할수록 느려진다. 분산 환경의 세션은 외부 스토어(Redis Cluster) + 무상태 인스턴스가 정석. Sticky Session은 확장성을 해치고, 세션 복제는 소규모에서만 쓴다.
4. TLS Handshake
Q4. TLS 1.3에서 round trip이 줄어든 이유는?
기대 답변:
- 1.2: ClientHello → ServerHello → 인증서 → 키교환 → Finished (2 RTT)
- 1.3: ClientHello에 키 공유 후보 포함 → ServerHello에서 즉시 키 결정 (1 RTT)
- 0-RTT (Early Data): 이전 세션 PSK로 첫 패킷에 데이터 동봉 (0 RTT)
대신 0-RTT는 재전송 공격 위험이 있어 멱등한 요청에만 허용.
💡 실제 사례 보기
시나리오: 모바일 앱 — 외부 네트워크에서 첫 페이지 로드 1.8초. 분석 결과 TLS 핸드셰이크가 600ms 차지.
기존 (TLS 1.2): 2 RTT 핸드셰이크 = 600ms (3G 환경)
개선 — TLS 1.3 + 운영 최적화:
| 단계 | 핸드셰이크 시간 |
|---|---|
| TLS 1.2 (기존) | 600ms |
| TLS 1.3 | 300ms (1 RTT) |
| TLS 1.3 + OCSP Stapling | 250ms |
| TLS 1.3 + Session Resumption | 80ms (재방문) |
| HTTP/3 0-RTT | 20ms (재방문) |
적용 항목:
ssl_protocols TLSv1.3 TLSv1.2;- Session ticket (서버 간 공유 불필요)
- OCSP Stapling (
ssl_stapling on) - 인증서 체인 최적화 (1.2KB → 750B)
0-RTT 보안:
- 첫 패킷 데이터 동봉 가능 → replay 공격 위험
- 멱등 요청(GET)만 허용, 결제·인증은 0-RTT 거부
결과:
- 모바일 첫 페이지 로드: 1.8s → 1.1s
- 재방문 사용자 체감 즉시
교훈:
- TLS는 보안 도구이지만 성능 영향이 큼
- TLS 1.3 + Session Resumption + OCSP Stapling은 기본 세팅
- 0-RTT는 멱등성을 반드시 분리
🔄 꼬리질문 1: Session Resumption의 운영 효과는?
기대 답변:
- 재연결 시 풀 핸드셰이크 생략 → CPU·지연 절감
- 서버 측: session cache vs session ticket
- 대규모 환경에선 ticket이 확장성 유리 (서버 간 상태 공유 불필요)
📋 실제 사례
상황: 대규모 API 게이트웨이 여러 대에서, 처음엔 server-side session cache로 TLS resumption을 구현. 그런데 LB가 사용자를 다른 게이트웨이로 보내면 그 캐시가 없어 resumption 실패 → 풀 핸드셰이크 반복, CPU 부담.
진단 — session cache는 서버 로컬:
- session cache는 각 서버 메모리에 저장 → 다른 서버로 가면 못 찾음
- 무상태 LB 분산 환경에선 resumption 적중률이 낮음
조치 — Session Ticket으로 전환:
- session ticket(암호화된 세션 상태를 클라이언트가 보관)으로 전환
- 모든 게이트웨이가 공유 ticket key로 복호화 → 어느 서버로 가도 resumption 성공
- ticket key는 주기적 rotation(1일)으로 보안 유지
결과:
- 재방문 핸드셰이크 80ms로 단축, 서버 CPU 25% 감소
- 어느 게이트웨이로 분산돼도 resumption 적중
교훈: Session Resumption은 server-side cache(서버 로컬) 와 session ticket(클라이언트 보관) 두 방식이 있다. 다중 서버·무상태 LB 환경에선 session cache는 적중률이 낮아, 공유 ticket key 기반 session ticket이 확장성에 유리하다. ticket key는 주기적 rotation으로 보안 유지.
🔄 꼬리질문 2: OCSP Stapling이 뭔가요?
기대 답변:
- 인증서 폐기 여부 확인을 클라이언트가 매번 OCSP 서버에 묻지 않고, 서버가 미리 받아둔 응답을 핸드셰이크에 첨부
- 사용자 지연 감소 + CA OCSP 서버 부하 감소
- nginx
ssl_stapling on같은 옵션으로 활성화
📋 실제 사례
상황: 모바일 사용자의 첫 접속 핸드셰이크가 가끔 수백 ms 추가 지연. 추적해보니 클라이언트가 인증서 폐기 확인(OCSP)을 위해 CA OCSP 서버에 별도 요청하는 시간이었고, CA OCSP 서버가 느릴 때는 더 심함.
진단:
- TLS 핸드셰이크 중 클라이언트가 “이 인증서가 폐기 안 됐나?” 를 CA의 OCSP 서버에 직접 질의
- 이 왕복이 수백 ms + CA OCSP 서버가 느리거나 장애면 핸드셰이크 전체가 지연
- 즉 우리 서버는 빠른데 CA OCSP가 병목
조치 — OCSP Stapling:
- nginx
ssl_stapling on; ssl_stapling_verify on; - 우리 서버가 미리 OCSP 응답을 받아두고, 핸드셰이크 시 인증서와 함께 첨부(staple)
- 클라이언트는 CA에 별도 질의 없이 첨부된 응답으로 폐기 확인 → 왕복 제거
결과:
- 첫 접속 핸드셰이크 200ms 절감
- CA OCSP 서버 장애와 무관하게 동작 (의존성 제거)
교훈: OCSP Stapling은 인증서 폐기 확인을 클라이언트가 CA에 묻지 않고, 서버가 미리 받아 핸드셰이크에 첨부하는 것이다. 사용자 지연 감소 + CA OCSP 서버 의존·장애 제거. ssl_stapling on만 켜면 되는 low-hanging fruit인데 안 켜면 손해다.
🔄 꼬리질문 3: mTLS는 언제 도입하나요?
기대 답변:
- 서비스 간 통신에서 서로 신뢰가 필요할 때 (예: 사내 zero-trust)
- 결제·인증 같은 고신뢰 경계
- Service Mesh(Istio, Linkerd)가 자동 mTLS 제공 — 운영 비용 크게 절감
📋 실제 사례
상황: 보안 감사에서 “내부 서비스 간 통신이 암호화·인증되지 않아, 침입자가 내부망에 들어오면 서비스를 자유롭게 호출할 수 있다” 는 zero-trust 지적. 모든 서비스에 mTLS를 코드로 넣자니 60개 서비스 작업량이 막대.
진단:
- 서비스 간 mTLS를 각 서비스 코드로 구현하면: 인증서 발급·배포·회전·검증 로직을 60번 작성·유지
- 일관성 보장도 어렵고 (한 서비스가 빠지면 구멍), 인증서 회전이 운영 지옥
조치 — Service Mesh로 자동 mTLS:
- Linkerd 사이드카 주입 → 모든 서비스 간 통신에 자동 mTLS(암호화 + 상호 인증)
- 인증서 발급·회전·검증을 Mesh가 전담 (코드 변경 0)
- 정책으로 “인증된 서비스만 호출 허용” zero-trust 강제
결과:
- 60개 서비스 코드 변경 없이 전부 mTLS 적용
- 인증서 회전 자동화 (운영 부담 0)
- 보안 감사 통과 (내부 zero-trust 입증)
교훈: mTLS를 서비스마다 코드로 구현하면 인증서 발급·회전·검증의 운영 지옥이다. Service Mesh(Istio/Linkerd)가 자동 mTLS를 코드 변경 없이 제공해 운영 비용을 거의 0으로 낮춘다. 사내 zero-trust·고신뢰 경계가 필요하면 Mesh가 정석이다.
5. ACL과 정책 엔진
Q5. 복잡한 ACL 권한 로직을 비즈니스 코드와 어떻게 분리하시나요?
기대 답변: 3축:
- PDP/PIP 분리: 정책 결정(Decision)과 정보 제공(Information)을 물리적으로 분리
- ABAC 모델: 속성 기반 평가 (역할·리소스 상태·시간 등)
- 결과 캐싱·비트마스크: 평가 결과를 1ms 미만으로 유지
💡 실제 사례 보기
시나리오: 협업 도구 — 권한 로직이 50개 컨트롤러에 if (user.role == ADMIN || resource.ownerId == user.id || ...) 형태로 산재. 신규 권한 추가마다 50군데 수정.
개선 — OPA (Open Policy Agent) 도입:
1) 권한을 Rego DSL로 분리:
package authz
default allow = false
allow {
input.action == "read"
input.resource.type == "document"
input.resource.owner_id == input.user.id
}
allow {
input.action == "read"
input.resource.shared_with[_] == input.user.id
}2) 아키텍처 — PDP/PIP 분리:
[Application] → check(user, action, resource)
[OPA Sidecar (PDP)] → fetch attrs
[PIP (User DB, Resource DB)]3) 캐싱 + 비트마스크:
- 사용자별 권한 비트마스크 미리 계산 (READ=1, WRITE=2, DELETE=4)
- Redis 캐시 (TTL 5분), 조회
(mask & required) == required - 평균 평가 0.3ms
도입 전후:
| 항목 | Before | After |
|---|---|---|
| 권한 추가 | 50개 파일 수정 | Rego 1개 수정 |
| 평가 시간 | 평균 8ms (DB) | 평균 0.3ms (캐시) |
| 정책 검토 | 코드 리뷰 분산 | Rego PR 한 곳 |
위험:
- OPA 사이드카 장애 시 인증 마비 → 로컬 fallback 정책 (기본 deny)
결과:
- 권한 관련 버그 70% 감소
- 신규 권한 도입 PR: 2일 → 4시간
교훈:
- 권한은 비즈니스 코드와 분리하는 것이 장기적으로 압도적 이득
- ABAC는 복잡도가 RBAC보다 높지만 표현력이 결정적
- 정책 엔진 도입은 코드 변경이 아니라 팀 R&R 변경에 가까움 (보안팀이 정책 책임)
🔄 꼬리질문 1: 정책 변경 시 캐시 일관성은?
기대 답변:
- 정책 변경 이벤트를 카프카로 발행 → 각 인스턴스가 캐시 무효화
- TTL은 짧게 (1~5분)
- 강한 일관성이 필요한 권한은 매번 PDP 조회 (캐시 미사용)
📋 실제 사례
상황: 권한 평가를 Redis + 로컬 캐시(5분) 로 최적화했는데, 관리자가 어떤 사용자의 권한을 박탈해도 최대 5분간 옛 권한이 유지되어 이미 박탈된 사용자가 결제 권한을 계속 행사하는 위험.
진단:
- 캐시 TTL 5분 = 권한 변경이 최대 5분 늦게 반영
- 일반 권한(문서 읽기)은 5분 지연이 무해하지만, 결제·환불 같은 민감 권한은 즉시 회수 필요
조치 — 도메인별 일관성 차등:
- 일반 권한: Redis + 로컬 캐시(5분) 유지 (성능 우선)
- 정책 변경 시 Kafka 이벤트 발행 → 모든 인스턴스가 즉시 로컬 캐시 무효화 (5초 내 반영)
- 결제·환불 등 강한 일관성 권한은 캐시 우회, 매번 PDP 직접 조회 (즉시 정확)
결과:
- 일반 권한은 캐시로 0.3ms 유지
- 민감 권한은 즉시 회수 (캐시 지연 0)
- 권한 박탈 후 결제 행사 사고 0건
교훈: 권한 캐시는 도메인별 일관성 요구가 다르다. 일반 권한은 캐시(TTL) + 정책 변경 시 Kafka 무효화, 민감 권한(결제)은 캐시 우회·매번 PDP 조회. “캐시 TTL 하나로 전부”는 민감 권한의 즉시 회수를 놓친다.
🔄 꼬리질문 2: RBAC vs ABAC, 언제 어느 쪽?
기대 답변:
- RBAC: 역할 수가 적고 안정적일 때 (관리자/일반/게스트)
- ABAC: 리소스 상태나 컨텍스트가 권한에 영향 (소유자 본인만 수정 등)
- 실무에선 보통 RBAC + 일부 ABAC 정책 혼용
📋 실제 사례
상황: “표현력이 좋다”는 이유로 처음부터 전면 ABAC를 도입했다가, 단순한 “관리자/일반” 구분까지 모든 권한을 속성 규칙으로 작성하게 되어 정책이 과도하게 복잡해지고 신규 개발자가 이해 못 함.
진단:
- 우리 권한의 90%는 단순 역할 기반(관리자는 전부, 일반은 본인 것) → RBAC면 충분
- 10%만 컨텍스트 의존(문서 소유자 본인만 수정, 영업시간에만 등) → ABAC 필요
- 전면 ABAC는 단순한 90%까지 복잡한 규칙으로 만들어 오버엔지니어링
조치 — RBAC 기본 + ABAC 선별:
- 기본은 RBAC: 역할(admin/member/guest)로 대부분 처리 → 단순·이해 쉬움
- 컨텍스트가 권한에 영향을 주는 케이스만 ABAC 정책으로 (소유자 본인 수정 등)
- 둘을 혼용 (역할로 거른 뒤 속성으로 세밀화)
결과:
- 정책 복잡도 대폭 감소 (90%는 단순 RBAC)
- 신규 개발자도 권한 구조를 빠르게 이해
- 필요한 10%만 ABAC 표현력 활용
교훈: 전면 ABAC는 단순한 권한까지 복잡하게 만드는 오버엔지니어링이다. 기본은 RBAC(역할), 컨텍스트가 권한을 좌우하는 케이스만 ABAC 로 혼용한다. “표현력이 좋다”가 “항상 써야 한다”는 아니다 — 복잡도 비용을 고려한다.
🔄 꼬리질문 3: 정책 엔진 도구는 뭐가 있나요?
기대 답변:
- OPA (Open Policy Agent): Rego DSL, 사이드카로 배포
- Cedar (AWS): 검증 도구 강력, JSON 기반
- Casbin: 가벼운 RBAC/ABAC 라이브러리
도입 시 정책 변경 빈도와 팀 학습 비용을 같이 평가.
📋 실제 사례
상황: 초기 작은 서비스에서 권한 분리가 필요해 “업계 표준”이라는 이유로 OPA를 도입하려다, Rego DSL 학습 곡선 + 사이드카 운영 부담이 작은 팀에 과하다고 판단. 도구를 규모에 맞춰 재선택.
판단 — 규모·변경 빈도로 도구 선택:
- 초기·소규모: Casbin(가벼운 라이브러리, 코드에 임베드, 별도 인프라 0) → 빠르게 시작
- 규모 성장 후: 권한 정책이 복잡해지고 보안팀이 직접 정책을 관리해야 하자 → OPA(Rego, 사이드카, 정책을 코드에서 분리)로 마이그레이션
- Cedar는 AWS 생태계 한정이라 우리 환경엔 부적합
결과:
- 초기엔 Casbin으로 빠르게 권한 분리 (오버엔지니어링 회피)
- 규모 성장 시점에 OPA로 진화 (정책-코드 분리, 보안팀 검토 가능)
교훈: 정책 엔진도 규모와 함께 진화한다. 소규모는 Casbin(가벼운 임베드), 규모 성장·보안팀 정책 관리 필요 시 OPA(Rego, 정책 분리). Cedar는 AWS 한정. 도입 기준은 정책 변경 빈도 + 팀 학습 비용이고, 처음부터 무거운 도구는 오버엔지니어링이다.
마무리: 9편 핵심 정리
- DNS TTL: 짧음/김의 트레이드오프 + 브라우저 핀닝 + 네거티브 캐시
- Refresh Token Rotation: 패밀리 전체 무효화 + grace window
- JWT vs Session: 즉각 회수 vs 확장성 + 분산 세션 전략
- TLS Handshake: 1.3 1-RTT, 0-RTT 위험, Session Resumption·OCSP stapling
- ACL 정책 엔진: PDP/PIP 분리 + 캐시 일관성 + RBAC/ABAC 혼용
다음 부록은 면접 마지막 5분에 던질 역질문 20선을 표 형식으로 정리합니다.