이번 글에서는 잠시 쉬어가는 시간으로 Exception처리를 해보려고 합니다. 기존에 예외처리는 모두 RuntimeException으로 처리를 해서 오류가 났을때 어디서 났는지 확인하기가 어려웠는데요, 이번에 각 기능별로 Exception처리를 달리하여 오류가 발생했을 때 어디서 발생했는지 확인하기 쉽게 만들어보겠습니다.
Exception 클래스 생성
각 기능에 대해서 Exceptino 클래스를 생성하겠습니다. 현재까지 구현된 기능중에서 발생할 만한 Exception들은 다음과 같습니다.
LoginFailureException.java
public class LoginFailureException extends RuntimeException {
}
MemberEmailAlreadyExistsException
public class MemberEmailAlreadyExistsException extends RuntimeException {
public MemberEmailAlreadyExistsException(String message) {
super(message);
}
}
MemberNicknameAlreadyExistsException
public class MemberNicknameAlreadyExistsException extends RuntimeException{
public MemberNicknameAlreadyExistsException(String message) {
super(message);
}
}
MemberNotFoundException
public class MemberNotFoundException extends RuntimeException {
}
RoleNotFoundException
public class RoleNotFoundException extends RuntimeException {
}
Exception 교체
다음으로 기존의 Exception 처리를 이번에 만든 Exception들로 교체하겠습니다.
SignService.java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SignService {
private final MemberRepository memberRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final TokenService tokenService;
@Transactional
public void signUp(SignUpRequest req){
validateSignUpInfo(req);
memberRepository.save(SignUpRequest.toEntity(
req,
//roleRepository.findByRoleType(RoleType.ROLE_NORMAL).orElseThrow(RuntimeException::new),
roleRepository.findByRoleType(RoleType.ROLE_NORMAL).orElseThrow(RoleNotFoundException::new),
passwordEncoder
));
}
private void validateSignUpInfo(SignUpRequest req) {
if(memberRepository.existsByEmail(req.getEmail()))
//throw new RuntimeException();
throw new MemberEmailAlreadyExistsException(req.getEmail());
if(memberRepository.existsByNickname(req.getNickname()))
//throw new RuntimeException();
throw new MemberNicknameAlreadyExistsException(req.getNickname());
}
public SignInResponse signIn(SignInRequest req){
//Member member = memberRepository.findByEmail(req.getEmail()).orElseThrow(RuntimeException::new);
Member member = memberRepository.findByEmail(req.getEmail()).orElseThrow(LoginFailureException::new);
validatePassword(req, member);
String subject = createSubject(member);
String accessToken = tokenService.createAccessToken(subject);
String refreshToken = tokenService.createRefreshToken(subject);
return new SignInResponse(accessToken, refreshToken);
}
private void validatePassword(SignInRequest req, Member member) {
if(!passwordEncoder.matches(req.getPassword(), member.getPassword())) {
//throw new RuntimeException();
throw new LoginFailureException();
}
}
private String createSubject(Member member) {
return String.valueOf(member.getId());
}
}
ExceptionAdvice 생성
이제 Exception이 발생했을 때 처리하기 쉽도록 ExceptionAdvice를 만들어보겠습니다.
ExceptionAdvice는 특정 예외가 발생했을 때 미리 지정해둔 메서드를 실행시켜줍니다.
ExceptionAdvice.java
@RestControllerAdvice //@ControllerAdvice + @ResponseBody -> 예외 발생할 경우 자동으로 JSON 형식의 응답을 반환
@Slf4j // Lombok 라이브러리에서 제공하는 로깅 어노테이션
public class ExceptionAdvice {
@ExceptionHandler(Exception.class) //예외 처리 메서드 : 특정 예외가 발생하면 함수 실행
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Response exception(Exception e) {
log.info("e = {}", e.getMessage());
return Response.failure(-1000, "오류가 발생하였습니다.");
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Response methodArgumentNotValidException(MethodArgumentNotValidException e) {
return Response.failure(-1003, e.getBindingResult().getFieldError().getDefaultMessage());
}
@ExceptionHandler(LoginFailureException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Response loginFailureException() {
return Response.failure(-1004, "로그인에 실패하였습니다.");
}
@ExceptionHandler(MemberEmailAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public Response memberEmailAlreadyExistsException(MemberEmailAlreadyExistsException e) {
return Response.failure(-1005, e.getMessage() + "은 중복된 이메일 입니다.");
}
@ExceptionHandler(MemberNicknameAlreadyExistsException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public Response memberNicknameAlreadyExistsException(MemberNicknameAlreadyExistsException e) {
return Response.failure(-1006, e.getMessage() + "은 중복된 닉네임 입니다.");
}
@ExceptionHandler(MemberNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response memberNotFoundException() {
return Response.failure(-1007, "요청한 회원을 찾을 수 없습니다.");
}
@ExceptionHandler(RoleNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Response roleNotFoundException() {
return Response.failure(-1008, "요청한 권한 등급을 찾을 수 없습니다.");
}
}
- @RestControllAdvice : @ControllerAdvice 어노테이션과 @ResponseBody 어노테이션이 합쳐진 것으로, 예외가 발생할 경우 자동으로 JSON 형식의 응답을 반환합니다.
- @ExcentionHandler : 특정 예외 타입을 지정하여 해당 예외가 발생했을 때 실행될 메서드를 설정할 수 있습니다. 예외 타입을 지정하지 않으면 모든 예외에 대해 처리됩니다.
- @ResponseStatus : 해당 예외가 발생했을 때 반환되는 HTTP 응답 코드를 설정하고, Response 객체를 반환합니다.
ExceptionAdvice 테스트
ExceptionAdvice를 만들었으니 이번에는 테스트를 진행해 보겠습니다. ExceptionAdvice테스트는 이전 테스트들과 동일하게 진행되니 설명은 생략하겠습니다.
@ExtendWith(MockitoExtension.class)
class ExceptionAdviceTest {
@InjectMocks SignController signController;
@Mock SignService signService;
MockMvc mockMvc;
ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void beforeEach(){
mockMvc = MockMvcBuilders.standaloneSetup(signController).setControllerAdvice(new ExceptionAdvice()).build();
}
@Test
public void signInLoginFailureExceptionTest() throws Exception{
//given
SignInRequest req = new SignInRequest("email@email.com","123456a!");
given(signService.signIn(any())).willThrow(LoginFailureException.class);
//when //then
mockMvc.perform(
post("/api/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isUnauthorized());
}
@Test
void signInMethodArgumentNotValidExceptionTest() throws Exception {
// given
SignInRequest req = new SignInRequest("email", "1234567");
// when, then
mockMvc.perform(
post("/api/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isBadRequest());
}
@Test
void signUpMemberEmailAlreadyExistsExceptionTest() throws Exception {
// given
SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
doThrow(MemberEmailAlreadyExistsException.class).when(signService).signUp(any());
// when, then
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isConflict());
}
@Test
void signUpMemberNicknameAlreadyExistsExceptionTest() throws Exception {
// given
SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
doThrow(MemberNicknameAlreadyExistsException.class).when(signService).signUp(any());
// when, then
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isConflict());
}
@Test
void signUpRoleNotFoundExceptionTest() throws Exception {
// given
SignUpRequest req = new SignUpRequest("email@email.com", "123456a!", "username", "nickname");
doThrow(RoleNotFoundException.class).when(signService).signUp(any());
// when, then
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isNotFound());
}
@Test
void signUpMethodArgumentNotValidExceptionTest() throws Exception {
// given
SignUpRequest req = new SignUpRequest("", "", "", "");
// when, then
mockMvc.perform(
post("/api/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)))
.andExpect(status().isBadRequest());
}
}
API 로 확인
테스트까지 완료하였으니 이번에는 Postman를 활용하여 API를 테스트를 진행해보겠습니다. 현재상태로 어플리케이션을 실행시키면 role에 등급이 저장되어 있지 않기때문에 정상적으로 api를 호출할 수 없습니다. 그래서 DB에 Role 관련 데이터들을 저장해주고, ROLE_NORMAL 권한 등급을 조회해서 회원가입시에 사용자에게 부여해주어야 합니다. 일단은 클래스를 하나 생성하여 실행과 동시에 데이터베이스에 값을 넣어주도록 하겠습니다.
InitDB.java
@Component // 해당 객체를 어플리케이션 내에서 사용할 수 있도록 Bean으로 관리하는 역할
@RequiredArgsConstructor
@Slf4j
@Profile("local") // InitDB 클래스가 local 프로파일에서만 실행되도록 설정하는 역할
public class InitDB {
private final RoleRepository roleRepository;
@PostConstruct //객체 생성 후 초기화를 수행하는 메소드를 정의
@Transactional
public void initDB(){
log.info("initialize database");
initRole();
}
private void initRole(){
roleRepository.saveAll( // 모든 Role_Type role에 저장
List.of(RoleType.values()).stream().map(roleType -> new Role(roleType)).collect(Collectors.toList())
);
}
}
/api/sign-up api 테스트
/api/sign-in api 테스트
postman으로 sign-up sign-in 모두 확인을 해보았습니다.
Data base에서도 확인해보니 정상적으로 데이터가 들어와 있습니다.
궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.
'spring > 게시판 api' 카테고리의 다른 글
Spring boot 게시판 API 서버 제작 (9) - 로그인 - 인증 로직 (0) | 2023.02.20 |
---|---|
Spring boot 게시판 API 서버 제작 (8) - 로그인 - 조회, 삭제 로직 (0) | 2023.02.19 |
Spring boot 게시판 API 서버 제작 (6) - 로그인 - 웹 계층 구현 (0) | 2023.02.19 |
Spring boot 게시판 API 서버 제작 (5) - 로그인 - 서비스 로직 구현 (0) | 2023.02.18 |
Spring boot 게시판 API 서버 제작 (4) - 로그인 - 비밀번호 암호화 및 토큰 발급과 검증 (0) | 2023.02.18 |