스프링 부트는 각 계층이 양 옆의 계층과 통신하는 구조를 따른다. 계층은 각자의 역할과 책임이 있는 어떤 소프트웨어의 구성 요소를 의미한다. 각 계층은 소통할 수 있지만 다른 계층에 직접 간섭하거나 영향을 미치지 않는다.
스프링 부트에는 프레젠테이션(컨트롤러), 비즈니스(서비스), 퍼시스턴스(리포지토리) 계층이 있다.
| 계층 구분 | 역할 설명 |
|---|---|
| 프레젠테이션 계층 | - 클라이언트의 HTTP 요청을 수신 - 요청을 비즈니스 계층에 전달하고 결과를 응답 |
| 비즈니스 계층 | - 비즈니스 로직 처리 전담 - 서비스 목적에 맞는 핵심 기능 수행 |
| 퍼시스턴스 계층 | - 데이터베이스와의 연동 담당 - DAO(Data Access Object)를 통해 데이터 조작 수행 |
톰캣에 HTTP 요청을 한다. 이 요청은 스프링 부트 내로 이동한다. 이때 스프링 부트의 디스패처 서블릿이 URL을 분석하고, 이 요청을 처리할 수 있는 컨트롤러를 찾는다.
컨트롤러가 비즈니스 계층과 퍼시스턴스 계층을 통하면서 필요한 데이터를 가져온다.
뷰 리졸버는 템플릿 엔진을 사용해 HTML 문서를 만들거나 JSON, XML 등의 데이터를 생성한다.
디스패처 서블릿에 의해 응답으로 클라이언트에게 반환된다.
스프링 부트는 애플리케이션을 테스트하기 위한 도구와 애너테이션을 제공한다.
spring-boot-starter-test 스타터에 테스트를 위한 도구가 모여 있다. 대표적으로 :
| 항목 | 설명 |
|---|---|
| JUnit | 자바 프로그래밍 언어용 단위 테스트 프레임워크 |
| Spring Test & Spring Boot Test | 스프링 및 스프링 부트 애플리케이션을 위한 통합 테스트 지원 도구 |
| AssertJ | 가독성 높고 풍부한 기능을 제공하는 어설션(검증문) 작성 라이브러리 |
자바 언어를 위한 단위 테스트 프레임워크
단위 테스트란, 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미합니다. 이때 단위는 보통 메서드가 된다.
JUnit은 테스트 클래스에 정의된 각 테스트 메서드(@Test)를 실행할 때마다 새로운 인스턴스(객체)를 생성해서 테스트를 수행하고, 테스트가 끝나면 그 인스턴스를 즉시 폐기합니다.
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class JUnitTest {
@DisplayName("1 + 2는 3이다")
@Test
public void junitTest() {
int a = 1;
int b = 2;
int sum = 3;
Assertions.assertEquals(sum, a + b);
}
}
@DisplayName 애너테이션은 테스트 이름을 명시해준다.
@Test 애너테이션을 붙인 메서드는 테스트를 수행하는 메서드가 된다.
assertEquals() 메서드의 첫 번째 인수에는 기대하는 값, 두 번째 인수에는 실제로 검증할 값을 넣어준다.
import org.junit.jupiter.api.*;
public class JUnitCycleTest {
@BeforeAll
static void beforeAll() {
System.out.println("@BeforeAll");
}
@BeforeEach
public void beforeEach() {
System.out.println("@BeforeEach");
}
@Test
public void test1() {
System.out.println("test1");
}
@Test
public void test2() {
System.out.println("test2");
}
@Test
public void test3() {
System.out.println("test3");
}
@AfterAll
static void afterAll() {
System.out.println("@AfterAll");
}
@AfterEach
public void afterEach() {
System.out.println("@AfterEach");
}
}
전체 테스트를 시작하기 전에 처음으로 한 번만 실행
전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언
테스트 케이스를 시작하기 전에 매번 실행
각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함
전체 테스트를 마치고 종료하기 전에 한 번만 실행
전체 테스트 주기에서 한 번만 호출되어야 하기 때문에 static으로 선언
각 테스트 케이스를 종료하기 전 매번 실행
각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 함
import static org.assertj.core.api.Assertions.assertThat;
assertThat(a + b).isEqualTo(sum);
assertThat을 사용시 주로 static import 사용
다양한 검증 메서드 제공 (isEqualTo, isNotEqualTo, contains, doesNotContain, startsWith, endsWith, isEmpty, isNotEmpty, isPositive, isNegative, isGreaterThan, isLessThan 등)
MockMvc를 생성하고 자동으로 구성하는 애너테이션
MockMvc는 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공하는 유틸리티 클래스이다. 즉, 컨트롤러를 테스트할 때 사용되는 클래스
@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Autowired
private MemberRepository memberRepository;
@BeforeEach
public void mockMvcSetUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@AfterEach
public void cleanUp() {
memberRepository.deleteAll();
}
@DisplayName("getAllMembers: 아티클 조회에 성공한다.")
@Test
public void getAllMembers() throws Exception {
// given
final String url = "/test";
Member savedMember = memberRepository.save(new Member(1L, "홍길동"));
// when
final ResultActions result = mockMvc.perform(get(url)
.accept(MediaType.APPLICATION_JSON));
// then
result
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(savedMember.getId()))
.andExpect(jsonPath("$[0].name").value(savedMember.getName()));
}
}
perform() 메서드는 요청을 전송하는 역할을 하는 메서드이다. 그 결과로 ResultActions 객체를 받으며, ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공해준다.
accept() 메서드는 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드이다.
jsonPath() 메서드는 JSON 응답값의 값을 가져오는 역할을 하는 메서드이다.
@SpringBootTest
@AutoConfigureMockMvc
class QuizControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
public void mockMvcSetUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.build();
}
@DisplayName("quiz(): GET /quiz?code=1 이면 응답 코드는 201, 응답 본문은 Created!를 리턴한다.")
@Test
public void getQuiz1() throws Exception {
String url = "/quiz";
final ResultActions result = mockMvc.perform(get(url)
.param("code", "1"));
result
.andExpect(status().isCreated())
.andExpect(content().string("Created!"));
}
@DisplayName("quiz(): GET /quiz?code=2 이면 응답 코드는 400, 응답 본문은 Bad Request!를 리턴한다.")
@Test
public void getQuiz2() throws Exception {
String url = "/quiz";
final ResultActions result = mockMvc.perform(get(url)
.param("code", "2"));
result
.andExpect(status().isBadRequest())
.andExpect(content().string("Bad Request!"));
}
@DisplayName("quiz(): POST /quiz 이면 응답 코드는 403, 응답 본문은 Forbidden!를 리턴한다.")
@Test
public void postQuiz1() throws Exception {
String url = "/quiz";
final ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Code(1)))
);
result
.andExpect(status().isForbidden())
.andExpect(content().string("Forbidden!"));
}
@DisplayName("quiz(): POST /quiz 이면 응답 코드는 200, 응답 본문은 OK!를 리턴한다.")
@Test
public void postQuiz2() throws Exception {
String url = "/quiz";
final ResultActions result = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Code(13)))
);
result
.andExpect(status().isOk())
.andExpect(content().string("OK!"));
}
record Code(int value) {}
}