관리 메뉴

bright jazz music

blog06: 응답클래스 분리(req: PostCreate / res:PostResponse) 본문

Projects/blog

blog06: 응답클래스 분리(req: PostCreate / res:PostResponse)

bright jazz music 2022. 12. 30. 11:33

요청 클래스와 응답 클래스를 나눠야 할 일이 생겼다.

 

클라이언트로부터 요청이 있었기 때문이다.

 

요청: 타이틀을 10자로 끊어서 보내주세요.

이 요청은 사실 클라이언트 측에서 처리하는 것이 적합한 듯하다.

어쨌든 서버단에서 처리해 보기로 한다.

 

실제로는 PostResponse 클래스 먼저 작성하였지만 보기 쉽도록 아래와 같은 순서로 배치하였다.

  • PostController => PostService => PostResponse => PostControllerTest => 결과

 

---

가장 중요한 PostResponse 클래스부터 먼저 보자. 나중에 똑같은 내용으로 다시 등장하기는 한다.

 

1. PostResponse 클래스 생성

클라이언트의 요청에 부합하는 응답을 보내기 위해 생성한 클래스이다.

//PostResponse.java

package com.endofma.blog.response;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
 * 서비스 정책에 맞는 응답 클래스 (10글자)
 */

@Getter
//@Builder
public class PostResponse {
    private final Long id;
    private final String title;
    private final String content;

//    10글자 반환
//    public String getTitle(){
//        return this.title.substring(0, 10);
//        //이것보다는 생성자를 만들어서 빌더를 달아주자
//    }

    @Builder
    public PostResponse(Long id, String title, String content){
        this.id = id;
//        this.title = title.substring(0, 10); //10글자만 세팅된다. 입력값이 10자보다 적으면 오류가 발생한다.
        this.title = title.substring(0, Math.min(title.length(), 10)); //Math.min():입력받은 두 인자 중 작은 값 리턴.
        this.content = content;
    }
}

 

 

2. PostController 수정

이전에는 Post 타입의 객체를 클라이언트에게 반환하였다.

그러나 이제 PostResponse 타입의 객체를 반환한다.

//PostController.java

package com.endofma.blog.controller;

//..

@Slf4j
@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostService postService;


    @PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
        postService.write(request);
//        return Map.of();
    }

    //단건 조회
    @GetMapping("/posts/{postId}")
    public PostResponse get(@PathVariable Long postId){
        //현재 요청 클래스와 응답클래스가 같지 않고 두 개로 나뉘어 있다.
        //Request 클래스 (PostCreate)
        //Response 클래스 (PostResponse:서비스 정책에 맞는 클래스)

        PostResponse response = postService.get(postId);

        return response;
    }
}

 

3. PostService 수정

응답 클래스 분리 작업을 여기서 수행한다.

DB에서 가져온 Post 객체의 값을 빼내어 PostResponse 클래스의 생성자에 넣을 파라미터로 사용한다.

생성된 PosResponse 객체를 생성하여 컨트롤러에 반환한다

//PostService.java

//..

@Slf4j
@Service
//@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;
    public PostService(PostRepository postRepository){
        this.postRepository = postRepository;
    }

    public void write(PostCreate postCreate) {
        //PostCreate 일반 클래스 ==> Post 엔티티
        Post post = Post.builder()
                .title(postCreate.getTitle())
                .content(postCreate.getContent())
                .build();

        postRepository.save(post);
    }

    //단건 조회
    public PostResponse get(Long id) {
//        Post post = postRepository.findById(id); optional 데이터로 감싸져서 오기 때문에 아래와 같이 할 수 있다.
//        Optional<Post> postOptional = postRepository.findById(id);
//        if(postOptional.isPresent()){
//            return postOptional.get();
//        }
//        return null; 그러나 가능한 한 옵셔널은 바로 꺼내서 변환해 주는 게 좋다.

        Post post = postRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 글입니다.")); //있으면 post반환 없으면 에러 반환

        
        
        
        
        /*응답 클래스 분리 작업*/
        //어떻게 10글자만 반환할까? 서비스 정책에 맞는 응답 클래스를 만들어(변환해)주는 것이다.
        
        PostResponse response = PostResponse.builder()
                .id(post.getId())
                .title(post.getTitle())
                .content(post.getContent())
                .build();

        //그런데 이 서비스 레이어에서 변환작업을 해주는 것이 맞을까? 기호와 경우에 따라 다르다.
        /**
         * PostController   =>  WebPostService(지금 이 클래스. 여기서 분류처리를 한다.)  =>  Repository
         *                      PostService(외부통신 하는 경우에만)
         */
         
        return response;
    }
}

 

