본문 바로가기

Project

[Project] Lv_1 스케줄 프로젝트

 

 


 

요구사항

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

  • ✅ 일정 생성 시, 포함되어야할 데이터
    • ✅ 할일, 작성자명, 비밀번호, 작성/수정일을 저장
    • ✅ 작성/수정일은 날짜와 시간을 모두 포함한 형태
  • ✅ 각 일정의 고유 식별자(ID)를 자동으로 생성하여 관리
  • ✅ 최초 입력 시, 수정일은 작성일과 동일

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

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

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

  • ✅ 선택한 일정 단건의 정보를 조회할 수 있습니다.
    • ✅ 일정의 고유 식별자(ID)를 사용하여 조회합니다.

 


 

문제 풀이

ScheduleController

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

    private final ScheduleService scheduleService;

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

    /**
     * 스케쥴 생성 API
     * param : {@link ScheduleRequestDto} 스케쥴 생성 요청 객체
     * @return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
     */
    @PostMapping
    public ResponseEntity<ScheduleResponseDto> createSchedule(@RequestBody ScheduleRequestDto requestDto) {
        return new ResponseEntity<>(scheduleService.saveSchedule(requestDto), HttpStatus.CREATED);
    }

    /**
     * 스케쥴 다건 조회 API
     * @return : {@link  List<ScheduleResponseDto>} JSON 응답
     */
    @GetMapping
    public List<ScheduleResponseDto> findAllSchedule() {
        return scheduleService.findAllSchedules();
    }

    /**
     * 스케쥴 단건 조회 API
     * @param id 식별자
     * @return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
     * @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
     */
    @GetMapping("/{id}")
    public ResponseEntity<ScheduleResponseDto> findScheduleById(@PathVariable Long id) {
        return new ResponseEntity<>(scheduleService.findScheduleById(id), HttpStatus.OK);
    }
  • scheduleService는 다형성을 위해, 인터페이스를 통해 구현체를 주입받는 구조입니다.
  • 반환타입은 Entity(Schedule) 자체를 주고 받는 것이 아닌, ScheduleRequestDto, ScheduleResponseDto를 통해서 내가 주고받기를 원하는 속성 값만 넣어 Dto 형태로 주고받습니다.

 

 

ScheduleService

더보기
public interface ScheduleService {

    ScheduleResponseDto saveSchedule(ScheduleRequestDto requestDto);

    List<ScheduleResponseDto> findAllSchedules();

    ScheduleResponseDto findScheduleById(long id);
}
  • 객체지향의 다형성을 위해 인터페이스를 만들어 구현체가 이를 구현하도록하였습니다.
    1. (Controller 코드는 `ScheduleService` 인터페이스에만 의존하게 되므로, 구현체가 바뀌더라도 클라이언트 코드를 수정하지 않아도 됩니다.)
    2. Spring의 DI 컨테이너는 인터페이스를 통해 구현체를 주입받는 구조를 권장합니다.

 

 

ScheduleServiceImpl

더보기
@Service
public class ScheduleServiceImpl implements ScheduleService{

    private final ScheduleRepository scheduleRepository;

    public ScheduleServiceImpl(ScheduleRepository scheduleRepository) {
        this.scheduleRepository = scheduleRepository;
    }

    @Override
    public ScheduleResponseDto saveSchedule(ScheduleRequestDto requestDto) {
        Schedule schedule = new Schedule(requestDto.getPassword(), requestDto.getUserName(), requestDto.getToDoContent());
        return scheduleRepository.saveSchedule(schedule);
    }

    @Override
    public List<ScheduleResponseDto> findAllSchedules() {
        // 전체 조회
        return scheduleRepository.findAllSchedules();
    }

    @Override
    public ScheduleResponseDto findScheduleById(long id) {
        Schedule schedule = scheduleRepository.findScheduleByIdElseThrow(id);
        return new ScheduleResponseDto(schedule);
    }
}
  • scheduleRepository는 다형성을 위해, 인터페이스를 통해 구현체를 주입받는 구조입니다.
  • 반환타입은 Entity(Schedule) 자체를 주고 받는 것이 아닌, ScheduleRequestDto, ScheduleResponseDto를 통해서 내가 주고받기를 원하는 속성 값만 넣어 Dto 형태로 주고받습니다.

 

 

