[Rest Docs] @AuthenticationPrincipal로 사용으로 인한 data null 오류

ran·2023년 7월 31일

오류

목록 보기
1/1

Rest Docs를 처음 접했기 때문에 무한 구글링과 rest docs 원서를 읽으면서 열심히(?) 테스트 코드를 바탕으로 API 명세서를 만들고 있었다.

잘 만들던 중 모든 파라미터와 요청응답을 맞게 설정했음에도 불구하고 응답 data 부분이 null로 뜨는 에러를 만났다.!
그럼 이제 어떻게 오류를 접했고, 어떻게 해결했는지에 대해 말해보겠다.


우선 나는 회원이 좋아요 한 상품을 조회하는 로직을 작성중이었다. 그에따라 컨트롤러를 아래와 같이 작성했다.

//좋아요한 데이터 상품 목록
    @GetMapping("/v1/users/hearts")
    public CustomResponseEntity<List<DataProductResponse.findHeartDataProducts>> findHeartDataProducts(@AuthenticationPrincipal Long userId){
        return CustomResponseEntity.success(userService.findHeartDataProducts(userId));
    }

@AuthenticationPrincipal을 이용하여 userId를 받아와서 서비스 로직으로 전달했다.

그리고 test code를 작성했다.

@Test
    @DisplayName("좋아요한 데이터 상품 목록")
    public void findHeartDataProducts() throws Exception {

        Category category1 = Category.builder()
                .title("패션").categoryCode(101).build();
        Category category2 = Category.builder()
                .title("사람").categoryCode(102).build();

        User user = new User(1l,"test","test","test","test",null);
        DataProduct dataProduct1 = new DataProduct(1l,1l,"test1",100l,"testing1",1l);
        DataProduct dataProduct2 = new DataProduct(2l,2l,"test2",200l,"testing2",2l);

        //given
        DataProductResponse.findHeartDataProducts test1 = DataProductResponse.findHeartDataProducts.builder()
                .productId(1l)
                .title("test1")
                .price(1000l)
                .description("test1 설명입니다.")
                .imageUrl("이미지 url")
                .createdAt(LocalDateTime.now())
                .categoriesName(List.of(category1.getTitle(),category2.getTitle()))
                .build();
        DataProductResponse.findHeartDataProducts test2 = DataProductResponse.findHeartDataProducts.builder()
                .productId(2l)
                .title("test2")
                .price(2000l)
                .description("test2 설명입니다.")
                .imageUrl("이미지 url")
                .createdAt(LocalDateTime.now())
                .categoriesName(List.of(category1.getTitle()))
                .build();

        List<DataProductResponse.findHeartDataProducts> response = List.of(test1, test2);

        Heart heart1 = Heart.builder().user(user).dataProduct(dataProduct1).build();
        Heart heart2 = Heart.builder().user(user).dataProduct(dataProduct2).build();


        given(userService.findHeartDataProducts(user.getId())).willReturn(response);

        ResultActions result = this.mockMvc.perform(
                get("/v1/users/hearts")
                        .header("Authorization", "Basic dXNlcjpzZWNyZXQ=")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
        );

        // when then
        result.andExpect(status().isOk())
                .andDo(
                        restDocs.document(
                                requestHeaders(
                                        headerWithName("Authorization").description("accessToken")),
                                responseFields(
                                        fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과코드"),
                                        fieldWithPath("message").type(JsonFieldType.STRING).description("결과메시지"),
                                        fieldWithPath("data[].productId").type(JsonFieldType.NUMBER).description("상품 아이디"),
                                        fieldWithPath("data[].title").type(JsonFieldType.STRING).description("상품 명"),
                                        fieldWithPath("data[].price").type(JsonFieldType.NUMBER).description("상품 가격"),
                                        fieldWithPath("data[].description").type(JsonFieldType.STRING).description("상품 설명"),
                                        fieldWithPath("data[].imageUrl").type(JsonFieldType.STRING).description("상품 이미지 url"),
                                        fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("상품 생성일자"),
                                        fieldWithPath("data[].categoriesName").type(JsonFieldType.ARRAY).description("상품 아이디")
                                        )
                        )
                )
        ;
    }

위의 코드를 보면, 응답도 알맞게 설정하고, 요청 헤더 형식 등 모두 알맞게 구성했다. 하지만 이 코드를 run하게 되면, 아래와 같이 data:[] 과 같은 아름다운(?) 데이터 증발 현상을 만나게 된다. ㅎㅎㅎ,,,,,

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"code":0,"message":"성공","data":[]}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented:
{
  "data" : [ ]
}
Fields with the following paths were not found in the payload: [data[].productId, data[].title, data[].price, data[].description, data[].imageUrl, data[].createdAt, data[].categoriesName]

삽질을 며칠동안 하면서, 롤모델(대학 동기)에게 자문을 구한 결과.. 해당 Mock 테스트는 mvc 테스트이므로 실제 컨트롤러에서 Security를 이용해서 @AuthenticationPrincipal를 통해 userId를 받아오는 것이 불가능하다는 해답을 얻었다.

