SPRING
[Spring Boot + JDBC] 일정 관리 웹 리팩토링 – 쿼리, 예외, 컬럼 분리
도원좀비
2025. 3. 24. 17:56
유지보수성과 가독성을 높이기 위해 진행한 리팩토링 과정을 정리
🚨 리팩토링 전 문제점
처음 구현 당시에는 빠르게 기능을 완성하는 데 집중했기 때문에 아래와 같은 구조적인 문제들이 있었다.
- 쿼리문이 JdbcTemplate 내부에 하드코딩돼 있음
- "author_id", "title" 같은 컬럼명이 문자열로 여기저기 흩어져 있음
- 예외 메시지, 상태 코드가 컨트롤러나 서비스에서 직접 작성되어 중복 발생
🎯 리팩토링 목표
- 쿼리문을 별도 클래스로 분리해 중복 제거 및 재사용성 향상
- 컬럼명을 enum으로 상수화하여 오타 방지 및 리팩토링 효율 향
- 예외 코드와 메시지를 ExceptionCode enum으로 통합 관리
1️⃣ 컬럼 이름 enum 분리
리팩토링 전 : 쿼리문에 직접 테이블명을 입력해서 구현
리팩토링 후 : 중복되는 쿼리문을 enum으로 관리하여 컴파일 타임 오류 감소 + 유지보수 성능 향상
더보기
package com.example.scheduler.repository.column;
import lombok.Getter;
/**
* 일정(schedule) 테이블의 컬럼명을 정의한 Enum
*/
@Getter
public enum ScheduleColumns {
SCHEDULE_ID("schedule_id"), // 일정 ID (PK)
TITLE("title"), // 제목
CONTENT("content"), // 내용
AUTHOR_ID("author_id"), // 작성자 ID (FK)
PASSWORD("password"), // 비밀번호 (수정/삭제 시 검증용)
CREATE_DATE("create_date"), // 생성일시
UPDATED_DATE("updated_date"); // 수정일시
private final String columnName;
ScheduleColumns(String columnName) {
this.columnName = columnName;
}
}
2️⃣ 쿼리문 분리
JdbcTemplateScheduleRepository에서 각 메서드에서 구현한 쿼리문을 query 패키지의 하위 클래스로 분리
더보기
package com.example.scheduler.repository.query;
import com.example.scheduler.repository.column.AuthorColumns;
import static com.example.scheduler.repository.column.ScheduleColumns.*;
/**
* 일정(schedule) 관련 SQL 쿼리를 정의한 클래스
* - 작성자(author) 테이블과의 조인 포함
* - JdbcTemplateScheduleRepository에서 사용
*/
public class ScheduleQuery {
// 전체 일정 조회 + 작성자 이름 포함 (최신순 정렬)
public static String findAllWithAuthor() {
return "SELECT s." + TITLE.getColumnName() + ", " +
"s." + CONTENT.getColumnName() + ", " +
"s." + UPDATED_DATE.getColumnName() + ", " +
"a." + AuthorColumns.NAME.getColumnName() + " AS author_name " +
"FROM schedule s " +
"JOIN author a ON s." + AUTHOR_ID.getColumnName() + " = a." + AUTHOR_ID.getColumnName() + " " +
"ORDER BY s." + UPDATED_DATE.getColumnName() + " DESC";
}
}
3️⃣ 예외 코드 Enum화 + 커스텀 익셉션
더보기
(ExceptionCode.java)
package com.example.scheduler.exception.exceptionCode;
import lombok.Getter;
/**
* 도메인 및 공통 예외 상황에 대한 코드 정의 Enum
* - 상태 코드, 에러 코드 문자열, 메시지를 포함
*/
@Getter
public enum ExceptionCode {
// Author 관련
AUTHOR_NOT_FOUND(404, "AUTHOR_001", "작성자를 찾을 수 없습니다."),
AUTHOR_INVALID_INPUT(400, "AUTHOR_002", "이름과 이메일을 입력해주세요."),
AUTHOR_UPDATE_FAILED(404, "AUTHOR_003", "작성자 수정에 실패했습니다."),
AUTHOR_DELETE_FAILED(404, "AUTHOR_004", "작성자 삭제에 실패했습니다."),
AUTHOR_EMAIL_DUPLICATED(409, "AUTHOR_005", "이미 사용 중인 이메일입니다."),
// Schedule 관련
SCHEDULE_NOT_FOUND(404, "SCHEDULE_001", "존재하지 않는 일정입니다."),
SCHEDULE_INVALID_INPUT(400, "SCHEDULE_002", "제목과 내용을 입력해주세요."),
SCHEDULE_UPDATE_FAILED(404, "SCHEDULE_003", "일정 수정 실패"),
SCHEDULE_DELETE_FAILED(404, "SCHEDULE_004", "일정 삭제 실패"),
// 인증 관련
UNAUTHORIZED_AUTHOR(403, "AUTH_001", "작성자만 일정을 수정할 수 있습니다."),
PASSWORD_MISMATCH(401, "AUTH_002", "비밀번호가 일치하지 않습니다."),
// 공통
PAGE_OUT_OF_RANGE(400, "COMMON_001", "요청한 페이지 범위가 유효하지 않습니다.");
private final int status;
private final String code;
private final String message;
ExceptionCode(int status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
(CustomException.java)
package com.example.scheduler.exception;
import com.example.scheduler.exception.exceptionCode.ExceptionCode;
import lombok.Getter;
/**
* 사용자 정의 예외 클래스
* - 예외 코드(Enum)를 기반으로 상세한 예외 정보를 전달
*/
@Getter
public class CustomException extends RuntimeException{
// 예외 코드 (상세한 상태, 코드, 메시지 포함)
private final ExceptionCode errorCode;
/**
* 예외 생성자
*
* @param errorCode 예외 코드 Enum
*/
public CustomException(ExceptionCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
(GlobalExceptionHandler.java)
package com.example.scheduler.exception;
import com.example.scheduler.exception.exceptionCode.ExceptionCode;
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를 통해 모든 예외를 공통 포맷으로 처리
*/
@ControllerAdvice
public class GlobalExceptionHandler {
// 예외 발생 시 현재 시각 반환 (yyyy-MM-dd HH:mm:ss 포맷)
private String getNow() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-mm-dd HH:mm:ss"));
}
/**
* CustomException 처리
*
* @param e 커스텀 예외
* @return ExceptionResponse 객체와 상태 코드
*/
@ExceptionHandler(CustomException.class)
public ResponseEntity<ExceptionResponse> handleCustomException(CustomException e) {
ExceptionCode error = e.getErrorCode();
ExceptionResponse response = new ExceptionResponse(
error.getStatus(),
error.getCode(),
error.getMessage(),
getNow()
);
return new ResponseEntity<>(response, HttpStatus.valueOf(error.getStatus()));
}
}
4️⃣ 도메인 주도 설계
리팩토링 전 : 서비스에서 전부 해결
리팩토링 후 : 도메인 객체가 스스로 유효성 검증
더보기
리팩토링 전 : (ScheduleServiceImpl.java)
@Override
public ScheduleResponseDto saveSchedule(ScheduleRequestDto dto) {
if (!authorRepository.existsByAuthorId(dto.getAuthorId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "존재하지 않는 작성자입니다.");
}
Schedule schedule = new Schedule(dto.getTitle(), dto.getContent(), dto.getAuthorId(), dto.getPassword());
return scheduleRepository.saveSchedule(schedule);
}
리팩토링 후 : (ScheduleServiceImpl.java).
// 일정 저장
@Override
public ScheduleResponseDto saveSchedule(ScheduleRequestDto dto) {
// 작성자 존재 확인
Author author = authorRepository.findByAuthorId(dto.getAuthorId())
.orElseThrow(() -> new CustomException(ExceptionCode.AUTHOR_NOT_FOUND));
author.validateExistence();
// 일정 도메인 생성
Schedule schedule = new Schedule(dto.getTitle(), dto.getContent(), dto.getAuthorId(), dto.getPassword());
return scheduleRepository.saveSchedule(schedule);
}
(Scherdule.java)
// 작성자 일치 유효성 검사
public void validateAuthor(Long authorId) {
if (!this.authorId.equals(authorId)) {
throw new CustomException(ExceptionCode.UNAUTHORIZED_AUTHOR);
}
}
// 비밀번호 일치 유효성 검사
public void validatePassword(String password) {
if (!this.password.equals(password)) {
throw new CustomException(ExceptionCode.PASSWORD_MISMATCH);
}
}
🔍 테스트 & 디버깅 중 발생한 문제
일정 삭제 시 비밀번호가 맞는데도 "비밀번호가 일치하지 않습니다"라는 예외가 발생
디버깅해보니...
if (this.password.equals(input)) {
throw new CustomException(...);
}
→ ❗ 비교 조건 반대로 작성한 실수 발견
→ !this.password.equals(input) 으로 수정 후 정상 동작
🧩마무리
이번 리팩토링은 기능적으로 큰 변화는 없지만,
코드의 안정성과 확장성, 협업 효율성을 모두 높이는 중요한 개선 작업이었다.
📌 GitHub 저장소: https://github.com/sukh115/scheduler
GitHub - sukh115/scheduler
Contribute to sukh115/scheduler development by creating an account on GitHub.
github.com