목차
요구사항
✅ 일정 생성(일정 작성하기)
- ✅ 일정 생성 시, 포함되어야할 데이터
- ✅ 할일, 작성자명, 비밀번호, 작성/수정일을 저장
- ✅ 작성/수정일은 날짜와 시간을 모두 포함한 형태
- ✅ 각 일정의 고유 식별자(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);
}
- 객체지향의 다형성을 위해 인터페이스를 만들어 구현체가 이를 구현하도록하였습니다.
- (Controller 코드는 `ScheduleService` 인터페이스에만 의존하게 되므로, 구현체가 바뀌더라도 클라이언트 코드를 수정하지 않아도 됩니다.)
- 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);
}
- 객체지향의 다형성을 위해 인터페이스를 만들어 구현체가 이를 구현하도록하였습니다.
- (Controller 코드는 `ScheduleService` 인터페이스에만 의존하게 되므로, 구현체가 바뀌더라도 클라이언트 코드를 수정하지 않아도 됩니다.)
- 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(컬럼명, ..) : 삽입할 컬럼들을 지정합니다.
'Project' 카테고리의 다른 글
[Project] Lv_5 스케줄 프로젝트 (0) | 2025.05.13 |
---|---|
[Project] Lv_4 스케줄 프로젝트 (2) | 2025.05.12 |
[Project] Lv_3 스케줄 프로젝트 (2) | 2025.05.12 |
[Project] Lv_3 스케줄 프로젝트 : ERD 재설계 (0) | 2025.05.12 |
[Project] Lv_0 스케줄 프로젝트 (0) | 2025.05.11 |