본문 바로가기
[Spring]/Spring MVC

Spring에서 HTTP 통신을 통해 JSON 형식의 데이터를 주고 받을 때 사용하는 애너테이션을 정리해보자.

by 황원용 2023. 8. 14.
728x90
💡 Java + Spring Boot을 이용하여 클라이언트와 http 통신을 할 때 전달받는 http 요청 메시지나, 전달하는 응답 메시지는 json 형식의 데이터로 이루어지는 경우가 많다. 이때 자바 객체(dto)에서 json 데이터로 어떻게 변환이 되는지를 잘 모르고 있는 것 같아 한 번 정리해보려고 한다.

 

우선 HTTP 통신에 대해 기본적인 내용을 훑고 넘어가자.

 

  • HTTP 통신 중 가장 많이 사용하는 HTTP/1.1 기반의 통신 방식에 대해 알아보겠다.
  • HTTP/1.1 기반의 통신 방식은 Request-Response 방식으로 동작한다. 클라이언트가 서버에 요청을 하면, 서버는 클라이언트의 요청에 대해 응답한다.
  • 이때, 클라이언트는 HTTP 메서드(GET, POST, PUT, DELETE 등)를 이용하여 요청을 보내며, 서버는 요청에 따라 처리된 결과를 HTTP 상태 코드(200 OK, 404 Not Found 등)와 함께 응답한다.

 

요청과 응답의 구조는 다음과 같다.

[Request]
HTTP Method (GET, POST, PUT, DELETE 등)
URI (Uniform Resource Identifier)
HTTP Version

[Headers]
Key: Value
Key: Value
...

[Body]
(Optional)

---------------------------------------

[Response]
HTTP Version
Status Code
Status Message

[Headers]
Key: Value
Key: Value
...

[Body]
(Optional)

메시지에서 [Body] 안에는 어떤 내용이 들어갈까?

  • HTTP 프로토콜에서 메시지 바디(body)는 요청(request)이나 응답(response)에 대한 본문 데이터를 담고 있는 부분이다. 
  • 이 데이터의 형식은 요청/응답의 목적에 따라 달라질 수 있다.
  • 대표적인 데이터 형식으로는 JSON, XML, HTML, 텍스트 등이 있으며, 이러한 데이터 형식으로 주고받은 데이터는 개발자가 분석 가능한 형태로 파싱 되어 처리된다.
  • 예를 들어, 클라이언트에서 서버로 POST 요청을 하면 요청 바디에는 클라이언트에서 서버로 전송하고자 하는 데이터가 들어있을 것이다.
  • 같은 방식으로 서버에서 클라이언트로 응답을 보낼 때도 응답 바디에 데이터가 들어있을 것이다.
  • 이 데이터를 분석하여 클라이언트와 서버는 데이터를 서로 주고받고 원하는 대로 처리할 수 있다.

 

 

Request 관련 애너테이션

@ModelAttribute

  • HTTP 요청의 다수의 매개변수(쿼리 스트링, 쿼리 파라미터)들을 일대일로 자바 객체에 바인딩하는 데 사용된다.
  • 컨트롤러의 메서드 파라미터에 적용할 수 있다.
  • 클라이언트가 전송한 요청 데이터를 자동으로 도메인 객체나 DTO(Data Transfer Object)로 변환하여 메서드에 전달할 수 있다.
  • 변환이 아닌 바인딩을 사용하기 때문에  Setter 메서드가 없으면 저장되지 않는다.
  • 스프링 MVC에서는 컨트롤러 메서드의 파라미터가 복합 타입(예: DTO 클래스)인 경우 암묵적으로 @ModelAttribute를 적용한다.
    • @ModelAttribute 애너테이션을 생략해도 된다는 말이다.
    • 따라서 코드 예제에서와 같이 @ModelAttribute 애너테이션을 생략해도 요청 매개변수가 DTO 객체에 자동으로 바인딩된다.

 

예시 코드

@GetMapping("/sample")
public ResponseEntity<SampleDto> getSampleData(@ModelAttribute SampleDto sampleDto) {
    // 로직 구현
    return ResponseEntity.ok(sampleDto);
}

 

 

