스프링부트와 AWS로 혼자 구현하는 웹 서비스 2장

2021. 12. 26. 19:43잡다한 IT/Springboot와 AWS로 혼자 구연하는 웹 서비스

이 내용은 이동욱님의 '스프링부트와 AWS로 혼자 구연하는 웹 서비스' 책 내용을 정리한 것입니다.

http://www.yes24.com/Product/Goods/83849117

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24

가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링

www.yes24.com

 

자 이제 본격적으로 개발하기에 앞서서 테스트코드에 대해 설명하는 시간을 가져보자. 이건 내가 정말 가고싶어하는 회사들중 한곳인 베스핀글로벌의 입사 체용 요건이다.

 

그 중 테스트 방법론 이해 및 테스트 코드 작성 경험이라는 문항이 보이는데 이곳뿐만 아니라 대부분의 회사가 테스트 코드를 요구하고 있는 상황이다. TDD정도가 못되더라도 최소한의 테스트 코드를 작성할 수 있는 인재를 요구하고 있다는 것이다.

 

* TDD와 단위 테스트(unit test)는 엄연히 다른것임을 알고 있어야 한다.

 

자! 그럼 TDD(test driven development)가 도대체 무엇이길레 '우대'요건에 넣을 정도로 중요한 것일까? 쉽게 말해서 테스트가 주도하는 개발을 의미한다. 쉽게 생각해서 내가 게임개발자 라고 생각해보자. 테트리스를 집에서 코딩하고 구동하기 위해서는 그냥 vscode에 파이선 코드 몇백줄 정도 대충 쓰고 f5만 누르면 보통 구현이 된다. 차지하는 용량도 적고 값을 string값으로 몽땅 줘 버려도 '혼자서'연습용으로 작성하기에는 별 상관이 없을것이다. 하지만 요즘 게임시장은 그렇지 않다. 최소 10GB~100GB까지 엄청난 양의 데이터가 있다. 코드 몇 줄 변경하고 f5 누르면 이 모든게 구동되기에 얼마나 오랜 시간이 걸릴까?

 

너무나 비 효율적인 것이다. 그렇기에 짧은 개발 사이클을 반복해서 요구사항을 검증하는 테스트 케이스를 작성해 이를 구연하기 위한 '최소한'의 코드를 생성하며 이것에 맞게 리팩토링 하는 과정이다.

아래의 내용은 채수원님의 TDD 실천법과 도구 라는 내용의 일부분이다.

보통 java는 junit, db는 dbunit, C++은 cppunit, .net은 nunit이 있는데 우리는 junit을 사용하도록 하자.

src의 하단 폴더인 java에 새로운 패키지를 작성하도록 하자. 예시로서 com.test.page라는 각각의 '페키지'를 생성하도록 하겠다.

 

반드시 build.gradle에 있는 group 파트와 똑같이 만들어 줘야지 사용이 가능하다. 정말 중요하니 꼭 확인 바란다.

 

이후 똑같은 방법으로 이번에는 application이라는 java 클레스를 형성하도록 한다.

이 후 다음과 같은 코드를 작성한다. 그런데 뭔가 이상한게 보이지 않는가? 보통 PC에서 빨간색이라면 위함하다는 의미이다. (빨간색은 주인공의 색갈이라서 좋다고 여름에 빨간 머플러를 두른 아이가 생각난다면 당신도 나와같은 씹덕이란 소리다.) 아무튼 이런 무서운것은 싫기 때문에 얌전히 인텔리제이가 안내해 주는것을 따라가자.

빨간색에 마우스를 올리면 아래와 같은 안내문이 나타난다. 임폴트가 되지 않았기 때문에 그렇다 라고 하기에 alt + enter을 저 빨간색 글자 바로 앞, 뒤 혹은 글자 아무데서나 클릭하자. 그러면 import를 할 수 있게된다.

완성된 코드는 다음과 같다.

package com.test.page;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//스프링부트의 자동 설정, 스프링 bean읽기와 생성 자동 설정. 
// 주의점은 이 위치부터 설정을 읽어들어가기 때문에 프로젝트 최상단에 있어야 한다
@SpringBootApplication
public class Application
{
    //메인 메소드에서 실행하는 SpringApplication.run 으로 인해
    //내장 WAS(Wep Application Server)가 실행된다.
    //서버에 톰켓을 설치할 필요가 없게 되고 스프링부트로 만들어진 jar파일을 실행하면된다.
    public static void main(String[] args)
    {
        SpringApplication.run(Application.class, args);
    }
}

