HTTP 요청에 따른 결과물 반환

Spring은 백엔드 프레임워크로 많이 사용하게 된다. 때문에 API서버처럼 특정 요청이 들어오면 HTTP에 맞게, 클라이언트에게 반환을 해주어야 합니다.

여기서 스프링을 접하고 나서 모두 아는 단어들이지만, HTTP는 많이는 들어봤지만 어떤 의미인지 모르는 상황이었습니다.

HTTP는 어떤 것일까요?

 

HTTP 응답 요소의 많은 구성

HTTP는 HyperText Transport Protocol의 약자로 웹 통신을 위한 하나의 약속이라고 보면 됩니다.

REST하게 짜져 있으면, HTTP 규격에 맞춘 요청만 보더라도, 어떤 요청인지를 알 수 있다고 합니다. 

 

HTTP의 경우 응답, 요청 모두 크게 세 가지 요소로 나뉘어져 있습니다.

네이버를 접속했을 때의 예시를 먼저 보도록 하겠습니다.

 

먼저 요청입니다.

GET / HTTP/2
Host: www.naver.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: (중략)
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
TE: Trailers

 

첫줄에 Get / HTTP /2 는 HTTP StartLine이라고 합니다.

메소드 및 요청 주소 그리고 HTTP 버전으로 구성되어 있습니다.

 

두번째로는 HTTP Header입니다.

요청 사항 및 요청 보내는 사람의 부가적인 정보들이 들어 있습니다.

 

세번째로는 Body입니다.

요청에 있어서 POST나 PUT과 같은 정보들을 함께 넣어줄 때 쓰이게 되는데 header에서는 보이지 않지만,

보통은 json 형태로 브라우저에서 확인할 수가 있습니다.

 

두 번째로는 응답입니다.

 

HTTP/2 200 OK
server: NWS
date: Sun, 09 May 2021 18:14:40 GMT
content-type: text/html; charset=UTF-8
cache-control: no-cache, no-store, must-revalidate
pragma: no-cache
p3p: CP= 중략
x-frame-options: DENY
x-xss-protection: 1; mode=block
content-encoding: gzip
strict-transport-security: max-age=63072000; includeSubdomains
referrer-policy: unsafe-url
X-Firefox-Spdy: h2

첫 줄에 HTTP/2 200 OK는 HTTP StatusLine이라고 합니다.

방금 주었던 요청에 대한 상태가 저장되어 있습니다.

 

나머지 HTTP Header와 Body는 응답의 경우와 같은 맥락의 정보를 담고 있습니다.

 

응답의 경우에는 우리 백엔드 개발자들의 주 관심분야가 될 것 같습니다. 실제로 생성해서 반환을 해주어야하니까요.

하지만, 저것을 일일히 지정하는 것은 까다로운 일일 것 같습니다.

이것을 Spring-boot에서 쉽게 적용시켜주는 것이 바로, ResponseEntity입니다.

응답을 자동으로 만들어주는 ResponseEntity

ResponseEntity<T>는 제네릭 형식으로 외부에서 Body로 보낼 데이터 형식을 지정이 가능합니다.

@RestController의 Annotation이 붙은 부분에서 많이 사용이 됩니다.

간단한 ResponseEntity 샘플을 보고 가도록 하겠습니다.

public ResponseEntity<String> createResponse(final String sample) {
	return ResponseEntity.ok()
      .body(sample);
}

위의 형식들의 경우에는 "SampleString"이라는 내용을 Body안에 담으며, Status Code는 200, 즉 OK로 발송하겠다라는 내용을 담게 됩니다.

생성자보다는 Builder를

public ResponseEntity<String> createResponse(final String sample) {
	return ResponseEntity.ok()
      .body(sample);
}

생성자로 사용하는 것도 좋지만, 빌더로 사용하는 것을 추천드립니다.

그 이유는 Status Code인데요.

200, 204, 400 그렇게 차이가 크게 나지 않을 것만 같은 숫자 코드들도 제각각의 상태를 가지고 있습니다.

이 때문에 오류나 실수가 날 수 있는 생성자 패턴보다는

빌더로 직접 ok, bad reqeust, no content 등을 지정해줌으로써, 번호에 대해서 실수를 할 것을 막을 수가 있습니다.

시작부터 예외에 대한 글

 

Exception 발생이란

Exception은 Error와 달리, 구현 로직에서 발생하는 프로그램이 모종의 이유에서 처리할 수 없는 현상을 말합니다. 구현 로직에서 발생하는 경우가 많기 때문에 개발자가 어느 정도 예상이 가능한데요. 하지만 Error와 마찬가지로 프로그램이 돌아가지 않는 것은 같습니다. 때문에 우리는 예외 처리를 통해서 예외의 종류별로 어떻게 처리할 것인지에 대해서 정할 수가 있습니다. 가장 유명한 것은 try - catch 블록이 있습니다.

