ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Boot와 AWS 로 혼자 구현하는 웹서비스 1, 2 장
    책/스프링부트와 AWS로 혼자 구현하는 웹서비스 2022. 4. 22. 23:45

    이동욱님이 쓰신 "스프링부트와 AWS로 혼자 구현하는 웹 서비스" 를 현재(SpringBoot 2.6.7) 버전에 맞게 일부 변형했습니다. 전반적인 내용은 동일합니다.

    1장

    intellij 의 간단한 세팅 및 github 와 intellij 를 연결한 부분이 나옵니다. 이 부분은 다른 블로그 글도 있으니 참고하셔도 될 거 같습니다.

    2장

    테스트 코드는 현재 웹 서비스에서 매우 중요한 요소입니다. 테스트 코드에서 꼭 짚고 넘어가야 하는것은 TDD와 단위테스트(unit test)입니다.

    2.1 테스트 코드 소개

    TDD

    테스트가 주도하는 개발(Test-Driven-Development, 혹은 Test-First-Development) 을 의미합니다.

    • 항상 실패하는 테스트를 먼저 작성하고(RED)
    • 테스트가 통과하는 프로덕션 코드를 작성하고(Green)
    • 테스트가 통과하면 프로덕션 코드를 리팩토링합니다.(Refacetor)

    더 자세한 내용은 (https://repo.yona.io/doortts/blog/issue/1) 를 참고하면 됩니다.

    단위 테스트

    TDD 의 첫번째 단계인 기능 단위의 테스트 코드를 작성 을 얘기합니다. 즉, 순수하게 테스트 코드만 작성하는 것을 얘기합니다.

    테스트 코드를 작성할 때 이점은 무엇일까요? 불확실성을 많이 제거 해준다, 나중에 개발자가 코드를 리팩토링 하거나 라이브러리를 업그레이드 등에 기존 기능이 올바르게 작동하는지를 확인할 수 있습니다.(예, 회귀 테스트) 등 여러가지가 있지만 가장 크게 공감할 수 있는건 톰캣을 띄워서 System.out.println 등으로 확인할 시간을 줄여준다. 는 측면이 있습니다. 톰캣을 띄우면 크게 몇 분 이상이 소요됩니다. 만약 테스트가 개발자가 원하는대로 나오지 않으면 다시 톰캣을 재시작해야 합니다. 이 과정을 계속하면 크게 몇 시간이 소비되기도 합니다.

    또한 자동검증 이 되게 합니다. 작성된 단위테스트를 실행만 하면 더는 수동검증은 필요가 없습니다.

    마지막으로, 개발자가 만든 기능을 안전하게 보호 해줍니다. 코드를 작성하다보면 여러 코드들이 상호간 영향을 주고 받게 됩니다. 이 과정에서 A 코드를 수정했을 때 B 코드에 영향을 줘 B 코드에 대한 테스트 케이스가 실패해 기존 코드에 영향이 없도록 수정해 줄 수있습니다.

    언어별로 테스트 코드 여러 테스트코드 프레임워크가 있습니다. 가장 대중적인 테스트 프레임워크로는 xUnit 이 있습니다.

    개발환경(x) 에 따라 Unit. 테스트를 도와주는 도구라고 생각하면 됩니다.

    • Java - JUnit
    • DB - DBUnit
    • .net - Unit

    본격적으로 코드를 작성해보겠습니다.

    package com.purple.purplebook;
    ​
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    ​
    @SpringBootApplication
    public class PurpleBookApplication {
    ​
        public static void main(String[] args) {
            SpringApplication.run(PurpleBookApplication.class, args);
        }
    ​
    }
    ​

    @SpringBootApplication

    • 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정됩니다.
    • @SpringBootApplication 이 있는 위치부터 설정을 읽어가기 때문에 이 클래스는 항상 프로젝트의 최상단에 위치 해야합니다.
    • main 메소드에서 실행하는 SpringApplication.run 으로 인해 내장 WAS(Web Application Server) 를 실행합니다.
      • 내장 WAS란 별도의 외부에 WAS 를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 의미합니다.
    • 스프링 부트에선 내장 WAS 사용을 권장합니다.
      • 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문입니다.

    이전 포스팅에서 다양한 Spring Annotation을 다루었으므로 이후 Spring Annotion이 궁금하면 해당 블로그 글을 참고하시면 됩니다.

    package com.purple.purplebook.web;
    ​
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    ​
    @RestController
    public class HelloController {
    ​
    ​
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    ​
    }
    ​

    컨트롤러 코드를 이제 다 작성했으니 테스트 코드를 작성해보겠습니다.

    package com.purple.purplebook.web;
    ​
    import static org.junit.jupiter.api.Assertions.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    ​
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.test.web.servlet.MockMvc;
    ​
    @WebMvcTest(HelloController.class)
    class HelloControllerTest {
    ​
        @Autowired
        private MockMvc mvc;
    ​
        @Test
        void hello가_리턴된다() throws Exception {
            //given
            String hello = "hello";
    ​
            //when
            mvc.perform(get("/hello"))
            //then
               .andExpect(status().isOk())
               .andExpect(content().string(hello));
        }
    }
    • private MockMvc mvc
      • 웹 API 를 테스트할 때 사용합니다.
      • 스프링 MVC 테스트의 시작점입니다.
      • 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있습니다.
    • mvc.perform(get("/hello"))
      • MockMvc를 통해 /hello 주소로 HTTP GET 요청을 합니다.
      • 체이닝(Chaining) 이 되기 때문에 아래와 같이 여러 검증 기능을 이어서 선언할 수 있습니다.
    • .andExpect(status().isOk())
      • mvc.perform 의 결과를 검증합니다.
      • HTTP Header 의 status를 검증합니다.
      • isOk() 는 200인지 아닌지를 검증합니다.
    • .andExpect(content().string(hello))
      • mvc.perform의 결과를 검증합니다.
      • 응답 본문의 내용을 검증합니다.
      • Controller에서 "hello" 를 리턴하기 때문에 이 값이 맞는지 검증합니다.

    위 코드대로 실행해보면 우리가 원하는 대로 테스트 코드가 잘 돌아가는것을 확인 할 수 있습니다. 참고로 수동으로 검증하고 테스트 코드를 작성하지는 않습니다. 역의 경우(테스트 코드 작성 -> 수동 검증) 는 충분히 가능합니다.

    HelloController 코드 롬복으로 전환하기

    이제 Lombok 을 사용한 DTO 를 만들어 보고 이와 관련된 테스트 코드도 만들어보겠습니다.

    package com.purple.purplebook.dto;
    ​
    import lombok.Getter;
    import lombok.RequiredArgsConstructor;
    ​
    @Getter
    @RequiredArgsConstructor
    public class HelloResponseDto {
    ​
        private final String name;
        private final int amount;
    ​
    }
    ​
    package com.purple.purplebook.dto;
    ​
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.junit.jupiter.api.Assertions.*;
    ​
    import org.junit.jupiter.api.Test;
    ​
    class HelloResponseDtoTest {
    ​
        @Test
        void 롬복_기능_테스트 () throws Exception {
          //given
          String name = "test";
          int amount = 1000;
          //when
            HelloResponseDto dto = new HelloResponseDto(name, amount);
          //then
            assertThat(dto.getName()).isEqualTo(name);
            assertThat(dto.getAmount()).isEqualTo(amount);
         }
    ​
    }

    참고로 테스트 코드의 assertThat 을 사용할 땐 Junit 의 assertThat 보다 assertjassertThat 을 사용하는게 더 좋습니다.

    1. CoreMatchers 와 달리 추가적인 라이브러리가 필요하지 않습니다.
      • Junit 의 assertThat 을 쓰게 되면 is() 와 같이 CoreMatchers 라이브러리가 필요합니다.
    2. 자동완성이 좀 더 확실하게 지원됩니다.
      • IDE에서는 CoreMathers 와 같은 Matcher 라이브러리의 자동완성 지원이 약합니다.

    자세한 설명은 백기선님의 유튜브를 참조하면 더 좋습니다.

    위 테스트 코드가 잘 작동하는걸 확인했습니다. 이제 HelloController 에도 새로 만든 ResponseDto 를 사용하도록 코드를 추가하겠습니다.

    package com.purple.purplebook.web;
    ​
    import com.purple.purplebook.dto.HelloResponseDto;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    ​
    @RestController
    public class HelloController {
    ​
    ​
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    ​
    ​
        @GetMapping("/hello/dto")
        public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) {
            return new HelloResponseDto(name, amount);
        }
    }
    ​

    위와 같이 /hello/dto 로 들어오게 되면 Dto를 리턴하도록 했습니다.

        
    package com.purple.purplebook.web;
    ​
    import static org.junit.jupiter.api.Assertions.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.hamcrest.Matchers.is;
    ​
    import com.purple.purplebook.dto.HelloResponseDto;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.test.web.servlet.MockMvc;
    ​
    @WebMvcTest(HelloController.class)
    class HelloControllerTest {
      @Test
        void helloDto가_리턴된다() throws Exception {
            //given
            String name = "hello";
            int amount = 1000;
            HelloResponseDto dto = new HelloResponseDto(name, amount);
            //when
            mvc.perform(get("/hello/dto")
                            .param("name", name)
                            .param("amount", String.valueOf(amount)))
            //then
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.name", is(name)))
               .andExpect(jsonPath("$.amount", is(amount)));
    ​
        }
      ...
    }
    • param
      • API 테스트 할 때 사용될 요청 파라미터를 설정합니다.
      • 단, 값은 String만 허용됩니다.
      • 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다.
    • jsonPath
      • JSON 응답값을 필드별로 검증할 수 있는 메소드입니다.
      • $를 기준으로 필드명을 명시합니다.
      • 여기서는 name과 amount를 검증하니 $.name, $.amount 로 검증합니다.

    댓글