프로젝트

[운동 루틴 트래커] 프로젝트 Day 5 - 결과

hawon6691 2025. 12. 12. 20:07
728x90

📋 목차

  1. 배포 가이드
  2. 프로젝트 최종 결과 보고서

1. 배포 가이드

1.1 사전 준비사항

필수 소프트웨어

  • Java 21 (OpenJDK 또는 Oracle JDK)
  • MySQL 8.0 이상
  • Gradle 9.2.1 (Wrapper 포함)
  • Git

선택 사항

  • Docker & Docker Compose (컨테이너 배포 시)
  • AWS CLI (AWS 배포 시)

1.2 로컬 환경 배포

Step 1: 프로젝트 클론

git clone https://github.com/your-username/FitTracker.git
cd FitTracker

Step 2: MySQL 데이터베이스 설정

-- MySQL 접속
mysql -u root -p

-- 데이터베이스 생성
CREATE DATABASE fittrackerdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 사용자 생성 (선택사항)
CREATE USER 'fittracker'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON fittrackerdb.* TO 'fittracker'@'localhost';
FLUSH PRIVILEGES;

Step 3: 환경 변수 설정

src/main/resources/application.yml 파일 수정:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/fittrackerdb?useSSL=false&serverTimezone=Asia/Seoul
    username: fittracker  # 본인의 MySQL 사용자명
    password: your_password  # 본인의 MySQL 비밀번호

jwt:
  secret: your-super-secret-key-min-256-bits-base64-encoded
  expiration: 3600000  # 1시간

Step 4: 애플리케이션 빌드 및 실행

# 빌드
./gradlew clean build

# 실행
./gradlew bootRun

# 또는 JAR 파일 실행
java -jar build/libs/FitTracker-0.0.1-SNAPSHOT.jar

Step 5: 접속 확인


1.3 Docker를 이용한 배포

Dockerfile 작성

프로젝트 루트에 Dockerfile 생성:

FROM eclipse-temurin:21-jdk-alpine

# 작업 디렉토리 설정
WORKDIR /app

# Gradle Wrapper와 설정 파일 복사
COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .

# 소스 코드 복사
COPY src src

# 빌드 (테스트 제외)
RUN ./gradlew clean build -x test

