1️⃣ API 작성
2️⃣ ERD 작성
1) 필수 과제 ERD
2) 도전 과제 ERD
3️⃣ SQL 문
-- author TABLE SQL
create table author
(
id bigint auto_increment comment '작성자 식별자'
primary key,
name char(10) null comment '작성자명',
email char(100) null comment '이메일',
created datetime null comment '작성일',
updated datetime null comment '수정일',
constraint check_name
check (true)
);
-- plan TABLE SQL
create table plan
(
id bigint auto_increment comment '할일 식별자'
primary key,
authorId bigint comment '작성자 식별자',
task text comment '할일',
pwd char(20) comment '비밀번호',
created timestamp comment '작성일',
updated timestamp comment '수정일',
constraint authorId_FK
foreign key (authorId) references author (id)
);
4️⃣ [TS] 수정 Service 로직
1) 문제 상황
7 이라는 아이디가 존재하지 않는 것이라 404 NOT_FOUND 가 떠야하는데, 500 에러가 뜸. 에러의 순서가 이상한 상황.
2) 문제 분석
에러를 잡는 우선순위가 지금 복잡해진 것 같음. 최소한 id 값을 먼저 확인 -> pwd 확인 -> 조회여부 확인 순으로 가야하는데, 지금은 pwd 부터 확인해서 이러한 500에러가 뜨는 것 같음.
3) 문제 해결
지금은 아래와 같은 방향으로 수정
1. 작성자 id 가 실제 author Table 에 존재하는지 확인
2. Edit 진행. Edit 값이 양수라면, id 존재하는 것.
3. pwd 올바른지 아닌지 확인, 틀렸을 경우 트랜잭션에 의해 수정 작업 롤백 if
4. 조회할 시 없으면, 트랜잭션에 의해 수정 작업 롤백
5️⃣ [TS] 요구사항 이해
1) 문제 상황
요구사항에서 작성자 테이블에 등록일과 수정일을 새로 만들어야 함. 그렇다면, 일정 테이블의 등록일과 수정일을 다 작성자 테이블로 옮겨야 하는 상황?
2) 문제 원인
알고보니, 작성자 테이블에서 작성자의 등록일과 수정일을 새로 만들라는 것 이었다.
보편적으로 생각하는 방식은 유저가 생성될때 -> 등록일, 유저가 수정될때 -> 수정일 이라고 한다.
3) 문제 해결
작성자 테이블로 옮겼던 일정테이블의 등록일과 수정일을 원위치 시키고, 작성자 테이블에 새로운 등록일과 수정일을 만들어 주었다.
6️⃣ [TS] Mapper 자료형 오류
1) 문제 상황
- ReparedStatementCallback; SQL [선택 ID, authorId, task, pwd, 생성, 업데이트된 FROM 플랜 WHERE ID = ?]; 문자열 '할일'에서 값 유형을 확인할 수 없습니다. (일단 이 부분은 한글로 번역한 것 !)
Request Body에서의 오류도 아님. 근데 문자열로 넣었는데, 문자열을 인식하지 못하는 것.
2) 문제 원인
해당 부분은 어느부분인지는 모르지만, 해당 task, authorId, pwd 부분의 값 유형을 잘 못 설정했다는 것이다. 매핑이 잘 못 되었다는 것! 그래서 하나하나 뜯어서 살펴보자.
① Request DTO
@Getter
public class ScheduleRequestDto {
@NotBlank(message = "할일은 필수값 입니다.")
@Size(max = 200, message = "할일은 200글자 이내로 제한됩니다.")
private String task;
@NotNull(message = "작성자의 id 값은 필수값 입니다.")
private Long authorId;
@NotBlank(message = "비밀번호는 필수값 입니다.")
private String pwd;
}
- String, Long, String 으로 잘 되어 있다
② Controller
@PatchMapping("/{id}")
public ResponseEntity<ScheduleResponseDto> editSchedule(
@PathVariable Long id,
@RequestBody @Valid ScheduleRequestDto dto
) {
return new ResponseEntity<>(scheduleService.editSchedule(id, dto.getTask(), dto.getAuthorId(), dto.getPwd()), HttpStatus.OK);
}
- 딱히 이상한 곳이 없다.
③ Service
@Transactional
@Override
public ScheduleResponseDto editSchedule(Long id, String task, Long authorId, String pwd) {
// 1. 작성자 id 가 실제 author Table 에 존재하는지 확인
if (!authorRepository.findAuthorByIdIsEmpty(authorId)) {
throw new DataNotFoundException("작성자의 아이디가 존재하지 않습니다. id : " + authorId);
}
// 2. Edit 진행. Edit 값이 양수라면, id 존재하는 것.
int row = scheduleRepository.editSchedule(id, task, authorId);
if (row == 0) {
throw new DataNotFoundException("일정의 id 가 존재하지 않습니다.");
}
// 3. pwd 올바른지 아닌지 확인, 틀렸을 경우 트랜잭션에 의해 수정 작업 롤백
if (!scheduleRepository.findScheduleByPwd(id, pwd)) {
throw new InvalidPasswordException("해당 일정의 비밀번호가 틀렸습니다. id : " + id);
}
// 4. 조회할 시 없으면, 트랜잭션에 의해 수정 작업 롤백
return new ScheduleResponseDto(scheduleRepository.findScheduleByIdOrElseThrow(id));
}
- task 를 String 으로, authorId 를 Long 으로, String 을 pwd 로 잘 받고 있다. 그러면 딱히 문제가 없는 것이다.
④ Repository
@Override
public int editSchedule(Long id, String task, Long authorId) {
String sql = "UPDATE plan p " +
"SET task = CASE WHEN ? is not null THEN ? ELSE task END, " +
" authorId = CASE WHEN ? is not null THEN ? ELSE authorId END, " +
" updated = ? " +
"WHERE id = ?";
return jdbcTemplate.update(sql, task, task, authorId, authorId, changeTimestamp(), id);
}
- 여기서도 ? 안에 제대로 된 값을 넣고 있다.
⑤ 서비스 로직 안 함수
- 그렇다면, 서비스 로직 안에 있던 함수가 잘못 된건가 싶어서 살펴보았다.
@Override
public Plan findScheduleByIdOrElseThrow(Long id) {
List<Plan> result = jdbcTemplate.query("SELECT * FROM plan WHERE id = ?", scheduleRowMapperV2(), id);
return result.stream().findAny().orElseThrow(() -> new DataNotFoundException("해당하는 일정이 존재하지 않습니다. id : " + id));
}
- 여기서 RowMapper() 를 확인해보겠다.
private RowMapper<Plan> scheduleRowMapperV2() {
return new RowMapper<Plan>() {
@Override
public Plan mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Plan(rs.getLong("id"),
rs.getLong("task"),
rs.getString("authorId"),
rs.getString("pwd"),
rs.getString("created"),
rs.getString("updated"));
}
};
}
- 잘 살펴보니 get String 이 아닌 Long 값으로 설정되어 있었다! 이게 문제였던 것이다.
- 이전에 작성자 테이블에 할일의 등록일과 수정일을 옮기고, 다시 원상복귀 시키다가 생긴 실수 같다.
3) 문제 해결
값의 자료형을 적절하게 수정해주었다.
private RowMapper<Plan> scheduleRowMapperV2() {
return new RowMapper<Plan>() {
@Override
public Plan mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Plan(
rs.getLong("id"),
rs.getString("task"),
rs.getLong("authorId"),
rs.getString("pwd"),
rs.getString("created"),
rs.getString("updated"));
}
};
}
7️⃣ 고민해야할 사항에 대한 답변
1) 적절한 관심사 분리를 적용하셨나요? (Controller, Service, Repository)
- 그러하다. 컨트롤러는 requst 를 받고, response 를 주는 역할만 주었고, Service 는 비즈니스 로직을 처리하는 역할과 Repository 에 데이터를 요청하고 응답받는 역할을 했다. Repository 에서는 실제 데이터를 찾는 역할만 주었다.
2) RESTful한 API를 설계하셨나요? 어떤 부분이 그런가요? 어떤 부분이 그렇지 않나요?
- 자원 기반의 엔드포인트 설계를 했다.
- HTTP 메서드를 적절하게 활용했다.
- 일관된 응답형식과 적절한 상태코드를 주고자 노력했다. 성공시에는 200OK, 생성시에는 201 CREATED, 잘못된 요청엔 400 Bad Request, 비밀번호가 틀릴 시 401 Uanuthorized, 데이터가 존재하지 않을 시 404 Not_found 를 주었다.
3) 수정, 삭제 API의 request를 어떤 방식으로 사용 하셨나요? (param, query, body)
- 수정할 경우에는 @PathVariable 와 @RequestBody 를 이용했다.
- 삭제 API 의 경우에는 @PathVariable 와 @RequestBody 를 이용했다. 삭제시에는 deleteDTO 를 이용했는데 pwd 만 받을 수 있게끔 했다.
'백엔드 부트캠프 > 문제풀이' 카테고리의 다른 글
Lv 5. 위 제시된 기능 이외 ‘내’가 정의한 문제와 해결 과정 (0) | 2025.04.18 |
---|---|
Lv 3. 도전 계산기 만들기 (1) | 2025.03.06 |
Lv 2. 클래스를 적용해 기본적인 연산을 수행할 수 있는 계산기 만들기 (0) | 2025.02.27 |
Lv 1. 클래스 없이 기본적인 연산을 수행할 수 있는 계산기 만들기 (0) | 2025.02.26 |
[Chapter02-1] 클래스와 객체 실습과제 (0) | 2025.02.25 |