CODING/스파르타 내일배움캠프 TIL

일정관리 개인과제 관련 트러블슈팅 TIL 모음

codingTrip 2025. 2. 3. 17:44

 2025. 1. 23(목)

필수

Lv 0. API 명세 및 ERD 작성 - 트러블 슈팅

 

사실... Spring 과제 발제 시간에 잘 이해하지 못했다.

그래서 시작부터 막막했다. 그래도 차근차근 시도하고자 한다.

아래 내용은 수정 가능성이 있지만 1차적으로 작성한 것에 의의를 둘 것이다.

 

API 명세

기능 Method URL Request response 상태코드
일정 생성 POST /schedules 요청 body 등록 정보 200: 정상등록
전체 일정 조회 GET schedules 요청 param 다건 응답 정보 200: 정상조회
선택 일정 조회 GET /schedules/{scheduleId} 요청 param 단건 응답 정보 200: 정상조회
선택 일정 수정 PUT /schedules/{scheduleId} 요청 body 수정 정보 200: 정상수정
선택 일정 삭제 DELETE /schedules/{scheduleId} 요청 param - 200: 정상삭제

scheduleId or id

이중에 고민했지만 분명하게 내용이 보이는 것이 좋을 것 같아서 위와 같이 작성했다.

 

ERD

일정관리 테이블 ERD

createDate -> createDt -> createdAt

updateDate -> updateDt -> updatedAt

이름 하나도 고민하게 되었다.

 

SQL - 테이블 생성에 필요한 query

CREATE TABLE schedule
(
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '일정 식별자',
    todo VARCHAR(100) NOT NULL COMMENT '할 일',
    writer VARCHAR(100) NOT NULL COMMENT '작성자명',
    password VARCHAR(100) NOT NULL COMMENT '비밀번호',
    createdAt DATETIME COMMENT '생성일',
    updatedAt DATETIME COMMENT '수정일'

);

id는 auto_increment로 하고, PK로 설정해주었다.

todo, writer, password는 not null로 설정했다.

생성일과 수정일은 직접 작성자가 입력하는 것이 아닌 자동으로 생성되는 것이므로 not null을 설정하지 않았다.

 

Lv 1. 일정 생성 및 조회 - 트러블 슈팅

사실 강의를 한 번 빠르게 수강했지만,
다 잘 이해 및 숙지하지 못한 상태에서
과제 구현 시도 vs 강의 복습에 대해 고민했다.

그러나 정해진 기한이 있으므로
과제를 수행하면서 강의 내용을 복습하고자 한다.

먼저 스파르타 강의에서 실습했던 내용 복습 차원에서
해당 내용을 천천히 복기하면서 과제에 맞게 구현하고자 한다.

 

일정 생성(일정 작성하기)

 

Controller

@RestController
@RequestMapping("/schedules")
public class ScheduleController {

    private final ScheduleService scheduleService;

    public ScheduleController(ScheduleService scheduleService) {
        this.scheduleService = scheduleService;
    }
}

Service를 생성자로 주입하려고 하는데 빨간 줄이 떴다.

Service 인터페이스만 만들고 구현체를 만들지 않았기 때문인 것으로 보인다.

public class ScheduleServiceImpl implements ScheduleService

구현 클래스에 implements 했는지 확인하자

 

작성일과 수정일에 현재 날짜와 시간 데이터 설정

작성일과 수정일에 현재 날짜와 시간 데이터를 입력해야 하는데

1) DB에서 처리할지

2) 서버에서 처리할지

고민을 했고,

튜터님께서는 JPA에서는 annotation을 통해 서버에서 설정할 수 있는 방법이 있다고 하셨다.

그러나 JDBC Templete에서는 없다고 하셨다.

 

그래서 아래와 같이 서버에서 처리하기로 했다.

LocalDateTime now = LocalDateTime.now();
        schedule.setCreatedAt(now); // 작성일 설정
        schedule.setUpdatedAt(now); // 수정일 설정

 

