주특기 숙련 주간 개인 과제
개인 과제가 막바지에 이르렀다.
CRUD API + JWT 로그인/회원가입 (Lv1) + 댓글 CRUD API, 회원 권한 부여, 예외처리 (Lv2)
어제 댓글 CRUD API까지 마무리했고,
오늘 회원 권한 부여와 예외처리를 작업하였다.
특히 예외처리 같은 부분은 강의에서도 따로 다루지 않아 굉장히 고생했는데
오늘의 TIL은 그 고생길을 담아보았다🙃
오늘의 궁금증
댓글을 작성 순으로 정렬하려면?
어제 댓글 CRUD를 작업하면서 빼먹은 요구사항이 있었다.
바로 댓글을 작성 순으로 정렬하는 것!
게시글 같은 경우는 Repository에서 Spring Data JPA 쿼리 메소드를 이용하여 정렬을 하였다.
이번엔 같은 조의 소영님이 알려주신 어노테이션을 이용해볼 것이다.
@OrderBy
@Getter
@Entity
@NoArgsConstructor
public class Board extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "board")
@OrderBy("createdAt DESC") // 정렬할 컬럼과 오름차순, 내림차순을 적어준다
private List<Comment> comments = new ArrayList<>();
}
- OrderBy 어노테이션은 데이터베이스의 ORDER BY 절을 사용해서 컬렉션을 정렬한다.
아주 간단하게 적용 끝!
댓글이 달린 게시글이 삭제가 안되잖아?!
전체적으로 테스트를 하던 중 게시글이 달린 댓글은 삭제가 안 되는 현상을 발견했다.
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException:
Referential integrity constraint violation:
"FKLIJ9OOR1NAV89JEAT35S6KBP1:
PUBLIC.COMMENT FOREIGN KEY(BOARD_ID) REFERENCES PUBLIC.BOARD(ID) (CAST(2 AS BIGINT))"; SQL statement:
참조 무결성 제약 조건을 위반했다는 것을 알 수 있다.
현재 댓글과 게시글은 1:N 양방향 연관관계로 연결되어 있다.
게시글이 삭제되면서 댓글이 참조할 수 없는 외래 키 값을 가지게 된 것이다.
영속성 전이를 통해 해결해보자!
영속성 전이란?
- 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용한다. JPA는 cacade 옵션으로 영속성 전이를 제공한다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
- 부모를 영속화할 때 연관된 자식들도 함께 영속화
- 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장
, cascade = CascadeType.REMOVE
- 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제
- CasecadeType의 다양한 옵션
- ALL, // 모두 적용
- PERSIST, // 영속
- MERGE, // 병합
- REMOVE, // 삭제 → 이거 적용해줌
- REFRESH,
- DETACH
cascade = CascadeType.REMOVE
@Getter
@Entity
@NoArgsConstructor
public class Board extends Timestamped{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE) // 영속성 전이
@OrderBy("createdAt DESC")
private List<Comment> comments = new ArrayList<>();
}
- 영속성 전이를 REMOVE로 설정하여 부모인 BOARD가 제거될 때 연관된 자식인 COMMENT도 함께 제거될 수 있도록 해주었다.
📚 참고자료
회원에게 권한을 부여해보자
다음은 Lv2에 추가된 요구사항이다.
회원 권한 부여하기 (ADMIN, USER) - ADMIN 회원은 모든 게시글, 댓글 수정 / 삭제 가능
먼저 UserRoleEnum 클래스를 만들어 권한을 정의한다.
public enum UserRoleEnum {
USER, // 사용자 권한
ADMIN // 관리자 권한
}
User Entity에 위의 UserRoleEnum클래스를 필드에 추가하고,
회원 가입 시 ADMIN 여부를 알 수 있도록 검증하는 로직을 Service에서 구현한다.
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";
@Transactional
public BaseResponse signup(SignupRequestDto signupRequestDto) {
String username = signupRequestDto.getUsername();
String password = signupRequestDto.getPassword();
Optional<User> found = userRepository.findByUsername(username);
if (found.isPresent()) {
throw new CutomException(DUPLICATED_USERNAME);
}
// 사용자 ROLE 확인
UserRoleEnum role = UserRoleEnum.USER;
if (signupRequestDto.isAdmin()) {
if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
throw new CutomException(INVALID_AUTH_TOKEN);
}
role = UserRoleEnum.ADMIN;
}
User user = new User(username, password, role);
userRepository.save(user);
return new BaseResponse(StatusEnum.OK);
}
}
- 7번 라인에 정의된 ADMIN_TOKEN과 회원 가입 시 가져온 TOKEN 값이 일치하면 ADMIN 권한을 부여받는다.
- 실제로 이렇게 허술하게 ADMIN 권한을 부여하지는 않는다.
이후 게시글과 댓글의 Service단에서 토큰에 포함된 회원의 Role을 확인하고,
Role에 따라 게시글, 댓글 수정/삭제 권한을 부여한다.
BoardService
// 사용자 권한 가져와서 ADMIN 이면 무조건 삭제 가능, USER 면 본인이 작성한 글일 때만 삭제 가능
UserRoleEnum userRoleEnum = user.getRole();
Board board;
if (userRoleEnum == UserRoleEnum.ADMIN) {
// 입력 받은 게시글 id와 일치하는 DB 조회
board = boardRepository.findById(id).orElseThrow(
() -> new CutomException(NOT_FOUND_BOARD)
);
} else {
// 입력 받은 게시글 id, 토큰에서 가져온 username과 일치하는 DB 조회
board = boardRepository.findByIdAndUsername(id, user.getUsername()).orElseThrow(
() -> new CutomException(AUTHORIZATION)
);
}
boardRepository.deleteById(id);
- 토큰에서 가져온 사용자 권한에 따라 다른 로직을 구현한다.
- ADMIN이면 게시글이 존재하는지만 검증하고, USER면 해당 게시글을 해당 사용자가 작성했는지까지 검증 후 게시글을 삭제한다.
예외처리를 기깔나게 할 수 없을까?
이제 요구사항 중 예외처리 하나만을 남기고 멘붕에 빠졌다.
예외 처리와 함께 ResponseEntity를 적용해보려 했는데
아무리 찾아봐도 당최 무슨 말인지 이해할 수가 없었다.
괜히 코드만 뒤적이던 중, 인광님이 좋은 예제를 던져 주셨다!
구글링으로 찾아낸 예제와 비슷한 부분이 많아 번갈아 참고하며
전역에서 발생하는 Exception 처리를 해주었다.
util 패키지에 exception 패키지를 만들고,
다음의 클래스들을 사용한다.
- ErrorCode : 핵심. 모든 예외 케이스를 이곳에서 관리함
- CustomException : 기본적으로 제공되는 Exception 외에 사용
- ErrorResponse : 사용자에게 JSON 형식으로 보여주기 위해 에러 응답 형식 지정
- GlobalExceptionHandler : Custom Exception Handler
- @ControllerAdvice : 프로젝트 전역에서 발생하는 Exception을 잡기 위한 클래스
- @ExceptionHandler : 특정 Exception을 지정해서 별도로 처리해줌
ErrorCode
package com.sparta.crud.util.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorCode {
/* Cutom Error Code */
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 유효하지 않습니다."),
AUTHORIZATION(HttpStatus.BAD_REQUEST, "작성자만 수정/삭제할 수 있습니다."),
DUPLICATED_USERNAME(HttpStatus.BAD_REQUEST, "중복된 username 입니다"),
NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "회원을 찾을 수 없습니다."),
NOT_FOUND_BOARD(HttpStatus.BAD_REQUEST, "게시글을 찾을 수 없습니다."),
NOT_FOUND_COMMENT(HttpStatus.BAD_REQUEST, "게시글을 찾을 수 없습니다.");
private final HttpStatus httpStatus;
private final String message;
}
- 에러 형식을 Enum클래스로 정의하였다.
- 응답으로 내보낼 HttpStatus와 에러 메시지로 사용한 String을 갖고 있다.
- 이렇게 하면 개발자가 정의한 새로운 Exception을 모두 한 곳에서 관리하고 재사용할 수 있다.
@ControllerAdvice and @ExceptionHandler
@ControllerAdvice는 프로젝트 전역에서 발생하는 모든 예외를 잡아준다.
@ExceptionHandler는 발생한 특정 예외를 잡아서 하나의 메소드에서 공통 처리해줄 수 있게 해 준다.
따라서 둘을 같이 사용하면 모든 예외를 잡은 후에 Exception 종류별로 메소드를 공통 처리할 수 있다.
package com.sparta.crud.util.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = {CutomException.class})
protected ResponseEntity<ErrorResponse> handleCustomException(CutomException e) {
log.error("handleDataException throw Exception : {}", e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
}
- View를 사용하지 않고 Rest API 로만 사용할 때 쓸 수 있는 @RestControllerAdvice를 사용하였다.
- handleCustomException 메소드는 직접 정의한 CustomException을 사용한다.
- Exception 발생 시 넘겨받은 ErrorCode 를 사용해서 사용자에게 보여주는 에러 메시지를 정의한다.
Postman으로 테스트
- 위와 같이 에러 메시지와 StatusCode를 잘 반환하는 걸 확인할 수 있다.
이 외의 클래스들은 참고자료와 같게 작성하여 따로 언급하지 않았다.
자료만 봤을 땐 너무 막막했는데 막상 한 줄씩 따라치며 구현해보니 할 만했다..!
항상 예외처리에 대한 부채가 있었는데 속이 다 시원하다.
앞으로는 더 꼼꼼하게 예외 처리해야지~
📚 대박 참고자료
ResponseEntity도 적용해봐야지?
ResponseEntity란?
- Spring Framework에서 제공하는 클래스 중 HttpEntity라는 클래스를 상속받아 구현한 클래스이다. 사용자의 HttpRequest에 대한 응답 데이터를 포함한다.
그럼 이걸 왜 사용할까?
- HTTP 아키텍처 형태에 맞게 Response를 보내줄 수 있다.
- 에러 코드와 같은 HTTP 상태 코드를 전송하고 싶은 데이터와 함께 전송할 수 있기 때문에 세밀한 제어가 가능하다.
예외처리 성공에 힘입어 제쳐놨던 ResponseEntity도 도전해보기로!
나는 이미 성공 여부를 반환하는 BaseResponse를 구현해두었기 때문에
Controller단에서 리턴 타입으로 ResponseEntity<BaseResponse>를 설정하고,
ResponseEntity로 내가 만들어둔 클래스를 감싸기만 하면 적용이 되었다!
(적용은 간단한데 이걸 깨닫기까지 반나절이 걸림..)
BoardController
// 게시글 작성
@PostMapping
public ResponseEntity<BaseResponse> createBorad(
@RequestBody BoardRequestDto requestDto,
HttpServletRequest request) {
return ResponseEntity.ok().body(boardService.createBoard(requestDto, request));
}
- BoardController의 게시글 작성 메소드 createBoard에 적용된 일부 모습이다.
- 나름 빌더 패턴까지 적용하여 구현해보았다!
- 모든 Controller의 리턴 타입을 하나로 통일해줄 수 있어 코드가 무척이나 깔끔해졌다.
Postman으로 테스트
- 댓글까지 추가된 응답 결과이다. 성공 여부와 게시글, 댓글 내용이 모두 잘 출력되는 걸 확인할 수 있다.
📚 참고자료
오늘의 나는
권한 부여와 예외 처리까지 Lv2의 기능을 모두 구현하였다.
제출을 하루 남기고 계획했던 목표를 이뤄서 기부니가 좋다!
물론 아직 도전해보지 못한 부분도 있지만,
일단 여기까지 해낸 나 자신 칭찬해😎
사실 혼자 했으면 이만큼 못 했을 것 같다.
B반 동료들끼리 서로 알게 된 지식을 공유하고,
선생님이 되기도, 학생이 되기도 하면서 과제를 해결해나갔다.
아쉬운 부분은 개인적인 목표였던 '조원들과 코드 리뷰'를 못했다는 점..!
조원들 모두 Lv2까지 구현하다 보니 각 잡고 리뷰를 할 시간이 마땅치 않았다.
다음 발제가 있기 전까지 시간을 내서라도 꼭 리뷰를 해볼 것이다!
내일은 주특기 숙련 주차 시험이 있는 날이다.
시험날은 괜히 뭔가 싱숭생숭한 분위기인데,
아직 개인 과제 문서화와 팀 과제가 남았으니
시간을 알차게 활용해보도록 하자!
'📝 TIL' 카테고리의 다른 글
[TIL] 4주차 주특기 심화ㅣ주간 시작 (0) | 2022.12.09 |
---|---|
[TIL] 4주차 주특기 숙련ㅣSpring 시험 (0) | 2022.12.08 |
[TIL] 4주차 주특기 숙련ㅣ댓글 CRUD API (2) | 2022.12.06 |
[TIL] 4주차 주특기 숙련ㅣ조각모음 (3) | 2022.12.05 |
[TIL] 3주차 주특기 숙련ㅣ컨디션 난조 (2) | 2022.12.03 |