Trouble-Shooting

406 Error - Not Acceptable

LEE티씨 2023. 3. 13. 18:36

소개 배경

최근 순환 참조 에러를 만나 이를 해결하기 위해 API Response Data를 DTO화 시키다가 만나게 된 에러이다.

 

난생 처음보는 오류와 함께 아래와 같은 ERROR도 아닌 WARN이 출력된다.

 

에러메시지

2023-03-13 15:14:09.835  WARN 11696 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]

 

 

PostMan Response

 

 

개발을 하며 처음 겪는 에러였기 때문에 우선, 에러를 파악하고자 HTTP 응답 코드를 살펴보던 도중 406에러에 대한 설명을 보게되었다.

 

What is 406?

 

💡 406 Not Acceptable
이 응답은 서버가 서버 주도 콘텐츠 협상 을 수행한 이후, 사용자 에이전트에서 정해준 규격에 따른 어떠한 콘텐츠도 찾지 않았을 때, 웹 서버가 보냅니다.

 

일단 설명부터 직관적이지 못하다.. 서버 주도 콘텐츠 협상? 

 

알아보니 서버에서 반환 값을 정의하여 사전 콘텐츠 협상 헤더에 정의 된 허용 가능한 값 목록과 일치하는 응답을 생성하여 반환값을 선택하는 것이라고 한다.

 

자세한 설명은 아래를 참고바란다.

 

GitHub - seonghoo1217/TodayILearnd: It's seonghoo1217's TIL

It's seonghoo1217's TIL. Contribute to seonghoo1217/TodayILearnd development by creating an account on GitHub.

github.com

 

 

다시 본론으로 돌아와서 저말의 뜻은 서버측에서 응답을 주기위해 시도했으나, Accept 등의 헤더에 적혀있는 형식을 생성해 낼 수 없을 경우 발생한다고한다.

 

스프링부트는 Content-Type에 선언된 형식으로 변환이 불가능 할 경우 위의 로그와 함께, 406 Not Acceptable 에러를 발생시킨다. Spring에서는 데이터를 HttpMessageConverter를 이용해서 변환한다.

 

해당 작업은 아래와 같은 순서로 동작한다.

  1. 응답하려는 MediaType을 식별한다.
  2. 등록된 HttpMessageConverter에서 MediaType으로 데이터를 변환 시킬 수 있는 녀석을 찾는다.
  3. 해당 HttpMessageConverter를 이용하여 데이터를 변환시킨다.

그럼 이걸 해결하기 위해서는 반환타입에 문제를 해결하거나 헤더의 생성이 막히는 이유를 찾아야하는데, 전자의 이유는 문제가 없을 것이라 생각해 그 이유를 구글링하였고 그 결과 , 다음 이유들이 원인이 될 수 있었다.

 

1. Jackson 라이브러의 부재

해당 원인이 문제를 일으키기에는 기본적으로 gradle에 추가하는 dependencyspring-boot-starter 에 기본적으로 포함되어 있어 문제를 일으킬 것같진않았다.

implementation 'org.springframework.boot:spring-boot-starter'

 

2. 적절한 뷰 리졸버가 없는 경우

MVC 형태의 프로젝트였다면 한번 쯤 의심할 만한 원인이었으나 나는 현재 Restful Server를 구축 하였기에 이 또한 직접적인 원인은 아닐 것이라 판단했다.

 

3. 인증되지 않은 요청의 경우

이는 현재 구성중인 프로젝트가 JWT 토큰 전략을 사용하여 Custom Filter를 구성해 만약, 허가되지 않은 요청의 경우 401을 반환하도록 만들었기에 이 또한 의심할 여지가 없었다. (실제로 Filter에 logging 작업을 하며 확인한 결과 문제가 없었다.)

 

4. 반환하는 Respons Entity 혹은 DTO에 Getter가 없는 경우

맨 처음에는 당황했다.

"이런 단순한 이유로 오류가 난다고?" 생각했기 때문이다.

