Spring Boot 유닛 테스트 와 통합 테스트 에 대한 상세 한 설명
1.개관
본 논문 에서 우 리 는 단원 테스트 를 어떻게 작성 하고 이 를Spring Boot환경 에 통합 하 는 지 알 게 될 것 이다.너 는 인터넷 에서 이 주제 에 관 한 많은 강 좌 를 찾 을 수 있 지만,한 페이지 에서 네가 필요 로 하 는 모든 정 보 를 찾기 는 매우 어렵다.나 는 초급 개발 자 들 이 유닛 테스트 와 통합 테스트 의 개념 을 헷 갈 리 게 하 는 것 을 자주 알 게 되 었 다.특히 Spring 생태 계 를 이야기 할 때.나 는 서로 다른 주해 가 서로 다른 문맥 에서 의 용법 을 분명하게 말 하려 고 시도 할 것 이다.
2.유닛 테스트 vs.통합 테스트
위 키 피 디 아 는 이렇게 말 했다유닛 테스트.
컴퓨터 프로 그래 밍 에서 유닛 테스트 는 소프트웨어 테스트 방법 으로 소스 코드 의 단일 유닛,하나 이상 의 컴퓨터 프로그램 모듈 의 집합 과 관련 된 제어 데이터,사용 과정 과 조작 과정 을 테스트 하여 사용 하기에 적합 한 지 확인한다.
통합 테스트 :
"통합 테스트(때로는 통합 과 테스트,줄 임 말 I&T)는 소프트웨어 테스트 의 한 단계 로 이 단계 에서 각 소프트웨어 모듈 이 조합 되 어 테스트 를 한다."
한 마디 로 하면 우리 가 유닛 테스트 를 할 때 하나의 코드 유닛 만 테스트 했 을 뿐 매번 하나의 방법 만 테스트 했 을 뿐 테스트 구성 요소 와 상호작용 을 하 는 다른 모든 구성 요 소 는 포함 되 지 않 았 다.
다른 한편,통합 테스트 에서 우 리 는 각 구성 요소 간 의 통합 을 테스트 합 니 다.유닛 테스트 로 인해 우 리 는 이 구성 요소 들 의 행동 이 필요 한 것 과 일치 하 는 것 을 알 수 있 지만 어떻게 함께 일 하 는 지 는 잘 모른다.이것 이 바로 통합 테스트 의 직책 이다.
3.자바 유닛 테스트
모든 자바 개발 자 들 은 JUnit 이 유닛 테스트 를 수행 하 는 주요 프레임 워 크 라 는 것 을 알 고 있 습 니 다.그것 은 기대 에 대해 단언 하기 위해 많은 주 해 를 제공 했다.
Hamcrest 는 소프트웨어 테스트 에 사용 되 는 추가 프레임 워 크 입 니 다.Hamcrest 는 기 존의 matcher 클래스 를 사용 하여 코드 의 조건 을 검사 할 수 있 고 사용자 정의 matcher 를 사용 할 수 있 습 니 다.JUnit 에서 Hamcrest matcher 를 사용 하려 면
assertThat
문 구 를 사용 해 야 합 니 다.그 다음 에 하나 이상 의 matcher 를 사용 해 야 합 니 다.여기 서 당신 은 이 두 가지 프레임 워 크 를 사용 하 는 간단 한 테스트 를 볼 수 있 습 니 다.
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
public class AssertTests {
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}
}
4.우리 의 사례 소개간단 한 프로그램 을 쓰 자.그 목적 은 만화 에 기본 적 인 검색엔진 을 제공 하 는 것 이다.
4.1.Maven 의존
우선 우리 프로젝트 에 의존 하 는 부분 을 추가 해 야 한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
4.2.정의 모델우리 의 모델 은 매우 간단 하 다.단지 두 가지 유형 으로 구성 된다.그것 이 바로 Manga 와 MangaResult 이다.
4.2.1.만화 류
Manga 클래스 는 시스템 에서 검색 한 Manga 인 스 턴 스 를 표시 합 니 다.Lombok 을 사용 하여 모델 코드 를 줄 입 니 다.
package com.mgiglione.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Manga {
private String title;
private String description;
private Integer volumes;
private Double score;
}
4.2.2. MangaResultManga Result 류 는 Manga List 를 포함 한 포장 류 입 니 다.
package com.mgiglione.model;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor
public class MangaResult {
private List<Manga> result;
}
4.3 서비스 실현본 서 비 스 를 실현 하기 위해 저 희 는 Jikan Moe 가 제공 하 는 무료 API 인 터 페 이 스 를 사용 할 것 입 니 다.
RestTemplate 는 API 에 REST 호출 을 시작 하 는 Spring 클래스 입 니 다.
package com.mgiglione.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
@Service
public class MangaService {
Logger logger = LoggerFactory.getLogger(MangaService.class);
private static final String MANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";
@Autowired
RestTemplate restTemplate;
public List<Manga> getMangasByTitle(String title) {
return restTemplate.getForEntity(MANGA_SEARCH_URL+title, MangaResult.class).getBody().getResult();
}
}
4.4.컨트롤 러 구현다음 단 계 는 두 개의 단점 을 드 러 낸 REST Controller 를 쓰 는 것 입 니 다.하 나 는 동기 화 되 고 하 나 는 비동기 적 이 며 테스트 목적 에 만 사 용 됩 니 다.이 컨트롤 러 는 위 에서 정의 한 서 비 스 를 사용 했다.
package com.mgiglione.controller;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RestController
@RequestMapping(value = "/manga")
public class MangaController {
Logger logger = LoggerFactory.getLogger(MangaController.class);
@Autowired
private MangaService mangaService;
@RequestMapping(value = "/async/{title}", method = RequestMethod.GET)
@Async
public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) {
return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
}
@RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) {
return mangaService.getMangasByTitle(title);
}
}
4.5.시스템 시작 및 테스트
mvn spring-boot:run
그리고,Let's try it:
curl http://localhost:8080/manga/async/ken
curl http://localhost:8080/manga/sync/ken
예제 출력:
{
"title":"Rurouni Kenshin: Meiji Kenkaku Romantan",
"description":"Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...",
"volumes":28,
"score":8.69
},
{
"title":"Sun-Ken Rock",
"description":"The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...",
"volumes":25,
"score":8.12
},
{
"title":"Yumekui Kenbun",
"description":"For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....",
"volumes":9,
"score":7.97
}
5.Spring Boot 응용 유닛 테스트Spring Boot 는 테스트 를 간단하게 하기 위해 강력 한 종 류 를 제공 합 니 다.@SpringBootTest설명
Spring Boot 를 기반 으로 실행 되 는 테스트 클래스 에서 이 주 해 를 지정 할 수 있 습 니 다.
일반적인 Spring Test Context Framework 를 제외 하고 다음 과 같은 기능 도 제공 합 니 다.
5.1.MangaService 에 대해 유닛 테스트 를 실시한다.
MangaService 를 테스트 하기 위해 서 는 외부 구성 요소 와 분리 해 야 합 니 다.이 예 에서 외부 구성 요소 만 필요 합 니 다:RestTemplate,원 격 API 를 호출 합 니 다.
우리 가 해 야 할 일 은 RestTemplate Bean 을 모 의 하고 항상 고정된 응답 으로 응답 하도록 하 는 것 입 니 다.Spring Test 를 결합 하여 Mockito 라 이브 러 리 를 확장 하 였 습 니 다.@MockBean 주 해 를 통 해 아 날로 그 Bean 을 설정 할 수 있 습 니 다.
package com.mgiglione.service.test.unit;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
import com.mgiglione.service.MangaService;
import com.mgiglione.utils.JsonUtils;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceUnitTest {
@Autowired
private MangaService mangaService;
// MockBean is the annotation provided by Spring that wraps mockito one
// Annotation that can be used to add mocks to a Spring ApplicationContext.
// If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.
@MockBean
private RestTemplate template;
@Test
public void testGetMangasByTitle() throws IOException {
// Parsing mock file
MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
// Mocking remote service
when(template.getForEntity(any(String.class), any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
// I search for goku but system will use mocked response containing only ken, so I can check that mock is used.
List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
assertThat(mangasByTitle).isNotNull()
.isNotEmpty()
.allMatch(p -> p.getTitle()
.toLowerCase()
.contains("ken"));
}
}
5.2.MangaController 에 대해 유닛 테스트 를 실시한다.MangaService 의 유닛 테스트 에서 보 듯 이 격 리 구성 요소 가 필요 합 니 다.이런 상황 에서 우 리 는 MangaService Bean 을 모 의 해 야 한다.
그리고 또 하나의 문제 가 있 습 니 다.Controller 부분 은 HttpRequest 를 관리 하 는 시스템 의 일부분 이기 때문에 완전한 HTTP 서버 를 시작 하 는 것 이 아니 라 시스템 이 필요 합 니 다.
MockMvc 는 이 동작 을 수행 하 는 Spring 클래스 입 니 다.다른 방식 으로 설정 할 수 있 습 니 다:
package com.mgiglione.service.test.unit;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerUnitTest {
MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@Autowired
MangaController mangaController;
@MockBean
MangaService mangaService;
/**
* List of samples mangas
*/
private List<Manga> mangas;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
// mockMvc = MockMvcBuilders.webAppContextSetup(wac)
// .build();
Manga manga1 = Manga.builder()
.title("Hokuto no ken")
.description("The year is 199X. The Earth has been devastated by nuclear war...")
.build();
Manga manga2 = Manga.builder()
.title("Yumekui Kenbun")
.description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....")
.build();
mangas = new ArrayList<>();
mangas.add(manga1);
mangas.add(manga2);
}
@Test
public void testSearchSync() throws Exception {
// Mocking service
when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title", is("Hokuto no ken")))
.andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
}
@Test
public void testSearchASync() throws Exception {
// Mocking service
when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(request().asyncStarted())
.andDo(print())
// .andExpect(status().is2xxSuccessful()).andReturn();
.andReturn();
// result.getRequest().getAsyncContext().setTimeout(10000);
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title", is("Hokuto no ken")));
}
}
코드 에서 보 듯 이 첫 번 째 해결 방안 을 선택 한 것 은 가장 가 벼 운 양 이 고 우 리 는 Spring 컨 텍스트 에 불 러 온 대상 을 더욱 잘 관리 할 수 있 기 때 문 입 니 다.비동기 테스트 에 서 는 먼저 서 비 스 를 호출 한 다음 에 asyncDispatch 방법 을 시작 하여 비동기 행 위 를 모 의 해 야 합 니 다.
6.Spring Boot 응용 통합 테스트
통합 테스트 에 대하 여 우 리 는 하위 통신 을 제공 하여 우리 의 주요 구성 요 소 를 검사 하 기 를 희망 합 니 다.
6.1.MangaService 에 대한 통합 테스트
이 테스트 도 매우 간단 하 다.우 리 는 원 격 Manga API 를 호출 하 는 것 이 목적 이기 때문에 어떤 것 도 모 의 할 필요 가 없다.
package com.mgiglione.service.test.integration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceIntegrationTest {
@Autowired
private MangaService mangaService;
@Test
public void testGetMangasByTitle() {
List<Manga> mangasByTitle = mangaService.getMangasByTitle("ken");
assertThat(mangasByTitle).isNotNull().isNotEmpty();
}
}
6.2 MangaController 에 대한 통합 테스트이 테스트 는 단원 테스트 와 매우 비슷 하지만 이 사례 에서 우 리 는 MangaService 를 더 이상 모 의 할 필요 가 없다.
package com.mgiglione.service.test.integration;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerIntegrationTest {
// @Autowired
MockMvc mockMvc;
@Autowired
protected WebApplicationContext wac;
@Autowired
MangaController mangaController;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
// mockMvc = MockMvcBuilders.webAppContextSetup(wac)
// .build();
}
@Test
public void testSearchSync() throws Exception {
mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
}
@Test
public void testSearchASync() throws Exception {
MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(request().asyncStarted())
.andDo(print())
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
}
}
7.결론Spring Boot 환경 에서 유닛 테스트 와 통합 테스트 의 주요 차이 점 을 알 고 Hamcrest 처럼 테스트 를 간소화 하 는 프레임 워 크 를 알 게 되 었 습 니 다.물론 내GitHub 창고에서 모든 코드 를 찾 을 수 있다.
원문:https://dzone.com/articles/unit-and-integration-tests-in-spring-boot-2
저자:Marco Giglione
이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
thymeleaf로 HTML 페이지를 동적으로 만듭니다 (spring + gradle)지난번에는 에서 화면에 HTML을 표시했습니다. 이번에는 화면을 동적으로 움직여보고 싶기 때문에 입력한 문자를 화면에 표시시키고 싶습니다. 초보자의 비망록이므로 이상한 점 등 있으면 지적 받을 수 있으면 기쁩니다! ...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.