스프링 부트와 AWS로 혼자 구현하는 웹 서비스 Chapter2 추가

This commit is contained in:
banjjoknim
2020-11-17 19:50:29 +09:00
parent 7d6b51ab7e
commit 07cd696f1d
8 changed files with 363 additions and 0 deletions

10
.gitignore vendored
View File

@@ -6,3 +6,13 @@ TIL(Today-I-Learned).iml
.idea/ .idea/
WebServiceBySpringBootAndAWS/settings.gradle WebServiceBySpringBootAndAWS/settings.gradle
WebServiceBySpringBootAndAWS/gradle/wrapper/gradle-wrapper.properties WebServiceBySpringBootAndAWS/gradle/wrapper/gradle-wrapper.properties
WebServiceBySpringBootAndAWS/build/reports/tests/test/index.html
WebServiceBySpringBootAndAWS/build/reports/tests/test/classes/com.banjjoknim.book.springboot.web.HelloControllerTest.html
WebServiceBySpringBootAndAWS/build/reports/tests/test/css/base-style.css
WebServiceBySpringBootAndAWS/build/reports/tests/test/css/style.css
WebServiceBySpringBootAndAWS/build/reports/tests/test/js/report.js
WebServiceBySpringBootAndAWS/build/reports/tests/test/packages/com.banjjoknim.book.springboot.web.html
WebServiceBySpringBootAndAWS/build/test-results/test/TEST-com.banjjoknim.book.springboot.web.HelloControllerTest.xml
WebServiceBySpringBootAndAWS/build/test-results/test/binary/output.bin
WebServiceBySpringBootAndAWS/build/test-results/test/binary/output.bin.idx
WebServiceBySpringBootAndAWS/build/test-results/test/binary/results.bin

View File