이 원인을 알 수 있었던 StackOverFlow 질문을 보면 도움이 되는 답변이 하나있었다.

 

I believe by default auto discovery is on and will try to discover your getters. You can disable it with 
@JsonAutoDetect(getterVisibility=Visibility.NONE)
, and in your example will result in 
[].

해석하자면 기본적으로 Jackson의 자동 검색이 켜져 있고 게터를 검색하려고 시도할 것이며 @JsonAutoDetect(getterVisibility=Visibility.NONE)를 사용하여 이를 비활성화할 수 있다는 뜻이다.

 

원인 분석

아까 Accept와 같은 헤더에 작성된 형식을 생성하지 못할 때 406에러를 리턴한다고 말했는데 Aceeptapplication/json로 설정되어있어 서버는 반드시 JSON 형태의 Response를 반환해야한다.

 

그런데 이때 JSON 형태를 생성하는 과정에서 spring-boot-starter에 속해있는 Jackson 라이브러리가 이용되는데 이때, 
반환 객체에 '@Getter' 어노테이션을 제거되어 있다면 JSON 객체를 생성하지 못하고 406에러를 반환하게 된것이다.

 

기본적으로 우리가 클라이언트에게 Response를 주기위해 Response Entity 또는 DTO를 사용하고, 이는 모두 application/json 타입이다. 이를 구성할 때 Getter를 통해 필드값을 가져와 재구성하여 최종적으로 JSON 객체를 만드는데 Getter가 제거되어있으니 당연히 되지 않았던 것이다.

 

문제코드

DTO 사용 컨트롤러

@GetMapping("/userInfo/one")
public ResponseEntity<?> getOneOfUserInfo(@RequestHeader("userInfo")String accessToken){
   System.out.println("userInfoaccess="+accessToken);
   String user_Nickname = jwtService.extractNickname(accessToken).get();
   log.info("user_Nickname+"+user_Nickname);
   MemberResponseDTO memberResponseDTO = memberService.getMemberInfoByNickname(user_Nickname).toDTO();

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

 

해당 코드를 보면 membrService라는 Service Layer에서 getMemberInfoByNickname이라는 메소드를 통해 Member Entity를 반환하고 MemberEntity 내부에 toDTO()라는 빌더를 만들어 이를 DTO화시킨다.

 

MemberSocial Entity 일부

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
public class MemberSocial extends BaseEntity {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long member_seq;


	...

   public MemberResponseDTO toDTO(){
      return MemberResponseDTO
            .builder()
            .member_seq(member_seq)
            .authToken(authToken)
            .member_imgUrl(member_imgUrl)
            .email(email)
            .nickname(nickname)
            .oauthId(oauthId)
            .socialType(socialType)
            .refreshToken(refreshToken)
            .role(role)
            .build();
   }
}

 

원래의 DTO 상태

@Builder
public class MemberResponseDTO {
   private Long member_seq;

   private String email;

   private String nickname;

   private String member_imgUrl;

   private String oauthId;

   private MemberRole role;

   private SocialType socialType;

   private String authToken;

   private String refreshToken;

}

 

이를 Lombok 라이브러리의 @Getter 어노테이션을 붙여 해결하였다.

package project.mogakco.domain.member.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import project.mogakco.domain.member.entity.member.MemberRole;
import project.mogakco.domain.member.entity.member.SocialType;


@Builder
@Getter
public class MemberResponseDTO {
   private Long member_seq;

   private String email;

   private String nickname;

   private String member_imgUrl;

   private String oauthId;

   private MemberRole role;

   private SocialType socialType;

   private String authToken;

   private String refreshToken;

}

 

결론

무한참조를 해결하다 우연히 만난에러이지만, 새삼스럽게 Spring Framework을 사용하면서도 정말 내부적 동작원리를 잘알지못하고 사용하는 것 같다. 

 

역시나 명세와 문서는 항상 참고하며 공부를 해야하는 것 같다.