아니 그래서 뭘 만들긴 했는데 이게 뭐하는 아이인고? 라는 질문이 든다면 당신은 충분히 좀 더 진화할 가능성이 있다는 의미다.

지금 만든 application이라는 클레스는 앞으로 만들 클레스의 매인 클레스가 될 것이다. 

아니 근데 서버도 없이 내장 was? 아니 이게 머슨일인고? 할 수 도 있지만 스프링부트에서는 내장was를 사용하는것을 권장하고 있다. 언제 어디서나 같은 환경에서 스프링부트를 배포할 수 있기 때문이다.

이거 어디서 많이 보지 않았나? CI/CD 관련 내용이다.

새로운 서버가 추가되면 모든 서버가 같은 was 환경을 구축해야한다. 추가되는 서버가 1~2대면 괜찮겠지만 100대, 1000대가 넘어가 버리게 되면 너무나 귀찮고 번거로운 일이기 때문에 어지간하면 내부 WAS를 사용하도록 하자.

 

* 책의 저자가 말하길 성능상 이슈는 딱히 생각할 필요없이 괜찮다고 한다.

 

다음으로는 web이라는 페키지를 새롭게 만든 후 그 안에 Conteroller라는 클레스를 새로 형성하도록 하자.

package com.test.page.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

//컨트롤러를 JSON을 반환하는 컨트롤러로 만들어 준다.
//각 매소드마다 @responsbody를 선언했지만 한 번에 할 수 있도록 통합시킨것이다.
@RestController
public class HelloController
{
    //http method인 get의 요청을 받을 수 있는 api를 만든것이다.
    // "/hello"로 요청이 오면 문자열 hello를 반환한다.
    @GetMapping("/hello")
    public String hello()
    {
        return "hello";
    }
}

 

 

이제는 이 코드를 테스트 코드로 검증 해 보는 시간을 가져보자. 하단의 test파일에 똑같은 페키지를 형성 후 아래의 코드를 작성하도록 하자. (springboot페키지, web페키지) controllertest라는 클레스를 만들도록 하자.(controller의 test파일이라는 의미)

 

package springboot.web;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

//테스트를 진행할 때 junit에 내장된 실행자 외 다른 실행자를 실행시킨다.
//스프링러너라는 스프링 실행자를 실행하는데 테스트와 junit사이에 연결자 역할을 한다.
@RunWith(SpringRunner.class)
//web(spring mvc)에 집중할 수 있는 어노테이션이다.
//선언시 controller, controllerAdvice등을 사용할 수 있지만
//@service, Component, respository 등은 사용할 수 없다.
@WebMvcTest(controllers=Controller.class)

public class ControllerTest
{
    //스프링이 관리하는 bean을 주입 받는다.
    @Autowired
    //웹 api를 테스트할 때 사용한다
    //스프링 mvc테스트의 시작점이다.
    //http get,post 등에 대한 api테스트를 할 수 있다.
    private MockMvc mvc;

    @Test
    public void hello_is_return() throws Exception
    {
        String hello = "hello";

        //mockmvc를 통해 /hello 주소로 http get을 요청한다.
        //체이닝이 지원되어 여러 검증 기능을 이어서 선언할 수 있다.
         mvc.perform(get("/hello"))
                 //mvc.perform의 결과를 검증한다.
                 //http header의 status를 검증한다
                 //200,404,500등의 상태를 검증하는데 isok는 200번, 즉 성공인지 아닌지 검증한다
                .andExpect(status().isOk())
                 //결과값과 응답본문의 내용을 검증해 준다. controller에서 hello를 리턴하기 때문에 이 값이 맞는지 검증하게 된다.
                .andExpect(content().string(hello));
    }
}

 

자 이제 테스트 코드도 완벽히 작동되는것을 확인했으니 실제로 구동을 해 보도록 하자. Application 클레스로 이동해서 실행을 하도록 하자. 녹색 화살표 버튼을 클릭 후 run application 을 클릭하면 구동이 되는것을 확인할 수 있다. 포트번호는 8080번이다,

포트번호 확인

 

자 그럼 처음에 만들었던 문자열 hello 가 잘 출력되는것을 확인할 수 있다.

여기서 주의할 점은 반드시 테스트 코드는 따라 해야한다.

 

