아래와 같은 서비스와 컨트롤러가 있다고 하자. 항상 적절한 값만 들어온다고 가정해서 만들어진 코드이다.
@Service
@RequiredArgsConstructor
@Slf4j
public class PatientService {
private final MemberDAO memberDAO;
private final CredentialsDAO credentialsDAO;
public void signup(SignupDTO signupDTO) {
String uuid = UUID.randomUUID().toString();
MemberCreateDTO memberCreateDTO = new MemberCreateDTO(
uuid,
signupDTO.getName(),
signupDTO.getRrn(),
signupDTO.getPhone()
);
memberDAO.createPatient(memberCreateDTO);
CredentialsCreateDTO credentialsCreateDTO = new CredentialsCreateDTO(
uuid,
signupDTO.getUserid(),
signupDTO.getPassword()
);
credentialsDAO.create(credentialsCreateDTO);
}
}
@RestController
@RequestMapping("/patient")
@RequiredArgsConstructor
@Slf4j
public class PatientController {
private final PatientService patientService;
@PostMapping("/signup")
public ResponseEntity<?> signup(@RequestBody SignupDTO signupDTO) {
patientService.signup(signupDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
이 경우에 예외에 따른 응답을 처리하려면 두 가지 방법이 있을 것이다.
하나는 컨트롤러에서 예외를 받아서 적절한 ResponseEntity
를 만드는 것이고,
// 컨트롤러 메소드
public ResponseEntity<?> signup(@RequestBody SignupDTO signupDTO) {
try {
patientService.signup(signupDTO);
return ResponseEntity.status(HttpStatus.CREATED).build();
} catch (DataAccessException dae) {
log.warn("회원 생성 중 중복된 아이디 발생: {}", signupDTO.getUserid(), dae);
Map<String, String> body = Collections.singletonMap(
"message", "이미 사용 중인 아이디입니다."
);
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(body);
} catch (Exception e) {
log.error("회원 생성 중 알 수 없는 오류 발생", e);
Map<String, String> body = Collections.singletonMap(
"message", "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(body);
}
}
하나는 서비스에서 ResponseEntity
를 만들어서 반환하는 것이다.
// 서비스 메소드
public ResponseEntity<?> signup(SignupDTO signupDTO) {
String uuid = UUID.randomUUID().toString();
MemberCreateDTO memberDto = new MemberCreateDTO(
uuid,
signupDTO.getName(),
signupDTO.getRrn(),
signupDTO.getPhone()
);
try {
memberDAO.createPatient(memberDto);
CredentialsCreateDTO credDto = new CredentialsCreateDTO(
uuid,
signupDTO.getUserid(),
signupDTO.getPassword()
);
credentialsDAO.create(credDto);
return ResponseEntity
.status(HttpStatus.CREATED)
.build();
} catch (DataAccessException dae) {
log.warn("회원 생성 실패 - 중복 아이디: {}", signupDTO.getUserid(), dae);
Map<String, String> body = Collections.singletonMap(
"message", "이미 사용 중인 아이디입니다."
);
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(body);
} catch (Exception e) {
log.error("회원 생성 중 알 수 없는 오류 발생", e);
Map<String, String> body = Collections.singletonMap(
"message", "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(body);
}
}
// 컨트롤러 메소드
public ResponseEntity<?> signup(@RequestBody SignupDTO signupDTO) {
// 서비스에서 바로 ResponseEntity를 만들어 반환
return patientService.signup(signupDTO);
}
하지만 전자의 경우에는 서비스의 비즈니스 로직의 수정에 따라서 컨트롤러도 수정되어야 한다는 문제가 있고, 후자의 경우에는 컨트롤러의 역할인 HTTP 응답의 처리를 서비스에서 처리하게 된다는 문제가 있다.
어느 쪽이든 단일 책임의 원칙에 위배된다는 문제점이 있는 것이다.
위에서 말한 문제를 해결하기 위해서 전역 예외 처리기를 등록해 모든 컨트롤러에서 던져지는 예외를 처리하도록 할 수 있다.
전역 예외 처리기를 등록하기 위해서는 @ControllerAdvice
와 @ExceptionHandler
를 사용해서 만든다.
@RestControllerAdvice
는 @RestController
와 같이 @ControllerAdvice
와 @ResponseBody
가 합쳐진 것으로 RestController에 대해서 등록하기 위한 어노테이션이다.
아래의 코드는 ResponseStatusException
예외를 받아서 적절한 HTTP 응답으로 변환해주는 역할을 하는 예외 처리기이다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException exception) {
Map<String, Object> map = new HashMap<>();
map.put(
"message",
Optional.ofNullable(exception.getReason()).orElse(((HttpStatus)exception.getStatusCode()).getReasonPhrase())
);
return ResponseEntity.status(exception.getStatusCode()).body(map);
}
}
위의 예외 처리기를 등록하면 서비스에서 로직할 때 적절한 ResponseStatusException
을 던지도록 한다.
// 변경 전
MemberCreateDTO memberCreateDTO = new MemberCreateDTO(
uuid,
signupDTO.getName(),
signupDTO.getRrn(),
signupDTO.getPhone()
);
memberDAO.createPatient(memberCreateDTO);
// 변경 후
MemberCreateDTO memberCreateDTO = new MemberCreateDTO(
uuid,
signupDTO.getName(),
signupDTO.getRrn(),
signupDTO.getPhone()
);
try {
memberDAO.createPatient(memberCreateDTO);
} catch (Exception exception) {
throw new ResponseStatusException(HttpStatus.CONFLICT);
}
그에 대한 HTTP 응답은 전역 예외 처리기에서 처리를 하므로 비즈니스 로직의 변경에도 컨트롤러를 수정할 필요가 없어지게 된다.