이번 프로젝트는 사용자가 일정을 관리할 수 있는 RESTful API를 개발하는 것이었습니다. 주요 기능으로는 일정 생성, 조회, 수정, 삭제가 있습니다. 각 일정은 할일(todo), 작성자(writer), 비밀번호(password)를 가지며, 수정 및 삭제 시에는 비밀번호를 확인합니다.
Spring Boot의 대표적인 아키텍처인 3계층 구조를 사용했습니다:
Controller 계층 - HTTP 요청 처리 및 응답 반환
Service 계층 - 비즈니스 로직 처리
Repository 계층 - 데이터베이스 접근
전체 일정 조회 기능을 구현했으나, API를 호출하면 다음과 같은 에러가 발생했습니다:
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version for the right syntax to use near 'BY modified_at DESC' at line 1
JdbcScheduleRepository 클래스의 findAll 메소드에서 동적 SQL 쿼리를 생성할 때 문제가 있었습니다. SQL 문에서 WHERE 절과 ORDER BY 절 사이에 공백이 없어서 발생한 구문 오류였습니다.
문제가 된 코드:
String sql = "SELECT * FROM schedule WHERE 1=1";
// 여러 조건 추가 후...
sql += "ORDER BY modified_at DESC"; // 공백 없음!
문자열 연결 시 앞에 공백을 추가하여 SQL 문법이 올바르게 작성되도록 수정했습니다:
String sql = "SELECT * FROM schedule WHERE 1=1";
// 여러 조건 추가 후...
sql += " ORDER BY modified_at DESC"; // 공백 추가
SQL 쿼리를 동적으로 생성할 때는 항상 각 절 사이에 공백을 추가해야 한다는 것을 배웠습니다.
일정 생성 API를 테스트할 때 다음과 같은 오류가 발생했습니다:
java.sql.SQLSyntaxErrorException: Table 'sparta_schedule.schedules' doesn't exist
두 가지 문제가 복합적으로 발생했습니다:
처음에는 코드에서 schedules(복수형)로 테이블을 참조했지만, 실제 데이터베이스에는 schedule(단수형)로 테이블이 생성되어 있었습니다.
jdbcInsert.withTableName("schedule").usingGeneratedKeyColumns("id");
CREATE TABLE IF NOT EXISTS schedule (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
todo VARCHAR(200) NOT NULL,
writer VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
modified_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
일정 조회 API의 응답에서 필드 값이 잘못 매핑되는 문제가 발생했습니다:
{
"id": 1,
"todo": "일정 내용", // 올바름
"writer": "일정 내용", // 잘못됨! todo 값이 중복됨
"createdAt": "2023-05-08T10:00:00",
"modifiedAt": "2023-05-08T10:00:00"
}
ScheduleResponseDto 클래스의 생성자에서 잘못된 필드 매핑이 있었습니다:
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.todo = schedule.getTodo();
this.writer = schedule.getTodo(); // 오류! writer에 todo 값을 할당
// ...
}
올바른 필드를 매핑하도록 수정했습니다:
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.todo = schedule.getTodo();
this.writer = schedule.getWriter(); // 수정됨
this.createdAt = schedule.getCreatedAt();
this.modifiedAt = schedule.getModifiedAt();
}
DTO와 Entity 간 변환 시 필드 매핑이 정확한지 꼼꼼히 확인해야 한다는 교훈을 얻었습니다.
일정을 조회할 때 NullPointerException이 발생했습니다. 데이터베이스에서 값을 가져왔지만, 엔티티의 필드에 제대로 매핑되지 않았습니다.
Entity 클래스의 필드명과 데이터베이스 컬럼명 간에 불일치가 있었습니다. 데이터베이스에서는 스네이크 케이스(created_at, modified_at)를 사용했지만, 처음 엔티티를 설계할 때는 createAt, modifiedAt으로 필드명을 지정했습니다.
이후 엔티티를 createdAt, modifiedAt으로 수정했으나, RowMapper에서 이 차이를 제대로 처리하지 않아 문제가 발생했습니다.
RowMapper에서 올바르게 필드명을 매핑했습니다:
private final RowMapper<Schedule> scheduleRowMapper = ((rs, rowNum) -> {
Schedule schedule = new Schedule();
schedule.setId(rs.getLong("id"));
schedule.setTodo(rs.getString("todo"));
schedule.setWriter(rs.getString("writer"));
schedule.setPassword(rs.getString("password"));
schedule.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime()); // 스네이크 케이스 사용
schedule.setModifiedAt(rs.getTimestamp("modified_at").toLocalDateTime()); // 스네이크 케이스 사용
return schedule;
});
데이터베이스 컬럼명과 자바 필드명 간의 명명 규칙 차이를 항상 고려해야 한다는 것을 배웠습니다.
일정 수정/삭제 시 비밀번호 확인 로직이 여러 곳에서 중복되고 있었습니다. 또한 비밀번호 오류 시 적절한 HTTP 상태 코드를 반환하는 부분에서 일관성이 부족했습니다.
Service 계층에서 비밀번호 확인 로직을 통일했습니다:
@Override
public ScheduleResponseDto updateSchedule(Long id, ScheduleRequestDto requestDto) {
// 일정 조회
Schedule schedule = scheduleRepository.findByIdOrElseThrow(id);
// 비밀번호 확인
if (!schedule.getPassword().equals(requestDto.getPassword())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}
// 일정 수정
int updatedRows = scheduleRepository.update(id, requestDto.getTodo(), requestDto.getWriter());
if (updatedRows == 0) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "일정 수정에 실패했습니다.");
}
// 수정된 일정 조회 및 반환
Schedule updatedSchedule = scheduleRepository.findByIdOrElseThrow(id);
return new ScheduleResponseDto(updatedSchedule);
}
비밀번호 불일치 시 항상 HttpStatus.UNAUTHORIZED(401)를 반환하도록 통일했고, 비밀번호 확인 후 수정/삭제 실패 시에는 HttpStatus.INTERNAL_SERVER_ERROR(500)를 반환하도록 했습니다.
이번 프로젝트를 통해 Spring Boot의 3계층 아키텍처와 JDBC를 활용한 데이터베이스 연동 방법을 실무적으로 익힐 수 있었습니다. 트러블슈팅 과정에서 많은 문제를 해결하며 다양한 교훈을 얻었고, 이를 통해 더 견고한 코드를 작성할 수 있게 되었습니다.
특히 일관성 있는 명명 규칙 적용, 동적 SQL 쿼리 작성 시 주의점, 적절한 예외 처리 등은 앞으로의 개발에도 큰 도움이 될 것입니다. 이번 경험을 바탕으로 더 복잡한 기능들도 구현해 나갈 예정입니다.
개발자로서 문제를 해결하는 과정은 때로는 고통스럽지만, 그만큼 성장할 수 있는 좋은 기회라고 생각합니다. 앞으로도 이런 트러블슈팅 경험을 지속적으로 기록하고 공유하며 발전해 나가고 싶습니다.