Spring

[Spring] JSON 응답과 요청 처리(@RestController, @RequestBody, ResponseEntity)

랩실외톨이 2023. 12. 31. 07:50
반응형

 

 

 

이 포스팅에서는 JSON 응답과 요청 처리에 대해서 다룹니다.

 

스프링 MVC에서 JSON 응답과 요청을 처리하는 방법을 살펴보겠습니다.

JSON(JavaScript Object Notation)은 간단한 형식을 갖는 문자열로 데이터 교환에 주로 사용합니다.

 

 

{
    "members": 
    {
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
    }
}

 

 

중괄호를 사용해서 객체를 표현하고, 객체는 (이름, 값) 쌍을 갖습니다. 이때 이름은 콜론(:)으로 구분합니다.

위의 예의 경우 name인 데이터의 값은 Molecule Man입니다.

문자열, 숫자, 불리언, null, 배열, 다른 객체 등이 올 수 있습니다.

 

그럼 스프링 MVC에서 객체를 자바 객체를 JSON으로 변환해 보겠습니다.

그러기 위해서는 Jackson 라이브러리를 추가하면 됩니다.

pom.xml에 의존성을 추가해 보겠습니다.

 

		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.9.4</version>
		</dependency>
		<!-- java8 date/time -->
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
			<version>2.9.4</version>
		</dependency>

 

 

Jackson은 프로퍼티의 이름과 값을 JSON 객체의 (이름, 값) 쌍으로 사용합니다.

프로퍼티 타입이 배열이나 List인 경우 JSON 배열로 변환됩니다.

스프링에서 MVC에서 JSON 형식으로 데이터를 응답하기 위해서는 @Controller 대신에 @RestController를 사용하면 됩니다.

 

RestMemberController.java

 

 

@RestController
public class RestMemberController {
  private MemberDao memberDao;
  private MemberRegisterService registerService;

  @GetMapping("/api/members")
  public List<Member> members() {
    return memberDao.selectAll();
  }

  @GetMapping("/api/members/{id}")
  public Member member(@PathVariable Long id, HttpServletResponse response) throws IOException {
    Member member = memberDao.selectById(id);
    if (member == null) {
     response.sendError(HttpServletResponse.SC_NOT_FOUND);
     return null;
    }
    return member;
  }

  public void setMemberDao(MemberDao memberDao) {
    this.memberDao = memberDao;
  }

  public void setRegisterService(MemberRegisterService registerService) {
    this.registerService = registerService;
  }
}

 

 

@RestController 애노테이션을 사용했고, 메서드의 리턴 값으로 일반 객체를 사용했습니다.

@RestController 애노테이션을 붙인 경우 스프링 MVC 요청 매칭 애노테이션을 붙인 메서드는 리턴한 객체를 알맞은 형식으로 변환해서 응답 데이터로 전송합니다. 이때 클래스 패스에 Jackson이 존재하면 JSON 형식의 문자열로 변환해서 응답합니다.

 

 

 

 

반응형

 

 

 

 

 

위의 코드를 실행시키기 위해서 ControllerConfig에 빈을 등록하겠습니다.

 

@Bean
public RestMemberController restApi() {
	RestMemberController cont = new RestMemberController();
	cont.setMemberDao(memberDao);
	cont.setRegisterService(memberRegSvc);
	return cont;
}

 

 

그럼 실행시켜 보겠습니다.

 

 

 

그런데 화면을 보면 password의 값도 그대로 리턴되는 것을 확인할 수 있습니다. 하지만 암호와 같이 민감한 데이터는 응답 결과에 포함시키면 안 되므로 응답 결과에서 제외시켜야 합니다.

Jackson이 제공하는 @JsonIgnore 애노테이션을 사용하면 JSON 응답에 포함시키지 않을 대상을 제거할 수 있습니다.

 

 

 

password에 @JsonIgnore 애노테이션을 붙여서 결과에서 제외한 모습입니다.

위의 결과에서 시간을 보면 LocalDateTime이 JSON에서 배열 값으로 변한 것을 볼 수 있습니다.

 

 

 

 

 

반응형

 

 

 

 

 

 

이를 날짜 형식으로 변환 처리하기 위해 @JsonFormat 애노테이션을 사용해 보겠습니다.

 

Member.java

 

 

@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime registerDateTime;

 

 

 

지정한 패턴대로 날짜 형식이 바뀐 걸 확인할 수 있습니다. shape나 pattern 속성을 사용해 자유롭게 원하는 형식대로 날짜를 지정할 수 있습니다.

 