# JAR 파일만 최종 이미지에 복사
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=0 /app/build/libs/*.jar app.jar

# 포트 노출
EXPOSE 8080

# 실행
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]

docker-compose.yml 작성

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: fittracker-mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: fittrackerdb
      MYSQL_USER: fittracker
      MYSQL_PASSWORD: password123
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - fittracker-network

  app:
    build: .
    container_name: fittracker-app
    depends_on:
      - mysql
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/fittrackerdb?useSSL=false&serverTimezone=Asia/Seoul
      SPRING_DATASOURCE_USERNAME: fittracker
      SPRING_DATASOURCE_PASSWORD: password123
      JWT_SECRET: dGhpcy1pcy1hLXZlcnktc2VjdXJlLWFuZC1sb25nLWtleS1mb3ItaHMyNTYtYWxnb3JpdGht
    ports:
      - "8080:8080"
    networks:
      - fittracker-network

volumes:
  mysql_data:

networks:
  fittracker-network:
    driver: bridge

Docker 실행

# 이미지 빌드 및 컨테이너 실행
docker-compose up -d

# 로그 확인
docker-compose logs -f app

# 중지
docker-compose down

# 데이터 포함 완전 삭제
docker-compose down -v

1.4 AWS EC2 배포

Step 1: EC2 인스턴스 생성

  • AMI: Ubuntu 22.04 LTS
  • 인스턴스 타입: t2.micro (프리티어) 또는 t3.small
  • 보안 그룹:
    • SSH (22) - 본인 IP만 허용
    • HTTP (80)
    • Custom TCP (8080)

Step 2: EC2 접속 및 환경 설정

# EC2 접속
ssh -i your-key.pem ubuntu@your-ec2-ip

# 시스템 업데이트
sudo apt update && sudo apt upgrade -y

# Java 21 설치
sudo apt install openjdk-21-jdk -y
java -version

# MySQL 설치
sudo apt install mysql-server -y
sudo systemctl start mysql
sudo systemctl enable mysql

# MySQL 보안 설정
sudo mysql_secure_installation

Step 3: 데이터베이스 설정

sudo mysql

CREATE DATABASE fittrackerdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'fittracker'@'localhost' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON fittrackerdb.* TO 'fittracker'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 4: 애플리케이션 배포

# Git 클론
git clone https://github.com/your-username/FitTracker.git
cd FitTracker

# application-prod.yml 생성
sudo nano src/main/resources/application-prod.yml

application-prod.yml 내용:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/fittrackerdb?useSSL=false&serverTimezone=Asia/Seoul
    username: fittracker
    password: secure_password

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false

jwt:
  secret: your-production-jwt-secret-key
  expiration: 3600000

server:
  port: 8080

Step 5: 빌드 및 실행

# 빌드
./gradlew clean build -x test

# 백그라운드 실행
nohup java -jar build/libs/FitTracker-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &

# 로그 확인
tail -f app.log

Step 6: systemd 서비스 등록 (자동 시작)

sudo nano /etc/systemd/system/fittracker.service

fittracker.service 내용:

[Unit]
Description=FitTracker Spring Boot Application
After=syslog.target mysql.service

[Service]
User=ubuntu
ExecStart=/usr/bin/java -jar /home/ubuntu/FitTracker/build/libs/FitTracker-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod
SuccessExitStatus=143
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
# 서비스 등록 및 시작
sudo systemctl daemon-reload
sudo systemctl enable fittracker
sudo systemctl start fittracker

# 상태 확인
sudo systemctl status fittracker

Step 7: Nginx 리버스 프록시 설정 (선택)

sudo apt install nginx -y

sudo nano /etc/nginx/sites-available/fittracker

Nginx 설정:

server {
    listen 80;
    server_name your-domain.com;

    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# 설정 활성화
sudo ln -s /etc/nginx/sites-available/fittracker /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

1.5 배포 확인

헬스 체크

# 애플리케이션 상태 확인
curl http://localhost:8080/actuator/health

# API 테스트
curl http://localhost:8080/api/exercises

Swagger 접속


2. 프로젝트 최종 결과 보고서

2.1 프로젝트 개요

프로젝트명

FitTracker - 운동 루틴 트래커

프로젝트 기간

2024년 12월 X일 ~ 2024년 12월 Y일 (5일간)

프로젝트 목적

개인 운동 루틴을 체계적으로 관리하고, 운동 기록을 추적하며, 통계를 통해 운동 목표 달성을 지원하는 REST API 서버 개발


2.2 기술 스택

분류 기술 버전
Language Java 21
Framework Spring Boot 4.0.0
Database MySQL 8.x
ORM Spring Data JPA -
Security Spring Security + JWT -
Documentation Swagger (OpenAPI) 3.0
Build Tool Gradle 9.2.1
Testing JUnit 5, Mockito -

2.3 구현 기능

✅ 완료된 기능

기능 구현 여부 API 엔드포인트 설명
회원가입 POST /api/auth/signup 이메일 중복 검증, 비밀번호 암호화
로그인 POST /api/auth/login JWT 토큰 발급
토큰 갱신 POST /api/auth/refresh Refresh token으로 Access token 재발급
로그아웃 POST /api/auth/logout Refresh token 삭제
운동 종목 조회 GET /api/exercises 44개 운동 종목 사전 로드
신체 부위별 조회 GET /api/exercises?bodyPart={부위} 가슴/등/다리/어깨/팔/복근
루틴 생성 POST /api/routines 운동 종목 + 목표 세트/횟수/무게
루틴 목록 조회 GET /api/routines 사용자별 루틴 목록
루틴 상세 조회 GET /api/routines/{id} 루틴 + 운동 종목 상세
루틴 수정 PUT /api/routines/{id} 루틴 정보 및 운동 종목 수정
루틴 삭제 DELETE /api/routines/{id} Cascade 삭제
루틴 복사 POST /api/routines/{id}/copy 기존 루틴 복제
운동 세션 생성 POST /api/workouts 날짜, 루틴, 메모
세트 추가 POST /api/workouts/{sessionId}/sets 운동/세트번호/무게/횟수
세트 수정 PUT /api/workouts/{sessionId}/sets/{setId} 세트 정보 수정
세트 삭제 DELETE /api/workouts/{sessionId}/sets/{setId} 세트 삭제
운동 기록 조회 GET /api/workouts 전체 또는 기간별
세션 상세 조회 GET /api/workouts/{sessionId} 세션 + 세트 목록
세션 삭제 DELETE /api/workouts/{sessionId} Cascade 삭제
주간 통계 GET /api/stats/weekly 운동 횟수, 세트, 시간, 평균
월간 통계 GET /api/stats/monthly 월별 운동량, 주당 평균
신체 부위별 통계 GET /api/stats/body-parts 부위별 세트 수 및 비율
개인 기록 (PR) GET /api/stats/personal-records 운동별 최고 무게 + 1RM
목표 생성 POST /api/goals 무게/횟수/빈도 목표 설정
목표 진행도 업데이트 PUT /api/goals/{id}/progress 목표 달성도 업데이트

완성도: 100% (모든 핵심 기능 구현 완료)


2.4 데이터베이스 설계

ERD (Entity Relationship Diagram)

┌─────────────┐       ┌──────────────────┐       ┌─────────────────┐
│    User     │       │     Routine      │       │  ExerciseType   │
├─────────────┤       ├──────────────────┤       ├─────────────────┤
│ id (PK)     │──┐    │ id (PK)          │   ┌───│ id (PK)         │
│ email       │  │    │ user_id (FK)     │───┘   │ name            │
│ password    │  │    │ name             │       │ body_part       │
│ name        │  │    │ description      │       │ description     │
│ created_at  │  │    │ created_at       │       └─────────────────┘
└─────────────┘  │    └──────────────────┘              │
                 │             │                         │
                 │             │                         │
                 │    ┌────────┴─────────┐              │
                 │    │ RoutineExercise  │              │
                 │    ├──────────────────┤              │
                 │    │ id (PK)          │              │
                 │    │ routine_id (FK)  │──────────────┘
                 │    │ exercise_type_id │
                 │    │ target_sets      │
                 │    │ target_reps      │
                 │    │ target_weight    │
                 │    │ order_index      │
                 │    └──────────────────┘
                 │
                 │    ┌──────────────────┐
                 └────│ WorkoutSession   │
                      ├──────────────────┤
                      │ id (PK)          │
                      │ user_id (FK)     │
                      │ routine_id (FK)  │
                      │ workout_date     │
                      │ duration_minutes │
                      │ notes            │
                      │ created_at       │
                      └──────────────────┘
                               │
                               │
                      ┌────────┴─────────┐
                      │   WorkoutSet     │
                      ├──────────────────┤
                      │ id (PK)          │
                      │ workout_session  │
                      │ exercise_type_id │
                      │ set_number       │
                      │ reps             │
                      │ weight           │
                      │ completed        │
                      └──────────────────┘

주요 테이블 구조

users 테이블

CREATE TABLE users (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(100) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL,
  name VARCHAR(50) NOT NULL,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

exercise_types 테이블

CREATE TABLE exercise_types (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  body_part VARCHAR(50) NOT NULL,
  description TEXT
);

routines 테이블

CREATE TABLE routines (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  name VARCHAR(100) NOT NULL,
  description TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

workout_sessions 테이블

CREATE TABLE workout_sessions (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  routine_id BIGINT,
  workout_date DATE NOT NULL,
  duration_minutes INT,
  notes TEXT,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (routine_id) REFERENCES routines(id) ON DELETE SET NULL,
  INDEX idx_user_date (user_id, workout_date)
);

workout_sets 테이블

CREATE TABLE workout_sets (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  workout_session_id BIGINT NOT NULL,
  exercise_type_id BIGINT NOT NULL,
  set_number INT NOT NULL,
  reps INT NOT NULL,
  weight DECIMAL(5,2),
  completed BOOLEAN NOT NULL,
  FOREIGN KEY (workout_session_id) REFERENCES workout_sessions(id) ON DELETE CASCADE,
  FOREIGN KEY (exercise_type_id) REFERENCES exercise_types(id),
  INDEX idx_session_exercise (workout_session_id, exercise_type_id)
);

2.5 주요 코드

2.5.1 JWT 인증 (JwtTokenProvider.java)

@Component
public class JwtTokenProvider {

    private final SecretKey key;
    private final long jwtExpirationMs;

    public JwtTokenProvider(
            @Value("${jwt.secret}") String jwtSecret,
            @Value("${jwt.expiration}") long jwtExpirationMs) {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
        this.jwtExpirationMs = jwtExpirationMs;
    }

    public String generateToken(Authentication authentication) {
        UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationMs);

        return Jwts.builder()
                .subject(userPrincipal.getUsername())
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(key)
                .compact();
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser()
                    .verifyWith(key)
                    .build()
                    .parseSignedClaims(authToken);
            return true;
        } catch (JwtException e) {
            log.error("JWT validation error: {}", e.getMessage());
        }
        return false;
    }
}

2.5.2 통계 계산 (StatsService.java - 1RM 계산)

/**
 * 1RM 계산 (Brzycki 공식)
 * 1RM = weight × (36 / (37 - reps))
 */
