iOS 쿠키 격리 검증: 세션 경계와 로그아웃 정리
같은 클라우드 Mac에서 계정 전환 회귀 테스트를 실행하다가 계정 B로 로그인했는데 계정 A의 데이터가 나타난다면, 시뮬레이터부터 초기화하지 마세요. 쿠키 저장소, 요청 헤더, 아직 실행 중인 작업을 나눠서 확인해야 합니다. 이전 세션이 남는 원인은 공유 저장소일 수도 있고, 네트워크 래퍼에서 직접 넣은 요청 헤더일 수도 있으며, 로그아웃한 뒤 도착한 응답일 수도 있습니다.
먼저 macOS에 기본 제공되는 네트워크 프레임워크로 네트워크 통신 없이 검증한 다음, 같은 검증 조건을 iOS 시뮬레이터 테스트에 적용하겠습니다. 검증 코드를 저장소에 넣어 두면 클라우드 Mac에서 코드를 체크아웃할 때마다 반복 실행할 수 있으며, 이전 GUI 세션이 남긴 상태에 의존하지 않아도 됩니다.
먼저 세션의 소유 관계를 명확히 하기
로그인한 계정마다 별도의 세션 객체를 만들고, 해당 객체가 구성, 쿠키 저장소, 작업 집합을 소유하도록 하세요. 변경 가능한 구성을 전역 싱글턴으로 사용해서는 안 됩니다. 또한 원래 구성을 수정하면 이미 생성된 URLSession도 바뀔 것이라고 생각해서는 안 됩니다. 세션은 생성 시점에 구성을 복사합니다.
최소한 세 가지 조건을 명시하세요. A의 쿠키가 B의 요청에 포함되지 않아야 하고, 로그아웃할 때는 해당 계정이 소유한 데이터만 삭제해야 하며, 로그아웃 후 도착한 이전 응답이 로그인 상태를 다시 활성화해서는 안 됩니다. 익명 요청에 쿠키가 전혀 필요 없다면 자동 처리를 끄는 동시에, 래퍼 계층에서 Cookie 요청 헤더를 직접 추가하지 않는지도 확인해야 합니다.
ephemeral은 세션 데이터를 디스크에 영구 저장하지 않는다는 뜻입니다. 메모리에 쿠키가 없다는 뜻도 아니고, 애플리케이션의 두 계정이 자동으로 격리된다는 뜻은 더더욱 아닙니다.
스크립트로 저장소가 실제로 분리됐는지 확인하기
아래 내용을 cookie-check.swift로 저장하세요. 이 스크립트는 가상의 세션 값만 사용하며, 도메인은 로컬 호스트인 localhost입니다. 네트워크 요청을 보내지 않고 기본 공유 저장소에도 접근하지 않습니다.
import Foundation
func require(_ condition: Bool, _ message: String) {
if !condition {
fatalError(message)
}
}
func makeCookie(_ value: String) -> HTTPCookie {
HTTPCookie(properties: [
.domain: "localhost",
.path: "/",
.name: "session",
.value: value
])!
}
let first = URLSessionConfiguration.ephemeral
let second = URLSessionConfiguration.ephemeral
guard let a = first.httpCookieStorage,
let b = second.httpCookieStorage else {
fatalError("Cookie storage unavailable")
}
require(a !== b, "Stores must be independent")
let target = URL(string: "http://localhost/account")!
a.setCookie(makeCookie("fixture-a"))
b.setCookie(makeCookie("fixture-b"))
func values(_ store: HTTPCookieStorage) -> [String] {
(store.cookies(for: target) ?? [])
.filter { $0.name == "session" }
.map(\.value)
.sorted()
}
require(values(a) == ["fixture-a"], "Account A mismatch")
require(values(b) == ["fixture-b"], "Account B mismatch")
for cookie in a.cookies ?? [] {
a.deleteCookie(cookie)
}
require(values(a).isEmpty, "Account A was not cleared")
require(values(b) == ["fixture-b"], "Account B was affected")
명령줄 도구 초기화가 끝난 머신에서 다음을 실행하세요.
set -eu
xcrun swift cookie-check.swift
정상적으로 실행되면 출력이 없으며, 검증에 실패하면 0이 아닌 종료 코드로 끝납니다. 여기서는 a 전체를 비우는데, 이는 해당 저장소가 A 전용이라는 전제하에 가능합니다. 애플리케이션이 아직 공유 저장소를 사용한다면 먼저 쿠키의 계정별 소유 관계에 따라 대상을 골라야 합니다. 전체 삭제 코드를 그대로 적용해서는 안 됩니다.
이 스크립트가 입증하는 것은 저장소의 독립성과 삭제 범위입니다. 실제 요청에 저장소가 올바르게 연결됐다는 사실까지 입증하지는 않습니다. 이 단계의 통과를 네트워크 계층 검증 통과와 동일하게 취급하지 마세요.
실제 요청과 응답에 연결하기
애플리케이션의 테스트 타깃에서 실제 네트워크 클라이언트를 재사용하고 A와 B의 세션을 각각 생성하세요. 제어 가능한 테스트 서버가 이름은 같고 값은 다른 쿠키를 Set-Cookie로 반환하도록 합니다. 이어서 받은 쿠키를 그대로 돌려주는 에코 엔드포인트를 호출하고, 서버가 받은 쿠키가 각 계정에 해당하는지 검증하세요.
요청은 반드시 애플리케이션의 래퍼 계층을 거쳐야 하며, 테스트를 위해 임시로 인터셉터를 우회해서는 안 됩니다. 특히 기본 요청 헤더, 재시도 로직, 리디렉션 처리에서 공유 저장소를 다시 읽고 있지 않은지 확인하세요. 쿠키 자동 처리 동작을 검증할 때도 요청 헤더를 직접 채우지 마세요. 그러면 하위 계층의 연결 오류가 가려질 수 있습니다.
쿠키 속성의 적용 범위 검증하기
Path, Secure, 만료 시각에 대한 테스트 케이스를 추가하세요. 특정 경로로 범위가 제한된 쿠키는 관련 없는 경로의 요청에 포함되면 안 되고, 만료된 쿠키는 더 이상 전송되면 안 됩니다. 보안 쿠키는 제어 가능한 HTTPS 환경에서 검증해야 합니다. 로컬 HTTP 테스트 픽스처만으로는 보안 연결에서의 동작을 입증할 수 없습니다.
이름이 같은 쿠키도 도메인과 경로가 다르면 함께 존재할 수 있습니다. 따라서 저장소에 session이 항상 하나만 있다고 가정하지 말고, 이름·도메인·경로·값으로 이루어진 집합을 비교해야 합니다. 요청 헤더 문자열의 정렬 순서도 지나치게 가정하지 마세요. 문자열을 파싱한 뒤 의미상 같은지 비교해야 합니다.
로그아웃 시 지연 응답까지 처리하기
먼저 계정 상태를 ‘로그아웃 중’으로 바꾸고 새 작업 생성을 막은 다음, 기존 작업을 취소하거나 모두 완료될 때까지 기다리세요. 그 뒤 해당 계정의 쿠키를 삭제하고 이전 세션을 무효화한 후, 다음 로그인을 위한 새 객체를 생성합니다.
삭제만 수행하면 경쟁 상태가 남습니다. 이전 요청이 나중에 Set-Cookie를 받으면 이전 저장소에 쿠키를 다시 기록할 수 있습니다. 취소 역시 동기화 장벽은 아닙니다. 콜백이 이미 실행 대기열에 들어가 있을 수 있기 때문입니다. 따라서 로그인할 때마다 세션 세대 번호를 부여하고, 응답을 처리하기 전에 그 번호를 확인해야 합니다. 이전 세대의 응답은 현재 사용자 상태를 갱신할 수 없어야 합니다.
테스트에서는 A의 응답을 지연시킨 상태에서 로그아웃을 실행하고 B로 로그인한 다음, 대기 중인 A의 응답을 전달하세요. 마지막에는 B의 UI 상태, 애플리케이션 로직이 인식하는 계정, 다음 요청의 쿠키를 함께 확인해야 합니다. 로그아웃 순간에 저장소가 비었는지만 확인해서는 이런 문제를 잡을 수 없습니다.
검증을 두 단계로 나눠 실행하기
코드를 병합할 때마다 먼저 네트워크를 사용하지 않는 저장소 스크립트를 실행한 뒤, 고정된 시뮬레이터에서 요청 에코 테스트와 지연 응답 테스트를 실행하세요. 각 테스트는 자체 구성과 픽스처를 생성하고, 종료 시 서버를 중지하고 작업을 취소해야 합니다. 변경 가능한 세션을 공유하지 않아야 병렬 실행 중 서로의 상태를 오염시키는 일을 막을 수 있습니다.
실패 기록에는 요청 식별자, 세션 세대 번호, 쿠키 이름과 속성을 남기되 실제 쿠키 값은 저장하지 마세요. 값을 비교해야 할 때는 테스트 픽스처의 고정 표식을 사용하고, 운영 환경의 로그인 상태를 클라우드 Mac으로 복사하지 마세요.
마지막으로 두 계정이 일부러 같은 저장소를 사용하게 하여 테스트가 실패하는지 확인하세요. 이어서 세대 번호 확인을 의도적으로 제거하고 지연 응답 문제를 잡아내는지도 확인합니다. 이 두 종류의 회귀를 안정적으로 발견할 수 있어야, 검증 게이트가 우연히 통과하는 몇 가지 조건이 아니라 계정 간 경계를 실제로 검사한다고 볼 수 있습니다.
자주 묻는 질문
ephemeral 설정이면 쿠키가 섞이지 않나요?
그렇지 않습니다. 임시 세션도 메모리에 쿠키를 보관합니다. 설정을 각각 만들고 저장소가 독립적인지 확인한 뒤, 네트워크 계층이 공유 저장소나 Cookie 헤더를 다시 주입하지 않는지 검사해야 합니다.
로그아웃할 때 reset만 호출하면 되나요?
아닙니다. 새 요청을 막고 기존 요청을 취소하거나 완료시킨 다음 해당 계정의 쿠키를 지우고 세션을 무효화해야 합니다. reset은 캐시와 자격 증명에도 영향을 주므로 명확한 로그아웃 절차를 대신할 수 없습니다.
다음 빌드에는 전용 Mac을 사용해 보세요.
가상 머신이 아닌 클라우드 Mac을 선택해 기존 Xcode와 CI 워크플로를 그대로 유지하세요. 프로젝트 요구 사항에 맞춰 머신을 선택할 수 있으며, 실제 사용 가능 여부는 콘솔에 표시되는 실시간 정보를 기준으로 합니다.