1. 🏋️‍♂️DO EAT FIT

DO EAT FIT은 단순히 운동법을 알려주는 웹사이트가 아닙니다. 이 프로젝트의 핵심 목표는 사용자와 관리자(트레이너)라는 두 개의 서로 다른 사용자 그룹에게 최적화된 경험을 제공하는 것입니다.

사용자에게는 개인화된 건강 데이터를 기반으로 과학적인 식단과 운동 가이드를 제공하고, 관리자에게는 서비스의 모든 콘텐츠를 효율적으로 생성하고 관리할 수 있는 CMS(콘텐츠 관리 시스템) 를 제공합니다.

이 문서는 DO EAT FIT을 개발하며 겪었던 기술적 결정, 문제 해결 과정, 그리고 인프라 구축 경험을 담은 포트폴리오입니다. 안정적인 아키텍처 위에서 어떻게 보안과 성능, 그리고 확장성을 동시에 고려했는지 그 고민의 과정을 상세히 기록했습니다.

2. 🏗️ 아키텍처: 분리와 확장을 고려한 설계

본 프로젝트는 현대적인 웹 서비스의 요구사항인 보안, 확장성, 관측 가능성을 중심으로 설계되었습니다.

2-1. 아키텍처 상세 분석 (구조도 기반)

제시된 구조도는 단순한 3-Tier 구조를 넘어, 서비스의 역할을 명확히 분리하고 인프라 요소를 체계화했습니다.

  1. 보안 및 트래픽 관리 (Edge):

    • Cloudflare: 최전선에서 CDN, DDoS 방어 및 Bot 차단(Turnstile) 역할을 수행하여 안정성을 확보합니다.

    • Nginx Proxy Manager: 리버스 프록시 서버로서, 각 애플리케이션(Frontend, Backend, CMS)으로의 라우팅을 담당하며 SSL 인증서를 관리합니다.

  2. 애플리케이션 계층 (Application Layer):

    • Frontend (Next.js): Page Router 기반으로 사용자에게 실제 서비스를 제공하는 메인 애플리케이션입니다.

    • Backend (Spring Boot): 모든 비즈니스 로직과 API를 처리하는 핵심 서버입니다.

    • CMS (Next.js): 사용자 앱과 완전히 분리된 관리자 전용 애플리케이션입니다. Next.js의 미들웨어(Middleware) 를 활용하여 admin.example.com과 같은 관리자 전용 서브도메인 또는 특정 경로로 접근 시, 별도의 관리자 페이지로 라우팅하여 사용자/관리자 간의 관심사를 명확히 분리하고 보안을 강화했습니다.

  3. 데이터 및 스토리지 계층 (Data Layer):

    • MySQL (Database): 사용자와 콘텐츠 정보 등 정형 데이터를 저장하는 메인 RDBMS입니다.

    • Redis (Cache): JWT Refresh Token 저장, 중복 로그인 방지, 이메일 인증번호 등 빠른 만료 시간이 필요한 휘발성 데이터를 처리하는 인메모리 데이터 저장소로 활용됩니다.

    • MinIO (Object Storage): 공지사항 이미지, 운동 가이드 영상, 사용자 프로필 사진 등 대용량 비정형 데이터를 저장합니다. API 서버의 부담을 줄이고 정적 파일을 효율적으로 서빙합니다.

  4. DevOps 및 모니터링:

    • Jenkins (CI/CD): GitHub와 연동되어 Jenkinsfile에 정의된 파이프라인(빌드-테스트-Docker 이미지 빌드-배포)을 자동화합니다.

    • Docker: 모든 애플리케이션을 컨테이너화하여 일관된 배포 환경을 보장합니다.

    • Prometheus & Grafana: Spring Actuator와 연동된 Prometheus가 애플리케이션 메트릭(JVM, API 응답 속도 등)을 수집하고, Grafana로 시각화하여 서비스 **관측 가능성(Observability)**을 확보합니다.

2-2. Mermaid: Frontend-Backend Flow Chart