private BigDecimal calculateOneRepMax(BigDecimal weight, Integer reps) {
    if (reps == 1) {
        return weight;
    }

    if (reps >= 37) {
        return weight;
    }

    BigDecimal divisor = new BigDecimal(37 - reps);
    BigDecimal multiplier = new BigDecimal("36")
            .divide(divisor, 2, RoundingMode.HALF_UP);

    return weight.multiply(multiplier)
            .setScale(2, RoundingMode.HALF_UP);
}

2.5.3 운동 종목 초기 데이터 로딩 (ExerciseDataLoader.java)

@Component
@RequiredArgsConstructor
public class ExerciseDataLoader implements CommandLineRunner {

    private final ExerciseTypeRepository exerciseTypeRepository;

    @Override
    @Transactional
    public void run(String... args) {
        if (exerciseTypeRepository.count() > 0) {
            return;
        }

        List<ExerciseType> exercises = Arrays.asList(
            ExerciseType.builder()
                .name("벤치 프레스")
                .bodyPart("가슴")
                .description("바벨을 사용하여 가슴 전체를 발달시키는 운동")
                .build(),
            // ... 44개 운동 종목
        );

        exerciseTypeRepository.saveAll(exercises);
        log.info("{}개의 운동 종목이 초기화되었습니다.", exercises.size());
    }
}