JdbcTempleteScheduleRepositoryImpl 에서

insert할 때, 현재 날짜와 시간을 setter를 통해 각각 작성일과 수정일에 설정해주기로 했다.

 

사실 막막하지만... 차근차근 하다보면 잘 될 거라고 믿는다.


 2025. 1. 24(금)

필수

Lv 1. 일정 생성 및 조회 - 트러블 슈팅

전체 일정 조회(등록된 일정 불러오기)

다음 조건을 바탕으로 등록된 일정 목록을 전부 조회
 - 수정일 (형식 : YYYY-MM-DD)
 - 작성자명
조건 중 한 가지만을 충족하거나, 둘 다 충족을 하지 않을 수도, 두 가지를 모두 충족할 수도 있습니다.

위의 조건에 대해 잘 이해하지 못해서 선택 일정 조회부터 구현해보기로 했다.

 

선택 일정 조회(선택한 일정 정보 불러오기)

아래와 같은 예외가 발생했다.

2025-01-24T13:31:10.865+09:00 WARN 4241 --- [schedule] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingPathVariableException: Required URI template variable 'id' for method parameter type Long is not present]

@PathVariable에서 ()로 url 값을 할당해주지 않았기 때문에 발생한 것으로 보인다.

출처 : https://wanggonya.tistory.com/38

@GetMapping("/{scheduleId}")
public ResponseEntity<ScheduleResponseDto> findScheduleById(@PathVariable("scheduleId") Long id){
    return ResponseEntity.ok(scheduleService.findScheduleById(id));
}

 

아래와 같이 200 OK로 잘 나오지만

id, createdAt, updatedAt이 null로 나온다.

ResponseDto와 Mapping 관련 문제인 것으로 보인다.

Postman으로 GET 실행 결과

 

public ScheduleResponseDto(Schedule schedule) {
        this.id = schedule.getId();
        this.todo = schedule.getTodo();
        this.writer = schedule.getWriter();
        this.createdAt = schedule.getCreatedAt();
        this.updatedAt = schedule.getUpdatedAt();
    }

ScheduleResponseDto 생성자에서 id를 설정하지 않았다.

따라서 위처럼 수정했다.

 

JdbcTempleteScheduleRepositoryImpl 에서 아래와 같이 설정했다.

LocalDateTime.parse(rs.getString("createdAt"))

그랬더니 아래와 같은 오류가 발생했다.

java.time.format.DateTimeParseException: Text '2025-01-23 19:18:59' could not be parsed at index 10

튜터님께 도움을 요청했고,

원인은 자바의 LocalDateTime 타입(2025-01-23T19:18:59)과

MySql의 DateTime 타입(2025-01-23 19:18:59)의

각각 형식이 달라서였다.

LocalDateTime now = LocalDateTime.now();
schedule.setCreatedAt(now); // 작성일 설정
schedule.setUpdatedAt(now); // 수정일 설정

위와 같이 설정해도, DB에는 DateTime 형식으로 들어오기 때문에 format해줘야 한다.

private RowMapper<Schedule> scheduleRowMapper() {
        return new RowMapper<Schedule>() {
            @Override
            public Schedule mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 문자열을 LocalDateTime으로 변환
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

                return new Schedule(
                        rs.getLong("id"),
                        rs.getString("todo"),
                        rs.getString("writer"),
                        LocalDateTime.parse(rs.getString("createdAt"),formatter),
                        LocalDateTime.parse(rs.getString("updatedAt"),formatter)
                );
            }
        };
    }

 

Postman으로 GET 실행 결과

이제는 위와 같이 잘 나오는 것을 볼 수 있다.

 

 

Lv1 전체 일정 조회(등록된 일정 불러오기)

위의 조회 부분 구현은 일단 두고 다음으로 넘어가기로 했다.

 

