공통 API 응답 처리와 ResponseEntity

KDG: First things first!·2024년 9월 18일
0

Spring

목록 보기
3/5



공통 응답 처리 의의

공통 응답 처리는 애플리케이션의 응답을 일관되게 관리하여 클라이언트와의 상호작용을 더 효율적으로 만들기 위해 중요하다.

이를 통해 얻을 수 있는 주요 장점과 필요성은 다음과 같다.


  1. 일관성 유지
  • 응답 형식의 일관성: 모든 API 응답이 동일한 형식을 따르도록 함으로써 서버를 호출하고 사용해야 하는 클라이언트가 응답을 예측하고 쉽게 처리할 수 있다.
    만약 응답이 다 다르다면 해당 서버의 특정 API를 호출할 때마다 프론트 엔드 개발자 혹은 클라이언트는 응답/요청 데이터 형식을 확인하기 위해 API 문서를 다 확인해야할 수도 있다.

    공통 API 응답의 형식으로는 예를 들어, 모든 응답에 일관되게 status, message, data 필드를 포함시킬 수 있다.

{
  "status": "success",
  "message": "요청 성공",
  "data": { ... }
}

  • 오류 응답 표준화: 오류 발생 시 일관된 형식의 오류 응답을 제공함으로써 클라이언트가 오류를 쉽게 이해하고 처리할 수 있다.
{
  "status": "error",
  "message": "에러 발생",
  "data": null
}

  1. 코드 중복 감소
  • 중앙 집중식 처리: 공통 응답 처리를 통해 예외 처리, 데이터 포맷팅(message, statuscode 등), 공통 데이터 추가 등의 로직을 한 곳에서 처리하면 코드 중복이 줄어들고, 유지보수에도 용이하다.

  1. 서비스 계층에서 응답 생성 책임 분리
  • 서비스의 비즈니스 로직 집중 용이: 만약 공통 API 클래스가 정의되어 있지 않다면 비즈니스 로직을 처리하는 데에만 집중해야 하는 서비스 계층에서 응답 생성 책임을 가져야 한다.

public class MemberService {

    private final CourseRepository courseRepository;
    private final MemberJpaRepository memberRepository;

    /**
     * 회원 저장 서비스
     */
    public MemberCreateResponseDto createMember(MemberCreateRequestDto data) {

        // 조회: 수업 아이디로 수업 조회
        Optional<Course> foundCourseOptional = courseRepository.findById(data.getCourseId());
        Course foundCourse = foundCourseOptional.get();

        // 생성: 회원 엔티티
        Member newMember = Member.createNewMember(
                foundCourse,
                data.getEmail(),
                data.getName()
        );

        // 저장: 회원
        Member savedMember = memberRepository.save(newMember);


// --- 공통 API 응답 부재로 서비스에서 직접 반환할 응답(상태 코드, message, 데이터 등) 생성(모든 메서드마다 응답 생성으로 인한 코드 중복도 발생) ---
        
        // 반환 응답
        return new MemberCreateResponseDto(
                "created",
                201,
                savedMember.getId()
        );
    }


공통 응답 처리 생성


이제 직접 공통 응답으로 사용할 ApiResponse라는 Dto 클래스를 정의해보자.


ApiResponse.java


@Getter
@AllArgsConstructor
public class ApiResponse<T> {

    private final String message;
    private final Integer statusCode;
    // 데이터가 Dto로 들어오는데 제네릭이기 때문에 어떤 Dto가 들어오든 상관 X
    private final T data;

    /**
     * 성공 응답 생성
     */
    public static <T> ApiResponse<T> createSuccess(String message, Integer statusCode, T data) { // 요청 성공시 응답 메서드 
        return new ApiResponse<>(message, statusCode, data); 
    }