다시 PostResponse의 내용을 다시 확인하자.(맨 위와 달라진 내용은 없음)

 

package com.endofma.blog.response;

import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
 * 서비스 정책에 맞는 응답 클래스 (10글자)
 */

@Getter
//@Builder
public class PostResponse {
    private final Long id;
    private final String title;
    private final String content;

//    10글자 반환
//    public String getTitle(){
//        return this.title.substring(0, 10);
//        //이것보다는 생성자를 만들어서 빌더를 달아주자
//    }

    @Builder
    public PostResponse(Long id, String title, String content){
        this.id = id;
//        this.title = title.substring(0, 10); //10글자만 세팅된다. 입력값이 10자보다 적으면 오류가 발생한다.
        this.title = title.substring(0, Math.min(title.length(), 10)); //Math.min():입력받은 두 인자 중 작은 값 리턴.
        this.content = content;
    }
}

Math.min()을 사용한 코드는 request로부터 전달받은 값이 10자가 되지 않는 경우를 대비해서 작성한 것이다.

이 메소드를 사용하지 않은 경우를 가정해 보자.

 

  • 클라이언트가 타이틀 필드에 들어갈 값으로 "12345" 를 입력하여 요청했다.
  • PostService레이어에서 DB에서 가져온 Post 객체의 타이틀 값을 꺼내서(post.getTitle())을 response의 .title()에 넣고, 그 값을 반환하려고 할 때 문제가 발생한다.

 

0번째(첫 번째) 문자에서 시작하여 10개의 문자를 잘라내야 하는데 입력값이 10개가 되지 않기 때문이다. 따라서 Math.min()함수를 사용하여 입력값과 10 가운데 작은 값을 기준으로 문자열을 잘라내도록 만든 것이다.

 

 

public class Math{

public static min(Long)

 

}

 

 

 

 

 

 

 

4. 테스트: test4()

//PostControllerTest.java

//..

//@WebMvcTest //간단한 웹테스트만 가능. 우린 스프링 전반에 걸쳐 여러 가지를 만들었기 때문에 @SpringBootTest가 필요
@AutoConfigureMockMvc //@WebMvcTest가 없어지면 기존 테스트가 안되므로 @WebMvcTest를 구성하는 애노테이션을 떼내어 붙였다.
@SpringBootTest
class PostControllerTest {



    @Autowired
    private MockMvc mockMvc; ////Could not autowire. No beans of 'MockMvc' type found.
    
    @Autowired
    private PostRepository postRepository; //DB저장 테스트를 위해 주입

    @BeforeEach
    void clean() {
        postRepository.deleteAll();
    }

    @Test
    @DisplayName("/posts 요청시 hello world를 출력한다")
    void test() throws Exception {
        //given
//        PostCreate request = new PostCreate("제목입니다.", "내용입니다.");
        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        //jackson을 사용하여 객체를 Json 형태로 바꿔준다.(json을 처리해 줌)
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);
//        @Autowired private ObjectMapper objectMapper; 이렇게 주입해서도 쓸 수 있다.

        System.out.println(json);

