개발기술/Spring

스프링 MVC

bsh6226 2024. 7. 18. 23:22

스프링 MVC의 개념, 흐름

 MVC는 R&R을 명확하게 하기 위해서 Layer을 나누는 디자인 패턴이다. Model은 데이터객체, View는 화면처리, Controller은 로직처리 후 모델과 뷰를 지정하는 역할을 분담한다.

   전체 프로세스 흐름은 Disptcher servelet에서 request를 최초로 접수받고 dispatcher은 request를 url과 http method을 참고하여 어떤 controller(handler)에 request를 전송할지 판단한다. 이 판단은 각 spring method에 붙어있는 @requestmapping을 통해서 mapping 해준다. 이후 controller로 보내면 서비스 호출 등을 통해, request를 처리한 후, 사용할 뷰의 정보를 dispatcher에게 돌려준다. dispatcher는 viewresolver를 통해서 view를 찾고 이를 데이터와 결합해 response로 반환한다.

 

스프링 MVC HTTP 맵핑 - Controller

Controller class들이 외부Request들과 맵핑되어 메소드를 실행하게끔 하기 위해서는 @Controller Annotation을 통해서 본 클래스가 Controller임을 명시함으로써 mapping annotation을 찾아 볼 것을 Spring에 통지한다. 그리고 method 별로 @mapping을 통해서 어떤 request와 mapping할 것인지 명시함. 

 

클래스 맵핑어노테이션

 

  Controller 종류로는 @Controller와 @RestController 두가지가 있음.

  • @Controller : 데이터를 처리하여 view와 model을 합쳐 ModelAndView 객체를 return하고 resolver를 통해 JSP 파일을 생성하는 기술, 과거 백/프론트 구분이 없었을때 주로 사용됨.
  • @RestController :  rest api요청에 대한 응답값(ResonseBody)으로 JSON/XML(API)으로 자동적으로 serialization을 진행한다는 표기로  @Controller+ @ResponseBody 추가한 것과 동일.
    •  일반적으로 반환값을 JavaBean 형식의 DTO로 설정하여 이를 Json형식의 response body로 전송하도록 설정함. 

 

메소드 맵핑어노테이션

 

@Controller component로 등록된 클래스의 내부의 메소드를 client 요청의 특정URL과 특정Method에 대한 request와 맵핑해줌. 

- @Requestmapping(value = "url", method = RequestMethod.get) : url과 그 url에 들어오는 메소드를 지정하면 해당 요청에 대해서 어떤 로직을 실행할지 지정함.

@RestController // componenet로 등록
public class SampleController {

    @RequestMapping(value ="order/1", method= RequestMethod.GET ) // 유저 request와 맵핑
    public String getOrder(){
        log.info("getOrder");
        return "orderId:1, orderAmount:1000";
    }
}

 

