개인 운동 루틴을 관리하고 운동 기록을 추적하는 백엔드 API 서버 JWT 인증, JPA 기반 데이터 관리, 통계 분석 기능 제공
운동 애호가들이 자신의 운동 루틴을 체계적으로 관리하고, 운동 기록을 추적하며, 진척도를 분석할 수 있는 백엔드 API 서비스입니다.
문제: 기존 운동 기록 앱들은 데이터 접근성이 낮고, 개인화된 분석 기능이 부족합니다.
해결:
- REST API 방식으로 다양한 클라이언트(웹, 모바일)에서 접근 가능
- JWT 기반 인증으로 안전한 개인 데이터 관리
- 통계 및 분석 기능으로 운동 패턴 시각화
- 1RM 계산 등 과학적 지표 제공
- 기간: 2025.01 ~ 2025.01 (1개월)
- 인원: 1명 (백엔드 개발)
- 역할: 설계, 개발, 테스트
- Java 21 - 최신 LTS 버전, Virtual Threads, Record 등 활용
- Spring Boot 4.0.0 - 최신 스프링 프레임워크
- Spring Data JPA - Hibernate 기반 ORM
- Spring Security 6 - JWT 토큰 기반 인증/인가
- MySQL 8.x - 운영 데이터베이스
- HikariCP - 커넥션 풀 관리
- JPA Auditing - 생성/수정 시간 자동 관리
- SpringDoc OpenAPI 3 - Swagger UI 자동 생성
- 실시간 API 문서 - http://localhost:8080/swagger-ui.html
- JWT (JSON Web Token) - Stateless 인증
- BCrypt - 비밀번호 암호화
- Access Token - 1시간 유효
- Refresh Token - 7일 유효
- Gradle 8.x - 빌드 자동화
- Lombok - 보일러플레이트 코드 제거
- SLF4J + Logback - 로깅
- JWT 기반 회원가입/로그인
- Access/Refresh Token 발급
- 이메일 중복 검증
- BCrypt 비밀번호 암호화
- 토큰 갱신 (Refresh)
- 로그아웃 (토큰 무효화)
- 운동 종목 카탈로그 조회
- 신체 부위별 필터링 (가슴, 등, 하체, 어깨, 팔, 코어)
- 장비 정보 포함
- 운동 종목 상세 정보
- 사용자별 커스텀 루틴 생성
- 루틴에 운동 종목 추가 (순서, 세트/반복 목표)
- 루틴 수정/삭제
- 루틴 목록 조회
- 루틴 상세 조회 (포함된 운동 목록)
- 운동 세션 생성 (날짜, 루틴 선택)
- 세트별 기록 (무게, 반복 횟수, 난이도)
- 운동 기록 조회 (날짜별, 기간별)
- 세션 삭제
- 운동 시간 자동 계산
- 주간 통계 - 운동 횟수, 세트 수, 총 시간, 일평균
- 월간 통계 - 총 운동량, 주당 평균, 실제 운동 일수
- 신체 부위별 통계 - 부위별 운동량 및 비율
- 개인 기록(PR) 추적 - 운동별 최고 무게 및 반복 횟수
- 1RM 계산 - Brzycki 공식을 이용한 추정 최대 중량
- 운동 목표 설정 (체중, 운동 횟수 등)
- 목표 진척도 업데이트
- 목표 달성 여부 추적
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (Controllers - REST API Endpoints) │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ Application Layer │
│ (Services - Business Logic) │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ Persistence Layer │
│ (Repositories - Data Access - JPA) │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ Database Layer │
│ (MySQL 8.x) │
└─────────────────────────────────────────┘
src/main/java/com/example/FitTracker/
├── config/ # 설정 클래스
│ ├── SecurityConfig.java # Spring Security 설정
│ └── SwaggerConfig.java # Swagger 설정
│
├── controller/ # REST API 컨트롤러
│ ├── AuthController.java
│ ├── ExerciseTypeController.java
│ ├── RoutineController.java
│ ├── WorkoutController.java
│ ├── StatsController.java
│ └── GoalController.java
│
├── domain/ # JPA 엔티티
│ ├── User.java
│ ├── ExerciseType.java
│ ├── Routine.java
│ ├── RoutineExercise.java
│ ├── WorkoutSession.java
│ ├── WorkoutSet.java
│ ├── Goal.java
│ └── RefreshToken.java
│
├── dto/ # 데이터 전송 객체
│ ├── request/ # 요청 DTO
│ │ ├── SignupRequest.java
│ │ ├── LoginRequest.java
│ │ ├── RoutineRequest.java
│ │ └── WorkoutRequest.java
│ └── response/ # 응답 DTO
│ ├── stats/
│ │ ├── WeeklyStatsResponse.java
│ │ ├── MonthlyStatsResponse.java
│ │ ├── BodyPartStatsResponse.java
│ │ └── PersonalRecordResponse.java
│ └── ...
│
├── repository/ # JPA 리포지토리
│ ├── UserRepository.java
│ ├── ExerciseTypeRepository.java
│ ├── RoutineRepository.java
│ ├── WorkoutSessionRepository.java
│ ├── WorkoutSetRepository.java
│ └── ...
│
├── service/ # 비즈니스 로직
│ ├── AuthService.java
│ ├── RoutineService.java
│ ├── WorkoutService.java
│ ├── StatsService.java # 통계 서비스
│ └── GoalService.java
│
├── security/ # 보안 관련
│ ├── JwtTokenProvider.java
│ ├── JwtAuthenticationFilter.java
│ └── CustomUserDetailsService.java
│
└── exception/ # 예외 처리
├── GlobalExceptionHandler.java
├── ResourceNotFoundException.java
└── UnauthorizedException.java
┌──────────────┐ ┌──────────────────┐
│ User │ │ ExerciseType │
├──────────────┤ ├──────────────────┤
│ id (PK) │ │ id (PK) │
│ email │ │ name │
│ password │ │ bodyPart │
│ name │ │ equipment │
│ created_at │ │ description │
└──────┬───────┘ └────────┬─────────┘
│ │
│ 1 │
│ │
│ N N │
┌──────▼───────┐ ┌────────▼─────────┐
│ Routine │────────▶│ RoutineExercise │
├──────────────┤ 1 N ├──────────────────┤
│ id (PK) │ │ id (PK) │
│ user_id (FK) │ │ routine_id (FK) │
│ name │ │ exercise_id (FK) │
│ description │ │ order_index │
│ created_at │ │ target_sets │
└──────┬───────┘ │ target_reps │
│ └──────────────────┘
│ 1
│
│ N
┌──────▼─────────┐ ┌──────────────────┐
│ WorkoutSession │──────▶│ WorkoutSet │
├────────────────┤ 1 N ├──────────────────┤
│ id (PK) │ │ id (PK) │
│ user_id (FK) │ │ session_id (FK) │
│ routine_id(FK) │ │ exercise_id (FK) │
│ workout_date │ │ set_number │
│ duration │ │ weight │
│ notes │ │ reps │
│ created_at │ │ difficulty │
└────────────────┘ └──────────────────┘
┌──────────────┐
│ Goal │
├──────────────┤
│ id (PK) │
│ user_id (FK) │─────┐
│ goal_type │ │
│ target_value │ │ N
│ current_value│ │
│ deadline │ │
│ achieved │ ▼
└──────────────┘ User
┌──────────────┐
│ RefreshToken │
├──────────────┤
│ id (PK) │
│ user_id (FK) │─────┐
│ token │ │ 1
│ expiry_date │ │
└──────────────┘ ▼
User
- User ↔ Routine: 1:N (사용자는 여러 루틴 소유)
- Routine ↔ RoutineExercise: 1:N (루틴은 여러 운동 포함)
- ExerciseType ↔ RoutineExercise: 1:N (운동 종목은 여러 루틴에 포함)
- User ↔ WorkoutSession: 1:N (사용자는 여러 운동 세션 보유)
- WorkoutSession ↔ WorkoutSet: 1:N (세션은 여러 세트 포함)
- User ↔ Goal: 1:N (사용자는 여러 목표 설정)
- User ↔ RefreshToken: 1:1 (사용자당 하나의 리프레시 토큰)
URL: http://localhost:8080/swagger-ui.html
모든 API는 실시간으로 테스트 가능하며, 요청/응답 스키마를 확인할 수 있습니다.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/auth/signup |
회원가입 | ❌ |
| POST | /api/auth/login |
로그인 (JWT 발급) | ❌ |
| POST | /api/auth/refresh |
토큰 갱신 | ✅ |
| POST | /api/auth/logout |
로그아웃 | ✅ |
회원가입 요청 예시:
{
"email": "user@example.com",
"password": "securePassword123!",
"name": "홍길동"
}로그인 응답 예시:
{
"accessToken": "eyJhbGciOiJIUzUxMiJ9...",
"refreshToken": "eyJhbGciOiJIUzUxMiJ9...",
"tokenType": "Bearer",
"user": {
"id": 1,
"email": "user@example.com",
"name": "홍길동"
}
}| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/exercises |
전체 운동 종목 조회 | ✅ |
| GET | /api/exercises?bodyPart=CHEST |
신체 부위별 조회 | ✅ |
| GET | /api/exercises/{id} |
운동 종목 상세 조회 | ✅ |
신체 부위 옵션: CHEST, BACK, LEGS, SHOULDERS, ARMS, CORE
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/routines |
루틴 생성 | ✅ |
| GET | /api/routines |
내 루틴 목록 조회 | ✅ |
| GET | /api/routines/{id} |
루틴 상세 조회 | ✅ |
| PUT | /api/routines/{id} |
루틴 수정 | ✅ |
| DELETE | /api/routines/{id} |
루틴 삭제 | ✅ |
루틴 생성 요청 예시:
{
"name": "상체 집중 루틴",
"description": "가슴과 팔 중심의 운동",
"exercises": [
{
"exerciseTypeId": 1,
"orderIndex": 1,
"targetSets": 3,
"targetReps": 10
},
{
"exerciseTypeId": 5,
"orderIndex": 2,
"targetSets": 4,
"targetReps": 12
}
]
}| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/workouts |
운동 세션 생성 | ✅ |
| POST | /api/workouts/{sessionId}/sets |
세트 추가 | ✅ |
| GET | /api/workouts |
운동 기록 조회 | ✅ |
| GET | /api/workouts?startDate=2025-01-01&endDate=2025-01-07 |
기간별 조회 | ✅ |
| GET | /api/workouts/{sessionId} |
세션 상세 조회 | ✅ |
| DELETE | /api/workouts/{sessionId} |
세션 삭제 | ✅ |
운동 세션 생성 요청 예시:
{
"routineId": 1,
"workoutDate": "2025-01-07",
"notes": "오늘 컨디션 좋음"
}세트 추가 요청 예시:
{
"exerciseTypeId": 1,
"setNumber": 1,
"weight": 60.0,
"reps": 10,
"difficulty": "MEDIUM"
}| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/stats/weekly?startDate={date}&endDate={date} |
주간 통계 | ✅ |
| GET | /api/stats/monthly?yearMonth=2025-01 |
월간 통계 | ✅ |
| GET | /api/stats/body-parts?startDate={date}&endDate={date} |
신체 부위별 통계 | ✅ |
| GET | /api/stats/personal-records |
전체 개인 기록 | ✅ |
| GET | /api/stats/personal-records/{exerciseTypeId} |
특정 운동 개인 기록 | ✅ |
주간 통계 응답 예시:
{
"totalWorkouts": 5,
"totalSets": 75,
"totalDuration": 300,
"averageWorkoutsPerDay": 0.71
}개인 기록 응답 예시:
{
"exerciseTypeName": "벤치프레스",
"maxWeight": 100.0,
"repsAtMaxWeight": 5,
"achievedDate": "2025-01-05",
"estimatedOneRepMax": 112.5
}1RM 계산 공식 (Brzycki):
1RM = weight × (36 / (37 - reps))
예시: 100kg × 5회 = 100 × (36 / (37 - 5)) = 112.5kg
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/goals |
목표 생성 | ✅ |
| GET | /api/goals |
내 목표 목록 조회 | ✅ |
| PUT | /api/goals/{id}/progress |
진척도 업데이트 | ✅ |
모든 인증이 필요한 API는 다음 헤더를 포함해야 합니다:
Authorization: Bearer {accessToken}
- Java 21 이상
- MySQL 8.x
- Gradle 8.x (또는 Gradle Wrapper 사용)
git clone https://github.com/yourusername/FitTracker.git
cd FitTrackerCREATE DATABASE fittrackerdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;src/main/resources/application.yml 파일을 수정합니다:
spring:
datasource:
url: jdbc:mysql://localhost:3306/fittrackerdb
username: your_username
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 최초 실행 시 테이블 자동 생성
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
jwt:
secret: your-secret-key-min-256-bits # 최소 256비트 이상의 시크릿 키
access-token-validity: 3600000 # 1시간 (밀리초)
refresh-token-validity: 604800000 # 7일 (밀리초)# Gradle Wrapper를 사용한 빌드
./gradlew clean build
# 애플리케이션 실행
./gradlew bootRun또는 JAR 파일로 실행:
./gradlew bootJar
java -jar build/libs/FitTracker-0.0.1-SNAPSHOT.jar- 서버: http://localhost:8080
- API 문서: http://localhost:8080/swagger-ui.html
- H2 콘솔: http://localhost:8080/h2-console (개발 환경)
문제: 세션 기반 인증은 서버 확장성에 제약이 있음
해결:
- JWT를 사용한 무상태(Stateless) 인증 구현
- Access Token (1시간) + Refresh Token (7일) 이중 토큰 전략
- RefreshToken을 DB에 저장하여 무효화 가능
public String generateAccessToken(String email) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidity);
return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}문제: N+1 쿼리 문제로 인한 성능 저하
해결:
@ManyToOne,@OneToMany적절한 관계 설정- 기본 FetchType을 LAZY로 설정
- 필요한 경우
@EntityGraph또는JOIN FETCH사용
@Entity
public class WorkoutSession {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToMany(mappedBy = "workoutSession", cascade = CascadeType.ALL, orphanRemoval = true)
private List<WorkoutSet> workoutSets = new ArrayList<>();
}문제: 여러 테이블을 JOIN하는 통계 쿼리의 성능 이슈
해결:
- JPA Query Methods의
@Query어노테이션 사용 - 집계 함수(COUNT, SUM, AVG) 활용
- DTO Projection으로 필요한 데이터만 조회
@Query("""
SELECT new com.example.FitTracker.dto.response.stats.PersonalRecordResponse(
e.name,
MAX(ws.weight),
ws.reps,
wss.workoutDate
)
FROM WorkoutSet ws
JOIN ws.exerciseType e
JOIN ws.workoutSession wss
WHERE wss.user.id = :userId
GROUP BY e.id, ws.reps
ORDER BY MAX(ws.weight) DESC
""")
List<PersonalRecordResponse> findPersonalRecords(@Param("userId") Long userId);문제: 사용자의 실제 최대 중량을 추정하는 과학적 방법 필요
해결:
- Brzycki 공식 적용:
1RM = weight × (36 / (37 - reps)) - Service 계층에서 비즈니스 로직으로 구현
- 반복 횟수가 1일 경우 자체 무게를 1RM으로 간주
private double calculateOneRepMax(double weight, int reps) {
if (reps == 1) {
return weight;
}
// Brzycki 공식
return weight * (36.0 / (37.0 - reps));
}문제: 다양한 예외 상황에서 일관성 있는 에러 응답 필요
해결:
@ControllerAdvice를 사용한 전역 예외 처리- Custom Exception 클래스 정의
- 표준 HTTP 상태 코드 사용
코드: GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
}문제: API 문서의 수동 관리는 유지보수 비용이 높음
해결:
- SpringDoc OpenAPI 3 라이브러리 사용
- 어노테이션 기반 API 문서 자동 생성
- Swagger UI를 통한 실시간 테스트 가능
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("FitTracker API")
.version("1.0")
.description("운동 루틴 추적 REST API"));
}
}- 인덱스 생성: 자주 조회되는 컬럼에 인덱스 추가
user_id,workout_date,exercise_type_id
- Connection Pooling: HikariCP 사용 (최대 10개 커넥션)
- Batch Insert: JPA Batch Size 설정 (100)
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 100
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 5- N+1 문제 해결:
@EntityGraph,JOIN FETCH사용 - Projection 활용: 필요한 필드만 조회
- 페이징 처리: 대량 데이터 조회 시 Pageable 사용
- Redis 도입 고려
- 운동 종목 데이터는 변경이 적으므로 캐싱 적합
- 통계 데이터도 일정 시간 캐싱 가능
- 소셜 기능 (친구 추가, 운동 기록 공유)
- 운동 추천 알고리즘 (AI 기반)
- 식단 관리 기능
- 체중 및 신체 사진 추적
- 푸시 알림 (운동 리마인더)
- Redis 캐싱 도입
- 테스트 커버리지 80% 이상
- CI/CD 파이프라인 구축 (GitHub Actions)
- Docker 컨테이너화
- AWS 배포 (EC2 + RDS)
- 모니터링 (Prometheus + Grafana)
- CQRS 패턴 적용 (조회/명령 분리)
- Event-Driven Architecture (비동기 처리)
- Microservices 전환 고려
This project is licensed under the MIT License.
프로젝트 관련 문의사항이나 버그 리포트는 Issues에 등록해주세요.
개발자: [Your Name] 이메일: your.email@example.com 포트폴리오: https://yourportfolio.com GitHub: https://github.com/yourusername