1. 개요
기존 서비스 설계에서는 사용자가 무조건 로그인을 마쳐야만 식단이나 심박수 계산 기능에 진입할 수 있었습니다. 이는 웹 브라우저에서 가볍게 기능을 먼저 체험해 보고 싶어 하는 대다수 신규 사용자의 이탈률을 높이는 악영향이 있었습니다. 비인가(게스트) 사용자가 아무런 제약 없이 유연하게 기본 분석 기능을 체험하게 돕고, 로그인/로그아웃 전환 시에 다중 사용자 캐시가 엉키거나 다른 사람의 이전 건강 정보가 화면에 잔류하는 상태 캐시 오염(State Leak) 결함을 프론트/백엔드 레이어에서 완벽하게 극복해 낸 기술 수복의 회고 기록입니다.
2. 핵심 내용
2-1. 🚨 주요 문제 현상 및 근본 원인 분석
- 현상 1: 비회원 상태 요청 시 401 & 500 에러 발생
- 원인: 기본 계산 API 엔드포인트가 Spring Security 필터체인의 무인증 통과 규칙인
NO_AUTH_API_PATHS경로 리스트에서 누락되어 접근이 아예 불가능했습니다. 더불어, 심박수 기록 DB 엔티티(HrCalculatorEntity) 구조에서 소유자 외래키(userId)에nullable=false하드 제약 조건이 꽂혀있어, 인증 객체가 없는 게스트 상태의 저장이 시도될 때 DB 레벨에서 널 예외가 튀며 500 에러가 뿜어지던 상황이었습니다.
- 원인: 기본 계산 API 엔드포인트가 Spring Security 필터체인의 무인증 통과 규칙인
- 현상 2: JPA 페이징 정렬 파라미터 누락에 따른 조회 실패
- 원인: 백엔드 심박수 조회 컨트롤러 메서드에
@PageableDefault어노테이션이 누락되어, 프론트에서 명시적인 페이징 인자가 날아오지 않으면 Spring Data JPA 내부에서 널 세이프하지 못하게 튕겨 나갔습니다.
- 원인: 백엔드 심박수 조회 컨트롤러 메서드에
- 현상 3: 로그인/아웃 전환 시 이전 유저의 개인 식단 기록 유출
- 원인: 프론트엔드 로그아웃 시에 유저 기본 프로필 쿼리만 한정적으로 지워주어, 유저 식별자 인자가 명시적으로 붙어있지 않던 식단 히스토리 쿼리 캐시(
['calculator', 'history'])가 그대로 살아 숨 쉬고 있었습니다. 결국 A유저가 나간 자리에 B유저가 로그인하면 화면에 A유저의 과거 기록이 그대로 팝업되는 아주 아찔한 데이터 보안 결함이 잔류했습니다.
- 원인: 프론트엔드 로그아웃 시에 유저 기본 프로필 쿼리만 한정적으로 지워주어, 유저 식별자 인자가 명시적으로 붙어있지 않던 식단 히스토리 쿼리 캐시(
2-2. 💡 프론트엔드와 백엔드의 유기적 해결책
- 백엔드: 단일 컨트롤러 통합 및 Null-Safe 바인딩
- 성격이 동일한 회원/비회원 API를 조잡하게 분리하지 않고 하나로 수렴했습니다. 스프링 시큐리티의
@AuthenticationPrincipal어노테이션을 사용하여 유저 세션이 감지되면 이메일을 매핑하고, 익명 유저일 경우null로 저장되게끔 방어 코드를 두었습니다.
- 성격이 동일한 회원/비회원 API를 조잡하게 분리하지 않고 하나로 수렴했습니다. 스프링 시큐리티의
- 프론트엔드: React Query 전면 캐시 초기화 및 Blur Overlay 적용
- 사용자 세션이 변경되는 순간
queryClient.clear()메서드를 강력하게 호출하여 로컬 메모리에 적재된 모든 캐시와 에러 상태를 깨끗이 폭파시켰습니다. - 비회원이 이용할 수 없는 고급 기능이나 기록 탭에는 Glassmorphism 기술을 가미한 부드러운 Backdrop 블러 카드(
LoginRequiredOverlay)를 장착했고,useQuery내부의enabled: isAuthenticated옵션을 걸어서 비인가 유저 시점에서의 잦은 401 네트워크 패치 낭비를 원천 봉쇄했습니다.
- 사용자 세션이 변경되는 순간
sequenceDiagram participant Guest as Guest User participant Front as Frontend (React Query) participant Spring as Spring Boot Guest->>Front: 기본 식단 계산 요청 Front->>Spring: POST /api/calculate/normal Note over Spring: UrlPaths 허용 검증 및 Null-Safe 바인딩 Spring-->>Front: 비회원 계산 결과 반환 (Success) Guest->>Front: 기록 보관함 탭 클릭 Note over Front: isAuthenticated == false 판정 Front-->>Guest: 쿼리 차단 (enabled:false) & Blur Overlay 렌더링
- Scrollbar Jitter (UI 흔들림) 픽스:
- 화면의 높이가 좁은 탭과 넓은 탭 간에 이동할 때 브라우저 기본 스크롤바가 우측에 생겼다 사라지며 전체 중앙 레이아웃이 좌우로 부르르 흔들리는 고질병을 해결하기 위해 CSS 전역 최후 방어선을 심어두었습니다.
/* globals.css */ html { scrollbar-gutter: stable; }
2-3. ✅ 보안 패치 (IDOR 취약점 대응) 및 추천 라이브러리
- IDOR (부적절한 직접 객체 참조) 보안 패치:
- 비회원 기능을 위해 DB 외래키 널 제약을 해제하면서, 데이터 조회가 단순히 “UUID가 일치하는가?”에만 의존하는 보안 허점이 생겼습니다. 무작위로 생성된 UUID이지만 공격자가 추측하거나 네트워크 패킷을 가로채 UUID를 획득할 경우, 로그인 여부와 관계없이 타인의 민감한 나이, 몸무게, 심박수를 전부 스니핑할 수 있는 IDOR 취약점이 우려되었습니다.
- 대응 조치:
HrCalculatorService조회 API에 교차 검증 비즈니스 자물쇠를 추가했습니다. 조회된 엔티티의 소유자 정보가 실존하는 ‘회원’의 데이터인 경우에는 현재 HTTP 요청을 보낸 보안 세션의 User ID와 강제로 일치 비교 검증을 거칩니다. 본인의 기록이 아니거나 권한이 맞지 않을 경우 즉각AccessDeniedException을 터뜨려 조회 접근을 완전히 차단하는 완벽한 보안 방어선을 구축했습니다.
- 추천 모던 오픈소스 라이브러리 검토:
- React Query (TanStack Query):
- 평가: 세션 라이프사이클에 맞춰 클라이언트 데이터의 은닉과 캐시 파기(
clear())를 가장 우아하게 지원하는 표준 상태 도구로 적극 권장합니다.
- 평가: 세션 라이프사이클에 맞춰 클라이언트 데이터의 은닉과 캐시 파기(
- Zustand:
- 평가: Redux의 헤비한 보일러플레이트 없이 비회원의 단건 계산 결과를 프론트 로컬 스토어에 초경량화된 가벼움으로 임시 저장하고, 로그인 상태와 격리하여 가볍게 관리하기 위한 최고의 전역 상태 라이브러리로 평가 및 검토를 완료했습니다.
- React Query (TanStack Query):