트러블슈팅 |
|
1. Patch로 유저가 넘겨주는 Json에 따라 가변적으로 업데이트?
|
Git
https://github.com/kimuky/schedule_management
1. Patch로 유저가 넘겨주는 Json에 따라 가변적으로 업데이트?
1) 개요
- 유저는 [제목], [제목,색깔], [제목, 내용,색깔] 이런식으로 원하는 속성만을 바꾸고 싶을 때 어떻게 레포지토리 단에서 쿼리를 던져줘야할지에 대한 고민
2) 문제 상황
- 유저는 쿼리는 한번 밖에 던질 수 없는 상황 물론 여러개의 쿼리를 구현해주면 되나 -> 좋지 않은 구조라고 판단
- 유저가 무엇을 바꿀지 모르는데 거기에 대해 쿼리를 어떻게 가변적으로 바꿀 것이냐..
@Override
public int updateSchedule(int id, String title, String content, String color) {
return jdbcTemplate.update
("UPDATE schedule SET title = ?, content = ?, color = ?, update_date = CURRENT_DATE()
WHERE id = ?", title, content, color, id);
}
3) 해결
- JdbcTemplate.update 메소드는 인자로 "String 쿼리, 배열" 을 받음
- 그럼 쿼리랑 파라미터를 따로 넣어주면 Json에 따라 구분이 가능
// JdbcTemplate.update 메소드에 대한 설명
public int update(String sql, @Nullable Object... args) throws DataAccessException {
return this.update(sql, this.newArgPreparedStatementSetter(args));
}
2차문제 쿼리는 가변적으로 어떻게 짜줄 것이냐
- 조건문을 걸어 쿼리에 하나씩 추가해주면 가능
@Override
public int updateScheduleTitle(int id, ScheduleRequestDto dto) {
// 사용자가 제목, 내용 바꿀 수도 있고, 색깔만 바꿀 수도 있을 것이라 생각해 이렇게 구현했습니다.
StringBuilder query = new StringBuilder("UPDATE schedule SET update_date = CURRENT_DATE()");
List<Object> params = new ArrayList<>();
if (dto.getTitle() != null) {
query.append(", title = ?");
params.add(dto.getTitle());
}
if (dto.getContent() != null) {
query.append(", content = ?");
params.add(dto.getContent());
}
if (dto.getColor() != null) {
query.append(", color = ?");
params.add(dto.getColor());
}
query.append(" WHERE id = ?");
params.add(id);
return jdbcTemplate.update(query.toString(), params.toArray());
}
4) 결론
- 쿼리를 가변적으로 바꿀 수 있음에도 여러 개의 쿼리를 구현할려고 했음...
- 쿼리는 String이기 때문에 수정이 비교적 자유로운 특정을 망각했던 것 같다.
- 또한, JdbcTemplate.update는 오버로딩되어 있어 여러 인자를 넣을 수 있다는 것을 망각..
-> 메소드 활용 시, 컨트롤 누르고 메소드 눌러서 어떻게 구현되어있는지 확인하기!
2. 페이지네이션?
1) 개요
- 컨트롤러 - 서비스 - 레포지토리 단에서 유연하게 페이지네이션을 구현을 어떻게 할지 감이 오지 않았다. 아무래도 프런트 단이 없어서 생각이 잘 나지 않았다.
2) 문제 상황
- 페이지 번호, 페이지 사이즈를 받는다고 생각했을 때,
- 데이터가 몇개 있는지 모른다. -> 몇 개 있는지 알아도 mapping 시키는 문제
- 스케쥴 id가 auto_increment라고 하나 중간에 삭제될 수 있기에 id 기준으로 자를 수 도 없다.
- 처음과 끝은 SQL의 lmit로 받아 올 수 있겠으나 중간에 있는 것들은 무엇을 기준으로 인덱스를 짜야할지 감이 오지 않는다.
3) 해결
- SQL limit은 한개의 인자만 받을 수 있는 것이 아니라 두 개의 인자를 넘겨 줄 수 있다.
- 식을 세워보면 pageSize가 10이면 페이지당 10개의 배열이 있는 것이며
(0번째 배열부터 10개, 10번 배열부터 10개, 20번 배열부터 10개...) 이다.
그렇기에 pageNum을 -1 해주고 pageSize를 곱해주면 모든 조건에 만족한다.
pageNum | pageNum -1 | pageSize | result |
1 | 0 | 10 | 0 * 10 = 0 |
2 | 1 | 10 | 1 * 10 = 10 |
.... | |||
1 | 0 | 15 | 0 * 15 = 0 |
2 | 1 | 15 | 1 * 15 = 15 |
@Override
public List<ScheduleResponseDto> findPageSchedules(int pageNum, int pageSize) {
return jdbcTemplate
.query("SELECT * FROM schedule ORDER BY update_date DESC, id LIMIT ?,?",
scheduleRowMapper(), (pageNum - 1) * pageSize, pageSize);
}
// SELECT * FROM schedule ORDER BY update_date DESC, id LIMIT 0,10 (0번째부터 10개) 0~9
// SELECT * FROM schedule ORDER BY update_date DESC, id LIMIT 10,10 (10번째부터 10개) 10 ~19
4) 결론
- SQL limit를 통해 쉽게 페이지네이션을 구현가능하다. 실제로는 paging 객체를 활용하는것이 쉽다고는 하는데 시간이 부족해서 활용을 못해보았다.
3. 예외처리?
1) 개요
- @ExceptionHandler 를 도저히 어떻게 활용하는지 감이 안왔다. 컨트롤러단에서 예외처리하는 것이라곤 하는데 실제로 예외처리할 곳은 서비스단에 있기 때문이다. 또한, 구글링해도 이해가 가지 않는 예제와 코드들 뿐이였다.
2) 문제 상황
- 그래도 일단은 서비스 단에서 예외처리를 해볼려고 throw할 예외를 찾는데 BadRequest, NotFound, Forbidden은 던져줄 수 가 없었다. 던져 줄 수 있는 것들은 밑에 예제와 같은게 있었으나 저런 예외로 처리하기에는 착오를 일으킬 수 있다.
throw new IllegalArgumentException
throw new ArithmeticException
실제 예외처리 해야 할 부분
- 일정 삭제/수정 시, 일정의 주인인 지 판별 (예외처리: Forbidden)
- 필수값 미전달/입력 (예외처리: BadRequest)
- 데이터가 잘 들어가지 않거나 없음 (예외처리: NotFound)
3) 해결
- 실제 예외처리 해야 할 부분은 사용자 정의 클래스로 정의해 예외처리를 해주어야한다. 그러면 여기서
@ControllerAdvice
@ExceptionHandler
2가지 방법으로 처리 가능할 것이다. @ExceptionHandler를 컨트롤러에 놓고 해당 예외처리를 처리하느냐 @ControllerAdvice로 여러 컨트롤러에서 발생하는 에러를 공통적으로 처리 할 것 이냐를 결정할 수 있다. 물론 @ExceptionHandler를 컨트롤러에도 넣고 @ControllerAdvice 안에도 넣는 다면 비교적 안전한 처리를 할 수 있을 것이다. 물론 때에 따라 글로벌하게 처리해야하고, 컨트롤에서만 예외처리를 따로 처리할 필요도 있을 것이다.
일단 컨트롤러가 두개라서
글로벌하게 처리하는 방식으로 진행했다.
각 사용자 정의 클래스를 정의하고
package com.example.schedule.exception;
//FobiddenException.java
public class ForbiddenException extends RuntimeException {
public ForbiddenException() {
super("해당 유저가 아님");
}
}
//BadRequestException.java
public class BadRequestException extends RuntimeException {
public BadRequestException() {
super("빈값이 있음");
}
}
//NotFoundException.java
public class NotFoundException extends RuntimeException {
public NotFoundException() {
super("없는 정보");
}
}
글로벌 예외처리 핸들러 구현
package com.example.schedule.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.List;
@ControllerAdvice
public class GlobalExceptionHandler extends RuntimeException {
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<String> handleCustomException(BadRequestException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<String> handleCustomException(ForbiddenException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getMessage());
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<String> NotFoundException(ForbiddenException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
// @Valid 에 따른 예외 처리 출력
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationExceptions(MethodArgumentNotValidException ex) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
StringBuilder errorString = new StringBuilder();
for (FieldError fe : errors) {
errorString.append(fe.getField()).append(": ")
.append(fe.getDefaultMessage())
.append("\n");
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorString.toString());
}
}
handleValidationExceptions 은 @Valid 어노테이션을 적용한 dto에 대한 오류들을 전부 보여주기 위해 저렇게 구현했다.
4) 결론
- 예외처리는 정말 방식이 다양하다. 완벽하게 적용을 시키는 것이 정말 헷갈린다. 물론 현 프로젝트에서는 컨트롤러가 2개 밖에 없기 때문에 컨트롤러에서 예외처리를 해주어도 무방할 것이다. 컨트롤러가 많아질수록 글로벌하게 처리하는것이 중요할 것이다. 글로벌하게 예외처리를 한뒤, 예외처리가 날 수 있는 컨트롤러에 구체적으로 다시 @ExceptionHandler를 구현하여 자세하게 예외를 처리할 수도 있을 것이다.
4. 레포지토리 예외처리?
1) 개요
- 예외처리를 글로벌하게 처리할 때, 서비스 단에서 모든 예외처리를 하였다. 그렇기에 레포지토리 단에서 예외를 처리하는게 착오를 일으킬 수도 있는 부분이고 모든 예외처리를 서비스 단에서 처리하고 싶었다.
2) 문제 상황
@Override
public Scheudle findMemoByIdOrElseThrow(int id) {
List<Scheudle> result = jdbcTemplate
.query("select * from schedule where id = ?", memoRowMapperV2(), id);
return result.stream().findAny()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "아이디가 없음"));
}
- 레포지토리 단에서 지금 예외처리가 되어 있다. 이것을 어떻게 옮겨야할지 고민 중에 있었다. 만약 예외처리를 하지 않는다면 저렇게 Stream 처리를 할때 null 값이 들어와 오류가 난다. Optional 하게 처리해야하는 것이다.
- Optional 로 처리하면 isPresent(), isEmpty()를 통해 null 값 검증을 해줘야 해 서비스 단에서 가독성이 매우 떨어진다.
@Transactional
@Override
public ScheduleResponseDto updateSchedulePart(int id, ScheduleRequestDto dto) {
Optional<Schedule> beforeSchedule = scheduleRepository.findScheduleById(id);
if( beforeSchedule.isEmpty()) {
throw new NotFoundException();
}
// 패스워드 대신 uid 로 구분
if (!beforeSchedule.getUser_uid().equals(dto.getUser_uid())) {
throw new ForbiddenException();
}
int updatedRow = scheduleRepository.updateScheduleTitle(id, dto);
if (updatedRow == 0) {
throw new NotFoundException();
}
Schedule afterSchedule = scheduleRepository.findScheduleById(id);
if( afterSchedule.isEmpty()) {
throw new NotFoundException();
}
return new ScheduleResponseDto(afterSchedule);
}
- 문제 정리
- 레포지토리 예외처리를 서비스단으로 옮기니 Optional 처리 해주어야함
- Optional 처리에 따른 null 값 검증이 필요
- null 값 검증에 따른 가독성 저하
3) 해결
- 정답은 가까이 있었다. 그냥.. 받아와서 orElseThrow()해서 예외처리를 날려주면 되는것이다.. 그럼 Optional도 필요가 없다.
@Transactional
@Override
public ScheduleResponseDto updateSchedulePart(int id, ScheduleRequestDto dto) {
Schedule beforeSchedule = scheduleRepository.findScheduleById(id)
.orElseThrow(NotFoundException::new);
// 패스워드 대신 uid 로 구분
if (!beforeSchedule.getUser_uid().equals(dto.getUser_uid())) {
throw new ForbiddenException();
}
int updatedRow = scheduleRepository.updateScheduleTitle(id, dto);
if (updatedRow == 0) {
throw new NotFoundException();
}
Schedule afterSchedule = scheduleRepository.findScheduleById(id).orElseThrow(NotFoundException::new);
return new ScheduleResponseDto(afterSchedule);
}
4) 결론
- 물론 람다와 스트림에 대해서 완벽히 이해하지 못하고 있지만 람다와 스트림을 활용하면 콜렉션 처리에 매우 편리하고 가독성도 좋게 구현할 수 있다. 이에 대해서 공부를 추가로 해보아야 할 것 이다.
'스파르타(부트캠프) > Troubleshooting' 카테고리의 다른 글
[내일배움캠프] 스프링심화주차 트러블슈팅 (TBU) (1) | 2024.12.26 |
---|---|
[내일배움캠프] 플러스주차(스프링 심화)에 대한 트러블슈팅 (0) | 2024.12.19 |
[내일배움캠프] 뉴스피드 프로젝트에 대한 트러블슈팅 (0) | 2024.11.22 |
[내일배움캠프] 일정관리 앱(develop)에 대한 트러블슈팅 (4) | 2024.11.15 |