@RequestParam

  • HTTP 요청에서 단일 파라미터를 컨트롤러의 메서드 파라미터와 바인딩하는 데 사용된다.
  • RequestParam 역시 생략 가능하다.
  • URL 경로 예시 : /members?id=1

 

예시 코드

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    ...

    @GetMapping
    public ResponseEntity getMemberByParam(@RequestParam("id") Long id) {
        Optional<Member> optionalMember = memberRepository.findById(id);

        MemberDto.Response response = MemberDto.Response.builder()
                .id(optionalMember.get().getId())
                .name(optionalMember.get().getName())
                .age(optionalMember.get().getAge())
                .build();

        return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

 

  • URL 경로에서 ? 뒤에 key-value 쌍을 전달하는 방식이다.

 

 

@PathVariable

  • URL 경로에 있는 변수를 컨트롤러의 메서드 매개변수로 바인딩한다.
  • {}(중괄호) 안에 변수 이름을 작성하며 이름을 지정하지 않으면 메서드의 매개변수 이름으로 자동 지정된다.
  • URL 경로 예시 : /members/{id}이다.

 

예시 코드

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
	
    private final MemberRepository memberRepository;
    
    ...
    
    @GetMapping("/{id}")
    public ResponseEntity getMemberByPathVariable(@PathVariable Long id) {

    Optional<Member> optionalMember = memberRepository.findById(id);

    MemberDto.Response response = MemberDto.Response.builder()
            .id(optionalMember.get().getId())
            .name(optionalMember.get().getName())
            .age(optionalMember.get().getAge())
            .build();

    return new ResponseEntity<>(response, HttpStatus.OK);
    }
}

 

 

  • URL에 member의 id를 입력하고 get 요청을 보내면 db에 id=1인 member의 id, age, name을 찾는 쿼리가 날아간다.

 

 

@PathVarialble VS @RequestParam

  • @PathVariable는 경로 상에서 매개변수 정보를 전달받고,
  • @RequestParam은 HTTP Request URL의 Query Parameter에서 매개변수 정보를 전달받는다.

 

 

@RequestBody

  • HTTP Request Body에 포함된 데이터를 자바 객체로 변환하여 매핑한다.
  • 보통 JSON이나 XML 형식의 데이터가 HTTP 요청 바디에 실려 컨트롤러 단에 오게 되면 이를 자바 객체로 변환하여 자바 언어로 비즈니스 로직을 수행하도록 돕는다.
  • HttpMessageConverter 인터페이스를 구현한 객체에 의해 수행된다.

 

@RequestBody를 메서드 파라미터에 붙이지 않을 경우

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @PostMapping("/dtos")
    public MemberDto.Response createMemberToDto(MemberDto.Post post) {

        Member member = Member.builder()
                .name(post.getName())
                .age(post.getAge())
                .build();

        Member savedMember = memberRepository.save(member);

        MemberDto.Response response = MemberDto.Response.builder()
                .id(savedMember.getId())
                .name(savedMember.getName())
                .age(savedMember.getAge())
                .build();


        return response;
    }
}

 

  • @RequestBody를 붙이지 않아 HTTP 요청 바디에 들어있는 JSON 데이터가 자바 객체로 변환되지 않아 dto 필드의 기본값인 null과 0이 데이터베이스에 그대로 저장되었다.

 

@RequestBody를 메서드 파라미터에 붙인 경우

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @PostMapping("/dtos")
    public MemberDto.Response createMemberToDto(@RequestBody MemberDto.Post post) {

        ...
}

 

  • @RequestBody를 붙이고 실행하면 정상적으로 JSON 데이터 -> 자바 객체로 변환이 되어 HTTP 요청 바디에 있는 데이터를 정상적으로 자바 언어로 다룰 수 있다.

 

 

 

Response 관련 애너테이션

@ResponseBody

  • HTTP 응답의 결과를 가진 자바 객체를 JSON 데이터로 변환한다.
  • 클래스 단, 메서드 단에 붙일 수 있다.
  • HttpMessageConverter 인터페이스를 구현한 객체에 의해 수행된다.

 

@RestController

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 * @since 4.0.1
	 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}
  • @Controller + @ResponseBody로 이루어져 있으며 스프링에게 컨트롤러 클래스임을 알림(빈으로 등록)과 동시에 클래스단에 @ResponseBody 애너테이션을 적용하여 따로 자바 객체를 JSON 응답 데이터로 변환하는 작업을 할 필요 없게 도와준다.

 