Spring에서는 조금 더 나아가서 일괄적으로 예외를 처리할 수 기능이 있습니다.

 

Try - catch를 쓰는 것도 좋지만

먼저, Controller Advice를 사용하지 않았을때의 문제를 살펴 보도록 하겠습니다.

@Controller
public class Controller{
    @GetMapping("/temporary")
    public String temporary(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (SampleException e) {
        	//예외 처리
        }
        return "temporary";
    }
}

 

위의 코드는 제가 임의로 짠 코드입니다.

별 문제가 없으면 "sample"이라는 내용을 model에 넣어줘서 랜더링하게 되고 temporary라는 html template 형태를 반환할 것이라고 예상이 됩니다.

하지만 코드를 짜면서 저는 여러 가지 불만을 느끼게 되었는데요.

 

1. 코드의 중복

특정 Exception 특히나, 제가 커스터마이징한 Exception에 대한 처리를 일괄적으로 하고 싶습니다. 하지만, 지금의 로직이라면 다음과 같이 각 url을 처리하는 메소드마다 같은 코드를 계속해서 중복해서 넣어줘야하는 문제가 생깁니다.

 

@Controller
public class Controller{
    @GetMapping("/temporary1")
    public String temporaryOne(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (CustomizedException e) {
        	//커스터마이징된 예외 처리						//중복
        } catch (SampleException e) {
        	//예외 처리
        }
        return "temporaryOne";
    }
    
    @GetMapping("/temporary2")
    public String temporaryTwo(Model model) {
        model.addAllAttributes("sample", "sample2");
        try {
        	//로직
        } catch (CustomizedException e) {
        	//커스터마이징된 예외 처리						//중복
        } 
        return "temporaryTwo";
    }
}

코드의 중복 없이 특정 예외에 대해서 일괄적으로 처리를 할 수 있는 로직이나 객체가 필요합니다.

 

2. 예외 발생 시에 반환 타입 변경

현재 로직에서는 String을 반환하도록 되어 있습니다. 하지만, Exception 발생 시에 Exception에 관련된 메세지를 Exception Dto에 담아서 전달을 하고 싶습니다.

 @Controller
 public class Controller{
    @GetMapping("/temporary")
    public String temporary(Model model) {
        model.addAllAttributes("sample", "sample");
        try {
        	//로직
        } catch (SampleException e) {
        	return new ExceptionDto(e.getMessage());	// 예외 발생시에 Dto를 전달하고 싶지만..
        }
        return "temporary";
    }
}

하지만 여기서 반환 Type은 String으로 지정을 해놓았기 때문에 반환하지 않을 가능성이 매우 높습니다.

 

이런 두 가지 문제가 저한테 다가왔었고, 저는 저의 코드 리뷰에서 ControllerAdvice, RestControllerAdvice를 사용한 예외처리를 해보라는 조언을 받게 되었습니다.

@ControllerAdvice, @RestControllerAdvice with @ExceptionHandler

@Controller, @RestController와 마찬가지로, ControllerAdvice는 페이지 렌더링만, @RestControllerAdvice는 json에 여러 정보들을 담아서 반환을 할 수가 있습니다.

 

@RestControllerAdvice
public class RestControllerAdvice {
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ExceptionResponseDto> illegalArgumentExceptionResponse(
    IllegalArgumentException illegalArugmentException) {
        String message = illegalArgumentException.getMessage();
        ExceptionResponseDto exceptionResponseDto = new ExceptionResponseDto(message);
        return ResponseEntity.badRequest()
            .body(exceptionResponseDto);
    }
    
    @ExceptionHandler(CustomException.class)
    public ResponseEntity<CustomResponseDto> customExceptionResponse(
    CustomException customException) {
        String message = customException.getMessage();
        CustomResponseDto exceptionResponseDto = new CustomResponseDto(message);
        return ResponseEntity.badRequest()
            .body(exceptionResponseDto);
    }
}

 

먼저, 원래의 Controller에서 예외가 발생하게 됩니다. 그러면 ExceptionHandler에서 해당하는 예외를 감지하게 됩니다. 특정 예외들에 대해서 일괄적으로 똑같이 처리를 할 수 있다는 점에서 앞서 언급한 문제 1번은 해결이 됩니다.

그렇게 예외를 잡게 되면, @RestControllerAdvice에서 처리를 할 수가 있는데요.

여기서도 똑같이 반환형 타입을 바꿀 수가 있습니다. 원래 String만 반환을 할 수가 있었는데, Dto를 활용하여 예외에 대한 내용이나 추가 자료를 같이 섞어서 보낼 수가 있습니다.

이렇게 @ExceptionHandler와 함께 @ControllerAdvice, @RestControllerAdvice를 활용하여, 일괄적으로 예외를 처리할 수 있게 되었습니다.

 

+ Recent posts