인프런 커뮤니티 질문&답변

김우영 김님의 프로필 이미지

작성한 질문수

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

LocalDateTime관련해서 질문드립니다.

해결된 질문

작성

·

1K

0

안녕하세요. 많이 고민해봐도 방향을 잡기 어려워 글을 남겨봅니다.

java.lang.AssertionError: Status expected:<200> but was:<400> 에러가 발생되었는데요.

모델링이 잘못된건지 아니면 @CreatedDate  ...  LocalDateTime 타입 사용을 잘 못하고 있는지

방향을 못잡아 문의드립니다.

아래 내용에서 registDt가 제외되면 정상적으로 처리가 됩니다. ㅠㅠ

post insert시에 bbs와 그 안에 다시 post가 나오는것이 맞는 구조인가요?;;

---------------------------------------------------------------------------------------------

엔티티를 bbs(1) post(N) 양방향관계로 생성을 하였습니다.

basEntity로 LocalDateTime registDt을 상속받았습니다.

[ 아래는 postDto 내용입니다. ]

       ...

       public Post toEntity() {

return Post.builder()

.bbs(bbs)

.title(title)

.content(content)

.build();

}

@Builder

public PostRequestDto(Bbs bbs, String title, String content ) {

this.bbs = bbs;

this.title = title;

this.content = content;

}

그리고 post 저장 테스트를 진행시 아래와 같이 dto가 전송됩니다.

{

   "bbs":{

      "registId":null,

      "registDt":{

         "dayOfMonth":6,

         "dayOfWeek":"MONDAY",

         "dayOfYear":97,

         "month":"APRIL",

         "monthValue":4,

         "year":2020,

         "hour":14,

         "minute":53,

         "nano":611000000,

         "second":36,

         "chronology":{

            "id":"ISO",

            "calendarType":"iso8601"

         }

      },

      "updateId":null,

      "updateDt":null,

      "id":1,

      "bbsNm":"TEST",

      "post":[

      ]

   },

   "title":"첫번째 테스트",

   "content":null,

}

답변 5

1

김우영 김님의 프로필 이미지
김우영 김
질문자

늦은 시간임에도 신속하게 확인 및 답변 주셔서 감사합니다. ^^

해주신 말씀을 토대로 기반을 다시 잘 잡아보도록 하겠습니다.

감사합니다.

0

김영한님의 프로필 이미지
김영한
지식공유자

어이쿠 오래 기다리셨군요. 저에게는 인프런 추가 질문과 답변이 메일로도 오는데, 강의는 듣는 분께는 오지 않는군요^^; 이번에 좋은 것을 알았습니다.

그럼 답변을 시작할께요^^

응답이든, 요청이든, 둘다 외부에 엔티티를 노출하지 않는 것이 좋습니다.

1) PostRequestDto에서 리턴 타입이 Entity인 함수를 하나 생성했었습니다.
       리턴 타입은 Entity여도 상관이 없다고 생각되는데 맞는지요?

-> 네 Dto는 엔티티에 의존해도 됩니다.


    2) Public Post toEntity() {
             return Post.builder()
                      .bbs() //bbs엔티티
                      .xxx() //post컬럼들
                      .build();
        }
        이런식으로 리턴을 하였는데.. Entity를 Dto로 바꾸게되면 bbs객체는 controller에서

        bbsId를 따로 받고 여기에서는 항목자체가 제외되는 것이라 이해 하였는데

        제가 생각한 것인 맞는 건지요?

-> Entity를 Dto로 변경할 때 이렇게 연관된 엔티티를 직접 엔티티를 생성하면 안되고, bbsId, xxxId로 영속성 컨텍스트를 통해서 다시 조회해야 합니다.

       3) PostResponseDto는..... 이부분이 많이 어렵습니다. ㅠㅠ

            Bbs를 BbsResponseDto로 변경하게 되면 데이터를 어떻게 처리하여야 하는것인가요?

            private BbsResponseDto bbs;

            public PostResponseDto(Post entity) {
                          this.id = entity.getId();
                          this.bbs = entity.getBbs();
                          this.xxx...
            }

            여기서 Post 엔티티를  가져오는 부분부터 변경이 되여야 하는걸까요?? ㅠㅠ

-> 여기서 PostResponseDto에서 Post 엔티티를 가지고 오는 것은 Dto가 엔티티에 의존하는 것이기 때문에 괜찮습니다. 중요한 것은 외부 API에 나가는 Dto Response를 만들 때 엔티티를 포함하면 안됩니다. 활용2편을 잘 보시면 모두 Dto로 변경해서 나가는 것을 볼 수 있습니다.

