티스토리 뷰
이 블로그는 팀원 중 한명인 찢다님 의 도움을 받아서 구현하였습니다.
해당 블로그에서는 개발 중인 프로젝트에서 API 서버와 소켓 서버를 모두 구현하면서 발생한 두 서버 간 인증 로직 상충 문제를 해결하는 과정을 다룬다.
1. 문제 파악
1️⃣ 채팅을 보내던 중 사용자 인증 오류가 발생한 경우
채팅을 3개 보냈을 때, 첫 번째와 두 번째 메시지는 정상적으로 전송되었지만, 세 번째 메시지는 401 오류로 인해 전송이 실패하는 경우가 있다.
이때, 이는 네트워크 오류가 아니라 사용자 인증 실패로 인해 발생한 문제다. 하지만 사용자는 인증 오류를 인지하지 못한 채, 보낸 메시지가 순서대로 정상적으로 도착할 것이라고 예상한다.
그러나 실제로 소켓 서버에는 첫 번째와 두 번째 메시지만 전달되었고, 세 번째 메시지를 어떻게 처리할 것인지 고민이 필요했다.
이를 해결하기 위해, 메시지를 보낸 후 queue에 저장한 뒤, 소켓 서버에 refresh 요청을 보낸다. 인증이 갱신되면 queue에 저장된 메시지를 순차적으로 다시 전송하고, 소켓 서버에서 메시지 전송 성공 응답을 받으면 해당 메시지를 queue에서 제거하는 방식으로 구현하였다.
🤔Queue에서 pop을 순차적으로 받으려면, 응답을 순차적으로 받을 거라는 보장이 있어야 하는데, 실제로 그렇지 않다면?
Queue에서 메시지를 순차적으로 pop하려면, 서버에서 응답을 보낼 때도 순서를 보장해야 한다. 하지만 실제로는 네트워크 상태나 서버 처리 방식에 따라 응답 순서가 뒤섞일 가능성이 있다.
이 문제를 해결하기 위해 BTree(균형 이진 탐색 트리) 를 사용했는데, 그 이유는 다음과 같다.
1. 정렬된 상태 유지
- 각 메시지는 고유한 UUID(시간순 ID) 를 가지므로, BTree를 사용해 메시지 ID를 기준으로 정렬할 수 있다.
- 이를 통해 특정 메시지가 나중에 도착하더라도 정렬된 상태를 유지하면서 올바른 순서대로 전송할 수 있다.
2. 빠른 검색 및 삭제
- Queue는 FIFO(First-In-First-Out) 방식이라 순차적인 처리가 기본이지만, 응답이 순서대로 오지 않으면 순서가 꼬일 수 있다.
- 반면, BTree는 O(log n) 의 시간 복잡도로 삽입, 삭제, 탐색이 가능하므로 메시지를 효율적으로 관리할 수 있다.
- 예를 들어, 특정 메시지가 누락되었을 경우에도 트리를 순회하며 올바른 순서대로 전송을 시도할 수 있다.
=> BTree를 사용하면 실제 응답 순서에 관계없이 정렬된 상태를 유지할 수 있어, 안정적으로 메시지를 처리할 수 있다!
2️⃣ API & Socket 서버 독립으로 인한 사용자 인증 갱신 문제
기존 구현 방식에서는 채팅을 전송하는 도중 Socket 서버에서 401 오류가 발생하면, API 서버에 refresh 요청을 보내고, 갱신된 토큰을 Socket 서버로 전송하여 인증을 갱신해야 했다.
이 방식은 Socket 서버가 401 오류를 감지하고 사용자 인증을 갱신할 수 있었다.
그러나 API 서버와 Socket 서버가 독립적으로 작동하다 보니, API 요청 중 401 오류가 발생했을 때 Socket 서버의 인증을 어떻게 갱신할지 문제가 되었다.
🤔 API 서버로 요청 중에 인증 에러가 발생하면, Socket 서버로 어떻게 인증 갱신을 해줘야하지...?
API 서버와 Socket 서버의 요청을 분리하여 처리하던 중, API 서버에서 401 권한 오류가 발생하면 API 서버뿐만 아니라 Socket 서버에서도 토큰을 갱신해야 한다. 그래서 API 요청 중 권한 오류(401)가 발생하면 API 서버와 Socket 서버 모두에 Refresh 요청을 수행하도록 구현해야 했다.
따라서 API와 Socket 요청 관리를 연동할 필요가 있었고, 이를 반영하여 리팩토링을 진행하였다.
2. 리팩토링을 위한 설계
API서버와 Socket 서버를 분리하여 구현했었는데 이걸 다시 통합하는 과정이 너무 복잡했다...
1️⃣ API와 Socket 서버 인증 분리해서 구현해보자!
처음 설계한 것은 DefaultChatStompService를 사용해서 여러 UseCase에서 Refresh 요청을 할 수 있도록 하였다.
- DefaultChatStompService: 싱글톤으로 존재하며, DefaultChatStompRepository를 생성하여 여러 UseCase에서 소켓 관련 기능(연결, 메시지 전송 등)을 수행할 수 있도록 한다.
- DefaultChatStompRepository: 소켓 관련 기능(연결, 메시지 전송 등)들을 수행한다.
- TokenManager: API 서버로 Refresh 요청을 보내고, DefaultChatStompService를 사용하여 소켓 서버에도 Refresh 요청을 수행한다.
먼저 첫번째로, API 요청 중 에러가 발생한 경우
❌ 문제점
- API 서버에서 401(토큰 만료) 발생 시 TokenManager가 토큰을 갱신.
- 하지만 API와 소켓 서버의 인증처리가 따로 관리되면서 소켓 서버에서는 만료된 인증 정보를 그대로 유지하고 있음.
- 결과적으로 소켓 서버는 여전히 만료된 인증 정보로 요청을 보낼 수 있어, 이후 소켓 요청이 실패할 가능성이 높음.
- API 인증과 소켓 인증이 따로 관리되면서 일관성이 부족함.
두번째로, 소켓 서버에 요청 중 에러가 발생한 경우
❌ 문제점
- 소켓 서버에서 401이 발생하면 TokenManager가 먼저 API 서버에 토큰 갱신 요청을 보냄.
- API 서버에서 갱신이 성공하면 다시 소켓 서버에 갱신 요청을 전달.
- 하지만 소켓 서버가 인증 갱신을 완료하기 전에 또 다른 요청이 들어오면 여전히 만료된 토큰으로 요청하게 되어 실패할 가능성이 있음.
- 즉, 동기화 문제가 발생할 수 있음.
2️⃣ API와 소켓 서버의 인증을 동기화하자!
이전 구조는 API 서버와 소켓 서버의 인증 갱신이 독립적으로 수행되어 동기화 문제가 발생할 가능성이 컸다.
이를 해결하기위해 API와 소켓 서버의 인증을 동기화하고, 대기 요청 처리까지 고려하여 더 안정적인 인증 갱신 구조를 만들었다.
📄 파일 정리
Domain
├── Service
│ ├── SocketAuthHandler.swift // 401(권한 오류)이 발생하면 API 및 Websocket 인증 갱신하도록 처리
│ ├── TokenRefreshHandler.swift // API 토큰 갱신(Refresh) 기능을 수행
✅ 개선한 점
1. API와 소켓 서버의 인증을 동기화
- API 토큰이 갱신되면 소켓 인증도 자동으로 갱신됨.
- SocketAuthHandler를 추가하여 API 갱신과 소켓 갱신을 하나의 흐름으로 묶음.
2. 소켓 서버에서 401이 발생하면 기존 요청을 대기시키고, 갱신이 완료된 후 재시도.
- 소켓 서버가 DefaultChatStompRepo를 통해 인증 갱신을 수행하는 동안, 인증 갱신 중임을 표시(isAuthUpdating).
- 갱신이 끝나면 기존에 대기했던 메시지들도 정상적으로 전송될 수 있도록 함.
1. 상태 변수
- isAuthUpdating: 소켓 서버의 인증 갱신 여부 판단
- isRefreshing: API 서버의 인증 갱신 여부 판단
2. 동작 원리
1. 메시지를 보내기 전에 isAuthUpdating 상태 확인
- 메시지 전송 전에 isAuthUpdating이 true라면, 메시지를 전송하지 않고 Queue에 저장
- 인증 갱신이 완료되면 Queue에서 메시지를 꺼내 순차적으로 전송
2. 모든 소켓 메시지 전송 요청은 SocketAuthHandler를 거쳐야 함
- 그래야 isAuthUpdating 상태를 체크하고, 메시지 전송 여부를 결정할 수 있음
- isAuthUpdating == true ➝ 메시지를 Queue에 저장
- isAuthUpdating == false ➝ 메시지를 전송
API와 소켓 서버의 인증 처리하는 로직을 동기화하는데에는 성공했지만, 다른 문제가 발생하였다...
❌ 문제점 - 순환참조 발생
1. TokenRefreshHandler와 SocketAuthHandler 사이에서 발생
- TokenRefreshHandler → SocketAuthHandler 의존: API 서버에서 리프레시 요청 발생위해
- SocketAuthHandler → TokenRefreshHandler 의존: 소켓 서버에서 401 오류 발생 → API 서버에 리프레시 요청위해
2. SocketAuthHandler와 DefaultChatStompRepo사이에서 발생
- SocketAuthHandler → DefaultChatStompRepo 의존: 메시지 전송 요청위해
- DefaultChatStompRepo → SocketAuthHandler 의존: 소켓 서버에서 401 오류 발생 → API 서버에 리프레시 요청위해
3️⃣ 순환 참조 해결하기
📄 파일 정리
Data
├── Network
│ ├── CustomStompClient.swift //네트워크 라이브러리(StompClientLib)를 사용하여 WebSocket을 다룸
│ ├── DefaultRefreshInterceptor.swift // 소켓 서버에 Refresh 성공 후 API 서버 Refresh 기능을 수행
├── Repository
│ ├── DefaultSocketAuthRepository.swift // WebSocket 인증 관련 요청을 처리
Domain
├── Service
│ ├── SocketAuthHandler.swift // WebSocket 인증 관련 오류가 발생한 경우 처리
│ ├── TokenRefreshHandler.swift // API 토큰 갱신(Refresh) 기능을 수행
│ ├── MessageQueue.swift // WebSocket 메시지 큐를 관리
✅ 개선된 점
1. CustomStompClient 추가
- 기존에는 DefaultChatStompRepo에서 직접 소켓을 관리했지만, 이를 CustomStompClient로 분리하여 소켓 연결, 해제, 구독(Subscribe) 등의 역할을 전담하도록 변경.
- 이를 통해 소켓 관련 로직이 한곳에서 집중 관리되며, 역할이 명확하게 분리됨.
2. BTreeMQ 도입
- 메시지 순서를 보장하기 위해 BTreeMQ를 추가하여, 도착 순서가 아닌 정렬된 순서대로 메시지 관리 가능.
- isAuthUpdating 상태에 따라 Lock/Unlock을 제어
- 인증 갱신(refresh)이 진행될 때는 메시지를 보내지 않음 (Queue에 저장)
- 인증이 완료되면 저장된 메시지를 순차적으로 전송
3. SocketAuthRepository & DefaultRefreshInterceptor 추가
- SocketAuthRepository:
- 기존에는 SocketAuthHandler가 직접 소켓 인증을 처리했지만, 소켓 인증을 담당하는 별도 Repository(SocketAuthRepository)를 생성하여 소켓 서버와의 인증 통신을 전담하도록 변경.
- DefaultRefreshInterceptor:
- 401 인증 실패 시 처리 로직 추가
- error queue에서 메시지를 관리하며, 인증 갱신 중 발생하는 오류를 적절히 처리
4. 순환 참조 해결
- 기존에는 TokenRefreshHandler → SocketAuthHandler와 SocketAuthHandler → DefaultChatStompRepo 간 순환 참조 문제가 발생.
- 이를 NotificationQueue를 활용하여 비동기적으로 해결, 특정 이벤트 발생 시 의존 관계 없이 알림을 전달하는 방식으로 개선
> 🔍 NotificationCenter 대신 NotificationQueue를 사용한 이유
📌 상황: API와 소켓 서버의 인증 갱신 동기화
API와 소켓 서버의 인증 갱신을 동기화하는 과정에서, 소켓 서버에서 인증 오류(401)가 발생하면 API 서버에 토큰 갱신 요청을 보내고, 이후 갱신된 토큰을 소켓 서버에도 적용해야 한다.
🔴 문제점 (NotificationCenter 사용 시)
- 소켓 서버에서 401 오류가 발생하면 NotificationCenter를 통해 API 서버에 토큰 갱신 요청을 보냄.
- 동시에 여러 개의 401 오류가 발생할 경우, 중복된 토큰 갱신 요청이 여러 번 발생할 가능성이 있음.
- API 서버에서 토큰을 갱신하는 동안에도 다른 이벤트가 발생할 수 있으며, 이벤트 실행 순서를 보장하기 어려움.
- API 갱신이 완료되기도 전에 소켓 서버로 인증 갱신 요청을 보내는 등의 타이밍 오류가 발생할 가능성이 있음.
🟢 해결책 (NotificationQueue 사용)
NotificationQueue를 사용하여 인증 갱신 요청을 큐에 저장한 후, 인증이 완료된 후에 순차적으로 실행하도록 변경.
- 소켓 서버에서 401 오류가 발생하면 NotificationQueue를 사용해 인증 갱신 요청을 큐에 저장함.
- 만약 API 서버에서도 401 오류가 발생하면, 기존에 대기 중이던 요청과 병합하여 중복 요청을 방지함.
- API 서버에서 인증 갱신이 완료되면 큐에 저장된 요청을 순차적으로 실행하여, 갱신된 토큰을 소켓 서버에 적용함.
이제 순환참조 다 해결했나~ 했더니 또 다른 순환참조가 기다리고 있었다,,,
❌ 문제점 - 순환참조 발생
- CustomStompClient → DefaultRefreshInterceptor 호출: 소켓 서버에서 401 에러가 발생하여 DefaultRefreshInterceptor으로API 서버에 refresh 요청을 하기위해
- DefaultRefreshInterceptor → CustomStompClient 호출: API 서버에서 401 에러가 발생하여 CustomStompClient으로 소켓서버에 refresh 요청을 하기 위해
위와 같은 이유로 서로의 상태를 감지해야하기 때문에 또 다른 순환참조가 발생하였다...
4️⃣ 순환 참조 다시 해결하기
📄 파일 정리
Data
├── Network
│ ├── CustomStompClient.swift //네트워크 라이브러리(StompClientLib)를 사용하여 WebSocket을 다룸
│ ├── DefaultRefreshInterceptor.swift // 소켓 서버에 Refresh 성공 후 API 서버 Refresh 기능을 수행
├── Repository
│ ├── DefaultSocketAuthRepository.swift // WebSocket 인증 관련 요청을 처리
Domain
├── Service
│ ├── SocketAuthHandler.swift // WebSocket 인증 관련 오류가 발생한 경우 처리
│ ├── TokenRefreshHandler.swift // API 토큰 갱신(Refresh) 기능을 수행
│ ├── MessageQueue.swift // WebSocket 메시지 큐를 관리
✅ 개선된 점
1. CustomStompClient ↔ DefaultRefreshInterceptor 사이에 프로토콜 추가
- SocketAuthHandler에게 이벤트로 소켓 서버 에러 응답을 넘겨주도록 구현.
- CustomStompClient와 DefaultRefreshInterceptor가 직접 참조하는 구조를 제거하고, 프로토콜을 통해 간접적으로 통신.
2. TokenRefreshHandler에서 SocketAuthRepository로 직접 응답 전달
- 기존에는 TokenRefreshHandler → SocketAuthHandler → SocketAuthRepository 로 응답을 전달해야 했음.
- 이를 생략하고 TokenRefreshHandler에서 SocketAuthRepository로 바로 API 응답을 전달하도록 개선.
❌ 문제점 - SocketAuthRepository 초기화 문제
- SocketAuthRepository가 어디서도 초기화되지 않음.
- 순환 참조 문제는 해결되었지만, SocketAuthRepository가 없어서 원하는 로직이 실행되지 않음.
5️⃣ SocketAuthRepository 초기화 문제 해결하기
📄 파일 정리
Data
├── Network
│ ├── CustomStompClient.swift //네트워크 라이브러리(StompClientLib)를 사용하여 WebSocket을 다룸
│ ├── DefaultRefreshInterceptor.swift // 소켓 서버에 Refresh 성공 후 API 서버 Refresh 기능을 수행
│ ├── DefaultRefreshSocketInterceptor.swift // API 서버에 Refresh 성공 후 소켓 서버 Refresh 기능을 수행
├── Repository
│ ├── DefaultSocketAuthRepository.swift // WebSocket 인증 관련 요청을 처리
Domain
├── Service
│ ├── SocketAuthHandler.swift // WebSocket 인증 관련 오류가 발생한 경우 처리
│ ├── TokenRefreshHandler.swift // API 토큰 갱신(Refresh) 기능을 수행
│ ├── MessageQueue.swift // WebSocket 메시지 큐를 관리
4️⃣번까지 구현하고 보니 SocketAuthRepository는 어디서도 초기화되지 않고 있었다. ㅎㅎㅎ
순환참조는 모두 해결되었는데 SocketAuthRepository를 생성하고 있지 않아서 원하는 로직이 실행되지 않았다.
그래서 TokenRefreshHandler와 SocketAuthRepository 사이에 DefaultRefreshSocketInterceptor를 만들어서SocketAuthRepository를 초기화하도록 구현했다.
이렇게 하니 모든 인증 갱신 로직이 정상적으로 동작할 수 있었다. ㅎㅎㅎ
3. 최종 개선된 인증 갱신 흐름
험난했던 시행착오들을 거치면서 드디어 API와 소켓 서버를 동기화한 인증 로직을 구현하는데 성공하였다. ㅎ
최종적으로 개선한 흐름은 다음과 같다.
1️⃣ 소켓 서버에서 401 인증 오류 발생하면?
(1) DefaultRefreshInterceptor에서 401 오류 감지
(2) SocketAuthRepository를 통해 API 서버에 토큰 갱신 요청
(3) isAuthUpdating = true로 변경하여 메시지 전송을 중단하고 Queue에 저장
(4) isProcessing = true라면, 이미 토큰 갱신 중이므로 추가 요청을 보내지 않고 대기
(5) API 서버에서 새 토큰 발급 완료 → DefaultSocketAuthRepository에서 응답 처리
(6) CustomStompClient에서 소켓 인증을 갱신하고, isAuthUpdating = false로 변경
(7) Queue에 저장된 메시지를 순차적으로 전송하여 정상 처리
2️⃣ API 서버에서 401 인증 오류 발생하면?
(1) TokenRefreshHandler에서 401 오류 감지 및 API 서버에 토큰 갱신 요청
(2) DefaultSocketAuthRepository로 이벤트 전달하여 소켓 서버 토큰 갱신 요청
(3) isRefreshing = true로 설정하여 API 요청을 잠시 중단
(4) API 서버에서 새 토큰 발급 완료 → DefaultSocketAuthRepository에서 응답 처리
(5) isRefreshing = false로 변경하여 API 요청 재개
이번 개선을 통해 API와 소켓 서버의 인증 갱신을 통합하여 인증 흐름을 보다 안정적으로 만들 수 있었다.
또한, 순환 참조 문제를 해결하기 위해 프로토콜을 도입하고, 옵저버 패턴을 활용한 NotificationQueue를 사용하여 의존성을 효과적으로 관리할 수 있었다.
단순히 기능을 개발하는 것을 넘어, 두 서버 간의 인증 흐름을 명확히 정리하고 코드의 역할을 분리하며, 순환 참조와 같은 다양한 문제를 고려하면서 구조를 설계하는 것이 중요하다는 것을 깨달았다.
'스위프트' 카테고리의 다른 글
[Swift] - fastlane + GitHub Actions CI 구축(3) (0) | 2025.01.20 |
---|---|
[Swift] - fastlane + GitHub Actions CI 구축(2) (0) | 2024.11.25 |
[Swift] - Quick, Nimble로 테스트 코드 작성 (1) | 2024.11.17 |
[Swift] - fastlane + GitHub Actions CI 구축(1) (0) | 2024.11.12 |
[Swift] - Rxswift 예제로 이해해보기 (0) | 2024.11.02 |