Spring Boot REST Controller, Jackson

강서진·2023년 12월 5일
0
post-custom-banner

REST Controller

@Slf4j
@RestController
@RequestMapping("/api")
public class RestApiController {

    @GetMapping(path = "/hello")
    public String hello(){
        var html ="<html><body><h1>SPRINGBOOT</h1><body></html>";

        return html;
    }

    @GetMapping("/echo/{msg}/{num}")
    public String echo(@PathVariable(name="msg") String msg, @PathVariable(name="num") int num){
        System.out.println("msg: "+msg);
        msg = msg.toUpperCase()+num;

        return msg;
    }

    // localhost:8080/api/book?category=IT&issuedYear=2023&issued-month=12&issued_day=1
    @GetMapping("/book")
    public void queryParam(@RequestParam String category, @RequestParam String issuedYear,
                           @RequestParam(name="issued-month") String issuedMonth,
                           @RequestParam(name="issued_day") String issuedDay){
        System.out.println(category+"\t"+issuedYear+"\t"+issuedMonth+"\t"+issuedDay);
    }

    // localhost:8080/api/book2?category=IT&issuedYear=2023&issued-month=12&issued_day=1
    @GetMapping("/book2")
    public void queryParam2(BookQueryParam bookQueryParam){

        System.out.println(bookQueryParam);
    }

    @PostMapping("/post")
    public String post(@RequestBody BookRequest bookRequest){
        System.out.println(bookRequest);
        return bookRequest.toString();
    }

    @PostMapping("/user")
    public UserRequest user(@RequestBody UserRequest userRequest){
        System.out.println(userRequest);
        return userRequest;
    }

    @PutMapping("/put")
    public void put(@RequestBody UserRequest userRequest){
        log.info("Request: {}",userRequest);
    }

    @DeleteMapping(path = {"/user/{userName}/delete","/user/{userName}/del"})
    public void delete(@PathVariable String userName){
        log.info("user-name: {}", userName);
    }

}

@Slf4j

실행된 내용을 로그에 출력한다.

@RestController

응답을 JSON으로 반환한다. html 리소스를 반환하려면 @Controller를 사용한다.
@Controller에서 JSON을 반환하려면 @ResponseBody 애너테이션을 사용한다.

@RequestMapping(path)

주어진 path로 오는 요청을 받는다. 옵션으로 method를 받으면, 밑의 GetMapping 등과 같은 역할을 수행하게 된다. 메서드를 명시해주지 않을 경우 모든 메서드를 다 받는다.

@GetMapping(path="")

RequestMapping 의 path+ GetMapping의 path로 오는 GET 요청을 처리한다.
oooMapping 류의 애너테이션들은 path= 옵션을 주지 않을 때는 단일한 path밖에 전달할 수 없지만, 옵션을 주고 {}를 사용할 경우 여러 개의 url을 처리하게 만들 수도 있다.

  • @PathVariable
    path로 제공받은 변수를 가리킨다. 여러 개를 받을 경우, name을 받아 구분한다.
  • @RequestParam
    QueryParameter로 제공받은 변수를 가리킨다. 쿼리로 들어온 변수의 이름과 서버 내에서의 변수 이름이 다를 경우에는 name을 받아 매칭해 줄 수 있다.
  • QueryParameter로 제공받은 변수를 객체로 만들어 전달할 수도 있다.

@PostMapping(path)

RequestMapping의 path + PostMapping의 path로 오는 POST 요청을 처리한다.

  • @RequestBody
    POST 를 사용할 때 PathVariable이나 QueryParameter를 사용하지 않고 바디에 내용을 담아 보내기 때문에, @RequestBody 애너테이션을 사용한다.
  • 주로 JSON 객체를 사용한다.

@PutMapping(path)

RequestMapping의 path + PutMapping의 path로 오는 PUT 요청을 처리한다.

  • 객체를 받아 없으면 생성하고, 이미 있으면 새로 받은 내용으로 수정한다.

@DeleteMapping

RequestMapping의 path + DeleteMapping의 path로 오는 DELETE 요청을 처리한다.

  • @PathVariable을 받아 해당하는 정보를 삭제할 수 있다.