축약형어노테이션

  • @requestmapping(value = "url", method = RequestMethod.get)에서 한발짝 더나아가서 method parameter을 포함한 축약형 annotaion.
    • @GetMapping(value="url) : @PostMapping(value="url) : @PutMapping(value="url) : @PatchMapping(value="url) : @DeleteMapping(value="url) :
  • 보통 각  class에는 requestmapping(공통url)을 통해서 공통되는 url을 포함해준다. 그리고 각 API를 나타내는 method에는 축약형 annotation을 사용하여 공통url 외의 세부 url을 넣어준다.
@RestController
@RequestMapping("/company")
public class CompanyController {
    @GetMapping("/autocompelete")
    public ResponseEntity<?> autoComplete(@RequestParam String keyword) {
        return null;
    }
    @GetMapping()
    public ResponseEntity<?> seachCompany() {
        return null;
    }

 

Browse의 url탐색은 get방식이라 post의 경우 browser로 바로 확인이 어렵고 Post의 경우에는 request 속을 까봐야 알기때문에, intelij의 Generate Request Http client 기능을 사용해서 클라이언트 측의 요청날리기 테스트가 가능하다.

### create order
POST http://localhost:8080/order/1
Content-Type: application/json

{"orderId": "123","orderAmount": "8500"}

 

스프링 MVC HTTP 요청 파라미터 송수신

  Class를 @Requestcontroller로 지정하고 각 메소드를 @requestmapping으로 지정하고나면 각 메소드의 파라미터 값을 받도록 annotation으로 지정할 수 있다.

Get, Delete : url로 데이터를 전송

Http Spec상 Get,Delete Method에서 주로 파라미터와 값을 송신한다

  • 전송방식1(PathVariable) ; URL의 일부로써 Parameter와 value를 URL path에 넣어서 표기한다. /order/{orderId}
    • 수신방식1  : 메소드의 @requestmapping부분에 @Getmapping(val=/url/{parmetername}으로 설정한후 메소드의 Parameter 앞에 @PathVariable("parametername"(생략가능))으로 표기하여 pathVariable와 메소드의 paramter을 맵핑
@RestController
public class UserController {

    @RequestMapping(value = "/users/{userId}", method = RequestMethod.GET)
    public String getUserById(@PathVariable("userId") String userId) {
        // logic to retrieve user details using userId
        return "User details for ID: " + userId;
    }
}

 

  • 전송방식2(Query-Params) : URL이후 ?이후 paramter=value와 같이 표기한다. /order?orderId=3
    • 수신방식2 :  메소드의 Parameter 앞에 @RequestParam("parametername"(생략가능))으로 표기하여 Query parameter와 메소드의 paramter을 맵핑함.
    • Query Parameter에 대해서 required, defaultValue를 설정해줄 수 있음. @RequestParam(value = "orderId", required = false, defaultValue ="1")
@RestController
public class ProductController {

    @RequestMapping(value = "/products", method = RequestMethod.GET)
    public String getProductByCategory(@RequestParam(name = "category") String category) {
        // logic to retrieve products based on category
        return "Products in category: " + category;
    }
}
  • URL은 길이가 한계가 있고 복잡한 데이터의 client 전송에는 http request body를 사용한다.

Post, Put, Patch : Request Body로 데이터를 전송

  마찬가지로 @Requestmapping이 완료된 메소드의 parameter들을 각각 @RequestHeader @RequestBody로 선언을 해준다. header와 body는 주로 json데이터이므로 검증의 편의성을 위해서 (@Valid를 사용하기위해서)  java bean을 DTO로 생성하고 각 parameter를 @RequestHeader 혹은 @RequestBody로 맵핑한다. 

  • @requestheader : 메소드의 parameter을 원하는 http header와 맵핑해줌
    • 메타정보로써, 계정, 인증, 토큰 정보등을 담음. http spec에 없는 Header라도 개발자가 정의하여 header을 추가할 수 있음
    • @requestheader와 mapping된 parameter variablename이 headername과 동일할 경우 자동적으로 assign해준다.
  • @requestbody : Java bean type의 메소드 parameter을 http body와 맵핑해줌. 단, http body내에 해당 필드가 없으면 Null값을 부여함
@PostMapping(value = "order") // 유저 Get request 맵핑. pathVariable
public String createOrder(
        @RequestBody CreateOrderRequest createOrderRequest,
        @RequestHeader String userAccountId
) {
    log.info("getOrder:" + createOrderRequest + ", userAccountId:" + userAccountId);
    return "get with param orderId:" + createOrderRequest.getOrderId() + ", " +
            "orderAmount:" + createOrderRequest.getOrderAmount();
}

@Data
public static class CreateOrderRequest{
    private String orderId;
    private Integer orderAmount;
}

 

Response Body로 데이터를 전송

  • ResponseEntity<responsebody> : 를 사용하면 header의 값 설정, status code설정, body값 설정 등 더 다양한 기능으로 응답을 설정할 수 있음
    • ResponseEntity.ok(body) : to create a ResponseEntity object with a status code of HTTP 200 OK

 

 

 

스프링 MVC 예외처리

  프로그램을 작성하면, 실제로 서비스를 동작시키면 제대로 동작하는 성공케이스는 많지 않고 대부분 에러 케이스를 거치면서 동작하게 된다.  에러가 발생하는 케이스가 매우 많고, 이러한 예외사항을 얼마나 잘 처리하는지가 개발자의 주요 역량중 하나이다.

   java에서의 예외처리는 try-catch를 통해서 진행되는 반면, Spring에서의 예외처리는 ExceptionHandler을 통해서 예외를 처리하게 된다.

 

HTTP 에러 Response 문제점

예외가 발생할때, Response로 Spring에서 설정된 기본 설정으로 error메세지가 전송된다. 그러나 정확하게 내부적으로 어떤 에러가 발생했는데 client/FE 쪽으로 소통이 안됨.

{
    "timestamp": "2022-06-08T15:51:02.984+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "path": "/account"
}

 

에러 처리방법

1. HTTP StatusCode 중 상황에 맞는 StatusCode를 선택해서 발송한다.

2. HTTP StatusCode Custom를 생성하여 발송한다

3. Exception Handler를 통해서 CustomError을 통해서 ErrorCode와 ErrorMessage를 발송한다. (추천)

 

  각 Custom Exception을 case별로 Throw하도록 코드를 작성하는 것은 한계가 있으므로, Controller 내에서 Throw되지 않은 예외를 처리하는 메소드 ExceptionHandler @ExceptionHandler()를 생성해서 컨트롤러 내의 Error에 대해서 Error Code와 Message를 보내는 API를 만들자.

 

Custom Exception Class생성 (Servic Layer 적용)

관련 Service에 대한 Error를 Custom 생성하기 위해서 Class를 생성한다. RuntimeException이라는 unchecked exception을 상속해서 try-catch없이 코드를 clean하게 유지함. 그리고 field 값으로 errorCode라는 enum 값으로 명확하게 에러의 원인을 표기해준다.

public class AccountException extends RuntimeException {
   private ErrorCode errorCode;
   private String Errormessage;

   public AccountException(ErrorCode errorCode) {
      this.errorCode = errorCode;
      this.Errormessage = errorCode.getDescription();
   }
}
@Getter
@AllArgsConstructor

public enum ErrorCode {
    USER_NOT_FOUND("사용자가 없습니다"),
    ACCOUNT_NOT_FOUND("계좌가 없습니다"),
    MAX_ACCOUNT_PER_USER_10("사용자 최대 계좌는 10개입니다"),
    USER_ACCOUNT_UN_MATCH("사용자와 계좌의 소유주가 다릅니다."),
    ACCOUNT_ALREADY_UNREGISTERED("계좌가 이미 해지되엇습니다"),
    BALANCE_NOT_EMPTY("잔액이 있는 게좌는 해지할 수 없습니다"),
    AMOUNT_EXCEED_BALANCE("거래 금액이 계좌 잔액보다 큽니다.");

    private final String description;
}

 

서비스 Layer에서 예외상황이 예상되면 new CustomExceptioin을 Throw하며, ExceptionHandler가 Catch하게끔 한다.

@Transactional
public AccountDto deleteAccount(Long userId, String accountNumber) {

    AccountUser accountUser =
            accountUserRepository.findById(userId).
                    orElseThrow(() -> new AccountException(USER_NOT_FOUND));

    Account account = accountRepository.findByAccountNumber(accountNumber)
            .orElseThrow(() -> new AccountException(ACCOUNT_NOT_FOUND));

 

ErrorHandler생성 (Controller Layer 적용)

  • @ExceptionHandler(class or classList) : Controller 내에서 예외처리되지 않은 예외에대해서 일괄 처리하는 ExceptionHandler를 지정해줌. class or classList는 예외처리를 해주고싶은 특정 예외 class or classList를 넣어주면 되고, 모든 예외에 대해서 처리하고싶다면 exception class를 입력하면 된다. 
  • @ResponseStatus(HttpStatus.CODE) : ExceptionHandler를 통해서 예외처리를 하면 Status code가 200으로 보내지는데 이를 400~500으로 지정할 수 있다.
  • ErrorResponse DTO : ExceptionHandler의 응답의 body를 JavaBean형태로 만들어 Json으로 보내도록 지정할 수 있다. ErrorResponse라는 별도의 ResponseDTO를 만들고 return 형식을 ErrorResponse 설정한다.
    • ErrorResponse를 응답객체로 사용할때 ResponseEntity <ErrorResponse>를 사용하면 header의 값 설정, status code설정 등 더 다양한 기능을 사용할 수 있음.
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> generalException(Exception e)
    {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("ERROR", e.getMessage()));
    }
}

 

RestControllerAdvice(Class)생성

 

@ExceptionHandler는 특정 컨트롤러 클래스 내에서 발생하는 예외를 처리하는 메소드에 사용되는 반면 @ControllerAdvice는 어플리케이션 전역에서 발생하는 모든 Exception에 대해서 처리할 수 있는 Handler를 지정한다.

  현재 백엔드 예외처리에서 일반적인 방법은 @RestControllerAdvice를 통해서 GlobalExceptionHandler을 만들고 개별적으로 처리가 필요한 Exception에 대해서는 개별적인 Handler를 만들고 그 외 일반 ExceptionHandler로 처리한다.

  • @ControllerAdvice(GlobalExceptionHandler)  : 만약, Handler가 위치한 Controller 외에도 모든 Controller에 일괄 적용하고자 하는 ErrorHandler을 만드려면 Config class의 일환으로 GlobalExceptionHandler 클래스를 만들고, @ControllerAdvice로 지정한다.  @ControllerAdvice 내에 @ExceptionHandler(class) 가 존재하게 된다.
  • @RestControllerAdvice : ControllerAdvice는 view를 응답하는 방식이고 RestControllerAdivce는 RestAPI용 객체를 응답하는 방식 (스프링 백엔드에서 가장 많이 사용되는 방식)
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> generalException(Exception e)
    {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("ERROR", e.getMessage()));
    }
}