테스트 시 @Transactional 사용한 이유, ObjectMapper 주입받아 사용하는 이유

HiroPark·2022년 9월 16일
1

문제해결

목록 보기
2/7
post-custom-banner
    @Test
    @WithMockUser(roles="USER")
    @Transactional // 프록시 객체에 실제 데이터를 불러올 수 있게 영속성 컨텍스트에서 관리
    public void comment_등록() throws Exception {
        // given
        String title = "title";
        String content = "content";
        PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();
        postsRepository.save(requestDto.toEntity());
        /* 게시글 등록 */        

        String comment = "comment";
        Posts posts = postsRepository.findAll().get(0);

        CommentSaveRequestDto saveRequestDto = CommentSaveRequestDto.builder()
                .comment(comment)
                .posts(posts)
                .build();
                /* requestDto 생성 */

        Long id = posts.getId();

        String url = "http://localhost:"+ port + "/api/posts/" + id + "/comments";

        //when

        mvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content(objectMapper.writeValueAsString(saveRequestDto)))
                        .andExpect(status().isOk())
                			.andDo(print());

    }

댓글 기능을 만들고, 이것을 post에 대한 테스트를 만들었습니다.

왜 @Transactional달았는가

댓글 기능을 추가한 뒤, 댓글 등록에 대한 테스트를 작성했습니다.

 @Transactional 

처음에는 해당 애노테이션 없이 테스트를 실행했는데, 이런 에러가 나왔습니다

com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: springboot.domain.posts.Posts.comments, could not initialize proxy - no Session (through reference chain: springboot.web.dto.comment.CommentSaveRequestDto["posts"]->springboot.domain.posts.Posts["comments"])

에러가 발생한 곳은

.content(objectMapper.writeValueAsString(saveRequestDto)))

dto를 통해 post를 실행하는 라인입니다.

무슨 에러인가 구글링해본 결과

  • 현재 post(1) - comment(다) 는 lazy loading으로 연결 돼 있다.
  • 이 상황에서 post나 comment는 연결된 상대방은 바로 초기화 되지 않고, 필요할때 채워지는 프록시 객체 로 채워진다.

라고 하는데..

  • 제 경우에는, CommentSaveRequestDto를 형성할때, dto의 posts 속성에 값을 채워넣는데, 채워넣는 반대편인 Posts의 comments 객체가 초기화되지 않아서 문제가 생긴 듯 해보입니다.
    즉, Posts가 가진 프록시 객체에 comments를 채워넣기 전에 세션이 종료되어 값을 가져올 수 없어서 생긴 오류인 듯 보입니다.

https://stackoverflow.com/questions/16752799/could-not-initialize-proxy-no-session

첫번째 답변에 따르면,

  • 스프링은 각 요청을 처리하기 위한 하나 이상의 서블렛을 가진다
  • 각 서블렛은 httpRequest를 HttpResponse를 최종적으로 생산해내는 쓰레드를 통해 처리한다, 각 request를 처리하는 메서드는 이 쓰레드 내에서 처리된다.
  • 요청의 시작에 애플리케이션은 처리를 위한 자원(트랜잭션, 하이버네이트 세션 등) 을 할당하고, 처리 사이클이 끝나면 해당 자원은 놓아진다(트랜잭션 commit, 세션 종료)
  • 어플리케이션이 무상태(Http)로 유지되게 하기 위하여, HttpSession 객체가 존재한다
  • 프레임워크는 클라이언트의 여러 요청 중 관련된 정보들을 HttpSession에 저장한다
  • 첫번째 요청을 처리하는 과정에서, 하이버네이트가 읽어온 객체의 일부는 lazy로 처리되기에 proxy 객체이다.
  • 두번째 요청을 처리할때, 프레임워크는 HttpSession객체에서 이전 요청의 엔티티를 찾으려 한다.
  • 하이버네이트 프록시는 해당 자원이 실제 필요할때 하이버네이트 세션을 통해서 실제 값을 채우려 하지만, 이전 요청의 끝에서 이미 하이버네이트 세션이 닫혀서 그러지 못한다.

라고 합니다. 답변을 거의 그대로 옮겼더니 장황해졌네요.. 저의 경우는 처리 사이클안에서 자원을 내려놓지 않게(세션이 열려있게) 해야할 것 같습니다.

lazy로딩으로 서로를 연결해주기 위해서는 Posts를 영속성 컨텍스트(트랜잭션 범위와 동일) 안에서 관리해줄 필요가 있어보입니다.

따라서 @Transaction 어노테이션을 통하여 메서드가 시작하고 끝날때까지의 일련의과정을 쪼갤수 없는 하나의 연산으로 만들어주었습니다.

왜 ObjectMapper 주입받았는가

mvc.perform(post(url)
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(new ObjectMapper().writeValueAsString(saveRequestDto)))
                               .andExpect(status().isOk())
                               .andDo(print())

이렇게 ObjectMapper 객체를 새로 생성하여 테스트를 진행하면

java.lang.AssertionError: Status 
Expected :200
Actual   :400

이렇게 에러가 발생합니다.
정확한 에러가 알고 싶어서 프린트 문 안에 mvc구문을 넣고 다시 돌려보았는데요,

DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Expected array or string.; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Expected array or string.
 at [Source: (PushbackInputStream); line: 1, column: 45] (through reference chain: springboot.web.dto.comment.CommentSaveRequestDto["posts"]->springboot.domain.posts.Posts["createdDate"])]

createdDate를 JSON으로 파싱하는 과정에서 에러가 발생한듯 합니다.

구글링을 해서 어렵지 않게 저의 경우에 딱 들어맞는 솔루션을 찾을 수 있었습니다.
https://www.inflearn.com/questions/30590

정리하자면,

  • new ObjectMapper().writeValueAsString(dto)) 로 직접 objectMapper를 생성하면 Jackson(java json)라이브러리는 LocalDateTime 객체인 createdDate를 객체로 처리해버린다.
  • 스프링은 내부적으로 LocalDateTime을 처리할 때 ISO8601 형태를 사용하는데, 객체형태로 값이 들어오기에 스프링 프레임워크가 이를 해석할 수 없다.

  • 따라서 스프링이 제공하는 ISO8601형태가 설정된 ObjectMapper를 @Autowired ObjectMapper objectMapper; 이렇게 주입받아서 사용하면 된다

라고 합니다.

참고
https://cantcoding.tistory.com/78
https://stackoverflow.com/questions/16752799/could-not-initialize-proxy-no-session
https://www.inflearn.com/questions/30590
https://stackoverflow.com/questions/50362883/what-is-the-advantage-of-declaring-objectmapper-as-a-bean

profile
https://de-vlog.tistory.com/ 이사중입니다
post-custom-banner

0개의 댓글