Lv 2. 일정 수정 및 삭제 - 트러블 슈팅

선택한 일정 수정

먼저 수정기능 구현이므로 PUT vs PATCH 중에서 고민하기 시작했다.

조건에는 일정, 작성자 즉, 일부만 수정할 수 있도록 구현해야 해서

PATCH를 선택했다.

Postman으로 PATCH 실행 결과

위와 같이 실행하니 400 오류가 발생했다.

 

@Override
public int updateSchedule(Long id, String todo, String writer, String password) {
    return jdbcTemplate.update("UPDATE schedule SET todo = ?, writer = ?, updatedAt = NOW()" +
            "WHERE id = ? AND password = ?;",todo, writer, id, password);
}

원인은 위의 sql문에서 볼 수 있듯이, 비밀번호를 같이 입력해야 하는데 입력하지 않아서 발생한 오류였다.

 

Postman으로 PATCH 실행 결과

비밀번호까지 입력하니 일정, 작성자, 그리고 수정일이 수정 당시 날짜과 시간으로

잘 수정된 것을 확인할 수 있다.

 

Lv1 전체 일정 조회(등록된 일정 불러오기)

다음 조건을 바탕으로 등록된 일정 목록을 전부 조회
 - 수정일 (형식 : YYYY-MM-DD)
 - 작성자명
조건 중 한 가지만을 충족하거나, 둘 다 충족을 하지 않을 수도, 두 가지를 모두 충족할 수도 있습니다.

위의 조건에 대해 이해가지 않는 부분이 있어서

튜터님께 문의를 드리고 답변을 받았다.

select *
from schedule
order by updatedAt desc

select *
from schedule
where DATE(updatedAt)=?
order by updatedAt desc

select *
from schedule
where writer=?
order by updatedAt desc

select *
from schedule
where DATE(updatedAt)=? AND  writer=?
order by updatedAt desc

위의 sql문처럼 크게 4가지의 경우를 가지고

JdbcTempleteScheduleRepositoryImpl에서 각 조건에 맞는 쿼리문을 넣어주는 방법을 제안해주셨다.

 

password = null

일정 작성, 수정, 조회 시 반환 받은 일정 정보에 비밀번호는 제외해야 합니다.

라는 조건에 의해 해당 부분 수정이 필요했다.

Postman으로 GET 실행 결과

 

public ScheduleResponseDto(Schedule schedule) {
        this.id = schedule.getId();
        this.todo = schedule.getTodo();
        this.writer = schedule.getWriter();
        this.createdAt = schedule.getCreatedAt();
        this.updatedAt = schedule.getUpdatedAt();
    }

ScheduleResponseDto에 위의 코드처럼 생성자로 비밀번호를 제외하고 값을 설정해주었다.

이 타입으로 반환하려고 했는데 컴파일 오류가 발생했다.

 

원인은 선택 일정 조회에 사용했던 RowMapper<Schedule> 타입의 메서드를 호출하려고 해서였다.

타입이 맞지 않아서 아래와 같이 타입에 맞는 새로운 메서드를 만들었다.

 private RowMapper<ScheduleResponseDto> scheduleRowMapper() {
        return new RowMapper<ScheduleResponseDto>() {
            @Override
            public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 문자열을 LocalDateTime으로 변환
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

                return new ScheduleResponseDto(
                        rs.getLong("id"),
                        rs.getString("todo"),
                        rs.getString("writer"),
                        LocalDateTime.parse(rs.getString("createdAt"), formatter),
                        LocalDateTime.parse(rs.getString("updatedAt"), formatter)
                );
            }
        };
    }

 

 

전체 일정 조회 가능

Postman으로 GET 실행 결과

작성자로 조회가능

Postman으로 GET 실행 결과

수정일 조회 문제 발생

Postman으로 GET 실행 결과

Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Method parameter 'updatedAt': Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; Failed to convert from type [java.lang.String] to type [java.time.LocalDateTime] for value [2025-01-23]]

