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 분리 등 실무에서 자주 쓰이는 개념들 실습
  • 테이블을 추가하며 스프링 리팩토링 시 생기는 어려움들을 학습