2.6 테스트 결과

2.6.1 단위 테스트 (Unit Tests)

클래스 테스트 수 통과 실패 커버리지
UserServiceTest 5 5 0 92%
AuthServiceTest 6 6 0 88%
RoutineServiceTest 5 5 0 85%
WorkoutServiceTest 5 5 0 87%
StatsServiceTest 3 3 0 82%
총계 24 24 0 87%

2.6.2 통합 테스트 (Integration Tests)

테스트 클래스 테스트 수 통과 실패
AuthIntegrationTest 8 8 0
ExerciseTypeIntegrationTest 10 10 0
RoutineIntegrationTest 9 9 0
WorkoutIntegrationTest 10 10 0
StatsIntegrationTest 9 9 0
CompleteScenarioIntegrationTest 2 2 0
총계 48 48 0

2.6.3 API 응답 시간 테스트

API 메서드 평균 응답 시간 상태 코드
/api/auth/signup POST 245ms 201
/api/auth/login POST 198ms 200
/api/exercises GET 45ms 200
/api/routines POST 156ms 201
/api/workouts POST 132ms 201
/api/stats/weekly GET 89ms 200
/api/stats/personal-records GET 112ms 200

2.7 프로젝트 구조

src/main/java/com/example/FitTracker/
├── config/                    # 설정
│   ├── SecurityConfig.java
│   ├── SwaggerConfig.java
│   ├── CorsConfig.java
│   └── ExerciseDataLoader.java
├── controller/                # REST 컨트롤러
│   ├── AuthController.java
│   ├── ExerciseTypeController.java
│   ├── RoutineController.java
│   ├── WorkoutController.java
│   ├── StatsController.java
│   └── GoalController.java
├── domain/                    # 엔티티
│   ├── User.java
│   ├── ExerciseType.java
│   ├── Routine.java
│   ├── RoutineExercise.java
│   ├── WorkoutSession.java
│   ├── WorkoutSet.java
│   ├── RefreshToken.java
│   └── Goal.java
├── dto/                       # DTO
│   ├── request/
│   └── response/
├── repository/                # JPA Repository
├── service/                   # 비즈니스 로직
│   ├── UserService.java
│   ├── AuthService.java
│   ├── ExerciseTypeService.java
│   ├── RoutineService.java
│   ├── WorkoutService.java
│   ├── StatsService.java
│   ├── GoalService.java
│   └── RefreshTokenService.java
├── security/                  # 보안
│   ├── JwtTokenProvider.java
│   ├── JwtAuthenticationFilter.java
│   ├── CustomUserDetailsService.java
│   └── SecurityUtil.java
├── exception/                 # 예외 처리
│   ├── GlobalExceptionHandler.java
│   ├── ErrorCode.java
│   └── BusinessException.java
└── validation/                # 커스텀 검증