ResponseEntity 클래스

  • HTTP 응답 데이터를 보다 개발자 친화적으로 보낼 수 있도록 도와주는 클래스이다.
  • 자바 객체를 JSON 응답 데이터로 변환한다.
  • HttpMessageConverter 인터페이스를 구현한 객체에 의해 수행된다.
  • 이에 더해 응답 데이터에 상태 코드, 헤더 등을 개발자가 원하는 대로 추가할 수 있다.

 

예시 코드

@Controller // 컨트롤러 애너테이션
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @PostMapping("/dtos")
    @ResponseBody // ResponseBody 애너테이션
    public MemberDto.Response createMemberToDto(@RequestBody MemberDto.Post post) {

        Member member = Member.builder()
                .name(post.getName())
                .age(post.getAge())
                .build();

        Member savedMember = memberRepository.save(member);

        MemberDto.Response response = MemberDto.Response.builder()
                .id(savedMember.getId())
                .name(savedMember.getName())
                .age(savedMember.getAge())
                .build();


        return response;
    }

    @PostMapping("/entities")
    public Member createMemberToEntity(@RequestBody MemberDto.Post post) {

        Member member = Member.builder()
                              .name(post.getName())
                              .age(post.getAge())
                              .build();

        Member savedMember = memberRepository.save(member);

        MemberDto.Response response = MemberDto.Response.builder()
                                                        .id(savedMember.getId())
                                                        .name(savedMember.getName())
                                                        .age(savedMember.getAge())
                                                        .build();

        return savedMember;
    }

    @PostMapping("/responseentities")
    public ResponseEntity createMemberToResponseEntity(@RequestBody MemberDto.Post post) {

        Member member = Member.builder()
                              .name(post.getName())
                              .age(post.getAge())
                              .build();

        Member savedMember = memberRepository.save(member);

        MemberDto.Response response = MemberDto.Response.builder()
                                                        .id(savedMember.getId())
                                                        .name(savedMember.getName())
                                                        .age(savedMember.getAge())
                                                        .build();

        return new ResponseEntity<>(response, HttpStatus.CREATED);
    }
}

 

  • createMemberToDto 메서드의 경우 @ResponseBody 애너테이션이 있어 HTTP 응답 데이터로 JSON 데이터가 정상적으로 변환되었음을 확인할 수 있다.

 

  • @ResponseBody를 제거하고 재실행하면 위와 같은 에러가 발생한다.
  • 404 Not Found가 발생한다.

 

  • createMemberToEntity 역시 @ResponseBody가 없으면 위와 같은 에러가 발생한다.

 

  • createMemberToResponseEntity의 경우 반환 타입을 ResponseEntity로 하여 JSON 데이터로 변환하고 상태코드까지 개발자가 설정한  201 Created로 응답하는 것을 확인할 수 있다.

 

 

결론

  • Spring MVC Restful 웹 서비스를 개발할 때 자바 객체와 JSON 데이터 간 변환을 도와주는 여러 애너테이션이 있다.
  • 클라이언트로부터 받은 HTTP 요청은 경로(@PathVariable), 쿼리 스트링(@RequestParam), 요청 바디(@RequestBody) 등의 데이터를 자바 객체로 변환해 주는 애너테이션을 이용하면 된다.
  • HTTP 응답의 경우에는 따로 구분할 필요 없이 @RestController를 사용하자.
  • 그럼 메서드 단에 따로 @ResponseBody를 적용할 필요없이 모든 컨트롤러 메서드에서 자바 객체 -> JSON 데이터로 HTTP 응답 데이터를 자동 변환해 준다.
  • 만약 좀 더 커스터마이징이 필요하다면 ResponseEntity를 사용하여 상태 코드 및 헤더 등을 개발자가 원하는 방향으로 설정할 수 있다.
  • 개발팀에서 응답 데이터에 대한 규약을 만들어 통일하고자 한다면 ResponseEntity를 상속받는 클래스를 만들어 새로운 응답 데이터 형식을 커스텀하면 된다.

 

 

 

참고

뤼튼

 

728x90