
목차
요구사항
✅ 작성자와 일정의 연결
- ✅ 설명
- ✅ 동명이인의 작성자가 있어 어떤 작성자가 등록한 ‘할 일’인지 구별할 수 없음
 - ✅ 작성자를 식별하기 위해 이름으로만 관리하던 작성자에게 고유 식별자를 부여합니다.
 - ✅ 작성자를 할 일과 분리해서 관리합니다.
 - ✅ 작성자 테이블을 생성하고 일정 테이블에 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 |