3. 덕분에 잘 이해했습니다. 그런데 예시를 주셨던 내용을 보고 문득 궁금증이 하나 생기는데요~

    Post post = postService.getXxx();
    post.addCommentCnt();

    저는 Service에서는 Repository를 다수 이용해 처리하는것을 당연하게 생각하고 있었는데요

    위에 예시와 같이 CommentService에서 PostService를 호출하여 처리를 하는 것이 더 명확하다고

    볼 수 있는 것인가요? 아니면 Service에서 불러오든 Repository에서 불러오든 의미가 없는 부분일까요? ^^;;

-> 좋은 질문입니다. 딱 정답이 있다기 보다는 현재 프로젝트 성격에 맞도록 설계를 잡으면 됩니다^^ 프로젝트 규모가 작다면 단순하게 Repository에서 불러오는게 더 나은 선택일 확율이 높습니다. 그런데 프로젝트 규모가 크고, Post와 Comment가 완전히 다른 모듈로 분리되어 있다면, 서비스를 통해서 조회하는 것이 더 좋을 수 있습니다. 제가 권장하는 방법은 가장 쉬운 방법을 우선 선택하고, 프로젝트 규모가 커지고, Post와 Comment를 크게 분리해서 관리해야 겠다는 확신이 들면 그때는 Post의 서비스 인터페이스만 외부에 노출하는 식으로 모듈(패키지 또는 멀티모듈)을 분리하는게 더 나은 선택일 확율이 높습니다^^

감사합니다

0

김우영 김님의 프로필 이미지
김우영 김
질문자

안녕하세요 ^^

이제야 답글을 확인했습니다. (사실 메일만 매일 보면서 기달렸는데ㅎ 여기 답변을 주셨네요~)

먼저 정성이 느껴지는 답변에 매우매우 감사드립니다. (_ _)

하지만 아직 이해도가 많이 부족한 나머지 한번만 더 문의를 드려봅니다..ㅠㅠ

1. 정말 상세한 답변으로 해결도 되고 이해하는데 도움이 많이 되었습니다. 감사합니다.

2. 제가 질문을 PostResponseDto 관련하여 드렸는데 답변은 PostRequestDto 예시로 주셨네요.

   일단 Response용도든 Request용도든 Entity가 노출되면 안된다는 의미로 이해했습니다. ^^

   RequstDto에서는 타Entity의 ID만 가지고 Service에서 처리한다. 머리에 쏙 들어왔습니다.

   하지만 이부분이 개념이 많이 어려워서 추가 질문을 드려봅니다. ㅠㅠ

    1) PostRequestDto에서 리턴 타입이 Entity인 함수를 하나 생성했었습니다.
       리턴 타입은 Entity여도 상관이 없다고 생각되는데 맞는지요?

    2) Public Post toEntity() {
             return Post.builder()
                      .bbs() //bbs엔티티
                      .xxx() //post컬럼들
                      .build();
        }
        이런식으로 리턴을 하였는데.. Entity를 Dto로 바꾸게되면 bbs객체는 controller에서

        bbsId를 따로 받고 여기에서는 항목자체가 제외되는 것이라 이해 하였는데

        제가 생각한 것인 맞는 건지요?

       3) PostResponseDto는..... 이부분이 많이 어렵습니다. ㅠㅠ

            Bbs를 BbsResponseDto로 변경하게 되면 데이터를 어떻게 처리하여야 하는것인가요?

            private BbsResponseDto bbs;

            public PostResponseDto(Post entity) {
                          this.id = entity.getId();
                          this.bbs = entity.getBbs();
                          this.xxx...
            }

            여기서 Post 엔티티를  가져오는 부분부터 변경이 되여야 하는걸까요?? ㅠㅠ

3. 덕분에 잘 이해했습니다. 그런데 예시를 주셨던 내용을 보고 문득 궁금증이 하나 생기는데요~

    Post post = postService.getXxx();
    post.addCommentCnt();

    저는 Service에서는 Repository를 다수 이용해 처리하는것을 당연하게 생각하고 있었는데요

    위에 예시와 같이 CommentService에서 PostService를 호출하여 처리를 하는 것이 더 명확하다고

    볼 수 있는 것인가요? 아니면 Service에서 불러오든 Repository에서 불러오든 의미가 없는 부분일까요? ^^;;

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 김우영님!

