레거시 플랫폼의 MSA 전환기

sundays·2026년 6월 30일

레거시 플랫폼 프론트엔드 전환 설계

Server: Spring Boot · Client: React + Next.js (BFF)
작성 목적: 리뷰용 설계 문서


1. 목적

  1. 현재 레거시 플랫폼은 front-end와 back-end가 긴밀하게 종속(tightly coupled)되어 있어, 책임 경계가 모호하고 독립적인 변경·배포가 어렵다.
  2. SSR(Server-Side Rendering)을 유지하여 SEO·초기 로딩 이점을 지키면서, 클라이언트 인터랙션(상태/DOM 동기화)을 강화해 사용자 UX를 개선한다.
    • 주의: "전부 SPA로 전환"이 아니다. 순수 SPA(CSR)는 SEO에 불리하므로, Next.js로 부분 SSR/RSC를 유지한 채 인터랙션만 더한다.
  3. 중복 기능을 제거하고, 일부 기능의 버전 관리·현행화를 수행한다.

--

전환 이유 (4축)

  1. 책임 분리 (Client / Server)
    • front/back의 책임 소재를 명확히 분리, 독립 배포 가능.
    • Trade-off: 운영 대상이 2배(Spring + Node 런타임), API 계약·빌드 파이프라인 추가. → 운영 복잡도 증가를 감수.
  2. 보안 (공격 표면 축소)
    • Spring API를 내부망(private network)에 격리 → 외부에서 직접 노출되지 않음.
    • 서버 컴포넌트(RSC, React Server Components)에서 민감 로직·데이터 접근을 처리하면 해당 코드가 브라우저로 전송되지 않음.
  3. 개발 생산성 (상태/DOM 동기화)
    • React의 선언적 렌더링으로 상태(state)와 DOM 동기화를 자동화 → 인터랙션 개발 생산성 향상.
  4. SEO (부분 SSR)
    • Next.js로 SEO가 필요한 영역에 SSR/RSC를 적용.

2. 현재 구조 (As-Is)

  • MPA(Multi-Page Application) + SSR 방식.
  • 페이지 이동마다 서버가 완성된 HTML을 렌더링해 전달.
  • 장점: 초기 로딩이 빠르고 SEO에 유리 → 이벤트 페이지에서 가장 중요한 요구사항.
  • 단점: front/back 종속, 인터랙션 한계.

3. 전환 결론 (To-Be)

구분기술
ServerSpring Boot
ClientReact + Next.js (BFF 역할)

4. 구조: BFF (Backend For Frontend)

Next.js가 브라우저와 Spring API 사이의 단일 진입점(BFF) 역할을 한다.

[브라우저]
   │  httpOnly 쿠키 (User JWT) 자동 첨부 + CSRF 토큰
   ▼  외부 (HTTPS)
[Next.js 서버 = BFF]   ← 외부에 노출되는 유일한 진입점
   │  쿠키에서 토큰 추출 → Authorization 헤더로 부착
   ▼  내부망 (server-to-server)
[Spring API]           ← 외부에서 직접 접근 불가
   ▼
[DB]

4.1 핵심 원칙

  • User JWT: httpOnly 쿠키에 저장 (브라우저 JS 접근 차단).
  • 브라우저 호출 방식: 모든 데이터 요청은 BFF의 /api/proxy를 경유한다. 브라우저가 Spring API를 직접 호출하지 않는다(same-origin 강제). → 서버 컴포넌트든 클라이언트 컴포넌트든 동일하게 적용.
  • 토큰 헤더 부착: 토큰을 요청 헤더에 싣는 작업은 서버 프록시(BFF)에서 수행. 브라우저는 토큰 값을 직접 다루지 않는다.
  • 쿠키 기반 인증 교환: POST /api/auth/login (BFF 라우트, fetch) → 내부에서 백엔드 /auth/cookie-login 호출.

5. 인증 / 세션 처리

5.1 토큰 발급·검증의 책임 분담

단계주체역할
토큰 발급Spring자격 검증 후 accessToken / refreshToken 발급 (발급 권한 독점)
1차 검증·중계Next.js (BFF)쿠키에 토큰 심기, 요청마다 서명·만료 검증, 내부망으로 중계. 로그인 여부·경로 보호 목적
최종 검증·권한 판단Spring토큰을 독립적으로 재검증 + 리소스 접근 권한(authorization) 판단

