1. 개요

안녕하세요! DoEatFit (v2.0) 프로젝트의 완성도를 한 단계 끌어올렸던 긴박하고 뜨거웠던 기술 혁신의 나날을 정리한 시스템 안정성 및 보안 고도화 통합 회고록입니다.

실제 서비스를 배포하고 나면 개발 환경에서는 결코 발견되지 않던 미묘한 런타임 결함이나 잠재적인 보안 위협이 곳곳에서 고개를 들기 마련이에요. 백엔드 에러 발생 시 악의적인 공격자에게 힌트를 주지 않도록 시스템을 굳히는 **에러 메시지 Hardening(보안 강화)**부터, 프론트엔드 React Query의 사소한 문법 순서가 일으킨 타입 페칭 오염 트러블슈팅, 그리고 DB 인프라 제약 조건과 ORM 프레임워크 간의 괴리로 발생한 NPE(NullPointerException) 장애 해결까지, 실전에서 직접 겪은 치열한 트러블슈팅의 원인과 해법을 아주 꼼꼼하게 공유해 드릴게요.


2. 핵심 내용

2-1. 🔒 보안 vs 편의성의 절충: 백엔드 에러 메시지 Hardening 및 다중화

API를 호출할 때 권한이 없거나 예외 상황이 발생하면, 서버는 응답 코드를 반환해요. 그러나 이때 무심코 전달하는 에러 메시지가 엄청난 보안 구멍이 될 수 있답니다.

  • 사용자 열거(User Enumeration) 취약점 문제:
    • 로그인 실패 시 “존재하지 않는 사용자입니다”와 “비밀번호가 틀렸습니다”를 구별해서 알려주면 개발자와 일반 사용자에겐 편리해요. 하지만 이는 공격자에게 **“이 사이트에 가입된 이메일이 무엇인지” 하나씩 대조하며 수집할 기회(사용자 열거 취약점)**를 제공하고 맙니다.
  • 에러 메시지 Hardening (정보 은폐) 전략:
    • 외부 유출용 메시지를 최대한 모호하게 뭉뚱그려(Generic) 정보를 감추는 보안 기법을 적용했어요.
    • “사용자를 찾을 수 없습니다” ➡ “이메일 또는 비밀번호가 올바르지 않습니다.”
    • “토큰 쌍이 일치하지 않습니다” ➡ “인증 세션 정보가 유효하지 않습니다.”
  • 보안과 개발 편의성의 이원화 설계:
    • 메시지를 다 추상화하면 Grafana나 에러 로그를 볼 때 개발자도 원인을 파악하지 못해 곤경에 빠지게 되죠.
    • 이를 해결하기 위해 클라이언트 응답에는 정형화된 보안 비즈니스 코드(예: A007)만 전송하고, 서버 로그에는 상세한 실제 원인(USERNAME_NOT_FOUND)이 선명하게 기록되도록 시스템을 다중화해 해결했답니다.
    • 동시에 ErrorResponse가 예외 enum의 status 필드를 참조하여 동적으로 HTTP Status 값을 생성하도록 튜닝하여 비즈니스 수정이 즉시 인프라 응답에 반영되는 유연함도 확보했답니다.

2-2. ⚡ React Query의 ‘enabled’ 오염 극복 및 비회원 500 에러 강제 정벌