ScheduleRepository

더보기
public interface ScheduleRepository {

    ScheduleResponseDto saveSchedule(Schedule schedule);

    List<ScheduleResponseDto> findAllSchedules();

    Schedule findScheduleByIdElseThrow(long id);
}
  • 객체지향의 다형성을 위해 인터페이스를 만들어 구현체가 이를 구현하도록하였습니다.
    1. (Controller 코드는 `ScheduleService` 인터페이스에만 의존하게 되므로, 구현체가 바뀌더라도 클라이언트 코드를 수정하지 않아도 됩니다.)
    2. Spring의 DI 컨테이너는 인터페이스를 통해 구현체를 주입받는 구조를 권장합니다.

 

 

JdbcTemplateScheduleRepository

더보기
@Repository
public class JdbcTemplateScheduleRepository implements ScheduleRepository{

    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplateScheduleRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public ScheduleResponseDto saveSchedule(Schedule schedule) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
                .withTableName("schedule") // 테이블명
                .usingGeneratedKeyColumns("scheduleId") // 자동 생성된 키
                .usingColumns("password", "userName", "toDoContent");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("password", schedule.getPassword());
        parameters.put("userName", schedule.getUserName());
        parameters.put("toDoContent", schedule.getTodoContent());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        long scheduleId = key.longValue();

        String sql = "SELECT userName, toDoContent, updateAt FROM schedule WHERE scheduleId = ?";
        return jdbcTemplate.queryForObject(sql, (rs, rowNum) ->
                new ScheduleResponseDto(
                        rs.getString("userName"),
                        rs.getString("toDoContent"),
                        rs.getDate("updateAt")
                )
                ,scheduleId
        );
    }

    @Override
    public List<ScheduleResponseDto> findAllSchedules() {
        return jdbcTemplate.query("select * from schedule ORDER BY updateAt DESC", scheduleRowMapper());
    }

    @Override
    public Schedule findScheduleByIdElseThrow(long id) {
        List<Schedule> result = jdbcTemplate.query("select * from schedule where scheduleId = ?", scheduleRowMapper2(), id);
        return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exists id = " + id));
    }

    @Override
    public Schedule findSchedulePasswordByIdElseThrow(long id) {
        List<Schedule> result = jdbcTemplate.query("select * from schedule where scheduleId = ?", scheduleRowMapper3(), id);
        return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exists id = " + id));
    }

    private RowMapper<ScheduleResponseDto> scheduleRowMapper() {
        return (rs, rowNum) -> new ScheduleResponseDto(
                rs.getString("userName"),
                rs.getString("todoContent"),
                rs.getDate("updateAt")
        );
    }

    private RowMapper<Schedule> scheduleRowMapper2() {
        return (rs, rowNum) -> new Schedule(
                rs.getString("userName"),
                rs.getString("todoContent"),
                rs.getDate("updateAt")
        );
    }
}

 

 

Schedule(Entity)

더보기
@Getter
@AllArgsConstructor
public class Schedule {

    private long scheduleId;
    private long password;
    private String userName;
    private String todoContent;
    private Date createAt;
    private Date updateAt;

    public Schedule(long password, String userName, String todoContent) {
        this.password = password;
        this.userName = userName;
        this.todoContent = todoContent;
    }

    public Schedule(String userName, String todoContent, Date updateAt) {
        this.userName = userName;
        this.todoContent = todoContent;
        this.updateAt = updateAt;
    }

    public Schedule(long password) {
        this.password = password;
    }
}

 

 

ScheduleRequestDto

더보기
@Getter
public class ScheduleRequestDto {

    private Long password;
    private String userName;
    private String toDoContent;
}
  • 요구사항에서 ` 할 일(toDoContent)`, `작성자 명(userName)`, `비밀번호(userId)`, `작성/수정일(내부적으로 처리)`의 값이 저장되어야 하므로, 다음과 같이 내부에서 처리되는 값을 제외한 나머지 값들을 속성으로 갖는 클래스를 생성하였습니다.

 

 

