Spring Web MVC는 Servlet API를 기반으로 구축된 웹 프레임워크다. 아래는 Java 코드를 이용하여 사용하는 방법을 정리했다.

컨트롤러 선언

@Controller@RestController를 어노테이션을 통해 컨트롤러를 선언할 수 있다. @Controller는 전통적인 Spring MVC의 컨트롤러에 해당하며, @RestController는 Restful 웹서비스의 컨트롤러다. Restful은 현재 문서의 주제 밖의 내용이라 생략한다. 간단하게 @Controller는 View를 반환하기 위해 사용, @RestController는 Data를 반환하기 위해 사용한다고 생각하자.

요청 매핑

@RequestMapping을 사용하여 컨트롤러 메서드에 매핑할 수 있다. URL, HTTP 메서드, request parameters, headers, media type 등 다양한 속성으로 매핑할 수 있다.

HTTP 메서드 별 매핑

HTTP 메서드에 대한 매핑은 @RequestMapping의 단축된 표현이 존재한다.

  • @GetMapping == @RequestMapping(method=HttpMethod.GET)
  • @PostMapping == @RequestMapping(method=HttpMethod.POST)
  • @PutMapping == @RequestMapping(method=HttpMethod.PUT)
  • @DeleteMapping == @RequestMapping(method=HttpMethod.DELETE)
  • @PatchMapping == @RequestMapping(method=HttpMethod.PATCH)
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}

URI 패턴

매핑시킬 URI의 패턴을 지정할 수 있다.

  • "/resources/ima?e.png" - 경로 세그먼트에서 한 문자 일치
  • "/resources/*.png" - 경로 세그먼트에서 0 개 이상의 문자와 일치
  • "/resources/**" - 여러 경로 세그먼트 일치
  • "/projects/{project}/versions" - 경로 세그먼트를 일치시키고 변수로 캡처
  • "/projects/{project:[a-z]+}/versions" - 정규식과 일치하고 변수를 캡처
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}

위와 같이 클래스 수준에서 URI 변수를 선언할 수도 있다.

소비 가능한 미디어 타입

consumes속성을 사용하여 요청으로 받을 수 있는 Content-Type의 종류를 매핑할 수 있다.

@PostMapping(path = "/pets", consumes = "application/json") 
public void addPet(@RequestBody Pet pet) {
    // ...
}
  • !text/plain 과 같이 부정의 표현이 가능하다.

생산 가능한 미디어 타입

produces 속성을 사용하여 요청으로 받을 수 있는 Accept의 종류를 매핑할 수 있다.

@GetMapping(path = "/pets/{petId}", produces = "application/json") 
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
  • !text/plain 과 같이 부정의 표현이 가능하다.

매개 변수, 헤더

요청의 매개 변수 조건에 따라서도 매핑할 수 있다. 매개 변수가 있는지 (myParam), 없는지 (!myParam), 특정 값이 있는지(myParam=myValue)에 따라 매핑할 수 있다.

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

같은 방법으로 요청의 헤더 조건에 따라서도 매핑할 수 있다.

@GetMapping(path = "/pets", headers = "myHeader=myValue") 
public void findPet(@PathVariable String petId) {
    // ...
}

핸들러 매서드

@RequestMapping 핸들러 메서드는 컨트롤러 메서드의 인자 및 반환 값의 종류를 선택할 수 있다. 받은 인자는 int, long, Date 등으로 자동으로 타입 변환이 가능하다.

Matrix Varaibles - @MatrixVariable

// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}

아래와 같이 경로 세그먼트 별 matrix varable을 인자로 받을 수 있다.

// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}

아래와 같이 기본값을 지정할 수도 있다.

// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}

모든 matrix varaible을 얻으려면 MultiValueMap을 사용할 수 있다.

// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}

쿼리 매개 변수 - @RequestParam

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { 
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...
  • Map<String, String> 또는 MultiValueMap<String, String>를 통해 매개 변수 값을 한꺼번에 받아올 수 있다.
  • @RequestParam 를 생략하면 메서드 인자와 같은 이름을 가진 매개 변수를 받는다.

요청 헤더

아래와 같은 헤더가 있다고 가정한다.

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, 
        @RequestHeader("Keep-Alive") long keepAlive) { 
    //...
}
  • Map<String, String> 또는 MultiValueMap<String, String>를 통해 매개 변수 값을 한꺼번에 받아올 수 있다.