원인은 처음에 controller에서 파라미터로 받아온 수정일을 LocalDateTime 타입으로 설정했고,

String 타입과 달라서 오류가 발생했다.

@GetMapping
    public ResponseEntity<List<ScheduleResponseDto>> findAllSchedule(
            LocalDate updatedAt,
            String writer
    ) {
        log.info("updatedAt={}", updatedAt);
        log.info("writer={}", writer);

        return ResponseEntity.ok(scheduleService.findAllSchedules(updatedAt,writer));
    }

 

따라서 아래와 같이 LocalDate 타입으로 매개변수를 받고,

이를 toString()을 통해 String으로 변환해서 이 값을 쿼리문에 넣어주었다.

@Override
    public List<ScheduleResponseDto> findAllSchedules(LocalDate updatedAt, String writer) {

        String sql;
        String strUpdateAt = updatedAt.toString();

        if (updatedAt != null && writer == null){
            sql = "select * from schedule where DATE(updatedAt)= ? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),strUpdateAt);
        }

        if (updatedAt == null && writer != null){
            sql = "select * from schedule  where  writer=? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),writer);
        }

        if (updatedAt != null && writer != null){
            sql = "select * from schedule where DATE(updatedAt)=? AND writer=? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),strUpdateAt,writer);
        }

        sql = "select * from schedule order by updatedAt desc";

        return jdbcTemplate.query(sql,scheduleRowMapper());
    }

 

Postman으로 GET 실행 결과

 

Postman으로 GET 실행 결과

그러나... 이번에는 작성자명만 입력했더니 아래와 같은 예외가 발생했다.

java.lang.NullPointerException: Cannot invoke "java.time.LocalDate.toString()" because "updatedAt" is null

원인은  String strUpdateAt = updatedAt.toString(); 

수정일을 파라미터로 받아오지 않을 경우 위와 같이 하면 예외가 발생했다.

따라서 아래와 같이 각 조건에 해당하는 경우에만 String으로 변환해주었다.

    @Override
    public List<ScheduleResponseDto> findAllSchedules(LocalDate updatedAt, String writer) {

        String sql;

        if (updatedAt != null && writer == null){
            sql = "select * from schedule where DATE(updatedAt)= ? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),String.valueOf(updatedAt));
        }

        if (updatedAt == null && writer != null){
            sql = "select * from schedule  where  writer = ? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),writer);
        }

        if (updatedAt != null && writer != null){
            sql = "select * from schedule where DATE(updatedAt)=? AND writer=? order by updatedAt desc";
            return jdbcTemplate.query(sql,scheduleRowMapper(),String.valueOf(updatedAt),writer);
        }

        sql = "select * from schedule order by updatedAt desc";

        return jdbcTemplate.query(sql,scheduleRowMapper());
    }

 

 

Postman으로 GET 실행 결과

처음 과제 발제를 들었을 때에는

막막하기만 했고

필수 과제도 못할 것 같아서 두려웠는데...

 

그래도 오늘은 필수 과제 구현은 다했으므로

마음이 한결 놓인다.


 2025. 1. 27(월)

도전

Lv 3. 연관 관계 설정 - 트러블 슈팅

작성자와 일정의 연결

ERD(최종 아님)

위와 같이 필수 단계에서는 1개의 테이블로 관리하던 것을

2개의 테이블로 나누었다.

 

그리고 작성자 관련 CRUD 로직을 새롭게 구현하기 시작했다.

 

작성자 생성 POST 요청 시 404 및 405에러

Postman으로 POST 실행 결과

 

schedule 테이블 외에 추가로 writer(작성자) 테이블을 생성했다.

해당 테이블에 값을 insert하기 위한 코드를 작성하고, 실행하는 도중 위와 같은 오류가 발생했다.

 

처음에는 내가 모르는 놓친 부분이 있을까 싶어서 튜터님께 문의를 드렸다.