메일로 주신 질문을 다른분도 볼 수 있게 여기에 다시 남겼습니다.

헉헉~~~ 시간좀 걸렸습니다! ㅋㅋ 그럼 답변 시작합니다.

===질문내용===

○ 테스트 과정

 - 경로 : src/test/java/ com.template.sample.api.customboard.PostRestControllerTest.java의 @Test apiPostInsert() 실행

 - 결과 : 정상 처리

 - 변경 : com.template.sample.domain.BaseEntity.java의 @CreatedDate를 주석 제거

 - 결과 : java.lang.AssertionError: Status expected:<200> but was:<400> 에러가 발생

질문

1. @Test에서 dto를 Log.info로 출력 시..

[ log.info("########## dto :: " + new ObjectMapper().writeValueAsString(dto)); ]

post내부에 bbs를 가지고 있고 그 bbs내부에 다시 post를 가지고 있습니다.

모델 연관 관계가 잘못되어 이러한 현상이 발생된 것인가요? 아니면 처리 방식이 잘못 된 것인지요?

아니면 이런 구조가 맞는것인지요? ( post를 추가하면 그안에 다시 > bbs엔티티 > post엔티티 > commet엔티티 )

만약 requestDto, responseDto에서 LocalDateTime을 String타입으로 변경 처리하더라도

지금 발생된 이슈는 dto와 관련이 없다고 생각하고 있는데 혹시 잘못 생각을 하고 있는건가요?

2. 강의 등 학습한 내용으로는 @JsonIgnore은 Entity가 아닌 Dto에 붙이는 것으로 알고 있습니다. (entity만 존재하는 경우 제외)

하지만 Post엔티티에 붙어있는 @JsonIgnore를 제거하면 무한루프에 빠지고 있는데요ㅠㅠ 무엇이 문제일까요?

현재는 PostResponseDto와 Post엔티티 두곳에 @JsonIgnore가 모두 존재해야 정상적인 결과가 나옵니다.

3. 마지막으로 CommentService에서 51번째줄에 addCommentCnt()가 동작이 안되는데 아무리 생각해봐도 개념이 부족한 탓인지 이해가 잘 안되네요ㅠㅠ

===답변 내용===

A. H2 데이터베이스는 우선 1.4.199 버전을 사용하세요.

H2 1.4.200에서는 하이버네이트가 테이블을 정상 삭제하지 못합니다.

만약 H2 1.4.200으로 설치해서 사용하신다면 build.gradle에 다음 코드를 추가해주세요. 최근 하이버네이트 패치가 되어서 H2 1.4.200 버전에서도 테이블을 정상 삭제할 수 있습니다.

ext["hibernate.version"] = "5.4.14.Final"

B. lombok을 테스트 코드에서 사용할 수 있게 해주세요. 현재 테스트가 정상 컴파일이 안됩니다.

compileOnly 'org.projectlombok:lombok'

testCompileOnly 'org.projectlombok:lombok' //이 부분 추가

우선 A,B는 진행해주세요 :)

1. 오류가 발생한 이유는 시간 데이터 때문입니다.

- 변경 : com.template.sample.domain.BaseEntity.java의 @CreatedDate를 주석 제거

- 결과 : java.lang.AssertionError: Status expected:<200> but was:<400> 에러가 발생

new ObjectMapper().writeValueAsString(dto)) -> 이런식으로 objectMapper를 직접 생성하면, jackson 라이브러리는 LocalDateTime 시간에 대한 처리를 다음과 같이 객체로 처리해버립니다.

{"dayOfYear":99,"dayOfWeek":"WEDNESDAY","month":"APRIL","dayOfMonth":8,"year":2020,"monthValue":4,"hour":17,"minute":17,"second":32,"nano":939000000,"chronology":{"id":"ISO","calendarType":"iso8601"}

이렇게 생성된 날짜를 API로 서버에 던지게 되면 스프링 프레임워크는 해석을 할 수 없습니다.

대신에 다음과 같이 ISO8601 형태로 던져야 합니다.

{... registDt":"2020-04-08T17:28:33.399"}

스프링은 기본적으로 LocalDateTime을 처리할 때 ISO8601 형태를 사용하도록 되어 있습니다.(스프링이 내부에서 사용하는 Jackson라이브러리에 설정을 다 해둔 것이지요.) 그래서 오류가 발생한 것이지요. 오류 메시지를 보시면 다음과 같습니다.

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: 36] (through reference chain: com.template.sample.dto.customboard.PostRequestDto["bbs"]->com.template.sample.domain.customboard.Bbs["registDt"])]

