1. 에러 발생
마이페이지를 위해 Member를 조회한 뒤, dto를 통해 반환하는 API입니다.
406 HttpMediaTypeNotAcceptableException 에러가 발생했습니다.
Controller
@GetMapping(value = "/user")
public ResponseEntity<MemberInfoResponse> getUserInfo(@AuthenticationPrincipal MemberPrincipal memberPrincipal) {
Long id = memberPrincipal.getMember().getId();
MemberInfoResponse memberInfoResponse = memberService.getMemberInfo(id);
return ResponseEntity.ok(memberInfoResponse);
}
Service
public MemberInfoResponse getMemberInfo(Long id) {
Member member = memberQueryRepository.findMemberById(id).orElseThrow(() -> new MemberNotFoundException());
return MemberInfoResponse.of(member);
}
Dto
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberInfoResponse {
private String email;
private String username;
private String oauthId;
private OAuthProvider oAuthProvider;
private boolean isBan;
private AccountType accountType;
public static MemberInfoResponse of(Member member) {
return MemberInfoResponse.builder()
.email(member.getEmail())
.username(member.getUsername())
.oauthId(member.getOauthId())
.oAuthProvider(member.getOAuthProvider())
.isBan(member.isBan())
.accountType(member.getAccountType())
.build();
}
}
Exception
org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
받아들일 수 있는 표현을 못 찾아..? 이게 무슨 소리야!
2. HttpMediaTypeNotAcceptableException
Exception thrown when the request handler cannot generate a response that is acceptable by the client.
공식 문서를 보니 클라이언트가 받아들일 수 있는 응답을 생성할 수 없을 때 발생하는 예외라고 한다.
그러면 언제 어디에서 발생하는가?
구글링을 한 결과 AbstractMessageConverterMethodProcessor라는 클래스의 writeWithMessageConverters 함수에서 발생하는 예외라는 결론이 나왔습니다.
요청의 Accept 헤더에서 나타낸 조건이 메시지 컨버터에서 충족되지 않을 때 발생한다고 합니다.
AbstractMessageConverterMethodProcessor 클래스는 무슨 역할을 해?
Extends AbstractMessageConverterMethodArgumentResolver with the ability to handle method return values by writing to the response with HttpMessageConverters
이를 해석해보면 다음과 같습니다.
AbstractMessageConverterMethodArgumentResolver를 상속한 것으로, HttpMessageConverters를 사용하여 메서드의 반환 값을 응답으로 작성할 수 있는 기능을 추가했습니다.
3. HttpMessageConverts
공식문서를 보니 AbstractMessageConverterMethodProcessor 클래스에서 HttpMessageConverts를 사용해 반환 값을 응답으로 작성해야 하는데 하지 못 한다는 결론이 나왔습니다.
HttpMessageConverts란?
read, write, canread, canwrite등 다양한 메서드를 지원하는 클래스이며 StringHttpMessageConverter, MappingJackson2HttpMessageConventer 등 다양한 구현 클래스를 지원합니다.
이때 Read, Write 함수를 주의깊게 봐야 합니다.
Read
- 주어진 입력 메시지에서 주어진 타입의 객체를 읽어 들여 반환합니다.
- @RequestBody 어노테이션을 지정할 경우, Request Header의 타입에 맞게 Body 정보를 변환합니다.
write
- 주어진 객체를 주어진 출력 메시지에 씁니다.
- @ResponseBody 어노테이션을 지정할 경우, 결과를 View가 아닌 Content Type에 맞게 반환합니다.
- 객체일 경우 Json으로 반환합니다.
@RestController
필자는 Controller가 아닌 RestController 어노테이션을 사용하고 있었다.
RestController는 Controller과 달리 Restful 한 웹 서비스를 제공하기 위한 Controller입니다.
@ResponseBody를 포함하고 있어 모든 메서드의 반환 값이 HTTP 응답의 본문으로 변환되어 전송됩니다.
그러면 맨 위에 있는 Controller의 반환 값은 Json임을 알 수 있습니다.
return ResponseEntity.ok(memberInfoResponse);
HttpMessageConverts를 사용해 반환 값을 응답으로 작성해야 하는데 하지 못 한다는 결론이 나왔습니다.
그러면 @ResponseBody에 문제가 있는 거네?
4. Request & Response Body
1. @RequestBody
You can use the @RequestBody annotation to have the request body read and deserialized into an Object through an HttpMessageConverter The following example uses a @RequestBody argument:
이를 해석해 보면 다음과 같습니다.
RequestBody는 HTTP 요청의 본문을 읽고 역직렬화하여 HttpMessageConverter을 통해 객체로 변환할 수 있습니다.
이때 HttpMessageConverter이 상황에 맞는 구현체를 사용하게 해 줍니다. 필자의 경우 application/json이므로 MappingJackson2HttpMessageConverter이다. MappingJackson2HttpMessageConverter은 ObjectMapper에 의해 Json 문자열을 객체로 역직렬화시켜줍니다.
*역직렬화 -> json, Byte등의 형식을 객체로 만드는 과정
2. @ResponseBody
You can use the @ResponseBody annotation on a method to have the return serialized to the response body through an HttpMessageWriter.
이를 해석해 보면 다음과 같습니다.
ResponseBody는 HttpMessageWriter을 통해 반환 값을 응답 본문으로 직렬화하여 전송할 수 있습니다.
이때 @RequestBody와 마찬가지로 MappingJackson2HttpMessageConverter 구현체를 사용해 객체를 Json으로 직렬화시켜줍니다.
*직렬화 -> 객체를 json, Byte등 통신하기 쉬운 형태로 만드는 과
5. ObjectMapper
dto의 모든 멤버 변수가 private인 것으로 가정하고 설명하겠습니다.
ObjectMapper는 기본적으로 필드를 대상으로 하는데 그중에 public 필드를 대상으로 합니다.
하지만 저희의 dto는 private이므로 에러가 생깁니다.
그러면 public으로 하면 되는 거 아닌가요?
음... 괜찮은 생각일 수도 있지만 public으로 하면 안 되는 이유는 다음 글에서 설명드리겠습니다.
@Getter
Getter 어노테이션을 사용하면 모든 멤버 변수에 대해 get~~ 메서드가 생성됩니다.
이때 Jackson은 getter 메서드 중 get을 제외한 이름의 첫 문자를 소문자로 치환하여 필드명을 유추합니다.
쉽게 말하자면 getter을 이용해 private 변수를 public처럼 인식할 수 있게 한다는 것입니다.
6. 추가적인 정보
ObjectMapper은 역직렬화 과정에서 객체를 생성할 때 기본생성자를 사용해 객체를 만듭니다.
객체를 생성한 뒤, getter을 이용해 정보들을 동적으로 추가합니다. (Reflection)
그러므로 기본 생성자를 생성해 주는 @ NoArgsConstructor 어노테이션을 붙여야 합니다.
@JsonIgnore 등 다양한 방법이 있지만, 다른 글에서 더 자세히 설명하겠다.
참고 문서
HttpMediaTypeNotAcceptableException 공식 문서