쿠키

아래와 같은 쿠키 정보가 있다고 가정한다.

JSESSIONID = 415A4AC178C59DACE0B2C9CA727CDD84
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { 
    //...
}

Model

메서드 인자에 @ModelAttribute 붙이면 모델의 attribute에 접근하거나 존재하지 않는 경우 인스턴스화 되도록한다. 또한 @ModelAttribute가 붙은 모델은 별도의 설정없이도 자동으로 뷰에 전달된다. @RequestParam를 사용하면 리퀘스트 파라미터의 개수가 늘어날 때마다, 컨트롤러와 서비스 메서드의 파라미터를 매번 수정해줘야되기 떄문에, @ModelAttribute를 사용하는 편이 효율적이다.

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }

위의 pet은 다음과 같이 처리된다.

  • 모델에 이미 추가되어 있는경우 모델에서
  • @SessionAttributes를 사용하면 HTTP session에서
  • Converter를 통해 전달된 URI 경로 변수에서
  • 기본 생성자 호출에서
  • 서블릿 요청 파라미터와 일치하는 인자를 가진 주 생성자에서. 이 때 인자 이름은 자바빈의 @ConstructorProperties 또는 바이트 코드의 런타임 보유 매개변수 이름을 통해 결정된다.

Session

컨트롤러에서 사용하는 세션속성을 선언할 때 @SessionAttributes를 사용할 수 있다.

@Controller
@SessionAttributes("pet") 
public class EditPetForm {
    // ...
}

위는 pet 이라는 모델의 속성이 자동으로 추가되어 HTTP 서블릿 세션에 저장된다.

@Controller
@SessionAttributes("pet") 
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
            status.setComplete(); 
            // ...
        }
    }
}
  • 위의 pet은 @ModelAttribute 가 생략되었다.
  • SessionStatussetComplete 를 통해 세션을 지울 수 있다.
@RequestMapping("/")
public String handle(@SessionAttribute User user) { 
    // ...
}
  • @SessionAttribute 로 기존 세션 속성에 접근할 수 있다.

기존 요청 속성

@RequestAttribute 를 이용하여 기존에 설정된 속성을 사용할 수있다.

@GetMapping("/")
public String handle(@RequestAttribute Client client) { 
    // ...
}

리다이렉션

아래와 같이 리다이렉션을 할 경로를 지정할 수 있다.

@PostMapping("/files/{path}")
public String upload(...) {
    // ...
    return "redirect:files/{path}";
}

리다이랙션 대상에 데이터를 전송하는 방법으로는 Flash 속성이 있다. Flash 속성은 HTTP 세션에 저장되므로 URL에 표시되지 않는다.

Request body

요청의 body를 역직렬화하여 사용할 수 있다.

@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}

@HttpEntity@RequestBody와 비슷하지만 헤더도 접근할 수 있다.

Response body

응답의 body를 직렬화 시킬 수 있다. 직렬화 할때는 Jackson JSON을 사용한다.

@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

참고로 @RestController@Controller @ResponseBody 를 함께 사용한 것과 같다.

@ResponseEntity@ResponseBody와 비슷하지만 헤더도 설정할 수 있다.

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}

Jackson JSON

@JsonView를 이용하여 직렬화 또는 역직렬화시 사용할 필드를 선택할 수 있다. 자세한 내용은 https://www.baeldung.com/jackson-json-view-annotation 를 참고한다.

@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}

예외

@Controller또는 @ControllerAdvice 클래스에서 발생하는 예외를 처리하는 메서드를 만들 수 있다.

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

IOException과 같이 예외의 타입으로 처리하고자 하는 예외를 제한할 수 있다. 또는 아래와 같이 어노테이션을 이용할 수도 있다.

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
    // ...
}

Controller Advice

일반적으로 @ExceptionHandler, @InitBinder, @ModelAttribute메서드는 @Controller 클래스 안에서 정의된다. 만약 컨트롤러 간에 전역적으로 메서드를 정의하고싶으면 @ControllerAdvice 또는 @RestControllerAdvice 를 사용할 수 있다.

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

참고 자료

https://mangkyu.tistory.com/49

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html

댓글남기기