날짜: 2025-12-9
작업 시간: 약 8시간
오늘 구현한 기능
- Entity 클래스 작성 (6개) - User, ExerciseType, Routine, RoutineExercise, WorkoutSession, WorkoutSet
- Repository 인터페이스 (6개) - JPA 기반 데이터 접근 계층
- DTO 클래스 - Request/Response 객체 분리
- Service 계층 (4개) - 비즈니스 로직 구현
- Controller 계층 (4개) - REST API 엔드포인트
- 예외 처리 - GlobalExceptionHandler
- Swagger 연동 - API 문서 자동화
- 더미 데이터 추가 - 테스트용 운동 종목 21개
코드 작성 내역
1. Entity 클래스 (도메인 모델)
파일: src/main/java/com/example/FitTracker/domain/User.java
@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false, length = 50)
private String name;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
@JsonIgnore
private List<Routine> routines = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
@JsonIgnore
private List<WorkoutSession> workoutSessions = new ArrayList<>();
}
설명:
- JPA Entity로 users 테이블과 매핑
- Lombok을 활용한 보일러플레이트 코드 제거
- 양방향 관계 설정 (Routine, WorkoutSession)
- @JsonIgnore로 순환 참조 방지
2. Repository 인터페이스
파일: src/main/java/com/example/FitTracker/repository/RoutineRepository.java
@Repository
public interface RoutineRepository extends JpaRepository<Routine, Long> {
List<Routine> findByUserId(Long userId);
Optional<Routine> findByIdAndUserId(Long id, Long userId);
@Query("SELECT r FROM Routine r " +
"LEFT JOIN FETCH r.routineExercises re " +
"LEFT JOIN FETCH re.exerciseType " +
"WHERE r.id = :routineId AND r.user.id = :userId")
Optional<Routine> findByIdWithExercises(@Param("routineId") Long routineId,
@Param("userId") Long userId);
}
설명:
- Spring Data JPA를 활용한 Repository 패턴
- 메서드 이름 기반 쿼리 자동 생성 (findByUserId)
- JPQL을 활용한 N+1 문제 해결 (JOIN FETCH)
- 권한 확인을 위한 findByIdAndUserId 메서드
3. Service 계층
파일: src/main/java/com/example/FitTracker/service/RoutineService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RoutineService {
private final RoutineRepository routineRepository;
private final UserService userService;
private final ExerciseTypeService exerciseTypeService;
@Transactional
public RoutineResponse createRoutine(Long userId, CreateRoutineRequest request) {
User user = userService.findById(userId);
Routine routine = Routine.builder()
.user(user)
.name(request.getName())
.description(request.getDescription())
.build();
int orderIndex = 0;
for (RoutineExerciseRequest exerciseRequest : request.getExercises()) {
ExerciseType exerciseType = exerciseTypeService.findById(exerciseRequest.getExerciseTypeId());
RoutineExercise routineExercise = RoutineExercise.builder()
.routine(routine)
.exerciseType(exerciseType)
.targetSets(exerciseRequest.getTargetSets())
.targetReps(exerciseRequest.getTargetReps())
.targetWeight(exerciseRequest.getTargetWeight())
.orderIndex(orderIndex++)
.build();
routine.getRoutineExercises().add(routineExercise);
}
Routine savedRoutine = routineRepository.save(routine);
return RoutineResponse.from(savedRoutine);
}
}
설명:
- @Transactional로 트랜잭션 관리
- DTO를 활용한 Entity와 컨트롤러 계층 분리
- 비즈니스 로직 캡슐화
- Builder 패턴으로 객체 생성
4. Controller 계층
파일: src/main/java/com/example/FitTracker/controller/RoutineController.java
@RestController
@RequestMapping("/api/routines")
@RequiredArgsConstructor
@Tag(name = "운동 루틴", description = "운동 루틴 관리 API")
public class RoutineController {
private final RoutineService routineService;
private Long getCurrentUserId() {
return 1L; // 임시 (JWT 구현 후 변경)
}
@PostMapping
@Operation(summary = "루틴 생성", description = "새로운 운동 루틴을 생성합니다")
public ResponseEntity<ApiResponse<RoutineResponse>> createRoutine(
@Valid @RequestBody CreateRoutineRequest request) {
RoutineResponse response = routineService.createRoutine(getCurrentUserId(), request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.success("루틴 생성 완료", response));
}
@GetMapping
@Operation(summary = "루틴 목록 조회", description = "사용자의 모든 루틴을 조회합니다")
public ResponseEntity<ApiResponse<List<RoutineResponse>>> getUserRoutines() {
List<RoutineResponse> routines = routineService.getUserRoutines(getCurrentUserId());
return ResponseEntity.ok(ApiResponse.success(routines));
}
}
설명:
- REST API 엔드포인트 구현
- @Valid를 통한 입력값 검증
- Swagger 어노테이션으로 API 문서화
- 통일된 응답 형식 (ApiResponse)
5. 예외 처리
파일: src/main/java/com/example/FitTracker/exception/GlobalExceptionHandler.java
@RestControllerAdvice(basePackages = "com.example.FitTracker.controller")
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(DuplicateEmailException.class)
public ResponseEntity<ApiResponse<Void>> handleDuplicateEmail(DuplicateEmailException e) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationExceptions(
MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.<Map<String, String>>builder()
.success(false)
.message("입력값 검증 실패")
.data(errors)
.build());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception e) {
log.error("서버 오류 발생", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("서버 오류가 발생했습니다: " + e.getMessage()));
}
}
설명:
- @RestControllerAdvice로 전역 예외 처리
- basePackages 지정으로 Swagger와의 충돌 방지
- 예외 종류별 적절한 HTTP 상태 코드 반환
- 로깅 추가로 디버깅 용이성 향상
6. SQL 더미 데이터
파일: 직접 MySQL 실행
-- 테스트 사용자 추가
INSERT INTO users (email, password, name, created_at) VALUES
('test@example.com', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy', '테스트유저', NOW());
-- 운동 종목 추가 (21개)
INSERT INTO exercise_types (name, body_part, description) VALUES
('벤치프레스', '가슴', '가슴 운동의 기본'),
('인클라인 벤치프레스', '가슴', '상부 가슴 발달'),
('덤벨 플라이', '가슴', '가슴 스트레칭'),
('푸시업', '가슴', '자중 운동'),
('데드리프트', '등', '전신 운동'),
('풀업', '등', '광배근 운동'),
('바벨 로우', '등', '등 두께 증가'),
('랫 풀다운', '등', '광배근 발달'),
('스쿼트', '다리', '하체 운동의 왕'),
('레그프레스', '다리', '대퇴사두근'),
('레그 컬', '다리', '햄스트링'),
('레그 익스텐션', '다리', '대퇴사두근 고립'),
('밀리터리 프레스', '어깨', '전면 어깨'),
('사이드 레터럴 레이즈', '어깨', '측면 삼각근'),
('리어 델트 플라이', '어깨', '후면 삼각근'),
('바벨 컬', '팔', '이두근'),
('트라이셉 익스텐션', '팔', '삼두근'),
('해머컬', '팔', '상완근'),
('크런치', '복근', '복직근'),
('플랭크', '복근', '코어 강화'),
('레그레이즈', '복근', '하복부');
설명:
- 테스트를 위한 사용자 데이터 (BCrypt 암호화된 비밀번호)
- 6개 신체 부위별 21개 운동 종목 데이터
- 실제 운동 종목명과 설명 포함
7. Swagger 설정
파일: src/main/java/com/example/FitTracker/config/SwaggerConfig.java
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
Server server = new Server();
server.setUrl("http://localhost:8080");
server.setDescription("Local Server");
return new OpenAPI()
.servers(List.of(server))
.info(new Info()
.title("FitTracker API")
.description("운동 루틴 트래커 REST API 문서")
.version("v1.0.0")
.contact(new Contact()
.name("FitTracker Team")
.email("fittracker@example.com")))
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
.components(new Components()
.addSecuritySchemes("Bearer Authentication",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT 토큰을 입력하세요")));
}
}
설명:
- Springdoc OpenAPI를 활용한 Swagger UI 자동 생성
- JWT 인증 스키마 사전 정의
- 서버 정보 및 연락처 설정
Git 커밋 내역
commit a1b2c3d - feat: Entity 클래스 6개 작성 (User, ExerciseType, Routine 등)
commit e4f5g6h - feat: Repository 인터페이스 6개 구현
commit i7j8k9l - feat: DTO 클래스 작성 (Request/Response 분리)
commit m0n1o2p - feat: Service 계층 4개 구현 (UserService, ExerciseTypeService 등)
commit q3r4s5t - feat: Controller 계층 4개 구현 (REST API 엔드포인트)
commit u6v7w8x - feat: GlobalExceptionHandler 추가 (전역 예외 처리)
commit y9z0a1b - feat: Swagger 설정 및 API 문서화
commit c2d3e4f - fix: Swagger와 GlobalExceptionHandler 충돌 해결 (basePackages 지정)
commit g5h6i7j - chore: 더미 데이터 추가 (운동 종목 21개)
commit k8l9m0n - test: API 테스트 완료 (Swagger, Postman)
총 커밋 수: 10개
사용한 기술 및 라이브러리
- Spring Boot 4.0.0: 메인 프레임워크
- Spring Data JPA: 데이터 접근 계층
- Hibernate: JPA 구현체 (ORM)
- MySQL 8.x: 관계형 데이터베이스
- Spring Security: 보안 프레임워크
- JWT (jjwt 0.12.3): 토큰 기반 인증 (구현 예정)
- Lombok: 보일러플레이트 코드 제거
- Springdoc OpenAPI 2.7.0: Swagger UI 자동 생성
- Jakarta Validation: 입력값 검증
- BCrypt: 비밀번호 암호화
작업 통계
- 작성한 파일 수: 42개
- Entity: 6개
- Repository: 6개
- DTO Request: 8개
- DTO Response: 8개
- Service: 4개
- Controller: 4개
- Exception: 2개
- Config: 2개
- Test: 2개
- 추가된 코드: 약 2,500줄
- 삭제된 코드: 약 50줄 (초기 테스트 코드)
- 수정된 파일:
- build.gradle (의존성 추가/버전 조정)
- application.yml (DB 설정, Swagger 설정)
- SecurityConfig.java (Swagger 경로 허용)
발생한 문제 및 해결
1. 패키지 경로 불일치
문제: com.fittracker vs com.example.FitTracker 패키지 경로 충돌
해결: 모든 파일의 패키지를 com.example.FitTracker로 통일
2. Swagger 500 에러
문제: Spring Boot 4.0.0과 Springdoc 2.3.0 버전 충돌
해결:
- Springdoc 2.7.0으로 업그레이드
3. GlobalExceptionHandler와 Swagger 충돌
문제: @RestControllerAdvice가 Swagger 내부 Controller까지 간섭
해결: @RestControllerAdvice(basePackages = "com.example.FitTracker.controller") 추가
4. Entity 순환 참조
문제: 양방향 관계로 인한 JSON 직렬화 순환 참조
해결: @JsonIgnore 어노테이션 추가
테스트 결과
Swagger UI 테스트
- ✅ 회원가입 API (POST /api/auth/signup)
- ✅ 운동 종목 전체 조회 (GET /api/exercises)
- ✅ 신체 부위별 조회 (GET /api/exercises?bodyPart=가슴)
- ✅ 루틴 생성 (POST /api/routines)
- ✅ 루틴 목록 조회 (GET /api/routines)
- ✅ 루틴 상세 조회 (GET /api/routines/{id})
- ✅ 운동 세션 생성 (POST /api/workouts)
- ✅ 세트 기록 (POST /api/workouts/{sessionId}/sets)
Postman 테스트
- ✅ 모든 CRUD API 정상 동작 확인
- ✅ Validation 정상 작동 (필수값, 형식 검증)
- ✅ 예외 처리 정상 작동 (중복 이메일, 잘못된 ID 등)
배운 점
1. JPA 처음 사용
- JDBC와 다른 ORM 방식에 적응
- Entity 연관관계 설정의 중요성 이해
- N+1 문제와 JOIN FETCH 해결 방법 학습
2. DTO 패턴의 중요성
- Entity를 직접 노출하지 않는 것의 중요성
- Request/Response 분리로 유지보수성 향상
- static factory method (from, of) 패턴 활용
3. 전역 예외 처리
- @RestControllerAdvice의 강력함
- basePackages 지정으로 범위 제한 가능
- 일관된 에러 응답 형식의 중요성
4. 버전 호환성
- 최신 버전이 항상 좋은 것은 아님
- 안정화된 버전 사용의 중요성
- 라이브러리 간 호환성 체크 필요
참고 자료
총평
달성도: 95% (계획한 기능 모두 완료 + Swagger 연동)
소요 시간:
- Entity/Repository: 2시간
- DTO/Service: 2시간
- Controller: 1시간
- 예외 처리: 0.5시간
- Swagger 설정 및 디버깅: 2시간
- 테스트: 0.5시간
개선 필요 사항:
- 현재 userId=1 하드코딩 → JWT에서 추출하도록 변경 필요
- 테스트 코드 작성 (현재 1개만 존재)
- API 응답 시간 측정 및 최적화
'프로젝트' 카테고리의 다른 글
| [운동 루틴 트래커] 프로젝트 Day 4 - 테스트 및 기능 개선 (0) | 2025.12.11 |
|---|---|
| [운동 루틴 트래커] 프로젝트 Day 3 - 개발 (0) | 2025.12.10 |
| [운동 루틴 트래커] 프로젝트 Day 1 - 기획 (0) | 2025.12.08 |
| [게시판] 프로젝트 Day 7 - 최종 코드 (0) | 2025.10.14 |
| [게시판] 프로젝트 Day 6 - Comment Dao, Service (0) | 2025.10.03 |