sequenceDiagram
    participant User as 사용자 (Browser)
    participant FE as Frontend (Next.js)
    participant BE as Backend (Spring Boot)
    participant DB as MySQL
    participant Cache as Redis
    participant Storage as MinIO

    User->>+FE: 로그인 요청 (ID/PW)
    FE->>+BE: /api/auth/login (LoginRequestDto)
    BE->>BE: 1. 사용자 정보 확인 (BCrypt)
    BE->>DB: 2. DB에서 유저 정보 조회
    BE->>BE: 3. JWT 생성 (Access/Refresh)
    BE->>Cache: 4. Refresh Token 저장 (Key: Email)
    BE-->>FE: TokenSetDto (Access/Refresh)  /* 수정 1: FE가 할 일이 남았으므로 '-' 제거 */
    FE->>FE: (LocalStorage) AccessToken 저장
    FE-->>User: (응답) 로그인 성공         /* 수정 2: 화살표 방향, User 비활성화(-) 제거 */

    User->>+FE: 마이페이지 접근
    FE->>FE: (LocalStorage) AccessToken 확인
    FE->>+BE: /api/user/my-info (Header + JWT)
    BE->>BE: (JwtFilter) 토큰 검증
    BE->>DB: 사용자 정보 조회
    BE-->>FE: UserDto (사용자 정보)
    FE-->>User: 마이페이지 렌더링

3. 🛠️ 핵심 기술 스택 (Tech Stack)

구분Backend (doeatfit_back)Frontend (doeatfit_front)
LanguageJava 17TypeScript
FrameworkSpring Boot 3Next.js (Page Router), React
DatabaseJPA (Spring Data JPA), Querydsl-
UI/State-shadcn/ui, React Context, React Query
SecuritySpring Security, JWT-
API/Editor-Axios, Editor.js
CacheRedis (In-Memory Data Store)-
StorageMinIO (Object Storage)-
ValidationSpring Validation (Custom Annotations)-
TestingJUnit 5, Mockito, Integration Test-
DevOpsDocker, Jenkins-
MonitoringPrometheus, Actuator-

4. 💡 주요 기능 및 기술적 포인트

4-1. 🛡️ 보안 및 인증/인가 (Security & Auth)