프론트엔드에서 API를 선언적으로 호출할 때와 백엔드 DB의 제약 조건이 맞물리며 발생했던 골치 아픈 런타임 오류들을 타파해 나간 실전 트러블슈팅 과정입니다.

  • React Query의 enabled 스프레드 오염 원인과 해결:
    • 현상: 비로그인 게스트 사용자인데도 자꾸만 회원 전용 API를 요청하여 브라우저 콘솔에 401 런타임 에러가 기록되는 미스터리한 오작동이 확인되었어요.
    • 원인: 범인은 쿼리 훅 내부의 스프레드 연산자(...options) 선언 순서에 있었답니다. enabled: isAuthenticated를 먼저 선언한 뒤 뒤이어 ...options를 적다 보니, 외부 컴포넌트에서 주입된 다른 쿼리 옵션 객체가 내부의 소중한 비로그인 차단막(enabled)을 성급하게 덮어써 버린(Override) 것이었죠.
    • 해결 코드:
      // Bad (외부 옵션이 enabled를 덮어씀)
      const useMyQuery = (options) => useQuery({ enabled: isAuthenticated, ...options });
       
      // Good (순서를 바꾸고 외부 옵션과 안전하게 교차 검증)
      const useMyQuery = (options) => useQuery({ ...options, enabled: isAuthenticated && (options?.enabled ?? true) });
      이와 더불어 런타임 널 단언(uuid!) 대신 안전한 논리 게이팅(!!uuid)을 적용해 프론트엔드의 방어벽을 더 견고히 굳혔습니다.
  • 심박수 계산기 비회원 500 에러 해결:
    • 현상: 게스트 회원도 마음껏 심박수를 계산하고 임시 저장할 수 있도록 게스트 접근을 전면 허용해 주었음에도 불구하고, 비회원이 계산하기 버튼을 누르는 순간 Column 'user_id' cannot be null 에러가 발생하며 서버가 500 Internal Server Error를 뱉으며 뻗어버리는 장애가 터졌어요.
    • 원인: 하이버네이트의 ddl-auto: update 옵션이 지닌 태생적 한계 때문이었습니다. 자바 엔티티 파일에서 @Column(nullable = true)로 수정한 뒤 서버를 재기동했음에도, 하이버네이트는 이미 DB 내부에 명시적으로 박혀 있는 기존 테이블 컬럼의 NOT NULL 물리 제약 조건(Constraints)을 자동으로 변경해 주지 못했던 것이었죠. 게다가 @Embedded 객체인 uuidEntity가 DB 로딩 시 필드 레벨에서 안전하게 초기화되지 못해 NPE가 동반되었습니다.
    • 해결책:
      1. LXC 컨테이너 데이터베이스 콘솔에 직접 진입하여 물리 쿼리(ALTER TABLE HEART_RATE_CALCINFO MODIFY user_id BIGINT NULL;)를 수동으로 강제 가동해 인프라 스키마를 동기화했습니다.
      2. 엔티티 내 선언과 동시에 @Embedded 인스턴스를 빈 생성자로 초기화하여 JPA 프록시 로딩 도중 터지는 NPE의 숨통을 영구히 끊어냈습니다.

2-3. ✅ 시스템 고도화 결실 및 맞춤 오픈소스 라이브러리 검토

  • 시스템 고도화의 값진 수확:
    • 개발 편의성과 보안성이라는 두 마리 토끼를 **‘로그는 상세하게, 응답은 모호하게’**라는 대원칙 하에 완벽하게 양립시켰습니다.
    • 또한 ORM 자동 생성 기능(ddl-auto)에만 안일하게 의존하지 않고, 물리 DB 제약 조건이 올바르게 리팩토링되었는지 콘솔 크로스 체크를 수행하는 인프라 수동 동기화 습관의 막중함을 다시금 깨달은 소중한 경험이었습니다.
  • 추천 오픈소스 라이브러리 검토:
    1. Bucket4j
      • 평가: 로그인 인증이나 갱신 재발급, 식품 검색 등 외부 트래픽 노출이 많은 엔드포인트에 대한 브루트 포스 및 DDoS 성격의 대량 인입 요청을 완벽하게 입구 컷하기 위해, Java 인메모리 토큰 버킷 속도 제한기인 Bucket4j 장착을 강력하게 권해드립니다.
    2. Nimbus JOSE + JWT
      • 평가: 장기적으로 서비스의 볼륨이 커져 MSA(마이크로서비스) 확장이나 멀티 테넌트 SSO 환경으로 발전해 나갈 때, 대칭 키 방식(HS256)을 넘어 비대칭 키(ECDSA) 서명 명세를 안전하게 발급하고 관리해 주는 표준 규격 라이브러리로서 Nimbus 라이브러리 탑재를 적극적으로 검토해 보시길 강력 제안합니다.