혼자서 해결하고자 했으나,

PostMapping을 실행할 때에는 아래처럼 임의로 작성한 로그도 보이지 않았다.

따라서 Controller 메서드도 접속하지 못한 것으로 보인다.

@PostMapping
public ResponseEntity<WriterResponseDto> createWriter(@RequestBody WriterRequestDto dto){
    log.info("dto={}", dto);
    return new ResponseEntity<>(writerService.saveWriter(dto), HttpStatus.CREATED);
}

 

그러나 목록 조회 GetMapping은 빈 배열이지만(데이터가 없으므로) 잘 연결 되어서 

결국 튜터님께 질문 드리기로 했다.

 

Postman으로 POST 실행 결과

원인은... 맥북 사용 시 발생하는 버그? 같은 것이었다.

경로를 입력할 때 localhost:8080와 writers 중간에 이상한 문자가 눈에 보이지는 않지만 들어간 것이 원인이었다.

따라서 잘 작성한 것 같음에도 오류가 발생했을 때는 위의 원인일 수 있으니 잘 확인해보자.

정말... 말도 안되지만 이런 경우로 문제가 발생하기도 하는 것이 개발자의 길이다.

 

이 일을 트러블슈팅에 적어야 하나 고민이 있었으나

그래도 이번에 알게 되었으니

이 글을 읽는 모든 분들은 나처럼 헤매지 않길 바란다.

 

추가로 튜터님께서 알려주신 방법은

Gradle -> clean 실행하기 였다.

나의 경우에는 효과가 없었으나, 혹시 모르니 이 기능도 사용해보길 바란다.

clean 기능

 

 

작성자 수정 PATCH 요청 시 500 서버에러

Postman으로 PATCH 실행 결과

2025-01-27T17:15:26.839+09:00 ERROR 5993 --- [schedule] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar [UPDATE writer SET name = ?, email = ?, updatedAt = NOW()WHERE id = ?;]] with root cause java.sql.SQLSyntaxErrorException: Unknown column 'id' in 'where clause'

다행히? 이번에는 오류 관련 로그가 나와서 잘 읽어볼 수 있었다.

원인은 sql문에 있었다.

where 조건에 id가 아닌 writer_id가 들어가야 한다.

 

Postman으로 PATCH 실행 결과

수정하고 다시 실행하니 위와 같이 잘 되는 것을 확인할 수 있다.

삭제하는 로직도 동일한 실수를 했음을 발견해서 미리 수정해서 오류를 방지했다.

 

Postman으로 DELETE 실행 결과

삭제는 잘 되는 것을 확인할 수 있다.

 

일정 생성 POST 요청 시 schedule 테이블에 writer_id null 값 입력

Postman으로 POST 실행 결과

 

schedule 테이블 조회 결과

 

Map<String, Object> parameters = new HashMap<>();
        parameters.put("todo", schedule.getTodo());
        parameters.put("writer_id", schedule.getWriter_id()); // "writer"로 설정해서 오류
        parameters.put("password", schedule.getPassword());
        parameters.put("createdAt", schedule.getCreatedAt());
        parameters.put("updatedAt", schedule.getUpdatedAt());

원인은 Map에서 key를 writer로 설정해서 발생한 오류였다.

문제는 컴파일 오류 일명 빨간 줄은 나지 않는 오류이기 때문에

런타임 에러가 발생할 확률이 높아진다는 것이다.

따라서 작은 실수도 잘 봐야 한다.

 

오류 수정 후

Postman으로 POST 실행 결과

 

schedule 테이블 조회 결과

 

 

 

일정 조회 GET 요청 시 schedule 테이블에 writer_id 찾지 못함

Postman으로 GET 실행 결과

