Spring Boot로 일정 관리 시스템 만들기

geoson·2025년 6월 5일

Spring & 백엔드

목록 보기
6/18

프로젝트 개요

이번 프로젝트는 사용자가 일정을 관리할 수 있는 RESTful API를 개발하는 것이었습니다. 주요 기능으로는 일정 생성, 조회, 수정, 삭제가 있습니다. 각 일정은 할일(todo), 작성자(writer), 비밀번호(password)를 가지며, 수정 및 삭제 시에는 비밀번호를 확인합니다.

기술 스택

  • Java 17
  • Spring Boot 3.x
  • Spring JDBC
  • MySQL
  • Lombok

핵심 기능

  1. 일정 생성 - POST /api/schedules
  2. 전체 일정 조회 (날짜, 작성자 필터링 가능) - GET /api/schedules
  3. 특정 일정 조회 - GET /api/schedules/{id}
  4. 일정 수정 (비밀번호 확인) - PUT /api/schedules/{id}
  5. 일정 삭제 (비밀번호 확인) - DELETE /api/schedules/{id}

3계층 아키텍처 구현

Spring Boot의 대표적인 아키텍처인 3계층 구조를 사용했습니다:

  1. Controller 계층 - HTTP 요청 처리 및 응답 반환

    • ScheduleController - API 엔드포인트 정의
  2. Service 계층 - 비즈니스 로직 처리

    • ScheduleService (인터페이스)
    • ScheduleServiceImpl (구현체)
  3. Repository 계층 - 데이터베이스 접근

    • ScheduleRepository (인터페이스)
    • JdbcScheduleRepository (구현체)

트러블슈팅 1: SQL 쿼리 문법 오류

문제 상황

전체 일정 조회 기능을 구현했으나, 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 쿼리를 동적으로 생성할 때는 항상 각 절 사이에 공백을 추가해야 한다는 것을 배웠습니다.

트러블슈팅 2: 테이블 존재하지 않음 오류

문제 상황

일정 생성 API를 테스트할 때 다음과 같은 오류가 발생했습니다:

java.sql.SQLSyntaxErrorException: Table 'sparta_schedule.schedules' doesn't exist

원인 분석

두 가지 문제가 복합적으로 발생했습니다:

  1. 데이터베이스에 테이블이 생성되지 않았거나
  2. 코드에서 참조하는 테이블 이름이 실제 테이블 이름과 다른 경우

처음에는 코드에서 schedules(복수형)로 테이블을 참조했지만, 실제 데이터베이스에는 schedule(단수형)로 테이블이 생성되어 있었습니다.

해결 방법

  1. 코드의 테이블 참조를 수정했습니다:
jdbcInsert.withTableName("schedule").usingGeneratedKeyColumns("id");
  1. 데이터베이스 스키마를 명확히 정의하기 위해 schema.sql 파일을 만들었습니다:
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
);

트러블슈팅 3: Entity와 DTO 간 필드 매핑 문제

문제 상황

일정 조회 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 간 변환 시 필드 매핑이 정확한지 꼼꼼히 확인해야 한다는 교훈을 얻었습니다.

트러블슈팅 4: 데이터베이스 컬럼명과 엔티티 필드명 불일치

문제 상황

일정을 조회할 때 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;
});

데이터베이스 컬럼명과 자바 필드명 간의 명명 규칙 차이를 항상 고려해야 한다는 것을 배웠습니다.

트러블슈팅 5: 비밀번호 확인 로직 개선

문제 상황

일정 수정/삭제 시 비밀번호 확인 로직이 여러 곳에서 중복되고 있었습니다. 또한 비밀번호 오류 시 적절한 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)를 반환하도록 했습니다.

배운 점과 개선 방향

주요 배운 점

  1. SQL 쿼리 작성의 중요성: 동적 SQL 생성 시 문법적으로 올바르게 작성해야 합니다.
  2. 명명 규칙 통일: 데이터베이스와 자바 코드 간의 명명 규칙 차이를 인식하고 일관되게 처리해야 합니다.
  3. 3계층 아키텍처의 책임 분리: 각 계층(Controller, Service, Repository)의 역할과 책임을 명확히 이해하고 적용해야 합니다.
  4. 적절한 예외 처리: HTTP 상태 코드와 에러 메시지를 일관되게 사용하여 클라이언트에게 유용한 피드백을 제공해야 합니다.
  5. DTO와 Entity 변환: 데이터 변환 시 필드 매핑을 정확하게 해야 합니다.

결론

이번 프로젝트를 통해 Spring Boot의 3계층 아키텍처와 JDBC를 활용한 데이터베이스 연동 방법을 실무적으로 익힐 수 있었습니다. 트러블슈팅 과정에서 많은 문제를 해결하며 다양한 교훈을 얻었고, 이를 통해 더 견고한 코드를 작성할 수 있게 되었습니다.

특히 일관성 있는 명명 규칙 적용, 동적 SQL 쿼리 작성 시 주의점, 적절한 예외 처리 등은 앞으로의 개발에도 큰 도움이 될 것입니다. 이번 경험을 바탕으로 더 복잡한 기능들도 구현해 나갈 예정입니다.

개발자로서 문제를 해결하는 과정은 때로는 고통스럽지만, 그만큼 성장할 수 있는 좋은 기회라고 생각합니다. 앞으로도 이런 트러블슈팅 경험을 지속적으로 기록하고 공유하며 발전해 나가고 싶습니다.

0개의 댓글