    /**
     * 에러 응답 생성
     */
    public static <T> ApiResponse<T> createError(String message, Integer statusCode) { // 요청 실패시 응답 메서드
        return new ApiResponse<>(message, statusCode, null);
    }
}

공통으로 사용되는 ResponseDto 클래스인 ApiReponse를 만들고


/**
 * 수업 목록 조회 응답 DTO
 */
@Getter
@AllArgsConstructor
public class CourseListResponseDto {
//    private String message;
//    private Integer statusCode;
    List<CourseDto> courseDtoList;
}

기존 Dto에 정의되어 있던 Http 응답 관련 필드들은 이제 공통 응답 Dto에 정의되어 있기 때문에 삭제(주석처리)하고


@Slf4j
@Service
@RequiredArgsConstructor
public class CourseService {

    private final CourseRepository courseRepository;

    /**
     * 수업 생성 서비스
     */
    public CourseCreateResponseDto createCourse(CourseCreateRequestDto data) {

        // 검증: 수업 이름이 중복은 허용 안하겠다.
        boolean flag = courseRepository.existsByNameAndIsDeleted(data.getName(), false);
        if (flag) {
            throw new DuplicateCourseNameException();
        }

        // 생성: 수업(course) 엔티티
        Course newCourse = Course.createNewCourse(data.getName());

        // 저장: 코스 저장
        Course savedCourse = courseRepository.save(newCourse);


        // ------ API 공통 응답 처리로 코드 변화된 부분 ------

        // 공통 응답으로 처리하기로 했기 때문에 HTTP 응답 관련 코드를 주석 처리
        return new CourseCreateResponseDto(
                savedCourse.getId()
//                "created",
//                201
        );
    }

}

서비스 계층에서도 상태 코드, message 등 이제 더 이상 반환할 필요 없는 부분을 제거한다.



@RestController
@RequestMapping("/course")
@RequiredArgsConstructor
public class CourseController {

    private final CourseService courseService;
    
    /**
     * ---- API 공통 응답 적용 전 수업(course) 생성 API ----
     */
//    @PostMapping
//    public CourseCreateResponseDto createCourseAPI(@RequestBody CourseCreateRequestDto request) {
//        return courseService.createCourse(request);
//    }

    /**
     * ---- API 공통 응답 적용 후 수업(course) 생성 API ----
     */
    @PostMapping
    public ApiResponse<CourseCreateResponseDto> createCourseAPI(@RequestBody CourseCreateRequestDto request) {
        CourseCreateResponseDto courseCreateResponseDto = courseService.createCourse(request);
        return ApiResponse.createSuccess(
                HttpStatus.CREATED.getReasonPhrase(), // (HTTP) message
                HttpStatus.CREATED.value(), // (HTTP) status code
                courseCreateResponseDto // (응답) 데이터
        );
    }

}

이제 Controller에서 ApiResponse에 정의된 message, statusCode, data를 채운 후 응답으로 반환하면


{
  "message" : "Created",
  "statusCode" : "201",
  "data" : {
    "courseId" : 1
  }
}  

위의 예시와 같은 형식으로 출력되는 것을 확인할 수 있을 것이다. 해당 메서드만 그런 게 아니라 컨트롤러에서 ApiResponse으로 Dto를 감싸서 반환한 모든 메서드에서 동일한 형식으로 출력된다.


[ApiResponse 적용한 예외 처리]


@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(DuplicateCourseNameException.class)
    public ApiResponse<?> handleCourseNameException(DuplicateCourseNameException e) {
//        return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
        return ApiResponse.createError(e.getMessage(), HttpStatus.BAD_REQUEST.value());
    }

}

{
  "message" : "수업 이름 중복",
  "statusCode" : 400,
  "data" : null
 
}



ResponseEntity


하지만 스프링 프레임워크에는 ResponseEntity라는 이미 구현된 API 공통 응답 처리 클래스가 존재한다.



ResponseEntity란?


주로 REST API에서 응답 상태 코드, 응답 본문, 응답 헤더 등을 설정할 때 사용된다.