2025-01-27T17:36:49.047+09:00 ERROR 6189 --- [schedule] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.jdbc.UncategorizedSQLException: StatementCallback; uncategorized SQLException for SQL [SELECT s.schedule_id, s.todo, w.name, s.createdAt, s.updatedAt FROM schedule s JOIN writer w ON s.writer_id = w.writer_id ORDER BY s.updatedAt DESC;]; SQL state [S0022]; error code [0]; Column 'writer_id' not found.] with root cause

매핑은 Long 타입의 writer_id로 설정했는데, 쿼리에서는 String 타입의 name을 조회하려니 문제가 발생했다.

 

생각해보니... writer 테이블에 있는 작성자명 컬럼도 같이 보여주려면

Schedule 클래스와  작성자명에 해당하는 필드도 만들어줘야 한다.

Schedule 클래스와 ScheduleResponseDto 클래스에 각각 작성자명(name) 필드를 생성했다.

private RowMapper<ScheduleResponseDto> scheduleRowMapper() {
        return new RowMapper<ScheduleResponseDto>() {
            @Override
            public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                // 문자열을 LocalDateTime으로 변환
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

                return new ScheduleResponseDto(
                        rs.getLong("scheduleId"),
                        rs.getString("todo"),
                        rs.getLong("writerId"),
                        rs.getString("name"),
                        LocalDateTime.parse(rs.getString("createdAt"), formatter),
                        LocalDateTime.parse(rs.getString("updatedAt"), formatter)
                );
            }
        };
    }

그리고 위의 코드처럼 RowMapper할 때에도 작성자명 필드를 추가해주었다.

 

작성자 id로 조회 시 오류

2025-01-27T18:36:17.129+09:00 ERROR 6912 --- [schedule] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.jdbc.UncategorizedSQLException: PreparedStatementCallback; uncategorized SQLException for SQL [select * from schedule where schedule_id = ?]; SQL state [S0022]; error code [0]; Column 'name' not found.] with root cause
@Override
public Schedule findScheduleByIdOrElseThrow(Long scheduleId) {
    String sql = "SELECT s.scheduleId, s.todo, w.writerId, w.name, s.createdAt, s.updatedAt\n" +
            "FROM schedule s\n" +
            "         JOIN writer w\n" +
            "              ON s.writerId = w.writerId\n" +
            "WHERE s.scheduleId = ?\n" +
            "ORDER BY s.updatedAt DESC";
    List<Schedule> result = jdbcTemplate.query(sql, scheduleRowMapperV2(), scheduleId);
    return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + scheduleId));
}

쿼리로 조회되는 컬럼과 매핑이 일치하지 않다보니 발생한 오류였다.

 

 

schedule 테이블에서 삭제 

2025-01-27T20:10:56.534+09:00 ERROR 7607 --- [schedule] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [delete from writer where writerId=?]; Cannot delete or update a parent row: a foreign key constraint fails (`schedule`.`schedule`, CONSTRAINT `schedule_ibfk_1` FOREIGN KEY (`writerId`) REFERENCES `writer` (`writerId`))] with root cause

schedule 테이블에 작성자 id가 남아있는 채로

writer 테이블에서 삭제하려고 해서 발생한 오류이다.

따라서 외래키 설정 시, ON DELETE CASCADE 조건에 대해 고민했으나,

연동됨에 따라 득보다는 실이 많을 것으로 여겨져

이번에는 하지 않기로 했다.

 

연휴 전날이라 그런지 오늘따라 집중이 어려웠다.

오늘은 계속 오류 로그를 읽으면서 원인을 파악하는 연습을 했다고 생각한다.

그래도 레벨 3까지는 구현했으니 뿌듯한 마음으로 설 연휴를 맞이 하고자 한다.

 


 2025. 1. 31(금)

도전

Lv 4. 페이지네이션 

중복된 매핑

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'scheduleController' method com.example.schedule.presentation.controller.ScheduleController#findAllSchedule(LocalDate, Long) to {GET [/schedules]}: There is already 'scheduleController' bean method

같은 주소로 같은 메서드가 중복 매핑되어서 발생한 예외이다.