대략 읽어보면 array or string을 기대했는데, 맞지 않다라는 것이지요.

그럼 어떻게 해결해야 하는가? ObjectMapper가 ISO8601 형태로 날짜 타입을 처리할 수 있게 설정을 넣어주면 됩니다.

(구글에 다음과 같이 검색하면 됩니다. jackson localdate iso 8601)

그런데 이 방법은 너무 복잡하니 스프링이 또 ISO8601 형태가 설정된, 그리고 스프링도 사용하는 ObjectMapper와 동일한 설정을 한 ObjectMapper를 제공해줍니다.

@Autowired ObjectMapper objectMapper;

이렇게 주입 받아서 이걸로 사용하시면 됩니다.

그래서 mvc 호출 코드를 다음과 같이 변경하면 됩니다.

mvc.perform(post(url)

            .contentType(MediaType.APPLICATION_JSON_UTF8)

            .content(objectMapper.writeValueAsString(dto))) //주입 받은 방식 사용

//            .content(new ObjectMapper().writeValueAsString(dto))) -> X

            .andExpect(status().isOk());

그럼 성공합니다^^!

2. 강의 등 학습한 내용으로는 @JsonIgnore은 Entity가 아닌 Dto에 붙이는 것으로 알고 있습니다. (entity만 존재하는 경우 제외)

지금 코드를 보면, PostRequestDto안에서 내부에 Bbs 엔티티를 가지고 있습니다. 제가 강의에서 설명드렸듯이! <- 엄청 강조!!! 외부에 엔티티를 노출하면 안됩니다. PostRequestDto 안에서 Bbs를 가지고 있어도 그것도 외부에 엔티티를 노출하는 코드입니다! 따라서 PostRequestDto가 Bbs 엔티티 대신에 별도의 Bbs용 Dto를 만드시는 것을 권장합니다. PostRequestDto->BbsDto 이런식으로 가지고 있도록 설계하면 대부분의 문제가 해결됩니다.

3. 마지막으로 CommentService에서 51번째줄에 addCommentCnt()가 동작이 안되는데 아무리 생각해봐도 개념이 부족한 탓인지 이해가 잘 안되네요ㅠㅠ

CommentRequestDto는 내부에 post엔티티를 가지고 있습니다.

JPA는 트랜잭션 안에서 엔티티를 조회해야합니다. 그래야 영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 됩니다.

CommentRequestDto에서 단순히 다른 엔티티들의 id만 가지고 실제 post엔티티 등을 insert 안에서 조회하도록 처리하시면 됩니다.

//안되는 이유가???

Post post = comment.getPost();

이 코드를 보면 Post를 JPA를 통해서 가져온게 아니라, 

Comment comment = commentRepository.save(dto.toEntity());

여기서 dto.toEntity() 안에서 직접 post를 설정하고 있습니다.

public Comment toEntity() {

return Comment.builder()

.post(post)

문제는 이 post가 트랜잭션 안에서 꺼낸 post가 아니라는 것이지요.

다음 코드와 같이 트랜잭션 안에서 post를 조회하신 다음에 설정하면, 트랜잭션이 끝나는 시점에 영속성 컨텍스트에 변경감지(dirty checking)이 발생하면서 엔티티가 정상 변경됩니다.

Post post = postService.getXxx();

post.addCommentCnt();

참고로 현재 테스트에서도 트랜잭션이 동작하지 않습니다.

@Test

@Transactional //추가 -> 이 코드가 없음

public void commentInsert() {

정리

이렇게 뭔가 본인이 직접 만들어봐야 자기 것이 됩니다. 아주 잘하셨습니다. 이렇게 뭔가 해보고, 막히게 되면 필요에 의해서 기본기가 쉽기 이해 됩니다. 이제는 기본편 강의가 확 머리에 들어오실꺼에요^^! 이제 기본편을 마스터하실 차례입니다^^

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 김우영님^^

Status expected:<200> but was:<400> 에러가 발생 되었다고 하셨는데, 정확히 어떤 상황에서 이런 오류가 발생했는지, 도움을 드리고 싶은데, 질문해주신 내용만 가지고는 정확한 파악이 어렵습니다.

재현할 수 있는 테스트 코드를 작성해서 전체 프로젝트를 압축해서 올려주세요.

추가로 질문이 몇가지 겹쳐 있는 것 같습니다. 질문을 명확하게 나누어 주시길 부탁드릴께요

감사합니다.