public class HttpEntity<T> {

	private final HttpHeaders headers;

	@Nullable
	private final T body;
}

HTTP 요청(request) 또는 응답(response)에 해당하는 HttpHeader와 HttpBody를 포함하는 클래스인 HttpEntity가 존재하는데


public class RequestEntity<T> extends HttpEntity<T>

public class ResponseEntity<T> extends HttpEntity<T>

HttpEntity 클래스를 상속받아 구현한 클래스가 바로 RequestEntity, ResponseEntity 클래스이다.


ResponseEntity는 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스이므로 HttpStatus, HttpHeaders, HttpBody도 ResponseEntity에 포함된다.



ResponseEntity의 주요 기능


  1. HTTP 상태 코드 제어: ResponseEntity를 사용하면 HTTP 응답 상태 코드를 직접 지정하는 것이 가능하다.
    성공 응답(200 OK, 201 Created)뿐만 아니라, 에러(400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error 등)에 대해서도 지정하고 싶은 적절한 상태 코드를 반환할 수 있게 한다.

  1. 응답 헤더 설정: 응답에 포함할 헤더를 직접 설정할 수 있다.
    예를 들어, 캐시 정책이나 콘텐츠 타입을 지정하는 등 응답 헤더를 세밀하게 제어할 수 있다.

  1. 응답 본문 설정: 클라이언트에게 보낼 응답 데이터를 설정할 수 있다.
    JSON, XML, 문자열 등 다양한 형식의 데이터를 응답 본문에 포함시킬 수 있다.


ResponseEntity의 주요 생성자 및 메소드


  • ResponseEntity< T >: 제네릭 타입 T는 응답 본문의 데이터 유형을 나타내며 이는 String, JSON, 또는 커스텀 객체 등 다양한 형식이 될 수 있다.


  • ResponseEntity.ok(): 응답 상태 코드를 요청에 대한 성공을 나타내는 200 OK로 설정하고, 응답 본문을 추가할 수 있다.
ResponseEntity<String> response = ResponseEntity.ok("Hello, World!");

@RestController
public class UserController {

    @GetMapping("/users")
    public ResponseEntity<List<User>> getUsers() {
        List<User> users = Arrays.asList(
            new User("Alice", 25),
            new User("Bob", 28)
        );
        return ResponseEntity.ok(users);  // 리스트 형태로 사용자 객체 반환
    }
}


  • ResponseEntity.status(HttpStatus): 특정 상태 코드를 설정하여 응답을 생성할 수 있다.

    예를 들어 404 Not Found 상태를 반환할 때 사용할 수 있다.


ResponseEntity<String> response = ResponseEntity.status(HttpStatus.NOT_FOUND).body("Resource not found");


  • ResponseEntity.headers(): 응답 헤더를 설정할 수 있다.

HttpHeaders headers = new HttpHeaders();
headers.add("Custom-Header", "value");
ResponseEntity<String> response = ResponseEntity.ok().headers(headers).body("Response with custom header");


ResponseEntity 사용 예시


1. 기본 사용


@RestController
public class MyController {

    @GetMapping("/greeting")
    public ResponseEntity<String> greeting() {
        return ResponseEntity.ok("Hello, World!");  // 200 OK 응답
    }

2. 상태 코드와 함께 사용


@RestController
public class MyController {

    @GetMapping("/data")
    public ResponseEntity<String> getData(@RequestParam(required = false) String param) {
        if (param == null) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Missing required parameter");
        }
        return ResponseEntity.ok("Valid parameter: " + param);
    }
}

3. 응답 헤더 설정

@RestController
public class MyController {

    @GetMapping("/header")
    public ResponseEntity<String> withHeader() {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Custom-Header", "CustomValue");

        return ResponseEntity.ok()
                .headers(headers)
                .body("Response with custom header");
    }
}

profile
알고리즘, 자료구조 블로그: https://gyun97.github.io/

0개의 댓글