ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [4-ch16 REST 방식] @RestController, ResponseEntity, @RequestParam , 어노테이션과 JSON 알아보기
    Back-End/Spring Legacy 2022. 8. 31. 15:07

     

     

    모바일 시대가 되면서 WEB 분야의 가장 큰 변화는 서버 역할의 변화라고 할 수 있다. 과거에는 서버의 데이터를 소비하는 주체가 '브라우저'라는 특정한 애플리케이션으로 제한적이었다면, 모바일의 시대가 되면서 앱이나 웹은 서버에서 제공하는 데이터를 소비하게 된다. 과거의 서버는 브라우저라는 하나의 대상만으로 데이터를 제공했기 때문에 아예 브라우저가 소화 가능한 모든 데이터를 HTML이라는 형태로 전달하고, 브라우저는 이를 화면에 보여주는 역할을 해 왔다. 

     

    스마트폰에는 앱(app)이라 불리는 고유한 애플리케이션을 이용해 데이터를 소비하게 되고, 보이는 화면 역시 자신만의 방식으로 서비스하게 된다. 앱에서 서버에 기대하는 것은 완성된 HTML이 아니라 그저 자신에게 필요한 순수한 데이터만을 요구하게 되었다. 이처럼 서버의 역할은 점점 더 순수하게 데이터에 대한 처리를 목적으로 하는 형태로 진화하고 있다. 또한, 브라우저와 앱은 서버에서 전달하는 데이터를 이용해서 앱 혹은 브라우저 내부에서 별도의 방식을 통해 이를 소비하는 형태로 전환하고 있다. 

     

    이런 변화 속 웹의 URI(Uniform Resource Identifier) 의미도 조금 다르게 변화하고 있다. 최근 웹페이지들은 대부분 페이지를 이동하면 브라우저 내의 주소 역시 같이 이동하는 방식을 사용한다. 

     

     


     

    REST란?

     

    REST는 'Representational State Transfer' 의 약어로 하나의 URI는 하나의 고유한 리소스(Resource)를 대표하도록 설계된다는 개념에 전송방식을 결합해 원하는 작업을 지정한다. 또한 부수적인 레이어나 세션 관리를 추가하지 않고도 HTTP 프로토콜 데이터를 전달하는 프레임워크이다. 

     

    예를들어 '/boards/123' 은 게시물 중에서 123번이라는 고유한 의미를 가지도록 설계하고, 이에 대한 처리는 GET, POST 방식과 같이 추가적인 정보를 통해 결정한다. 

     

     

    <특징>

    - 모든 것은 URI를 가지고 있어야 한다. 모든 Resource는 클라이언트가 바로 접근할 수 있는 URI가 존재한다.
    - 웹 어플리케이션은 클라이언트의 상태에 대한 정보를 보관하지 않는다.
    -모든 HTTP 요청은 완전 독립적이다
    -클라이언트가 요청을 할 때마다 필요한 모든 정보를 준다.
    -서버는 클라이언트의 time out에 대해 신경쓰지 않아도 된다.
    -클라이언트에게 요청할 때마다 필요한 모든 정보를 주기 때문이다.
    -REST에서는 상태가 서버가 아닌 클라이언트에 유지된다.
    -모든 Resource는 일반적으로 HTTP 인터페이스인 GET, POST, PUT, DELETE로 접근되어야 한다.
     

     

    Restful APIs를 만드는 가장 큰 이유는 Clident Side를 정형화된 플랫폼이 아닌 모바일, pc, 어플리케이션 등 플렛폼에 제약을 두지 않는 것을 목표로 한다. 2010년 이전만 해도 Server Side에서 데이터를 전달해주는 Client 프로그램 대상이 명확했다. PC 브라우저가 그 대상이었다. 그렇다 보니 그냥 JSPASP, PHP 등을 이용한 웹페이지를 구상하고 작업을 진행하면 됐다. 하지만) 스마트 기기들이 등장하면서 Client 프로그램이 다양화 되고 그에 맞춰 Server Side를 일일이 만드는 것이 꽤 비효율적인 일이 되었다. 이런 과정에서 개발자들을 Client Side를 전혀 고려하지 않고 메시지 기반, xml, JSON과 같은 Client에서 바로 객체로 치환 가능한 형태의 데이터 통신을 지향하게 되었다. 어런 변화를 겪으며 HTTP의 표준 규약을 지켜 API를 만든 것이다.

     

    예를들어 '/boards/123' 은 게시물 중에서 123번이라는 고유한 의미를 가지도록 설계하고, 이에 대한 처리는 GET, POST 방식과 같이 추가적인 정보를 통해 결정한다. 

     

     

     

    Spring MVC 컨트롤러와 REST 컨트롤러 차이점

     

    이 둘의 차이점은 HTTP Response Body가 생성되는 방식이다. MVC 컨트롤러는 View 기술을 사용하는 반면 REST 컨트롤러는 객체를 반환하면 객체 데이터가 JSON/XML 형식의 HTTP 응답에 직접 작성된다. 모든 경우가 그런 것은 아니지만, MVC 컨트롤러는 View를 반환하는 것이고, REST 컨트롤러는 데이터를 반환하는 것이다. MVC 컨트롤러의 경우에도 @ResponseBody 어노테이션을 사용하면 객체를 반환할 수 있긴하다.

     

     

    < 전통적인 Spring MVC 컨트롤러 >

     

    : 요청이 들어오면 Dispatcher Servlet 에서 공통 처리 작업을 수행하고 컨트롤러에게 작업을 위임하게 된다. 

    (컨트롤러 -> Service -> DAO -> dataSource(쿼리문) ) 이후 컨트롤러는 응답을 Dispatcher Servlet으로 반환하고 Dispatcher Servlet은 view를 클라이언트에게 반환한다. 

     

     

    <Spring 4.x MVC 컨트롤러 >

     

    : 반면 REST 컨트롤러의 경우 spring MVC 컨트롤러에 @ResponseBody가 결합한 것으로, @RestController는 반환값에 자동으로 @ResponseBody가 붙게 되면서 HTTP 응답 데이터 Java 객체가 매핑되어 전달되게 된다. 이런 방식은 전통적인 Spring MVC 컨트롤러와는 다르게 컨트롤러에서 직접 데이터를 반환할 수 있게 된다. 단 @RestController 어노테이션은 Spring 4.x 버전 이상에서부터 사용 가능하다. 

     

     

     

     

     

    REST관련 어노테이션

     

     
     

     

     

    예제를 위해 Spring Legacy Project를 이용해서 wex03 프로젝트를 생성했다. pom.xml의 스프링 버전을 5.3.21으로 수정 필수!! 작성된 프로젝트에는 우선적으로 JSON 데이터 처리를 위한 jackson-databind 라이브러리를 추가해준다. 

     

    xml과 json을 출력값으로 둘다 사용하려고 하면 일반 브라우저를 띄었을 때 xml이 우선순위가 더 높아 xml 결과값이 나오게 되는데,  스프링 버전 5.2x 이전에서는 예를들어 /sample/getList  url을 입력했을때 josn 값으로 보려면 /sample/getList.json을 입력하면 됐었는데 5.2x 버전 이상부터는 그렇게 접근이 불가능하다. xml과 json의 우선순위를 변경해주는 방법도 있지만 아래 포스팅에선 json 라이브러리만 추가해주겠다. 

     

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.3</version>
    </dependency>
    <!-- 		<dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.13.3</version>
    </dependency>	 -->	
    
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.9.0</version>
    </dependency>

     

     

     

    JSON 이란? 

     

    JSON은 'JavaScript Object Notation'의 약어로 구조가 있는 데이터를 '{}'로 묶고 '키'와 '값'으로 구성하는 경량의 데이터 포맷이다. 프로그래밍 언어에서 말하는 객체(Object)들의 구조는 '{}'를 이용해 아래와 같이 표현할 수 있다. 

     

     

    구조를 표현한 문자열은 프로그래밍 언어에 관계없이 사용할 수 있기 때문에 xml과 더불어 가장 많이 사용되는 데이터의 표현 방식이다. 

     

     

     

    @RestController

     

    @RestController

    : REST 방식에서 가장 먼저 기억해야 하는 점은 서버에서 전송하는 것이 순수한 데이터라는 점이다. 기존의 Controller에서 Model에 데이터를 담아 JSP 등과 같은 뷰(view)로 전달하는 방식이 아니므로 기존의 Controller와는 조금 다르게 동작한다. 스프링 4에서부터는 @Controller의 모든 메서드의 리턴 타입을 기존과 다르게 처리한다는 것을 명시한다.@RestController 이전에는 @Controller와 메서드 선언부에 @ResponseBody를 이용해 동일한 결과를 만들 수 있었다. 

     

    스프링의 @RestController는 JSP와 달리 순수한 데이터를 반환하는 형태이므로 다양한 포맷의 데이터를 전달할 수 있다. 주로 많이 사용하는 형태는 일반 문자열이나 JSON, XML 등을 사용한다. 

     

     

    @RestController
    @RequestMapping("/sample")
    @Log4j2
    public class SampleController {
    	
    	@GetMapping(value = "/getText", produces = "text/plain; charset=UTF-8")
    	public String getText() {
    
    		log.info("MIME TYPE: " + MediaType.TEXT_PLAIN_VALUE);
    
    		return "안녕하세요";
    
    	}
    }

     

     

    위에서도 설명했지만 기존의 @Controller는 문자열을 반환하는 경우 JSP 파일의 이름으로 처리했지만 @RestController의 경우 순수한 데이터가 된다. @GetMapping에 사용된 produces 속성은 해당 메서드가 생산하는 MIME 타입을 의미한다. 예제와 같이 문자열로 직접 지정할 수 있고, 메서드 내의 MediaType이라는 클래스를 이용할 수도 있다. 

     

    객체를 반환하는 작업은 JSON, XML을 이용한다. 전달된 객체를 생산하기 위해 SampleVO라는 클래스를 작성한다. 

     

     

    package org.zerock.domain;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Data
    //모든 속성 사용하는 생성자 SampleVO(Integer, String, String)
    @AllArgsConstructor
    //비어있는 생성자 SampleVO()
    @NoArgsConstructor
    public class SampleVO {
    	
    	private Integer mno;
    	private String firstName;
    	private String lastName;
    	
    
    }

     

     

    SampleController

    package org.zerock.controller;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.zerock.domain.SampleVO;
    import org.zerock.domain.Ticket;
    
    import lombok.extern.log4j.Log4j2;
    
    @RestController
    @RequestMapping("/sample")
    @Log4j2
    public class SampleController {
    	
    	@GetMapping(value = "/getText", produces = "text/plain; charset=UTF-8")
    	public String getText() {
    
    		log.info("MIME TYPE: " + MediaType.TEXT_PLAIN_VALUE);
    
    		return "안녕하세요";
    
    	}
    
    	@GetMapping(value = "/getSample", 
    			produces = { MediaType.APPLICATION_JSON_VALUE,
    			MediaType.APPLICATION_XML_VALUE })
    	public SampleVO getSample() {
    
    		return new SampleVO(112, "스타", "로드");
    
    	}
    
    	@GetMapping(value = "/getSample2")
    	public SampleVO getSample2() {
    		return new SampleVO(113, "로켓", "라쿤");
    	}
    
    	@GetMapping(value = "/getList")
    	public List<SampleVO> getList() {
    
    		return IntStream.range(1, 10).mapToObj(i -> new SampleVO(i, i + "First", i + " Last"))
    				.collect(Collectors.toList());
    
    	}
    
    	@GetMapping(value = "/getMap")
    	public Map<String, SampleVO> getMap() {
    
    		Map<String, SampleVO> map = new HashMap<>();
    		map.put("First", new SampleVO(111, "그루트", "주니어"));
    
    		return map;
    
    	}
    	
    	
    	@GetMapping(value = "/check", params = { "height", "weight" })
    	public ResponseEntity<SampleVO> check(Double height, Double weight) {
    
    		SampleVO vo = new SampleVO(000, "" + height, "" + weight);
    
    		ResponseEntity<SampleVO> result = null;
    
    		if (height < 150) {
    			result = ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(vo);
    		} else {
    			result = ResponseEntity.status(HttpStatus.OK).body(vo);
    		}
    
    		return result;
    	}
    	
    	
    	@GetMapping("/product/{cat}/{pid}")
    	public String[] getPath(@PathVariable("cat") String cat,
    							@PathVariable("pid") Integer pid) {
    		return new String[] {"category: " + cat, "productId: " + pid};
    	}
    	
    	
    	@PostMapping("/ticket")
    	public Ticket convert(@RequestBody Ticket ticket) {
    		log.info("convert...." +ticket);
    		return ticket;
    	}
    	
    	
    	
    	
    	
    }

     

     

    xml

     

    json

     

     

    ReponseEntity

     

    HttpEntity 클래스 = request 또는 response의 HttpHeader + HttpBody를 포함하는 클래스이다. 

    ResponseEntity 클래스 = 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스이며 HttpStatus + HttpHeaders + HttpBody를 의미한다. 생성자로 body, header, status를 파라미터로 받는데, body는 내용이 적혀있고 , header에는 요청/응답에 대한 요구사항이 적혀있다.  

     

     

    REST 방식으로 호출하는 경우 화면 자체가 아니라 데이터 자체를 전송하는 방식으로 처리되기 때문에 데이터를 요청한 쪽에서는 정상적인 데이터인지 비정상적인 데이터인지 구분할 수 있는 확실한 방법을 제공해야한다. ResponseEntity 는 데이터와 함께 HTTP 헤더의 상태 메시지 등을 같이 전달하는 용도로 사용한다. HTTP의 상태 코드와 에러 메시지 등을 함께 데이터를 전달할 수 있기 때문에 받는 입장에서 확실하게 결과를 알 수 있다. 

     

     

    @GetMapping(value = "/check", params = { "height", "weight" })
    public ResponseEntity<SampleVO> check(Double height, Double weight) {
    
        SampleVO vo = new SampleVO(000, "" + height, "" + weight);
    
        ResponseEntity<SampleVO> result = null;
    
        if (height < 150) {
            result = ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(vo);
        } else {
            result = ResponseEntity.status(HttpStatus.OK).body(vo);
        }
    
        return result;
    }

     

     

    check는 반드시 'height'와 'weight'을 파라미터로 전달 받는다.  이때 만약 'height' 값이 150보다 작다면 502(bad gateway) 상태 코드와 데이터를 전송하고, 그렇지 않다면 200(ok) 코드와 데이터를 전송한다. 

     

    아래 좌측의 경우 height 값으로 140을 전달했기에 502메시지와 데이터가 전달된다. 

     

     

     

     

    @PathVariable

     

    REST 방식에는 URL에 최대한 많은 정보를 담으려고 노력한다. 예전에는 '?' 뒤에 추가되는 쿼리 스트링이라는 형태로 파라미터를 이용해 전달되던 데이터들이 REST 방식에서는 경로의 일부로 차용되는 경우가 많다. 스프링 MVC에서는 @PathVariable 어노테이션을 이용해 URL 상에 경로의 일부를 파라미터로 사용할 수 있다. 

     

     

    URL에서 {}로 처리된 부분은 컨트롤러의 메서드에서 변수로 처리 가능하다. @PathVariable은 { }의 이름을 처리할 때 사용한다. 

     

     

     

    {}을 이용해 변수명을 지정하고 @PathVariable을 이용해 지정된 이름의 변숫값을 얻을 수 있다. 값을 얻을 때는 int, double과 같은 기본 자료형은 사용할 수 없다.  브라우저에 /sample/product/bag/123이라고 호출하면 cat과 pid 변수의 값으로 처리되는 걸 확인할 수 있다.

     

     

     

     

     

    @PathVariable 과 @RequestParam 사용 예제

     

    스프링에서 controlle로 이동 시 view에서 url로 파라미터를 전달할 수 있는데 대표적으로 2가지 형태가 있다.

     

    1. @RequestParam

    : http://www.test.com/board?seq=2

    @GetMapping(“/board”)

    public String save (@RequestParam(“seq”) int seq) {}

    파라미터로 전달받은 seq의 값을 int seq 변수에 저장하겠다.

    따라서 int seq로 들어온 값은 2 된다.

    파라미터 valuenull이 들어오는 것을 방지하기 위해서 required 설정을 false로 할 수 있다.

     

     

    2.  @PathVariable

    : http://www.test.com/board/1

    @GetMapping(“/board/{seq}”)

    public String save (@PathVariable int seq) {}

    url 호출 시 전달된 매개변수 값을 int seq에 저장한다.

    따라서 int seq 값은 1이 되는 것

    변수명을 바꿔서 사용하고 싶은 경우 @PathVariable(‘템플릿 변수명‘) url로 들어온 템플릿 변수명과 맵핑 시켜주면 된다.

     

    http://www.test.com/user/user?name=test

    @PostMapping(“/user/{category}”)

    public String save(@PathVarialbe(“category”) String category, @RequestParam(value=“name”, required=false) String name){}

    위와 같이 코드를 실행하게 되면 category= user, name= test가 된다!

    반응형

    댓글

Designed by Tistory.