하지만 모든 날짜를 이런 식으로 지정하는 것은 번거롭습니다. Jackson의 변환 규칙을 모든 날짜 타입에 적용하려면 스프링 MVC 설정을 변경해야 합니다.

 

MvcConfig.java

 

 

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
			.json()
			.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
			.build();
	converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
}

 

 

Jackson2ObjectMapperBuilder는 ObjectMapper를 쉽게 생성하기 위해서 스프링이 제공하는 클래스입니다.

featuresToDisable 설정은 Jackson이 날짜 형식을 출력할 때 유닉스 타임스탬프로 출력하는 기능을 비활성화합니다.

비활성화를 하게 되면 날짜 타입 값을 ISO-8601 형식으로 출력합니다.

이를 MappingJackson2HttpMessageConverter에 첫 번째 항목으로 등록하면 설정이 끝납니다.

 

 

 

 

 

반응형

 

 

 

 

 

지금까지 응답이 JSON으로 반환하는 것에 대해 살펴보았습니다. 이제 반대로 JSON 형식의 요청 데이터를 자바 객체로 변환하는 기능에 대해 살펴보겠습니다. 

 

POST, PUT 방식을 사용하면 name=이름&age=17과 같은 쿼리 문자열 형식이 아니라 다음과 같은 JSON 형식의 데이터를 요청 데이터로 전송할 수 있습니다.

 

{"name":"이름", "age":17}

 

 

JSON 형식으로 전송된 요청 데이터를 커맨드 객체로 전달받으려면 커맨드 객체에 @RequestBody 애노테이션을 붙이면 됩니다.

 

 

RestMemberController.java

 

  @PostMapping("/api/members")
  public ResponseEntity<Object> newMember(
      @RequestBody @Valid RegisterRequest regReq, HttpServletResponse response) throws IOException {
    try {
      Long newMemberId = registerService.regist(regReq);
      URI uri = URI.create("/api/members/" + newMemberId);
      return ResponseEntity.created(uri).build();
    } catch (DuplicateMemberException dupEx) {
      return ResponseEntity.status(HttpStatus.CONFLICT).build();
    }
  }

 

 

@Valid 애노테이션을 사용한 경우 검증에 실패하면 400(Bad Request) 상태 코드를 응답합니다.

그럼 이제 회원 가입 POST 폼을 JSON 형식으로 전송해 보겠습니다.

 

 

{
    "email":"b@b.com",
    "password":"1234",
    "confirmPassword":"1234",
    "name":"b"
}

 

 

 

 

포스트맨을 사용해 JSON 데이터를 전송하고 나니 응답 코드로 201 Created를 전송받았습니다.

 

 

 

중복된 코드를 한 번 더 전송하니 이번엔 409 Conflict 코드를 응답받았습니다.

 

그럼 이번에는 JSON 형식의 데이터를 날짜 형식으로 변환하는 방법에 대해 알아보겠습니다.

별도의 설정을 하지 않으면 LocalDateTime을 Date로 변환하는데, 특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하고 싶다면 @JsonFormat 애노테이션의 pattern 속성을 사용해서 패턴을 지정합니다.

 

@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;

 

 

특정 속성이 아니라 해당 타입을 갖는 모든 속성에 적용하고 싶자면 스프링 MVC 설정을 추가하면 됩니다.

 

MvcConfig.java

 

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
            .json()
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
            .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
            .simpleDateFormat("yyyy-MM-dd HH:mm:ss")
            .build();
        converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
    }

 

 

deserializerByType()은 JSON 데이터를 LocalDateTime 타입으로 변환할 때 사용할 패턴을 지정하고,

simpleDateFormat()은 Date 타입으로 변환할 때 사용자 패턴을 지정합니다.

 

 

 

 

반응형

 

 

 

 

 

지금까지의 코드는 상태 코드를 지정하기 위해 HttpServletResponse의 setStatus() 메서드와 sendError() 메서드를 사용했습니다.

하지만 이 방법은 JSON 형식이 아니라 HTML로 응답 결과를 제공하게 됩니다.

API를 호출하는 프로그램 입장에서 JSON, HTML 응답을 모두 처리하는 것은 부담스럽습니다.

400, 500 에러처럼 요청 처리에 실패한 경우 JSON 형식의 응답 데이터를 전송해야 응답을 처리할 수 있습니다.

 

요청이 정상, 비정상인 경우 모두 JSON 응답을 전송하는 방법은 ResponseEntity를 사용하는 것입니다.

 

ErrorResponse.java

 

 