        //expected
        //기본적으로 Content-Type을 application/json으로 보냄. 예전에는 application/x-www-form-urlencoded를 썼다.
        mockMvc.perform(post("/posts")
                        .contentType(APPLICATION_JSON) //기본값이라 주석처리
//                        .content("{\"title\": \"제목입니다.\", \"content\": \"내용입니다.\"}") //json 형태로 값 넣어주기
                        //.content에는 byte나 String만 들어가진다. 따라서 jackson으로 Json변환처리를 하지 않은 request를 넣을 수는 없다.

                        .content(json) //jackson을 사용하여 json이 된 객체. 클래스에 게터가 존재해야 한다.
                )
                .andExpect(status().isOk()) //http response 가 200인지
                .andExpect(content().string("")) // 내용이 hello world인지
                .andDo(print()); //요청에 대한 전반적인 요약을 출력해준다.

    }


    @Test
    @DisplayName("/posts 요청시 title 값은 필수다")
    void test2() throws Exception {

        //given
        PostCreate request = PostCreate.builder()
                //.title("제목입니다.")
                .content("내용입니다.")
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);

        //expected
        mockMvc.perform(post("/posts")
                        .contentType(APPLICATION_JSON)
                                .content(json)

                )
                .andExpect(status().isBadRequest()) //.OK()
                .andExpect(jsonPath("$.code").value("400")) //json 검증
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다!"))
//                .andExpect(jsonPath("$.validation.title").value("타이틀을 입력하세요!"))
                .andDo(print());

    }



    @Test
    @DisplayName("/posts 요청시 DB에 값이 저장된다.")
    void test3() throws Exception {
        //given
        PostCreate request = PostCreate.builder()
                .title("제목입니다.")
                .content("내용입니다.")
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(request);

        //when
        mockMvc.perform(post("/posts")
                                .contentType(APPLICATION_JSON)
                                .content(json)
                )
                .andExpect(status().isOk())
                .andDo(print());

        //then
        assertEquals(1L, postRepository.count()); //하나의 값이 있을 거라고 예상. 일치

        //DB에 잘 들어갔는지 확인
        Post post = postRepository.findAll().get(0); //가장 처음 데이터 가져옴
        assertEquals("제목입니다.", post.getTitle());
        assertEquals("내용입니다.", post.getContent());
    }

    @Test
    @DisplayName("글 1개 조회")
    void test4() throws Exception{
        //given
        Post post = Post.builder()
                .title("123456789012345")
                .content("bar")
                .build();
        System.out.println("post.getId!!1="+ post.getId()); //==null

        postRepository.save(post);
        System.out.println("post.getId!!2="+ post.getId()); //==1

        //expected
        mockMvc.perform(get("/posts/{postId}", post.getId())
                        .contentType(APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(post.getId()))
                .andExpect(jsonPath("$.title").value("1234567890"))
                .andExpect(jsonPath("$.content").value("bar"))
                .andDo(print());
    }

}

 

 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2022-12-30 11:56:28.107  INFO 32496 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Starting PostControllerTest using Java 11.0.12 on DESKTOP-8H1PTVG with PID 32496 (started by markany-hjcha in D:\personal\blog)
2022-12-30 11:56:28.108  INFO 32496 --- [    Test worker] c.e.blog.controller.PostControllerTest   : No active profile set, falling back to 1 default profile: "default"
2022-12-30 11:56:28.684  INFO 32496 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-12-30 11:56:28.727  INFO 32496 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36 ms. Found 1 JPA repository interfaces.
2022-12-30 11:56:29.145  INFO 32496 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-12-30 11:56:29.342  INFO 32496 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-12-30 11:56:29.403  INFO 32496 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-12-30 11:56:29.461  INFO 32496 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2022-12-30 11:56:29.639  INFO 32496 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2022-12-30 11:56:29.766  INFO 32496 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2022-12-30 11:56:30.299  INFO 32496 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-12-30 11:56:30.307  INFO 32496 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-12-30 11:56:30.758  WARN 32496 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2022-12-30 11:56:31.409  INFO 32496 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2022-12-30 11:56:31.410  INFO 32496 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2022-12-30 11:56:31.411  INFO 32496 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2022-12-30 11:56:31.443  INFO 32496 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Started PostControllerTest in 3.63 seconds (JVM running for 5.907)
post.getId!!1=null
post.getId!!2=1

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /posts/1
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8"]
             Body = null
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#get(Long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"1234567890","content":"bar"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2022-12-30 11:56:31.974  INFO 32496 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2022-12-30 11:56:31.975  INFO 32496 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2022-12-30 11:56:31.980  INFO 32496 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2022-12-30 11:56:31.984  INFO 32496 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 7s
4 actionable tasks: 1 executed, 3 up-to-date
AM 11:56:32: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerTest.test4"'.

 

 

 

5. Gradle 탭에서 전체 테스트 수행하기

새로 만든 기능이 기존 기능과 충돌하는지 테스트 하기 위해 수행한다.

  • 인텔리제이 오른쪽 끝 Gradle 탭 => Tasks => verification => test 실행하면 전체 테스트 실행.

성공

Comments