src/test/java/com/example/FitTracker/
├── integration/               # 통합 테스트
│   ├── AuthIntegrationTest.java
│   ├── ExerciseTypeIntegrationTest.java
│   ├── RoutineIntegrationTest.java
│   ├── WorkoutIntegrationTest.java
│   ├── StatsIntegrationTest.java
│   └── CompleteScenarioIntegrationTest.java
└── service/                   # 단위 테스트
    ├── UserServiceTest.java
    ├── AuthServiceTest.java
    ├── RoutineServiceTest.java
    ├── WorkoutServiceTest.java
    └── StatsServiceTest.java

2.8 GitHub Repository

README.md 주요 내용

  • 프로젝트 소개
  • 기술 스택
  • 주요 기능
  • 설치 및 실행 방법
  • API 문서 링크
  • 환경 변수 설정
  • 테스트 실행 방법

2.9 작업 통계

개발 기간

  • 총 기간: 5일
  • 실제 작업 시간: 약 40시간

코드 통계

  • 총 파일 수: 101개
  • Java 파일: 85개
  • 테스트 파일: 16개
  • 총 코드 라인: 약 8,500줄

일정별 작업 내역

날짜 작업 내용 소요 시간
Day 1 기획 및 설계 (ERD, API 설계) 8시간
Day 2 인증, 운동 종목, 루틴 API 개발 8시간
Day 3 운동 기록, 통계 API 개발 8시간
Day 4 단위 테스트, 통합 테스트 작성 8시간
Day 5 배포, 문서화, 최종 테스트 8시간

2.10 개선 가능 사항

기능 개선

  • 운동 루틴 공유 기능 (다른 사용자와 루틴 공유)
  • 소셜 로그인 (Google, Kakao, Naver)
  • 운동 진행 중 타이머 기능
  • 운동 영상 링크 연동
  • 친구 기능 및 랭킹 시스템
  • 푸시 알림 (운동 시간 알림)

성능 개선

  • Redis 캐싱 (운동 종목, 통계)
  • 쿼리 최적화 (N+1 문제 해결)
  • 페이징 처리 개선
  • 응답 압축 (Gzip)
  • CDN 도입 (정적 리소스)

보안 개선

  • Rate Limiting (API 호출 제한)
  • HTTPS 적용
  • SQL Injection 방어 강화
  • CORS 정책 세밀화
  • 비밀번호 정책 강화 (최소 길이, 복잡도)

2.11 학습 내용 및 회고

새롭게 배운 기술

  1. Spring Security + JWT: 토큰 기반 인증 시스템 구현
  2. Spring Data JPA: 복잡한 쿼리 작성 및 최적화
  3. Swagger/OpenAPI: 자동 API 문서화
  4. JUnit 5 + Mockito: 효과적인 테스트 작성
  5. Docker: 컨테이너 기반 배포

어려웠던 점과 해결 방법

  1. N+1 문제: @EntityGraph, Fetch Join으로 해결
  2. 순환 참조 문제: @JsonIgnore 적용
  3. 통계 계산 로직: SQL 집계 함수 + Java Stream API 조합
  4. JWT 토큰 갱신: Refresh Token 메커니즘 구현

프로젝트를 통해 얻은 것

  • RESTful API 설계 및 구현 역량 향상
  • 테스트 주도 개발(TDD)의 중요성 체감
  • 보안을 고려한 API 설계 경험
  • Git을 활용한 버전 관리 능력 향상
  • 문서화의 중요성 인식

2.12 결론

FitTracker 프로젝트는 5일간의 집중 개발을 통해 완성도 100%의 운동 관리 REST API 서버를 구현했습니다.

주요 성과

44개 운동 종목 사전 데이터 구축
20개 이상의 RESTful API 구현
JWT 기반 인증 시스템 완성
72개의 테스트 케이스 작성 (100% 통과)
Swagger를 통한 API 문서화
Docker 및 AWS 배포 가이드 작성

본 프로젝트를 통해 Spring Boot 생태계를 깊이 이해하고, 실무에서 바로 활용 가능한 백엔드 개발 역량을 키울 수 있었습니다.


📎 참고 자료

728x90