또한, 매번 컨트롤러에 @AuthenticationPrincipal를 선언하는 것도 매우 매우 귀찮기 때문에, 이참에 별도로 context에서 user를 받아오는 로직을 작성하기로 했다.

public class SecurityUtils {

    private static List<SimpleGrantedAuthority> notUserAuthority = new ArrayList<>();

    public static Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw new CustomException(Result.FAIL);
        }

        if (authentication.isAuthenticated()
                && !CollectionUtils.containsAny(
                authentication.getAuthorities(), notUserAuthority)) {
            return Long.valueOf(authentication.getName());
        }

        return 0L;
    }
}

위의 코드를 통해 context 안에 들어있는 userId를 받아올 수 있고, 해당 id로 repository를 조회하여 user를 얻을 수 있었다.!!

자 그럼 이제 코드를 수정해보겠다.


컨트롤러

//좋아요한 데이터 상품 목록
    @GetMapping("/v1/users/hearts")
    public CustomResponseEntity<List<DataProductResponse.findHeartDataProducts>> findHeartDataProducts(){
        return CustomResponseEntity.success(userService.findHeartDataProducts());
    }

@AuthenticationPrincipal를 제외하고 위에서 설정한 securityUtils를 통해 service로직에서 회원을 받아온다.

테스트

@Test
    @DisplayName("좋아요한 데이터 상품 목록")
    public void findHeartDataProducts() throws Exception {

        Category category1 = Category.builder()
                .title("패션").categoryCode(101).build();
        Category category2 = Category.builder()
                .title("사람").categoryCode(102).build();

        User user = new User(1l,"test","test","test","test",null);
        DataProduct dataProduct1 = new DataProduct(1l,1l,"test1",100l,"testing1",1l);
        DataProduct dataProduct2 = new DataProduct(2l,2l,"test2",200l,"testing2",2l);

        //given
        DataProductResponse.findHeartDataProducts test1 = DataProductResponse.findHeartDataProducts.builder()
                .productId(1l)
                .title("test1")
                .price(1000l)
                .description("test1 설명입니다.")
                .imageUrl("이미지 url")
                .createdAt(LocalDateTime.now())
                .categoriesName(List.of(category1.getTitle(),category2.getTitle()))
                .build();
        DataProductResponse.findHeartDataProducts test2 = DataProductResponse.findHeartDataProducts.builder()
                .productId(2l)
                .title("test2")
                .price(2000l)
                .description("test2 설명입니다.")
                .imageUrl("이미지 url")
                .createdAt(LocalDateTime.now())
                .categoriesName(List.of(category1.getTitle()))
                .build();

        List<DataProductResponse.findHeartDataProducts> response = List.of(test1, test2);

        Heart heart1 = Heart.builder().user(user).dataProduct(dataProduct1).build();
        Heart heart2 = Heart.builder().user(user).dataProduct(dataProduct2).build();


        given(userService.findHeartDataProducts()).willReturn(response);

        ResultActions result = this.mockMvc.perform(
                get("/v1/users/hearts")
                        .header("Authorization", "Basic dXNlcjpzZWNyZXQ=")
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON)
        );

        // when then
        result.andExpect(status().isOk())
                .andDo(
                        restDocs.document(
                                requestHeaders(
                                        headerWithName("Authorization").description("accessToken")),
                                responseFields(
                                        fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과코드"),
                                        fieldWithPath("message").type(JsonFieldType.STRING).description("결과메시지"),
                                        fieldWithPath("data[].productId").type(JsonFieldType.NUMBER).description("상품 아이디"),
                                        fieldWithPath("data[].title").type(JsonFieldType.STRING).description("상품 명"),
                                        fieldWithPath("data[].price").type(JsonFieldType.NUMBER).description("상품 가격"),
                                        fieldWithPath("data[].description").type(JsonFieldType.STRING).description("상품 설명"),
                                        fieldWithPath("data[].imageUrl").type(JsonFieldType.STRING).description("상품 이미지 url"),
                                        fieldWithPath("data[].createdAt").type(JsonFieldType.STRING).description("상품 생성일자"),
                                        fieldWithPath("data[].categoriesName").type(JsonFieldType.ARRAY).description("상품 아이디")
                                        )
                        )
                )
        ;
    }

given절에 기존에 파라미터였던 user.getId를 빼줬다.

위와 같이 하면 data가 null뜰일은 없다!!


번외
MockMVC 테스트를 작성하다가 알게된 사실인데,
given 절이 잘못되는 경우 즉, 응답이 올바르지 않는 경우는 통신을 잘 되어서 200으로 응답이 오지만 data가 제대로 전달되지 않는다.
그와 다르게, given은 알맞는데, 단순히 파라미터등의 전달이 잘 안되는 경우에는 400대 에러가 발생하게 된다.!!
뭔가를 또 깨달았다~💪🏻

profile
Backend Developer

2개의 댓글

comment-user-thumbnail
2023년 7월 31일

정보 감사합니다.

1개의 답글