해당 내용은 이동욱 저자의 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스'를 읽고 공부한 내용을 정리한 것입니다.
스프링부트 공부가 이번이 처음은 아니다. 하지만 때때로 핑계를 대가며 미루다보니 벌써 작년이 된 2022년 가을 K-해커톤 대회를 끝으로 스프링에 손을 안댄지 4개월이란 시간이 지났다. 다시 해보려하니 기억이 안나는 부분도 많고 익숙했던 것이 어색하지기도 하여 다시금 공부를 해보려 책을 펼쳤다.
스프링 공부는 이동욱 저자의 '스프링 부트와 AWs로 혼자 구현하는 웹 서비스' 라는 책을 통해 공부하려 한다.
이 책의 1장에서는 간단하게 스프링에 대한 설정 및 사용하는 IDE에 대해서 다뤘다. gradle 설정과 자바, 스프링부트의 버전 등을 설정했는데 기본적인 설정이라 크게 다룰 부분은 없었다.
buildscript {
ext {
springBootVersion = '2.1.7.RELEASE'
}
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
jcenter()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
2번째 장부터는 본격적으로 진도가 나간다. 2번째 장에서는 테스트코드 작성법과 테스트 코드를 작성해야 하는 이유에 대해 서술되어 있다.
이 책에 따르면 대부분의 서비스 회사에서는 테스트 코드에 관해 요구하고 있으며 최근 뜨고 있는 어떤 서비스 회사에서는 코딩 테스트로 알고리즘이 아닌 프로젝트 생성과 함께 단위 테스트를 진행하는 방식의 코딩테스트를 시행하고 있다고 한다.
TDD란 테스트가 주도하는 개발을 의미한다.

위의 그림이 레드-그린 사이클을 보여준다.
Red - 항상 실패하는 테스트 먼저 작성
Green - 테스트가 통과하는 프로덕션 코드 작성
Refactor - 테스트가 통과하면 프로덕션 코드를 리팩토링 한다.
단위 테스트는 TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 의미한다. 테스트 코드를 꼭 먼저 작성해야하는 것도 아니고 리팩토링도 포함되지 않는다. 즉, 순수하게 테스트 코드만 작성하는 것을 단위 테스트라고 한다.
테스트 코드를 작성하는 이유?
1. 단위 테스트는 개발단계 초기에 문제를 발견하게 해준다.
2. 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있다.
3. 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있다.
4. 단위 테스트는 시스템에 대한 불확실성을 감소시킬 수 있다.
5. 단위 테스트는 시스템에 대한 실제 문서를 제공한다. 즉, 단윝 테스트 자체를 문서로 사용 가능하다.
위와 같은 이유들로 테스트 코드는 작성하는 것이 반드시 이롭다. 위의 내용을 추가하면
1. 빠른 피드백
2. 자동검증 가능
3. 개발자가 만든 기능을 안전하게 보호
가 있다.
테스트 코드 작성을 도와주는 프레임워크
- JUnit - Java
- DBUnit - DB
- CppUnit - C++
- NUnit - .net
이번에는 한번 테스트 코드를 간단하게 작성해보자. 먼저 java 디렉토리에서 마우스 오른쪽 버튼으로 패키지를 생성한다. 일반적으로 패키지는 웹 사이트 주소의 역순으로 생성하니 참고하자.
패키지를 만든 후 해당 패키지 아래에 java 클래스를 생성해보자. 클래스 이름은 Application으로 하고 코드는 다음과 같다.
package com.cchj.admin.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}
}
import 시에는 코드 작성하면서 나오는 빨간줄 위에서 윈도우 기준 alt + enter 키를 누르면 패키지를 import할 수 있다.
여기서 작성한 Application 클래스는 앞으로 만들 프로젝트의 메인 클래스가 된다.
@SpringBootApplication 으로 인해 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성을 모두 자동으로 설정한다.
특히, @SpringBootApplication 이 있는 위치부터 설정을 읽어가기 때문에 이 클래스는 항상 프로젝트의 최상단에 위치해야 한다!!
main 메소드의 SpringApplication.run 으로 인해 내장 WAS(Web Application Server)가 실행된다. 이렇게 되면 항상 서버에 톰캣을 설치할 필요가 없다. 스프링 부트에서는 내장 WAS를 사용하는 것을 권장한다.
언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문!
현재 패키지 하위에 web이란 패키지를 만들어보자. 앞으로 이 패키지에서는 컨트롤러와 관련된 모든 클래스들을 넣을 것이다.
package com.cchj.admin.springboot.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";
}
}
@RestConroller
- 컨트롤러를 JSON으로 반환하는 컨트롤러로 만들어준다.
- 예전에는 @ResponseBody를 각 메소드마다 선언해줬는데 이를 한번에 사용할 수 있게 해주는 어노테이션이다.
@GetMapping- HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어준다.
- 예전에는 @RequestMapping(method = RequestMethod.GET)으로 사용되었다.
src/test/java 디렉토리 하위에 앞에서 생성한 패키지들을 그대로 다시 만든다.
일반적으로 테스트 클래스는 대상 클래스 이름 뒤에 Test를 붙여서 만든다.
package com.cchj.admin.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.web.servlet.ResultActions;
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;
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void hello_리턴() throws Exception{
String hello = "hello";
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
}
- @RunWith(SpringRunner.class)
- 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킨다.
- 여기서는 SpringRunner라는 스프링 실행자를 사용한다.
- 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.
- @WebMvcTest
- 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
- 선언 시 @Controller, @ControllerAdvice 사용 가능
- 단, @Service, @Component, @Repository 등은 사용 불가
- 여기서는 컨트롤러만 사용하기에 선언
- @AutoWired
- 스프링이 관리하는 빈(Bean)을 주입 받는다.
- private MockMvc mvc
- 웹 API를 테스트할 때 사용
- 스프링 MVC 테스트의 시작점
- 이 클래스를 통해 HTTP GET, POST 등에 대한 API를 테스트할 수 있다.
- mvc.perform(get("/hello"))
- MockMvc를 통해 /hello 주소로 HTTP GET 요청을 한다.
- 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언 가능
- .andExpect(status().isOk())
- mvc.perform의 결과를 검증
- HTTP Header의 Status를 검증한다.
- 우리가 흔히 알고 있는 200, 404, 500 등의 상태 검증
- 여기선 OK 즉, 200인지 아닌지 검사
- .andExpect(content().string(hello))
- mvc.perform의 결과를 검증
- 응답 본문의 내용을 검증
- Controller에서 "hello"를 리턴하기 때문에 이 값이 맞는지 검증
테스트 코드는 꼭 해볼 것! -> 견고한 소프트웨어를 만드는 역량 성장의 발판
절대 수동으로 검증하고 테스트 코드를 작성하지 않는다! 테스트 코드로 먼저 검증 후 그래도 의심가면 프로젝트를 실행해 확인하는 습관을 들일것 !
롬복은 자바 개발 시 자주 사용하는 Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해준다.
build.gradle에 다음의 코드를 추가한다.
compile('org.projectlombok:lombok')
이후 refresh로 새로고침을 해서 내려받는다. 그 후 롬볼 플러그인을 검색하여 설치해주면 끝이다.
지금 하고 있는 작은 규모의 프로젝트에서는 롬복으로 전환하는 것이 쉽지만 큰 규모의 프로젝트에서는 쉽지 않다. 어떤 기능이 제대로 작동될지 안될지 예측할 수 없이 때문이다.
우리는 테스트 코드가 본 코드를 지켜주고 있기 때문에 맘 놓고 변경해도 된다.
먼저 web 패키지에 dto 패키지를 추가한다. 앞으로 모든 응답 Dto는 이 dto 패키지에 추가한다.
package com.cchj.admin.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
private final String name;
private final int amount;
}
- @Getter
- 선언된 모든 필드의 get 메소드를 생성해주는 어노테이션
- @RequiredArgsConstructor
- 선언된 모든 final 필드가 포함된 생성자를 생성해준다.
- final이 없는 필드는 생성자에 포함되지 않는다.
위의 코드에 대한 테스트코드를 작성한다. 패키지는 전의 HelloController코드의 테스트 코드를 작성할때 방식 처럼 패키지를 생성한 뒤 클래스를 작성한다.
package com.cchj.admin.springboot.web.dto;
import org.junit.Test;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class HelloResponseDtoTest {
@Test
public void 롬복_기능_테스트(){
//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
- asserj 라는 테스트 검증 라이브러리의 검증 메소드이다.
- 검증하고 싶은 대상을 메소드 인자로 받는다.
- 메소드 체이닝이 지원된다.
- isEqualTo
- assertj 의 동등 비교 메소드이다.
- assertThat에 있는 값과 isEqualTo의 값을 비교하여 같을 때만 성공
여기서 Junit의 기본 asserThat이 아닌 assertj의 assertThat을 사용했는데 장점은 다음과 같다.
1. CoreMatcher와 달리 추가적으로 라이브러리가 필요하지 않다.
2. 자동완성이 좀 더 확실하게 지원된다.
위의 코드를 실행해보면 테스트가 무사히 통과되는 것을 확인할 수 있다.
HelloController 코드에 밑의 코드를 추가한다.
@GetMapping("/hello/dto")
public HelloResponseDto helloResponseDto(@RequestParam("name") String name, @RequestParam("amount") int amount){
return new HelloResponseDto(name, amount);
@RequestParam
- 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션이다.
- 여기서는 외부에서 name (@RequestParam("name"))이란 이름으로 넘긴 파라미터를 메소드 파라미터 name(String name)에 저장하게 된다.
HelloControllerTest를 다음과 같이 변경한다.
package com.cchj.admin.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.web.servlet.ResultActions;
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_리턴() throws Exception{
String hello = "hello";
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string(hello));
}
@Test
public void helloDto가_리턴() throws Exception{
String name = "hello";
int amount = 1000;
mvc.perform(get("/hello/dto")
.param("name", name)
.param("amount", String.valueOf(amount)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name",is(name)))
.andExpect(jsonPath("$.amount", is(amount)));
}
}
- param
- API 테스트 시 사용될 요청 파라미터를 설정한다.
- 단, 값은 String만 허용된다.
- 숫자/날짜 등의 데이터도 등록 시에는 문자열로 변경해야 한다.
- jsonPath
- JSON 응답값을 필드별로 검증할 수 있는 메소드이다.
- $를 기준으로 필드명을 명시힌다.
- 여기서는 name과 amount를 검증하니 $.name, $.amount로 검증한다.