@GetMapping("/page")

페이지네이션 부분을 위의 주소에서 실행하도록 수정하니 정상적으로 작동하였다.

 

 

limit, offset 이용해서 페이징 처리하기

SELECT s.scheduleId, s.todo, w.writerId, w.name, s.createdAt, s.updatedAt
FROM schedule s
JOIN writer w
ON s.writerId = w.writerId
ORDER BY s.updatedAt DESC
LIMIT 5 OFFSET 0

먼저 해당하는 sql문을 먼저 작성했다.

 

@GetMapping("/page")
    public ResponseEntity<List<ScheduleResponseDto>> findAllSchedulePaging(
            @RequestParam(value="pageNo", defaultValue="1") int pageNo,
            @RequestParam(value="pageNo", defaultValue="10") int pageSize) {
        log.info("pageNo={}", pageNo);
        log.info("pageSize={}", pageSize);

        return ResponseEntity.ok(scheduleService.findAllSchedulePaging(pageNo,pageSize));
    }

컨트롤러에서 페이지 번호와 페이지 크기를 각각 pageNo, pageSize 파라미터로 받아왔다.

 

offset 값을 구하는  공식을 노트에 정리하면서  스스로 구했다.(아래 이미지 참조)

offset 구하는 공식 도출 과정

 

 @Override
    public List<ScheduleResponseDto> findAllSchedulePaging(int pageNo, int pageSize) {
        String sql = "SELECT s.scheduleId, s.todo, w.writerId, w.name, s.createdAt, s.updatedAt\n" +
                "FROM schedule s\n" +
                "JOIN writer w\n" +
                "ON s.writerId = w.writerId\n" +
                "ORDER BY s.updatedAt DESC\n" +
                "LIMIT ? OFFSET ?";

        int offsetValue = (pageNo-1) * pageSize;

        return jdbcTemplate.query(sql,scheduleRowMapper(),pageSize,offsetValue);
    }

따라서 위와 같은 코드가 완성되었다.

 

+ 튜터님께서 자바에서 제공하는 객체인 Pageable에 대해 알려주셨다.

추후 이에 대해 적용할 예정이다.

 

Lv 5. 예외 발생 처리

수정, 삭제 시 요청할 때 보내는 비밀번호가 일치하지 않을 때 예외가 발생

먼저, 구상한 계획은 아래와 같다.

일정관리 레파지토리 - update 메서드

- scheduleId에 해당하는 password를 DB에서 받아오기(dbPassword)- dbPassword와 requestBody에서 입력 받은 비밀번호 일치여부 확인

- 불일치 시 return -1

 

일정관리 서비스 - update 메서드

- 만약 -1이면 비밀번호 불일치 관련 예외처리 발생

 

그러나 튜터님께서 update 메서드에는 update 관련 내용만 있고,

비밀번호를 검증하는 부분에 대한 메서드를 분리하는 것을 제안하셔서 분리했다.

 

@Override
    public int updateSchedule(Long scheduleId, String todo, String password) {
        return jdbcTemplate.update("UPDATE schedule SET todo = ?,updatedAt = NOW()" +
                    "WHERE scheduleId = ? AND password = ?",todo, scheduleId, password);
    }

    @Override
    public String validatePassword(Long scheduleId) {
        return jdbcTemplate.queryForObject("select password from schedule where scheduleId =?", String.class, scheduleId);
    }

    @Override
    public int deleteSchedule(Long scheduleId, String password) {
        return jdbcTemplate.update("delete from schedule where scheduleId=? and password=?",scheduleId,password);
    }

 