심층 방어(defense in depth): BFF가 검증했더라도 Spring은 자기에게 도착한 토큰을 독립적으로 다시 검증한다. BFF 우회 호출 시에도 데이터가 보호되도록, 데이터를 내주는 Spring이 최종 방어선을 갖는다.

5.2 토큰 저장 위치

  • 토큰은 브라우저 JS가 읽을 수 있는 곳(localStorage)이 아니라 JS 접근이 차단된 httpOnly 쿠키에 보관한다.
  • 쿠키는 브라우저에 저장되지만 JS 접근이 차단되며, 읽고 검증하는 주체는 Next.js 서버(BFF)다.
  • 대비 축은 "브라우저 vs 서버"가 아니라 "JS가 읽을 수 있는가(localStorage) vs 없는가(httpOnly)"이다.

5.3 토큰 클레임(claim) 원칙

  • JWT payload는 암호화가 아니라 base64 인코딩이므로, 누구나 디코딩하면 내용을 읽을 수 있다(서명은 위조만 막을 뿐 열람은 못 막는다).
  • 따라서 토큰에는 개인정보를 넣지 않는다.
    • 담는 것: 내부 식별자(sub = USER_ID 등), 권한(role/scope), 메타데이터(iat, exp, iss).
    • 절대 넣지 않는 것: 비밀번호, 주민번호, 카드번호, 이름·이메일·전화번호 등 직접 식별 가능한 개인정보.
  • 실제 개인정보(USERNAME 등)는 토큰에서 꺼내지 않고, 토큰의 ID로 Spring이 DB를 조회하여 제공한다.

5.4 키 관리 전략 (다중 서버 대응)

  • stateless 토큰은 키만 있으면 어느 서버든 독립 검증 가능 → 스케일아웃에 강함.
  • 단, 서버마다 키가 다르면 검증이 실패하므로 키 일관성이 필수.
  • 비대칭키(RS256) 권장: Spring(인증 서버)이 private key로 발급, BFF·여러 Spring 인스턴스는 public key로 검증만. 공개키는 노출돼도 위조 불가.
  • 키 교체(rotation) 시 무중단을 위해 옛 키·새 키를 일정 기간 동시 인정(또는 JWKS 엔드포인트로 공개키 동적 배포).

5.5 로그인 흐름

신규 플랫폼의 accessToken 유효성에 따라 분기한다. (단순 "유/무"가 아니라 서명·만료 검증 통과 여부로 판단)

  1. 유효한 accessToken이 없는 경우 (없거나 검증 실패)
    • POST /api/auth/login → 내부 /auth/cookie-login 호출하여 accessToken 발급·수신.
  2. 유효한 accessToken이 있는 경우
    • /api/proxy/auth/profile 호출 → 로그인 정보(USER_ID, USERNAME 등)를 서버 조회로 획득.

5.6 토큰 만료·갱신

  • accessToken 만료 시간: 15분.
  • API 호출 시마다 토큰의 서명·만료를 검증.
  • 토큰이 만료/부재면 refreshToken으로 재발급 요청.
  • refreshToken으로도 재발급에 실패하면 최종 실패 → 세션 종료 후 로그인 페이지로 리다이렉트.

6. CSRF 방어

httpOnly 쿠키는 XSS(토큰 탈취)는 막지만, 쿠키 자동 첨부 성질 때문에 CSRF(cross-site request forgery)에 노출된다. XSS와 CSRF는 별개의 공격이므로 둘 다 방어해야 한다.

공격노리는 것방어
XSS토큰 탈취httpOnly 쿠키 (JS 접근 차단)
CSRF토큰은 그대로, 자동 첨부 악용SameSite 속성 + CSRF 토큰
  • SameSite: 쿠키에 SameSite=Lax(현대 브라우저 기본·권장) 설정 → cross-site 요청의 쿠키 자동 첨부 차단.
  • CSRF 토큰: 상태 변경 요청(POST/PUT/DELETE)에 예측 불가능한 토큰을 요구. 외부 사이트는 same-origin 정책상 이 값을 읽을 수 없어 위조 요청이 걸러짐.
  • BFF 구조에서는 브라우저 ↔ Next.js 구간에 방어를 건다(Next.js ↔ Spring은 내부망 통신이라 무관).
  • 쿠키 설정 시 httpOnly + secure(HTTPS 전용) + sameSite를 한 세트로 적용.
