SPRING
[Spring Boot + JDBC] 일정 관리 웹 프로젝트 개발기 (CRUD, 예외처리, 페이징까지)
도원좀비
2025. 3. 21. 17:40
📆 프로젝트 개요
이번 프로젝트는 Spring Boot + JDBC를 기반으로 일정 관리 시스템을 구현한 개인 백엔드 학습 프로젝트입니다.
CRUD를 중심으로, 작성자와 일정 간의 관계, 비밀번호 검증, 예외처리, 페이징 처리 등을 구현했습니다.
📌 GitHub 저장소: https://github.com/sukh115/scheduler
GitHub - sukh115/scheduler
Contribute to sukh115/scheduler development by creating an account on GitHub.
github.com
⚙️ 사용 기술 스택
| 구분 | 기술 |
| Language | Java 21 |
| Framework | Spring Boot 3.4.3 |
| DBMS | MySQL 8.x |
| ORM | 직접 구현 (Spring Data X) |
| 빌드 툴 | Gradle |
| 테스트 도구 | Postman |
| 버전관리 | GitHub |
🧩 데이터베이스 설계

🛠️ 주요 기능 요약
| 기능 | 설명 |
| 작성자 등록 | 이름과 이메일로 등록 |
| 일정 등록 | 제목/내용/작성자/비밀번호 포함 |
| 일정 수정 | 작성자 본인만 가능, 비밀번호 확인 필수 |
| 일정 삭제 | 비밀번호 검증 필요 |
| 페이징 조회 | page, size로 리스트 조회 |
| 예외 처리 | @ControllerAdvice 기반 글로벌 예외 처리 |
🔄 MVC 아키텍처 흐름
예시의 흐름은 일정 등록으로 간단하게 코드로 설명하려고 한다.
⏩ Controller → Service → Repository → Database 구조
더보기
(ScheduleController.java)
@RestController
@RequestMapping("/schedules")
public class ScheduleController {
private final ScheduleService scheduleService;
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
(ScheduleSevice.java)
@Service
public class ScheduleServiceImpl implements ScheduleService {
private final ScheduleRepository scheduleRepository;
private final AuthorRepository authorRepository;
public ScheduleServiceImpl(ScheduleRepository scheduleRepository, AuthorRepository authorRepository) {
this.scheduleRepository = scheduleRepository;
this.authorRepository = authorRepository;
}
(ScheduleSeviceImpl.java)
@Service
public class AuthorServiceImpl implements AuthorService {
private final AuthorRepository authorRepository;
public AuthorServiceImpl(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
(ScheduleRequestDto.java)
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ScheduleRequestDto {
@NotBlank(message = "제목을 적어주세요.")
private String title;
@NotBlank(message = "내용을 적어주세요.")
private String content;
@NotNull(message = "작성자 ID가 없습니다.")
private Long authorId;
@NotNull(message = "비밀번호가 없습니다.")
private String password;
}
(ScheduleRepository.java)
public interface ScheduleRepository {
ScheduleResponseDto saveSchedule(Schedule schedule);
}
(JdbcTemplateScheduleRepository.java)
@Repository
public class JdbcTemplateScheduleRespository implements ScheduleRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateScheduleRespository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public ScheduleResponseDto saveSchedule(Schedule schedule) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("schedule").usingGeneratedKeyColumns("schedule_id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("title", schedule.getTitle());
parameters.put("content", schedule.getContent());
parameters.put("author_id", schedule.getAuthorId());
parameters.put("password", schedule.getPassword());
parameters.put("create_date", schedule.getCreateDate()); // 추가
parameters.put("updated_date", schedule.getUpdatedDate()); // 추가
Number key = jdbcInsert.executeAndReturnKey(parameters);
Long generatedId = key.longValue();
return new ScheduleResponseDto(generatedId, schedule.getTitle(), schedule.getContent(), schedule.getUpdatedDate().toString(), schedule.getAuthorId());
}
}
(Schedule.java)
@Getter
@AllArgsConstructor
public class Schedule {
private Long scheduleId;
private String title;
private String content;
private Timestamp createDate;
private Timestamp updatedDate;
private Long authorId;
private String password;
public Schedule(String title, String content, Long authorId, String password) {
this.title = title;
this.content = content;
this.authorId = authorId;
this.password = password;
this.createDate = new Timestamp(System.currentTimeMillis());
this.updatedDate = new Timestamp(System.currentTimeMillis());
}
- Service 계층에서 비즈니스 로직 처리 (작성자 존재 여부, 패스워드 검증)
- Repository 계층에서 직접 SQL
🚨 글로벌 예외 처리
모든 예외를 하나의 클래스에서 처리
더보기
package com.example.scheduler.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
private String getNow() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-mm-dd HH:mm:ss"));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ExceptionResponse> handleResponseStatusException(ResponseStatusException e) {
ExceptionResponse response = new ExceptionResponse(
e.getStatusCode().value(),
e.getReason(),
getNow()
);
return new ResponseEntity<>(response, e.getStatusCode());
}
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<ExceptionResponse> handleNullPointerException(NullPointerException e) {
ExceptionResponse response = new ExceptionResponse(
HttpStatus.BAD_REQUEST.value(),
"잘못된 요청 : " + e.getMessage(),
getNow()
);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResponse> handleException(Exception e) {
ExceptionResponse response = new ExceptionResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"서버 내부 오류 : " + e.getMessage(),
getNow()
);
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationExceptionResponse> handleValidationException(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : e.getBindingResult().getFieldErrors()) {
String message = error.getDefaultMessage();
errors.put(error.getField(), message != null ? message : "알 수 없는 오류");
}
return ResponseEntity.badRequest().body(
new ValidationExceptionResponse(400, errors, getNow())
);
}
}
📑페이징 처리
/schedules/paged?page=0&size=5 형태로 요청 시
- LIMIT, OFFSET 쿼리 활용
- 빈 페이지면 [ ] 반환
💡 배운 점
- Spring Data 없이도 충분히 MVC + JDBC 기반으로 구조화된 백엔드 구현이 가능함을 체감
- 페이징/예외처리/DTO 분리 등 실무에서 자주 쓰이는 개념들 실습
- 테이블을 추가하며 스프링 리팩토링 시 생기는 어려움들을 학습