@Transactional
    @Override
    public ScheduleResponseDto updateSchedule(Long scheduleId, String todo, String password) {

        if (todo == null || password == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The todo, writer and password are required values.");
        }

        String dbPassword = scheduleRepository.validatePassword(scheduleId);

        if (!dbPassword.equals(password)){
            throw new ApplicationException(ErrorMessageCode.BAD_REQUEST,
                    List.of(new ApiError("password", "비밀번호가 일치하지 않습니다.")));
        }

        int updatedRow = scheduleRepository.updateSchedule(scheduleId, todo, password);

        if (updatedRow == 0) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + scheduleId);
        }

        Schedule schedule = scheduleRepository.findScheduleByIdOrElseThrow(scheduleId);

        return new ScheduleResponseDto(schedule);
    }

    @Transactional
    @Override
    public void deleteSchedule(Long scheduleId, String password) {

        if (password == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The password are required values.");
        }

        String dbPassword = scheduleRepository.validatePassword(scheduleId);

        if (!dbPassword.equals(password)){
            throw new ApplicationException(ErrorMessageCode.BAD_REQUEST,
                    List.of(new ApiError("password", "비밀번호가 일치하지 않습니다.")));
        }

        int deletedRow = scheduleRepository.deleteSchedule(scheduleId, password);

        if (deletedRow == 0) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + scheduleId);
        }
    }

 

Lv 6. null 체크 및 특정 패턴에 대한 검증 수행

Entity가 아닌 reqestDto에 관련 어노테이션 추가

@Getter
public class ScheduleRequestDto {

    @Size(max = 200)
    @NotEmpty
    private String todo;
    private Long writerId;
    @NotEmpty
    private String password;

}

위와 같이 해야 200자 이내 입력과 필수값 처리가 가능하다.

 

 

+ 추후 리팩토링 사항

Repository에서 DTO가 아닌 entity 사용하도록 수정

이는 입문 단계에서 필수는 아니므로 시간적 여유가 있다면 할 것이다.

 

아직 다 하지는 못했지만

일단 도전 과제를 모두 도전하고자 한다.

월요일에는 남은 과제를 도전하고, 리팩토링 및 문서 정리할 계획이다.

 


 2025. 2. 3(월)

도전

Lv 5. 예외 발생 처리

선택한 일정 정보를 조회할 수 없을 때 예외가 발생합니다.

 - 잘못된 정보로 조회하려고 할 때

 - 이미 삭제된 정보를 조회하려고 할 때 - soft delete 사용

테이블에 deleted 컬럼 추가 -> entity 클래스에 deleted 필드 추가

-> 삭제 시, delete문 대신 update문 사용(deleted 컬럼 값을 false -> true로 수정)

-> 조회 시, deleted = false인 값만 조회

 

2025-02-03T10:08:51.582+09:00 ERROR 2401 --- [schedule] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0] with root cause
    @Override
    public ScheduleResponseDto findScheduleById(Long scheduleId) {

        try{
            boolean deleted = scheduleRepository.validateDeleted(scheduleId);

            if (deleted){
                throw new ApplicationException(ErrorMessageCode.NOT_FOUND,
                        List.of(new ApiError("deleted", "이미 삭제된 정보입니다. 다시 입력하세요")));
            }

            Schedule schedule = scheduleRepository.findScheduleByIdOrElseThrow(scheduleId);
            
            return new ScheduleResponseDto(schedule);
        } catch (EmptyResultDataAccessException e) {
            throw new ApplicationException(ErrorMessageCode.NOT_FOUND,
                    List.of(new ApiError("id", "잘못된 정보입니다. 다시 입력하세요")));
        }
    }

Spring 에서 queryForObject()을 사용할 때,

결과값이 1 이어야 하는데 실제 결과값은 0 일때 다음과 같은 에러가 발생한다.

따라서 try- catch문으로 감싸서 예외처리 했다.

출처 : https://coderanch.com/t/468501/frameworks/handling-simpleJdbcTemplate-error

 

처음 막막했던 것에 비해

내가 생각했던 나의 한계에 비해

더 도전했고, 노력했다.

 

그래서 더 스스로가 자랑스러운 시간이었다.