DTO

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonNaming(value= PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRequest {
    private String userName;
    private Integer userAge;
    private String email;
    private Boolean isKorean;
}

@Data

Lombok의 애너테이션으로, 해당하는 클래스의 모든 멤버변수를 가지고 Getter, Setter, toString() 메서드를 생성한다.

@AllArgsConstructor

Lombok의 애너테이션으로, 해당하는 클래스의 모든 멤버변수를 받아 생성과 함께 초기화하는 생성자메서드를 자동으로 만들어준다.

@NoArgsConstructor

Lombok의 애너테이션으로, 해당하는 클래스의 기본 생성자 메서드를 자동으로 만들어준다.

@JsonNaming

전달받는 Json의 네임 컨벤션을 자동으로 바꿔준다. 위에서는 value로 PropertyNamingStrategies.SnakeCaseStrategy.class를 받았는데, 요청하는 쪽에서 보낸 JSON이 Snake Case일 때 이를 자동으로 CamelCase로 바꿔 인식하게 한다.
PropertyNamingStrategy는 deprecated 버전으로 사용하지 않는 것이 좋다.


Response Entity

@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ResponseApiController {

    @GetMapping("")
    public ResponseEntity<UserRequest> user(){

        var user= new UserRequest();
        user.setUserName("ABC");
        user.setUserAge(10);
        user.setEmail("aaaa@aaa@com");

        log.info("user: {}",user);

        var response = ResponseEntity.
                status(HttpStatus.CREATED).
                header("x-custom","hi").
                body(user);
        return response;
    }
}

응답을 plain text로 전달하는 경우는 별로 없고, 대부분의 경우 JSON 객체를 전달한다고 보면 된다.
상황에 따라 StatusCode를 다르게 응답해야 할 때가 있는데, 이 때 ResponseEntity를 사용해 응답을 수정해 반환하면 된다. 이 ResonseEntity는 주로 예외를 처리할 때 사용한다.


ObjectMapper

ObjectMapper는 스프링 부트에서 JSON을 자바 DTO형태로, 그리고 DTO에서 JSON으로 바꿔주는 역할을 맡고 있는 Jackson 라이브러리이다. 스프링 부트는 기본적으로 Jackson을 사용하나, 잭슨 외에도 Gson 등 다양하게 존재한다.

@SpringBootTest
class RestApiApplicationTests {

	@Autowired
	private ObjectMapper objectMapper;

	@Test
	void contextLoads() throws JsonProcessingException {
		var user = new UserRequest();
		user.setUserName("aaa");
		user.setUserAge(20);
		user.setEmail("aaa@aaa.com");
		user.setIsKorean(true);

		// dto -> json
		var json = objectMapper.writeValueAsString(user);
		System.out.println("json = " + json);

		// json -> dto
		var dto = objectMapper.readValue(json, UserRequest.class);
		System.out.println("dto = " + dto);
	}
}

Getter, Setter, toString, 생성자 메서드를 모두 갖춘 UserRequest 객체를 직렬화 및 역직렬화하면 만족스러운 결과가 나오지만, Getter나 Setter가 없으면 제대로 실행되지 않는다.

예시로 getter()들을 없애버리면 ObjectMapper는 JSON을 객체로 변환하지 못한다. ObjectMapper가 객체의 변수가 아니라 getter 메서드를 사용하기 때문이다.

Getter를 아예 새로 만들어보면 확실히 알 수 있는데, 실제 멤버변수와 다른 이름을 주면, get으로 시작하는 메서드를 찾아 그 뒤에 오는 이름을 가지고 json 키값으로 삼는 것을 볼 수 있다.

즉 간혹 가다가 특정 필드를 찾지 못하는 오류가 나는 경우는 클래스에서 누군가가 특정 get 메서드를 만들어둔 것일 수 있다. 이런 상황을 피하기 위해 @JsonIgnore 애너테이션을 붙이면 JSON 생성에 상관없는 메서드가 사용되는 것을 막을 수 있다.

반대로 특정 프로퍼티의 이름을 바꿔 넣어줄 수도 있다. 멤버변수에 애너테이션으로 @JsonProperty를 붙이고 바꿀 이름을 값으로 주면, JSON으로 변환할 때 애너테이션에서 설정한 이름으로 들어간다.
* 이 에너테이션을 사용하면 Getter나 setter가 없어도 멤버변수와 매칭이 된다.

또, 생성자 메서드를 private으로 생성한다 할지라도 ObjectMapper는 리플렉션을 기반으로 동작하기 때문에 접근제어자와 상관없이 인스턴스를 생성할 수 있다.

JSON -> DTO
setter 메서드를 참고한다. 없으면 getter 메서드를 참고하여 세팅한다.

기본적으로 클래스를 설계할 때 필요한 getter, setter, toString() 등을 다 갖춰놓고 일부 예외만 애너테이션으로 처리하는 것이 가장 바람직하다.

post-custom-banner

0개의 댓글