안전한 서비스 제공을 위해 Spring Security를 기반으로 강력한 인증 및 인가 체계를 구축했습니다.

  • JWT (JSON Web Token) 인증:

    • UsernamePasswordAuthenticationFilter를 커스텀하여 로그인 시 Access Token과 Refresh Token을 발급합니다.

    • JwtFilter를 구현하여 매 요청마다 Access Token을 검증합니다.

  • 토큰 관리 전략:

    • (Frontend) LocalStorage: Access Token을 저장하며, Axios Interceptor를 통해 모든 API 요청 헤더에 자동으로 주입합니다.

    • (Backend) Redis: Refresh Token을 사용자 값을 Key로 하여 Redis에 저장합니다. 이는 서버에서 토큰을 제어(로그아웃, 중복 로그인 방지)할 수 있게 합니다. 또한, 회원가입 시 발급되는 이메일 인증번호 등 휘발성 데이터도 Redis를 통해 관리합니다.

  • RBAC (역할 기반 접근 제어):

    • UserRole Enum(USER, ADMIN)을 통해 사용자 권한을 분리했습니다.

    • UserSecurityConfig에서 requestMatchers를 통해 API 엔드포인트별로 접근 권한을 세밀하게 제어합니다. (e.g., /api/admin-portal/**ADMIN만 접근)

  • 프론트엔드 라우트 보안 (HOC):

    • with-auth.tsx 고차 컴포넌트(HOC)를 구현하여, /my-page 등 인증이 필요한 페이지 접근 시 인증 상태를 확인하고, 비인증 사용자는 /login 페이지로 리디렉션시킵니다.
  • 비밀번호 암호화:

    • PasswordConfigBCryptPasswordEncoder를 Bean으로 등록하여 사용자의 비밀번호를 안전하게 해시하여 저장합니다.
  • 🤖 봇 차단 (Cloudflare Turnstile):

    • 회원가입, 로그인 등 주요 인증 API에 Cloudflare Turnstile을 도입했습니다.

    • TurnstileApiController (Backend)를 통해 프론트엔드에서 받은 턴스타일 토큰을 검증하여 악의적인 봇 트래픽을 원천 차단합니다.

  • 입력값 검증:

    • @Valid@Password, @Nickname, @Enum 등 커스텀 Annotation Validator를 제작하여 DTO의 입력값을 1차(Controller), 2차(Entity)로 검증합니다.

4-2. 📡 API 설계 및 예외 처리 (Backend)

안정적인 API 운영을 위해 일관된 응답 및 예외 처리 전략을 수립했습니다.

  • 통합 예외 처리 (GlobalExceptionHandler):

    • @RestControllerAdvice를 사용하여 전역 예외 처리기를 구현했습니다.

    • InvalidTokenException, PasswordNotMatchesException 등 직접 정의한 커스텀 예외는 물론, MethodArgumentNotValidException (Validation 실패) 등 Spring의 내장 예외까지 모두 감지하여 일관된 에러 응답을 반환합니다.

  • 표준화된 응답 포맷 (ApiResponse, ErrorResponse):

    • ErrorCodeEnum을 통해 모든 오류 케이스를 코드로 정의하고, ErrorResponse DTO를 사용하여 “상태 코드, 에러 코드, 메시지”를 포함하는 표준화된 JSON 형식으로 클라이언트에 응답합니다. 이는 프론트엔드에서의 오류 처리를 매우 용이하게 합니다.

4-3. 🧪 테스트 전략 (Backend)

코드의 품질과 안정성을 보장하기 위해 src/test/java에 다양한 계층의 테스트 코드를 작성했습니다.

  • 단위 테스트 (Unit Test): TestUserEntities.java 등 Entity와 DTO의 핵심 로직을 테스트합니다.

  • JPA/Repository 테스트: @DataJpaTest를 활용하여 SearchUserJpaTest.java 등 리포지토리 계층의 쿼리(Querydsl 포함)가 정확히 동작하는지 검증합니다.

  • 통합 테스트 (Integration Test): @SpringBootTest를 사용하여 SearchUserIntegratedTest.java 등 실제 서비스 로직이 API 엔드포인트부터 데이터베이스까지 올바르게 동작하는지 통합 테스트를 수행합니다.

  • 동시성 테스트 (Concurrency Test): TokenUtilConcurrencyTest.java에서 ExecutorServiceCountDownLatch를 사용하여 멀티 스레드 환경에서 JWT 토큰 생성이 동시에 발생해도 안전한지(Thread-safe) 검증했습니다.

4-4. 🏛️ 분리된 CMS와 콘텐츠 관리

운영 효율성을 극대화하기 위해 사용자 애플리케이션과 관리자 CMS를 분리했습니다.

  • 서브도메인 분리: Next.js의 미들웨어(middleware.ts)를 활용하여 관리자 전용 서브도메인(e.g., admin.example.com)으로 접근 시, 관리자 전용 레이아웃과 인증 로직을 타도록 구현하여 사용자/관리자 간의 관심사를 명확히 분리하고 보안을 강화했습니다.

  • Editor.js 활용:

    • ToastUI 에디터의 지원 중단 이슈(Issue: ToastUI 지원 중단)에 대응하여, 현대적인 블록 기반 에디터인 Editor.js를 도입했습니다.

    • (Backend) EditorJsStorageService: Editor.js에서 생성된 JSON 형식의 콘텐츠 데이터를 파싱하고, 이미지/영상 등 첨부파일을 MinIO에 업로드한 후 URL을 반환하여 JSON 데이터에 삽입하는 로직을 구현했습니다.

  • MinIO (Object Storage) 연동:

    • 모든 리소스(운동 영상, 공지 이미지, 프로필 사진)를 MinIO에 저장합니다.

    • 이를 통해 백엔드 서버의 파일 I/O 부담을 제거하고, 향후 CDN 연동 및 스토리지 확장에 용이한 구조를 확보했습니다.

4-5. 📊 맞춤형 헬스케어 기능 (설계 및 UX)

  • 클린 아키텍처 (Backend):

    • 백엔드 설계 시, UserService (Application Service - DTO를 받아 로직을 조율)와 UserDomainService (Domain Service - 핵심 비즈니스 로직, Entity 처리)를 분리했습니다. 이는 로직의 응집도를 높이고, 테스트 용이성을 확보하며, 향후 비즈니스 로직 변경에 유연하게 대응할 수 있는 구조입니다.
  • 식단/심박수 계산기 (Frontend/Backend):

    • (Backend) 사용자의 신체 정보, 활동량(ActivityLevel), 목표(DietGoal)를 Enum으로 받아 기초대사량(BMR) 및 TDEE, 카보넨 공식(심박수)을 계산하는 비즈니스 로직을 구현했습니다.

    • (Frontend) DietCalculatorNoAuthService를 통해 비회원도 사용 가능하도록 개방하고, 회원의 경우 DietCalculatorService를 통해 계산 이력을 저장 및 조회할 수 있게 했습니다.

  • UX 개선 (Skeleton Loader):

    • Issue: 운동 추천에 skeleton 적용을 반영하여, workout-card-skeleton.tsx 컴포넌트를 구현했습니다.

    • 운동 목록처럼 데이터를 비동기로 불러오는 페이지에서, 데이터가 로드되기 전까지 스켈레톤 UI를 노출하여 사용자가 로딩 중임을 인지하고 더 나은 사용자 경험(Perceived Performance)을 제공합니다.

  • 인터랙티브 운동 추천 (SVG):

    • Beginner_Front.svg 등 인터랙티브 SVG 인체 지도를 제공하여, 사용자가 직관적으로 근육 부위를 클릭하면 Querydsl로 해당 부위(majorMuscle, minorMuscle)의 운동 목록을 동적으로 조회합니다.

4-6. 🚀 CI/CD 파이프라인 및 모니터링

  • Jenkins (CI/CD):

    • Jenkinsfile (Declarative Pipeline)을 통해 GitHub main 브랜치 Push 이벤트 시 CI/CD 파이프라인이 자동 실행됩니다.

    • Pipeline: Build (Gradle/npm)Test (JUnit/Jest)Docker Build & PushDocker Deploy

  • Prometheus (Monitoring):

    • build.gradlemicrometer-registry-prometheus 의존성을 추가하고, PrometheusExemplarSamplerConfiguration 설정을 통해 Spring Boot 애플리케이션의 메트릭을 수집합니다.

    • 이를 통해 JVM 상태, API 응답 시간, DB 커넥션 풀 상태 등을 모니터링하며 서비스 관측 가능성을 확보했습니다.

5. 🎯 트러블슈팅 및 기술적 결정

프로젝트 진행 중 발생한 이슈(📜Issue 폴더 기반)와 해결 과정은 다음과 같습니다.

  1. [UI/UX] MUI에서 shadcn/ui로 마이그레이션

    • 문제: 기존 MUI는 번들 사이즈가 크고, 커스터마이징이 복잡한 단점이 있었습니다.

    • 해결: Issue: mui → shadcn ui를 통해 컴포넌트 단위로 설치하고 Tailwind CSS 기반으로 스타일을 자유롭게 제어할 수 있는 shadcn/ui로 전면 교체했습니다. 이 과정에서 디자인 시스템을 재정립하고, 다크 모드를 구현하며, 웹 성능을 향상시켰습니다.

  2. [Security] 인증 토큰 저장 방식 결정 (HttpOnly Cookie vs. LocalStorage)

    • 초기 의도: 백엔드 코드의 CookieUtil.java에서 볼 수 있듯이, 초기에는 XSS 공격에 더 안전한 HttpOnly 쿠키 방식을 사용하려 했습니다.

    • 문제: 하지만 Next.js의 CSR(클라이언트 사이드 렌더링) 환경에서 HttpOnly 쿠키를 자바스크립트로 제어할 수 없어, Axios Interceptor에서의 토큰 주입 및 갱신(Refresh) 로직 구현이 복잡했습니다.

    • 결정: Issue: jwt 토큰 쿠키 → 로컬 스토리지에 저장하기를 통해 기술 전략을 수정했습니다.

      1. Access Token: localStorage에 저장하여 use-axios-interceptor.ts가 요청 시마다 헤더에 쉽게 주입하도록 했습니다. (XSS 위험은 있으나 편의성 확보)

      2. Refresh Token: 탈취 시 보안 위협이 큰 Refresh Token은 서버 Redis에 저장하고, 클라이언트에는 일반 Token으로 발급하여 localStorage에 저장합니다.

      3. 보완: 이 하이브리드 전략을 통해, Redis에 저장된 Refresh Token을 서버가 직접 제어(로그아웃, 중복 로그인 방지)할 수 있게 하여 localStorage의 단점을 일부 보완했습니다.

  3. [Infra] Nginx 500/502 Gateway Error (File Descriptors 고갈)

    • 문제: Issue: 간헐적으로 Nginx Proxy Manager 500Error 발생에서 보듯이, Jenkins 배포 직후 또는 서비스 운영 중 트래픽이 쌓이면 Nginx에서 500/502 Gateway Error가 간헐적으로 발생했습니다.

    • 분석: 초기에는 Docker 네트워크나 리졸버 문제로 추정했으나, Nginx 로그에서 socket() failed (24: No file descriptors available), accept4() failed (24: No file descriptors available) 등 “File Descriptors 고갈” 오류를 확인했습니다.

    • 핵심 원인: Nginx가 동작하는 LXC 컨테이너의 File Descriptor(열린 파일) 한도(ulimit) 가 낮게 설정되어, 트래픽이 증가함에 따라 Nginx가 새로운 소켓(연결)이나 로그 파일을 열지 못해 오류가 발생한 것이었습니다. 재부팅 시 이 핸들이 초기화되어 일시적으로 해결됐던 것입니다.

    • 해결: Nginx Proxy Manager 컨테이너가 아닌, 이를 호스팅하는 LXC 컨테이너 자체의 ulimit 설정을 상향하여(e.g., /etc/security/limits.conf 수정) Nginx가 사용할 수 있는 FD 한도를 넉넉하게 확보하여 문제를 근본적으로 해결했습니다.

  4. [Performance] 비효율적인 리소스 서빙

    • 문제: 초기 설계에서는 운동 영상이나 이미지를 Spring Boot 서버가 직접 서빙하여 I/O 병목이 발생했습니다.

    • 해결: Issue: 영상, 프로필 이미지... 오브젝트 스토리지로 마이그레이션을 통해 MinIO를 도입했습니다. EditorJsStorageServiceFileValidator를 구현하여 파일 업로드 로직을 MinIO로 완전히 이관, 서버의 부담을 획기적으로 줄이고 리소스 서빙 속도를 개선했습니다.

6. 향후 계획 및 개선점

DO EAT FIT은 완성된 프로젝트가 아닌, 지속적으로 성장하는 플랫폼을 목표로 합니다.

  • 스토리지 마이그레이션 (MinIO → Cloudflare R2):

    현재 사용 중인 MinIO가 CE(Community Edition)의 라이선스 정책 변경 및 지원 중단 이슈가 있습니다. 이에 대응하여 S3 API와 호환되며 저렴한 Egress 비용을 제공하는 Cloudflare R2로의 마이그레이션을 긍정적으로 검토하고 있습니다.

  • 핵심 기능 추가 (헬스 다이어리):

    현재의 계산기 기능을 넘어, 사용자가 매일의 운동, 식단, 컨디션(수면, 피로도 등)을 기록할 수 있는 다이어리 기능을 추가할 예정입니다. 이는 장기적인 건강 데이터를 축적하여 사용자에게 더 정밀한 피드백을 제공하는 기반이 될 것입니다.

  • SNS 공유 기능 (인증 이미지 생성):

    사용자가 기록한 운동 및 식단 다이어리를 기반으로 SNS에 공유할 수 있는 ‘인증 이미지’ 생성 기능을 추가할 계획입니다. 이는 사용자의 성취감을 높이고 서비스의 바이럴 마케팅 요소로 작용할 것입니다.

  • 모니터링 고도화 (Alerting):

    현재 Prometheus로 메트릭을 수집하는 것을 넘어, 주요 장애 상황(HTTP 500, 400, 404 등) 발생 시 Alertmanager와 연동된 봇을 통해 개발용 Private Discord 채널로 실시간 알림을 전송하도록 설정되어 있습니다. 향후에는 수집된 메트릭을 기반으로 Grafana 대시보드를 더욱 구체화하여, 장애 발생 전 징후를 파악할 수 있는 관측 가능성 시스템을 완성할 계획입니다.