목차
요구사항
✅ 작성자와 일정의 연결
- ✅ 설명
- ✅ 동명이인의 작성자가 있어 어떤 작성자가 등록한 ‘할 일’인지 구별할 수 없음
- ✅ 작성자를 식별하기 위해 이름으로만 관리하던 작성자에게 고유 식별자를 부여합니다.
- ✅ 작성자를 할 일과 분리해서 관리합니다.
- ✅ 작성자 테이블을 생성하고 일정 테이블에 FK를 생성해 연관관계를 설정해 봅니다.
✅ 조건
- ✅ 작성자는 이름 외에 이메일, 등록일, 수정일 정보를 가지고 있습니다.
- ✅ 작성자의 정보는 추가로 받을 수 있습니다.(조건만 만족한다면 다른 데이터 추가 가능)
- ✅ 작성자의 고유 식별자를 통해 일정이 검색이 될 수 있도록 전체 일정 조회 코드 수정.
- ✅ 작성자의 고유 식별자가 일정 테이블의 외래키가 될 수 있도록 합니다.
문제풀이
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);
}
/**
* 스케줄 단건 수정 API
* Param id 식별자
* Return : {@link ResponseEntity<ScheduleResponseDto>} JSON 응답
* @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
*/
@PatchMapping("/{id}")
public ResponseEntity<ScheduleResponseDto> updatetoDoContentOrUserName(
@PathVariable long id,
@RequestBody ScheduleRequestDto requestDto
) {
return new ResponseEntity<>(scheduleService.updateUserNameOrToDoContent(id, requestDto.getPassword(), requestDto.getUserName(), requestDto.getToDoContent()), HttpStatus.OK);
}
/**
* 스케줄 단건 수정 API
* Param id 식별자
* return {@link ResponseEntity<Void>} 성공시 Data 없이 200OK 상태코드만 응답.
* @exception ResponseStatusException 식별자로 조회된 Schedule이 없는 경우 404 Not Found
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteSchedule(
@PathVariable long id,
@RequestBody ScheduleRequestDto requestDto
) {
scheduleService.deleteSchedule(id, requestDto.getPassword());
// 성공한 경우
return new ResponseEntity<>(HttpStatus.OK);
}
}
ScheduleService
더보기
public interface ScheduleService {
ScheduleResponseDto saveSchedule(ScheduleRequestDto requestDto);
List<ScheduleResponseDto> findAllSchedules();
ScheduleResponseDto findScheduleById(long id);
ScheduleResponseDto updateUserNameOrToDoContent(long id, long password, String userName, String toDoContent);
void deleteSchedule(long id, long password);
}
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);
}
@Override
public ScheduleResponseDto updateUserNameOrToDoContent(long id, long password, String userName, String toDoContent) {
// 필수값 검증
if (userName == null || toDoContent == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The userName and toDoContent are required values.");
}
// 비밀번호 검증
if (password != scheduleRepository.findSchedulePasswordByIdElseThrow(id).getPassword()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The password is not the same as the password for this schedule");
}
int updatedRow = scheduleRepository.updateUserNameOrToDoContent(id, userName, toDoContent);
// NPE 방지
if (updatedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
// 식별자의 schedule 없다면?
Schedule schedule = scheduleRepository.findScheduleByIdElseThrow(id);
return new ScheduleResponseDto(schedule);
}
@Override
public void deleteSchedule(long id, long password) {
// 비밀번호 검증
if (password != scheduleRepository.findSchedulePasswordByIdElseThrow(id).getPassword()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The password is not the same as the password for this schedule");
}
int deletedRow = scheduleRepository.deleteSchedule(id);
// NPE 방지
if(deletedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
}
}
- 스케줄을 수정 및 삭제 할 때 : 새로 입력받은 비밀번호와 수정, 삭제하길 원하는 스케줄에 저장된 비밀번호가 일치하는지 확인하는 로직을 구현하여, 입력값(비밀번호)를 검증합니다.
ScheduleRepository
더보기
package org.example.scheduledweb.repository;
import org.example.scheduledweb.dto.ScheduleResponseDto;
import org.example.scheduledweb.entity.Schedule;
import java.util.List;
public interface ScheduleRepository {
ScheduleResponseDto saveSchedule(Schedule schedule);
List<ScheduleResponseDto> findAllSchedules();
Schedule findScheduleByIdElseThrow(long id);
Schedule findSchedulePasswordByIdElseThrow(long id);
int updateUserNameOrToDoContent(long id, String userName, String toDoContent);
int deleteSchedule (long id);
}
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));
}
@Override
public int updateUserNameOrToDoContent(long id, String userName, String toDoContent) {
return jdbcTemplate.update("update schedule set userName = ?, toDoContent = ? where scheduleId = ?", userName, toDoContent, id);
}
@Override
public int deleteSchedule(long id) {
return jdbcTemplate.update("delete from schedule where scheduleId = ?", 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")
);
}
private RowMapper<Schedule> scheduleRowMapper3() {
return (rs, rowNum) -> new Schedule(
rs.getLong("password")
);
}
}
- findSchedulePasswordByIdElseThrow : 비밀번호 검증을 위해 비밀번호만 갖는 객체를 반환합니다.
ScheduleRequestDto
더보기
@Getter
public class ScheduleRequestDto {
private Long password;
private String userName;
private String toDoContent;
}
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();
}
}
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;
}
}
회고
- Lv_0 API 명세서 작성을 통해 전체적인 틀을 만들어 놓고, 기능을 순서대로 구현하기 때문에, 큰 어려움은 없었습니다.
- 3 Layerd Architecture 방식을 이용하니, 역할이 분리되어있어 확실히 코드를 짤 때 편리하다고 느꼈습니다.
'Spring' 카테고리의 다른 글
[Spring] Bean 등록 방법 (4) | 2025.05.23 |
---|---|
[Spring] Spring Bean 어떻게 등록될까? (0) | 2025.05.23 |
[Spring] SOLID 원칙과 Spring의 등장 배경 (0) | 2025.05.15 |
[Spring] Spring Framework와 Spring Boot: 왜 등장했고, 무엇이 다른가? (0) | 2025.05.10 |
[Spring] 어노테이션(Annotation) (0) | 2025.05.10 |