ScheduleResponseDto

더보기
@Getter
@AllArgsConstructor
public class ScheduleResponseDto {

    private String userName;
    private String todoContent;
    private Date updateAt;

    public ScheduleResponseDto(Schedule schedule) {
        this.userName = schedule.getUserName();
        this.todoContent = schedule.getTodoContent();
        this.updateAt = schedule.getUpdateAt();
    }
}
  • 목록을 보여줄 때, `사용자 명(userName)`, `할 일(toDoContent)`, `수정일(updateAt)`을 전달하고 싶끼 때문에, 그 값들을 갖는 클래스를 생성하였습니다. 
  • `Entity(Schedule)`를 매개변수로 받는 생성자도 필요하여 생성자를 정의하였습니다.
  • 각 값들을 한 번에 매개변수로 받는 생성자도 필요하기 때문에, `@AllArgsConstructor` 어노테이션을 활용하였습니다.

 


 

 

트러블 슈팅

1. application.properties 오류

설명

spring.datasource.url=jdbc:mysql://localhost:3306/memo :
→ MySQL 데이터베이스에 연결하기 위한 URL입니다. localhost는 데이터베이스 서버 주소 (자신의 컴퓨터), 3306은 MySQL의 기본 포트, memo는 사용할 데이터베이스 이름입니다.

 

 

문제

하지만 나의 데이터베이스 이름은 `scheduler`이다.
→ 데이터 베이스의 이름을 잘못 설정하여, `com.mysql.cj.jdbc.exceptions.MySQLSyntaxErrorException: Unknown database 'memo'와 같은 오류가 발생하였다.


해결

데이터베이스의 이름을 알맞게 설정하였더니 오류가 발생하지 않았습니다.

 


2. `5xx` 서버 오류

설명

위는, SCHEDULE Table userId는 `FK(Foreign key)`이며, USER Table의 userId를 참조하고 있습니다.

`userId`는 `NOT NULL`로 설정되어 있습니다.

 

 

문제

1. `userId`가 `USER` 테이블에 존재하지 않는 값을 참조하면 외래 키 제약 조건 위반 오류가 발생합니다.

2. `userId`가 `NULL`이면 NOT NULL 제약 조건 위반 오류가 발생합니다

 

 

해결

현재 모든 코드에서 USER Table과 userId도 사용하지 않기 때문에 두 부분을 임시로 주석처리하여 해결하였습니다.

 


 

3. Table에 값 저장 안되는 오류

설명

withTableName(테이블 명) : `schedule` 테이블에 데이터를 넣는다는 의미입니다.

usingGeneratedKeyColumn(컬럼 명) : 자동 생성되는 기본 키 컬럼을 명시합니다. (예: AUTO_INCREMENT 키)

 

✨`SimpleJdbcInsert`는 내부적으로 `INSERT INTO ...` 구문을 생성하는데, 어떤 컬럼을 사용할지 알아야 쿼리를 만들 수 있습니다. 만약 `usingColumns`를 명시하지 않으면, 모든 필드를 삽입하려고 시도하거나 예외가 발생할 수 있습니다.

 

 

문제

Schedule Table을 확인해 보았더니, `DEFAULT CURRENT_TIMESTAMP`로 지정된 필드 `createAt`과 `updateAt`의 값이 `null`값입니다.

 

❗`SimpleJdbcInsert`는 데이블 메타데이터를 자동 추론해서, 모든 컬럼을 Insert 대상 컬럼으로 자동 지정했을 때 발생할 수 있습니다. 위 코드에서 `parameters`에 `createAt`, `updateAt`이 없어도 자동으로 `INSERT INTO schedule (userName, toDoContent, createAt, updateAt)` 식으로 만들어지고, 그 자리에 `null`이 삽입됩니다.

 

 

해결

usingColums(컬럼명, ..) : 삽입할 컬럼들을 지정합니다.