cookies().set('token', jwt, {
  httpOnly: true,   // XSS 방어 (JS 접근 차단)
  secure: true,     // HTTPS에서만 전송
  sameSite: 'lax',  // CSRF 방어 (cross-site 자동첨부 차단)
  path: '/',
});

7. Data Fetching

  • 경로: 브라우저 → Next.js 서버(BFF) → Spring API.
  • 클라이언트 컴포넌트의 모든 데이터 요청은 Next.js Route Handler(/api/proxy)를 경유하며, 브라우저에서 Spring API를 직접 호출하지 않는다(same-origin 강제). → CORS 문제 회피, 토큰을 브라우저가 만지지 않음.
  • 서버 컴포넌트(RSC)는 Next.js 서버에서 직접 Spring을 호출(내부망). 민감 데이터는 RSC에서 처리해 클라이언트 번들로 나가지 않게 한다.

8. 하이드레이션 불일치 (Hydration Mismatch) 대응

  • 하이드레이션: SSR이 보낸 정적 HTML에 JS로 이벤트·상태를 부착해 인터랙티브하게 만드는 과정.
  • 불일치: 서버가 그린 HTML과 브라우저가 재계산한 HTML이 다를 때 발생. SSR을 쓰는 이상 반드시 고려해야 함.
  • 원인: 시간(new Date()), 랜덤 값(Math.random()), 타임존·로케일 차이, 브라우저 전용 API(window, localStorage).
  • 대응: 환경에 따라 달라지는 값은 첫 렌더에서 제외하고, useEffect하이드레이션 후 채우거나 클라이언트 전용 렌더로 격리한다.
    • 서버에서는 useEffect가 실행되지 않으므로, 첫 렌더 결과가 서버·클라이언트 동일해져 불일치가 방지됨.
  • 이벤트 페이지 대표 사례: 카운트다운 타이머, "마감까지 D-3" 같은 시간 의존 표시.
function Countdown() {
  const [remaining, setRemaining] = useState(null); // 첫 렌더: 서버·클라 모두 null
  useEffect(() => {
    setRemaining(calcRemaining()); // 하이드레이션 후 채움
  }, []);
  return <span>{remaining ?? "로딩 중"}</span>;
}

9. 마이그레이션 전략

  • 점진 전환(strangler fig 패턴): 기존 MPA를 한 번에 갈아엎지 않는다.
  • 신규 이벤트 도메인부터 적용 → 새로 생성되는 이벤트부터 신규 구조(React + Next.js)로 점차 확대.
  • 기존 안정 페이지는 그대로 두어 마이그레이션 위험을 최소화.

10. 운영 고려사항

  • Node 런타임 운영: Spring 외에 Next.js(Node) 프로세스의 모니터링·패치·스케일링·배포 주체를 정의해야 함.
  • 세션 전략: stateless(서명된 토큰) 방식 채택 → 서버 메모리 세션 불필요, 수평 확장 용이. (refresh 토큰 관리에서 일부 상태가 필요할 수 있음.)
  • BFF 자체 보안: 외부 노출 진입점이므로 Next.js 서버의 의존성 취약점·SSRF 등을 관리.
  • HTTPS 적용: 개인정보가 지나가는 브라우저 ↔ BFF 구간 암호화 필수. 로그에 개인정보·토큰을 남기지 않도록 주의.

11. 책임 분담 요약

항목Next.js (BFF)Spring API
토큰 발급○ (private key 서명)
토큰 검증○ (1차 관문: 서명·만료)○ (최종 재검증)
권한 판단 (authorization)△ (경로 보호 수준)○ (리소스 소유권·권한)
개인정보 조회✕ (통로 역할)○ (ID로 DB 조회)
쿠키 설정·CSRF 방어
외부 노출○ (유일한 진입점)✕ (내부망)

부록: 용어

  • BFF (Backend For Frontend): 프론트엔드 전용 백엔드. 여기서는 Next.js 서버가 브라우저와 Spring 사이의 중계·보안 계층 역할.
  • RSC (React Server Components): 서버에서만 실행되어 클라이언트로 JS를 전송하지 않는 컴포넌트.
  • stateless 토큰: 서버가 세션을 저장하지 않고, 토큰 자체에 정보를 담아 검증하는 방식(JWT).
  • 하이드레이션(hydration): SSR HTML에 클라이언트 JS로 동작을 부착하는 과정.
profile
develop life

0개의 댓글