@@ -26,5 +26,6 @@ repositories {
dependencies { dependencies {
compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.boot:spring-boot-starter-web')
compile('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.boot:spring-boot-starter-test')
} }

View File

@@ -0,0 +1,246 @@
# Chapter2. 스프링 부트에서 테스트 코드를 작성하자
---
## 2.1 테스트 코드 소개
`TDD`와 단위 테스트는 다른 이야기이다. `TDD`는 **테스트가 주도하는 개발**을 이야기하는데, **테스트 코드를 먼저 작성**하는 것부터 시작합니다.
>**레드 그린 사이클**
>- 항상 실패하는 테스트를 먼저 작성하고(`Red`)
>- 테스트가 통과하는 프로덕션 코드를 작성하고(`Green`)
>- 테스트가 통과하면 프로덕션 코드를 리팩토링 합니다(`Refactor`).
반면 단위 테스트는 `TDD`의 첫 번째 단계인 **기능 단위의 테스트 코드를 작성**하는 것을 이야기한다. `TDD`와 달리 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않는다. 순수하게 테스크 코드만 작성하는 것을 말한다.
>**단위 테스트 코드를 작성함으로써 얻는 이점**
>- 단위 테스트는 개발단계 초기에 문제를 발견하게 도와줍니다.
>- 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있습니다(예, 회귀테스트).
>- 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있습니다.
>- 단위 테스트는 시스템에 대한 실제 문서를 제공합니다. 즉, 단위 테스트 자체가 문서로 사용할 수 있습니다.
---
## 2.2 Hello Controller 테스트 코드 작성하기
- 일반적으로 패키지 명은 **웹 사이트 주소의 역순**으로 합니다. 예를 들어 `admin.jojoldu.com`이라는 사이트라면 패키지명은 `com.jojoldu.admin`으로 하면 됩니다.
- `@SpringBootApplication` 어노테이션으로 인해 스프링 부트의 자동 설정, 스프링 `Bean` 읽기와 생성이 모두 자동으로 설정됩니다.
- 특히, `@SpringBootApplication`이 있는 위치부터 설정을 읽어가기 때문에 이 어노테이션이 선언된 클래스는 항상 **프로젝트의 최상단에 위치**해야만 합니다.
- 내장 WAS(`Web Application Server`)란 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 이야기합니다. `SpringApplication.run`으로 인해 내장 WAS를 실행합니다. 이렇게 되면 항상 서버에 **톰캣을 설치할 필요가 없게 되고,** 스프링 부트로 만들어진 `Jar` 파일(실행 가능한 Java 패키징 파일)로 실행하면 됩니다.
- 스프링 부트에서는 **내장 WAS를 사용하는 것을 권장**한다. **언제 어디서나 같은 환경에서 스프링 부트를 배포**할 수 있기 때문이다.
>
- **컨트롤러와 관련된 클래스들은 모두 web 패키지에 추가**
```java
package com.banjjoknim.book.springboot.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController // 1.
public class HelloController {
@GetMapping("/hello") // 2.
public String hello() {
return "hello";
}
}
```
**1. @RestController**
- 컨트롤러를 `JSON`을 반환하는 컨트롤러로 만들어 줍니다.
- 예전에는 `@ResponseBody`를 각 메소드마다 선언했던 것을 한번에 사용할 수 있게 해준다고 생각하면 됩니다.
**2. @GetMapping**
- `HTTP Method``Get`의 요청을 받을 수 있는 API를 만들어 줍니다.
- 예전에는 `@RequestMapping(method = RequestMethod.GET)`으로 사용되었습니다.
```java
package com.banjjoknim.book.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;
@RunWith(SpringRunner.class) // 1.
@WebMvcTest(controllers = HelloController.class) // 2.
public class HelloControllerTest {
@Autowired // 3.
private MockMvc mvc; // 4.
@Test
public void hello가_리턴된다() throws Exception {
String hello = "hello";
mvc.perform(get("/hello")) // 5.
.andExpect(status().isOk()) // 6.
.andExpect(content().string(hello)); // 7.
}
}
```
**1. @RunWith(SpringRunner.class)**
- 테스트를 진행할 때 `JUnit`에 내장된 실행자 외에 다른 실행자를 실행시킵니다.
- 여기서는 `SpringRunner`라는 스프링 실행자를 사용합니다.
- 즉, 스프링 부트 테스트와 `JUnit` 사이에 연결자 역할을 합니다.
**2. @WebMvcTest**
- 여러 스프링 테스트 어노테이션 중, `Web(Spring MVC)`에 집중할 수 있는 어노테이션입니다.
- 선언할 경우 `@Controller`, `@ControllerAdvice` 등을 사용할 수 있습니다.
- 단, `@Service`, `@Component`, `@Repository` 등은 사용할 수 없습니다.
- 여기서는 컨트롤러만 사용하기 때문에 선언합니다.
**3. @Autowired**
- 스프링이 관리하는 빈(`Bean`)을 주입 받습니다.
**4. private MockMvc mvc**
- 웹 API를 테스트할 때 사용합니다.
- 스프링 `MVC` 테스트의 시작점입니다.
- 이 클래스를 통해 `HTTP GET`, `POST` 등에 대한 API 테스트를 할 수 있습니다.
**5. mvc.perform(get("/hello"))**
- `MockMvc`를 통해 `/hello` 주소로 `HTTP GET` 요청을 합니다.
- 체이닝이 지원되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있습니다.
**6. .andExpect(status().isOk())**
- `mvc.perform`의 결과를 검증합니다.
- `HTTP Header``Status`를 검증합니다.
- 우리가 흔히 알고 있는 `200`, `404`, `500` 등의 상태를 검증합니다.
- 여기선 `OK` 즉, `200`인지 아닌지를 검증합니다.
**7. .andExpect(content().string(hello))**
- `mvc.perform`의 결과를 검증합니다.
- 응답 본문의 내용을 검증합니다.
- `Controller`에서 `"hello"`를 리턴하기 때문에 이 값이 맞는지 검증합니다.
---
## 2.3 롬복 소개 및 설치하기
**자바 개발자들의 필수 라이브러리 롬복**
- 롬복은 자바 개발할 때 자주 사용하는 코드 `Getter`, `Setter`, 기본생성자, `toString` 등을 어노테이션으로 자동 생성해 줍니다.
- `build.gradle`에 다음의 코드를 추가하여 의존성(라이브러리)을 추가합니다.
```java
compile('org.projectlombok:lombok')
```
- `plugins`에서 `lombok` 플러그인을 검색하여 설치합니다.
- 롬복은 프로젝트마다 설정해야 합니다. 플러그인 설치는 한 번만 하면 되지만, `build.gradle`에 라이브러리를 추가하는 것과 `Enable annotation processing`를 체크하는 것은 프로젝트마다 진행해야 합니다.
---
## 2.4 Hello Controller 코드를 롬복으로 전환하기
- **모든 응답 Dto는 Dto 패키지에 추가**
```java
package com.banjjoknim.book.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter // 1.
@RequiredArgsConstructor // 2.
public class HelloResponseDto {
private final String name;
private final int amount;
}
```
**1. @Getter**
- 선언된 모든 필드의 `get` 메소드를 생성해줍니다.
**2. @RequiredArgsConstructor**
- 선언된 모든 `final` 필드가 포함된 생성자를 생성해 줍니다.
- `final`이 없는 필드는 생성자에 포함되지 않습니다.
```java
package com.banjjoknim.book.springboot.web.dto;
import org.junit.Test;
import static org.assertj.core.api.Assertions.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); // 1. , 2.
assertThat(dto.getAmount()).isEqualTo(amount);
}
}
```
**1. assertThat**
- `assertj`라는 테스트 검증 라이브러리의 검증 메소드입니다.
- 검증하고 싶은 대상을 메소드 인자로 받습니다.
- 메소드 체이닝이 지원되어 `isEqualTo`와 같이 메소드를 이어서 사용할 수 있습니다.
**2. isEqualTo**
- `assertj`의 동등 비교 메소드입니다.
- `assertThat`에 있는 값과 `isEqualTo`의 값을 비교해서 같을 때만 성공입니다.
**Junit과 비교하여 assertj의 장점은 다음과 같습니다.**
- `CoreMatchers`와 달리 추가적으로 라이브러리가 필요하지 않습니다.
- `JUnit``assertThat`을 쓰게 되면 `is()`와 같이 `CoreMatchers` 라이브러리가 필요합니다.
- 자동완성이 좀 더 확실하게 지원됩니다.
- IDE에서는 `CoreMatchers`와 같은 `Matcher` 라이브러리의 자동완성 지원이 약합니다.
```java
@GetMapping("/hello/dto")
public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) { // 1.
return new HelloResponseDto(name, amount);
}
```
**1. @RequestParam**
- 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션입니다.
- 여기서는 외부에서 `name (@RequestParam("name"))`이란 이름으로 넘긴 파라미터를 메소드 파라미터 `name (String name)`에 저장하게 됩니다.
`name``amount`는 API를 호출하는 곳에서 넘겨준 값들입니다. 추가된 API를 테스트하는 코드를 `HelloControllerTest`에 추가합니다.
```java
@Test
public void helloDto가_리턴된다() throws Exception {
String name = "hello";
int amount = 1000;
mvc.perform(get("/hello/dto")
.param("name", name) // 1.
.param("amount", String.valueOf(amount)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is(name))) // 2.
.andExpect(jsonPath("$.amount", is(amount)));
}
```
**1. param**
- API 테스트할 때 사용될 요청 파라미터를 설정합니다.
- 단, 값은 `String`만 허용됩니다.
- 그래서 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능합니다.
**2. jsonPath**
- `JSON` 응답값을 필드별로 검증할 수 있는 메소드입니다.
- `$`를 기준으로 필드명을 명시합니다.
- 여기서는 `name``amount`를 검증하니 `$.name`, `$.amount`로 검증합니다.
---

View File

@@ -0,0 +1,11 @@
package com.banjjoknim.book.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);
}
}

View File

@@ -0,0 +1,20 @@
package com.banjjoknim.book.springboot.web;
import com.banjjoknim.book.springboot.web.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);
}
}

View File

@@ -0,0 +1,12 @@
package com.banjjoknim.book.springboot.web.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
private final String name;
private final int amount;
}

View File

@@ -0,0 +1,41 @@
package com.banjjoknim.book.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.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)));
}
}

View File

@@ -0,0 +1,22 @@
package com.banjjoknim.book.springboot.web.dto;
import org.junit.Test;
import static org.assertj.core.api.Assertions.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);
}
}