public class ErrorResponse {
	private String message;

	public ErrorResponse(String message) {
		this.message = message;
	}

	public String getMessage() {
		return message;
	}

}

 

 

RestMemberController.java

 

@GetMapping("/api/members/{id}")
  public ResponseEntity<Object> member(@PathVariable Long id) {
    Member member = memberDao.selectById(id);
    if (member == null) {
      return ResponseEntity
          .status(HttpStatus.NOT_FOUND)
          .body(new ErrorResponse("no member"));
    }
    return ResponseEntity.ok(member);
  }

 

 

스프링 MVC는 리턴 타입이 ResponseEntity면 body로 지정한 객체를 사용해서 변환을 처리합니다.

이 코드의 경우 member body로 지정했는데 이를 JSON으로 변환하게 됩니다.

성공한 경우에는 200(ok), 실패한 경우 404(not found)를 반환하게 됩니다.

 

 

회원 번호가 존재하지 않는 경우

 

회원 번호가 존재하는 경우

 

 

 

 

 

 

반응형

 

 

 

 

 

 

하지만 위의 예시처럼 코드를 작성하면 ResponseEntity를 생성하는 코드가 중복됩니다.   

@ExceptionHandler 애노테이션을 적용한 메서드에 에러 응답을 처리하도록 하면 중복을 없앨 수 있습니다.

 

@GetMapping("/api/members/{id}")
  public Member member(@PathVariable Long id) {
    Member member = memberDao.selectById(id);
    if (member == null) {
      throws new MemberNotFoundException();
    }
    return member;
  }
  
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
	return ResponseEntity
    		.status(HttpStatus.NOT_FOUND)
            .body(new ErrorResponse("no member"));
}

 

 

이 코드에서 member() 메서드는 Member 자체를 리턴합니다.

회원 데이터가 존재하면 Member 객체를 리턴하므로 JSON으로 변환한 결과를 응답합니다.

회원이 존재하지 않으면 MemberNotFoundException 발생합니다.

그렇게 되면 @ExceptionHandler에 의해  handleNoData()가 에러를 처리하게 됩니다.

 

@RestControllerAdvice 애노테이션을 이용해서 에러 처리 코드를 별도 클래스로 분리할 수도 있습니다.

@ControllerAdvice와 동일하지만 JSON이나 XML과 같은 형식으로 변환하는 것이 차이점입니다.

 

ApiExceptionAdvice.java

 

@RestControllerAdvice("controller")
public class ApiExceptionAdvice {

	@ExceptionHandler(MemberNotFoundException.class)
	public ResponseEntity<ErrorResponse> handleNoData() {
		return ResponseEntity
				.status(HttpStatus.NOT_FOUND)
				.body(new ErrorResponse("no member"));
	}
    
}

 

 

이처럼 에러 처리 코드가 한 곳에 모여 효과적으로 에러 응답을 관리할 수 있습니다.

 

 

 

반응형

 

 

 

 

한편, @Valid 애노테이션을 붙인 커맨드 객체가 값 검증에 실패하면 400 상태 코드를 응답합니다.

문제는 이를 HTML로 응답을 전송한다는 점입니다. 이를 JSON으로 제공하고 싶다면 Errors 타입 파라미터를 추가해 직접 에러 응답을 생성하면 됩니다.

 

@PostMapping("/api/members")
  public ResponseEntity<Object> newMember(
      @RequestBody @Valid RegisterRequest regReq, Errors errors) {
		
		if (errors.hasErrors()) {
			String errorCodes = errors.getAllErrors()
					.stream()
					.map(error -> error.getCodes()[0])
					.collect(Collectors.joining(","));
			return ResponseEntity
					.status(HttpStatus.BAD_REQUEST)
					.body(new ErrorResponse("errorCodes = " + errorCodes));
		}
		
    try {
      Long newMemberId = registerService.regist(regReq);
      URI uri = URI.create("/api/members/" + newMemberId);
      return ResponseEntity.created(uri).build();
    } catch (DuplicateMemberException dupEx) {
      return ResponseEntity.status(HttpStatus.CONFLICT).build();
    }
  }

 

 

hasErrors() 메서드를 이용해 검증 에러가 존재하는지 확인하고, 존재하면 getAllErrors() 메서드로 모든 에러 정보를 구하고,

각 에러코드 값을 연결할 문자열을 생성해 errorCodes 변수에 할당합니다.

이를 통해 에러가 발생했을 경우 HTML 대신 JSON 응답이 발생하게 됩니다.

 

 

출처: 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문

 

 

반응형