@Builder
는 빌더 패턴을 적용하는 어노테이션으로 클래스의 필드를 설정할 수 있는 빌더 클래스를 자동으로 생성해줌
이를 통해 객체를 생성할 때 생성자 대신 체이닝 방식으로 필드를 설정할 수 있음
- 빌더 패턴을 사용하는 경우
UserResponse user = UserResponse.builder() // 빌더 객체를 생성 .id("1") .name("kim") .age(20) // 빌더의 메소드를 통해 필드를 설정 .build(); // 최종적으로 객체를 생성하고 반환
- 빌더 패턴을 사용하지 않는 경우
// 매개변수 순서가 중요하여 생성자가 많은 인수를 받는 경우에 어려움이 있음 UserResponse user = new UserResponse("1", "kim", 20);
/**
* API 응답을 위한 제네릭 클래스
* API 응답에서 일관된 구조를 제공함
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {
private String resultCode; // 응답 코드 (200, 404, 500 등)
private String resultMessage; // 응답 메시지 ("OK", "Not Found" 등)
private T data; // 응답 데이터
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserResponse { // 응답 객체
private String id;
private String name;
private Integer age;
}
@RestControllerAdvice
: REST API에서 발생하는 예외를 전역적으로 처리하는 클래스에 사용하며 예외를 처리하는 메소드가@ExceptionHandler
로 정의되어 클라이언트에게 JSON 응답을 반환@Order
: 여러 핸들러나 예외를 처리하는 메소드가 있을 때 실행 순서를 지정할 수 있게 해주는 어노테이션으로 작은 값일수록 우선순위가 높아져 먼저 실행됨
@Order
를 선언하지 않으면 기본값은Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE)
로 설정되어 가장 낮은 우선순위가 적용되어 나중에 실행됨ResponseEntity
: HTTP를 구성하고 반환하는데 사용되는 클래스로 응답의 상태 코드, 헤더, body 등을 자유롭게 설정할 수 있음
// 모든 예외를 처리하는 글로벌 예외 핸들러
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 예측하지 못한 모든 예외를 처리하는 핸들러
@ExceptionHandler(value = {Exception.class})
public ResponseEntity<Api> exception(Exception e) {
var response = Api.builder()
.resultCode(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()))
.resultMessage(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.build();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
}
// 특정 예외를 처리하는 예외 핸들러
@RestControllerAdvice
@Order(1) // GlobalExceptionHandler보다 먼저 실행됨
public class RestApiExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(RestApiExceptionHandler.class);
/**
* IndexOutOfBoundsException 예외를 처리하는 핸들러
* 클라이언트에게 200 상태 코드를 반환
*/
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public ResponseEntity<Void> outOfBound(Exception e) {
return ResponseEntity.status(200).build();
}
/**
* NoSuchElementException 예외를 처리하는 핸들러
* 클라이언트에게 404 Not Found 응답을 반환
*/
@ExceptionHandler(value = {NoSuchElementException.class})
public ResponseEntity<Api> noSuchElement(NoSuchElementException e) {
var response = Api.builder()
.resultCode(String.valueOf(HttpStatus.NOT_FOUND.value()))
.resultMessage(HttpStatus.NOT_FOUND.getReasonPhrase())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
}
}
Request
http://localhost:8080/api/user/id/1
Response
- 정상 (resultCode: 200)
{ "result_code": "200", "result_message": "OK", "data": { "id": "1", "name": "kim", "age": 20 } }
- 일치하는 데이터가 없을 때 (resultCode: 404)
{ "result_code": "404", "result_message": "Not Found", "data": null }
- 예측할 수 없는 예외 발생 (resultCode: 500)
{ "result_code": "500", "result_message": "Internal Server Error", "data": null }
/**
* 사용자 관련 API를 제공하는 컨트롤러
*/
@RestController
@RequestMapping("/api/user")
public class UserApiController {
private static final List<UserResponse> userList = List.of(
UserResponse.builder().id("1").name("kim").age(20).build(),
UserResponse.builder().id("2").name("park").age(30).build()
);
/**
* 사용자 id로 특정 사용자를 조회하는 API
*/
@GetMapping("/id/{userId}")
public Api<UserResponse> getUser(@PathVariable String userId) {
// 사용자 조회
// 해당 id의 사용자가 없으면 예외 발생
var user = userList.stream().filter(it -> it.getId().equals(userId))
.findFirst()
.get();
// 정상 응답 반환
return Api.<UserResponse>builder()
.resultCode(String.valueOf(HttpStatus.OK.value()))
.resultMessage(HttpStatus.OK.name())
.data(user)
.build();
}
}
안드로이드 앱에서 API 연동을 통해 서버와 데이터를 송수신할 때 주고 받는 데이터에만 중점을 두고 개발을 해서 백엔드에서는 요청에 대한 데이터나 오류를 어떤 방식으로 처리하여 클라이언트에게 응답을 전달하는지 알지 못했다..😅 그래도 Spring에서 예외처리하는 법을 정리하며 조금은 이해할 수 있게 된 것 같다 🐣