이제는 자바 개발자들의 필수 라이브러리인 롬복을 설치해 보도록 하자. getter,setter등과 같은 기본 생성자, 등을 이노테이션으로 자동 생성해 주는 도구이다. 이 전 장에서 dependencies에는 의존성을 선언할 수 있다고 한 사실 기억하고 있을 것이다. 그곳으로 가서 아래의 코드를 추가하도록 하자. 이 후 새로고침을 하여 의존성을 내려받자.

compile('org.projectlombok:lombok')

ctrl+shift+a를 입력해 plugins을 입력한 후 marketplace에 들어가자. 이곳에서 lombok을 다운로드 받으면 된다.

설치 후 재시작을 한 후 file => settings => build, execution, deployment => complier => annotaion processors 에 들어가서 enable annotation processing을 체크해 주자.

 

이제 기존에 설정했던 hello 코드를 롬복으로 변경해 보도록 하자. web 패키지 안에 dto라는 페키지를 새롭게 추가한 후 HelloResponseDto라는 클레스를 생성하자.

package com.test.page.web.Dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

//선언된 모든 필드의 get매소드를 생성해 준다.
@Getter
//선언된 모든 final필드가 포함된 생성자를 생성해 준다.
//final이 없는 필드는 생성자에 포함되지 않는다.
@RequiredArgsConstructor
public class HelloResponseDto {
    
    private final String name;
    private final int amount;
}

이 후 이 Dto가 잘 작동되는지 테스트 코드에서 작성해보자.

package com.test.page.web.Dto;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {
    @Test
    public void lombok_test(){
        //given
        String name="test";
        int amount = 1000;
        
        //when
        HelloResponseDto dto =new HelloResponseDto(name, amount);
        
        //then
        //assertj라는 테스트 검증 라이브러리안의 검증 메소드다
        //검증하고 싶은 대상을 메소드 인자로 받는다
        //메소드 체이닝이 지원되어 isEqualTo와 같이 이어서 사용할 수 있다.
        assertThat(dto.getName()).isEqualTo(name);
        //isEqualTo는 assertj의 동등 비교 메소드다
        //assertThat안에 있는 값과 isEqualto의 값을 비교해서 같을 경우에만 성공이다.
        assertThat(dto.getAmount()).isEqualTo(amount);
    }

}

 

대부분 이 근처에서 막혔을것이다. 아니 나는 하라는데로 다 따라했는데 왜 안되는거지? 라고 생각하기 쉬운데 너무 걱정하지 말자. 최근에 gradle이 4ver에서 5ver로 업데이트 됨에 따라 이것저것 바뀌게 되어서 안되는 경우가 생겨서 그렇다. gradle => wrapper => gradle-warpper.properties에 들어간 후 gradle-4.10.2 라고 수정만 해 주면된다. 대부분 5.x.x로 되어있을 것이다.

이제 다시 돌아와서 lombok이 잘 돌아가는지 확인해보자. 문제없이 잘 돌아가고 있음을 확인할 수 있다.

자 그럼 HelloController에 DTO를 사용할 수 있도록 코드를 '추가' 해 보자.

@GetMapping("/hello/dto")
public HelloResponseDto helloDto
        //외부에서 API로 넘긴 파라미터를 가저오는 어노테이션이다.
        //외부에서 name(@RequestParam("name"))이라는 이름으로 넘긴 파라미터를 메소드 파라미터name(String name)에 저장하게 된다.
        (@RequestParam("name") String name,
@RequestParam("amount") int amount)
{
    return new HelloResponseDto(name,amount);
}

name과 amount는 api를 호출하는 곳에서 넘겨준 값들이다. 추가된 api를 테스트 하는 코드를 HelloControllerTest에 추가해 보자.

package com.test.page.web;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;


@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void hello_is_return() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }

    @Test
    public void helloDto_is_return() throws Exception{
        String name ="hello";
        int amount = 1000;

        mvc.perform(
                get("/hello/dto")
                        //parme은 api테스트 할 때 사용할 요청 파라미터를 설정하는 것 이다.
                        //string값만 설정할 수 있으며 int등의 값을 받고싶을땐 문자열로 변경해야 한다.
                .param("name",name)
                .param("amount",String.valueOf(amount)))
                .andExpect(status().isOk())
                //sjonpath는 json응답값을 필드별로 검증할 수 있는 메소드 이다.
                //$를 기준으로 필드명을 명시한다. 여기서는 name, amount를 검증한다.
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));
    }
}