티스토리 뷰
이번엔, 앞에서 TDD 로 기반을 만든 서비스를 다충 아키텍처로 설계 하고 서비스 해보도록 하겠다.
우선 웹 애플리케이션을 설계할때 아래 3계층이 가장 많이 사용 된다.
- 클라이언트 계층
- 사용자 인터페이스를 제공하는 계층 (프론트 엔드)
- 애플리케이션 계층
- 비지니스 로직, 상호작용을 위한 인터페이스, 데이터를 저장하는 인터페이스를 포함하는 계층 (백엔드)
- 데이터 저장 계층
- 애플리케이션의 데이터를 보관하는 계층, (DB, File System ...)
여기서 애플리케이션 계층을 새분화 하면 아래와 같다
- 비니지스 레이어
- 도메인과 비지니스 명세를 모델링한 클레스가 있음, 도메인(개체)과 애플리케이션(서비스)로 나누기도 함
- 프레젠테이션 레이어
- 웹 클라이언트에 기능을 제공하는 컨트롤러 클래스가 프레젠테이션 레이어에 해당, Rest API 구현
- 데이터 레이어
- 개체들을 데이터 스토리지나 데이터베이스를 보관
- 액세스 객체 (Data Access Object; DAO) 또는 저장소 클래스를 포함한다.
이번에 사용할 아키텍처 패턴은 아래와 같다.
위와 같은 아키텍처는 레이어를 분리하고 결합도를 낮춥니다.
- 도메인과 솔루션이 분리돼 있기 때문에, 인터페이스나 데이터베이스 명세가 섞여있지 않습니다.
- 프레젠테이션과 데이터 레이어는 다른 레어로 교체할 수 있습니다.
- 각 레이어의 역할이 명확히 구분됩니다. 비지니스 로직을 처리하는 클래스, REST API를 구현하는 클래스, 객체를 데이터베이스에 저장하는 클래스
웹어플리 케이션 개발을 위해 이제 도메인 설계를 진행 하도록 하겠다.
- Cert : 인증데이터
- User : 인증 하는 사용자 식별
- CertResultAttempt : Cert 과 User의 참조를 포함하고 사용자가 제출한 인증키와 인증 결과 포함
Domain 정의
package com.sw.cert.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 인증 도메인
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class Cert {
private final String certKey;
private final String certResult;
// JSON (역) 직렬화를 위한 빈 생성자
Cert() {
this("","");
}
}
package com.sw.cert.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 인증 도메인
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class Cert {
private final String certKey;
// JSON (역) 직렬화를 위한 빈 생성자
Cert() {
this("");
}
}
package com.sw.cert.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* {@link User} 가 {@link Cert}을 인증한 데이터를 정의한 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class CertResultAttempt {
private final User user;
private final Cert cert;
private final int attempt;
// JSON (역)직렬화를 위한 빈 생성자
CertResultAttempt() {
user = null;
cert = null;
attempt = -1;
}
}
final class 로 도메인을 정의하여, 불변성(Immutability)를 만들어서 스레드 세이프하게 만들어 준다.
비지니스 로직 레이어
아래 요구 사항에 맞게 비지니스를 만들어 보자.
- 인증 5번 실패시 해당 인증키 사용 불가
- 인증 5번 실패시 초기화
위의 조건에 맞는 메서드를 CertService 에 구현해 보자
package com.sw.cert;
import com.sw.cert.domain.CertResultAttempt;
public interface CertService {
/*
* 인증키를 조회 하여 인증 결과를 내려줌
*/
CertResDto cert(String certKey);
/**
*
* @return 일정 인증시도가 초과 하면 false
*/
boolean checkAttempt(final CertResultAttempt certResultAttempt);
}
역시 TDD 방법으로 실패 케이스를 먼저 테스트 코드로 만들고 시작하자.
package com.sw.cert;
import com.sw.cert.domain.Cert;
import com.sw.cert.domain.CertResultAttempt;
import com.sw.cert.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@SpringBootTest
public class CertServiceTest {
@MockBean
private GeneratorCertKeyService generatorCertKeyService;
@Autowired
private CertService certService;
@Test
public void createGeneratorCertKeyTest() {
// 초기화
CertResDto certResDto = new CertResDto("0000");
// given (generatorCertKeyService 가 처음 정상 인증키 2023A, 나중에는 비정상 인증키 2022B를 반환 하도록 설정)
given(generatorCertKeyService.generatorCertKey()).willReturn("2023A", "2023B");
// when
certResDto = certService.cert(generatorCertKeyService.generatorCertKey());
// assert
assertThat(certResDto.getCertResult()).isEqualTo("success");
// when
certResDto = certService.cert(generatorCertKeyService.generatorCertKey());
// assert
assertThat(certResDto.getCertResult()).isEqualTo("fail");
}
@Test
public void checkRightCertAttemptTest() {
// given
Cert cert = new Cert("2023A");
User user = new User("Han", "000001");
CertResultAttempt certResultAttempt = new CertResultAttempt(user, cert, 0);
// when
boolean attemptResult = certService.checkAttempt(certResultAttempt);
// assert
assertThat(attemptResult).isTrue();
}
@Test
public void checkWrongCertAttemptTest() {
// given
Cert cert = new Cert("2023B");
User user = new User("Han", "000001");
CertResultAttempt certResultAttempt = new CertResultAttempt(user, cert, 0);
// when
IntStream.range(1, 5)
.filter(i -> i <= 5)
.forEach(i ->
certService.checkAttempt(certResultAttempt)
);
boolean attemptResult = certService.checkAttempt(certResultAttempt);
// assert
assertThat(attemptResult).isFalse();
}
}
checkAttempt 는 빈 로직에 false 만 리턴하도록 되어 있기 때문에, 정상 케이스 (checkRightCertAttemptTest) 에는 실패가 발생하고, checkWrongCertAttemptTest 에는 성공이 발생한다. 이제 로직을 추가해서 checkRightCertAttemptTest 가 제대로 작동 하도록 하자.
@Override public boolean checkAttempt(CertResultAttempt certResultAttempt) {
return certResultAttempt.getAttempt() != 5;
}
이제 다시 테스트 코드를 돌리면 성공~!
프레젠테이션 레이어(REST API)
이제는 도메인 개체와 간단한 비지니스 로직을 작성했으니, 웹 클라이언트와 다른 애플리케이션이 우리가 만든 기능과 상호작용할 수 있도록 REST API 를 개발하자.
API 요구 사항은 아래와 같이 정의한다.
- 사용자가 얼마나 인증 횟수를 초과 했는지 조회가능한 API
- 사용자 인증 횟수를 초기화 하는 API
위의 요구 사항으로 REST API 를 설계 하면 아래와 같다.
- GET /cert/attempt:인증시도 횟수 조회
- POST /cert/reset:인증시도 횟수 초기화
- GET /cert/인증내역 리스트
CertController 을 생성하고, 역시 테스트 코드부터 작성을 시작한다.
package com.sw.cert;
import com.sw.cert.domain.CertResultAttempt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/cert")
public class CertController {
private final CertService certService;
@Autowired
public CertController(final CertService certService) {
this.certService = certService;
}
}
package com.sw.cert;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sw.cert.domain.Cert;
import com.sw.cert.domain.CertResultAttempt;
import com.sw.cert.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.event.annotation.BeforeTestMethod;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
@WebMvcTest(CertController.class)
public class CertControllerTest {
@MockBean
private CertService certService;
@Autowired
private MockMvc mvc;
// 이 객체는 initFields() 메서드를 이용해 자동으로 초기화
private JacksonTester<CertResultAttempt> json;
@BeforeTestMethod
public void setUp() {
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void getCertAttemptTest() throws Exception {
// given
Cert cert = new Cert("2023A");
User user = new User("Han", "000001");
CertResultAttempt certResultAttempt = new CertResultAttempt(user, cert, 0);
// when
MockHttpServletResponse response = mvc.perform(
get("/cert/attempt")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString())
.isEqualTo(json.write(certResultAttempt).getJson());
}
}
해당 코드에서 유심히 봐야할 것은, 아래와 같다
- @WebMvcTest는 스프링의 웹 애플리케이션 컨텍스트를 초기화 한다. 하지만 @SpringBootTest처럼 모든 설정을 불러오는 것이 아니라 MVC 레이어(컨트롤러)와 관련된 설정만 불러온다. 이 애너테이션은 MockMvc 빈도 불러온다.
- @MockBean 사용 하였는데, 이는 서비스 테스트가 아니라 컨트롤러 테스트이기 때문에, given 에서 지정한 값으로 테스트 하기 위함이다.
- JacksonTester 객체를 통해 JSON 내용을 쉽게 확인할 수 있다. @JsonTest 애노테이션을 통하여 자동주입이 가능하다.
또한, @WebMvcTest 와 @SpringBootTest의 차이는 아래와 같다.
- @WebMvcTest는 컨트롤러를 테스트하는 애너테이션이다. 그러므로 http 요청과 응답은 목을 이용해 가짜로 이뤄지고 실제 연결은 생성되지 않는다.
- @SpringBootTest는 웹 애플리케이션 컨텍스트와 설정을 모두 불러와 웹서버와 연결을 시도 하기 때문에, 이런 경우 MovkMvc 가 아니라 TestRestTemplate 을 사용 해야 한다.
- 그렇다면 @WebMvcTest는 서버에서 컨트롤러만 테스트할 때 사용하고, @SpringBootTest는 클라이언트부터 상호작용을 확인하는 통합 테스트에서 사용하는 것이 좋다.
Controller 생성
package com.sw.cert;
import com.sw.cert.domain.CertResultAttempt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/cert")
public class CertController {
private final CertService certService;
@Autowired
public CertController(final CertService certService) {
this.certService = certService;
}
@GetMapping("/attempt")
public boolean getCheckAttempt(@RequestBody CertResultAttempt certResultAttempt) {
return certService.checkAttempt(certResultAttempt);
}
}
완료된 테스트 코드
package com.sw.cert;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sw.cert.domain.Cert;
import com.sw.cert.domain.CertResultAttempt;
import com.sw.cert.domain.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
@WebMvcTest(CertController.class)
public class CertControllerTest {
@MockBean
private CertService certService;
@Autowired
private MockMvc mvc;
// 이 객체는 initFields() 메서드를 이용해 자동으로 초기화
private JacksonTester<CertResultAttempt> json;
@BeforeEach
public void setUp() {
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void getCertAttemptTest() throws Exception {
// given
given(certService.generatorCertResultAttempt())
.willReturn(new CertResultAttempt(new User("Han", "000001"), new Cert("2023A"), 0));
// when
CertResultAttempt attempt = certService.generatorCertResultAttempt();
String a = json.write(attempt).getJson();
MockHttpServletResponse response = mvc.perform(
get("/cert/attempt")
.contentType(MediaType.APPLICATION_JSON)
.content(json.write(attempt).getJson())
)
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString())
.isEqualTo("false");
}
}
이로서 GET /cert/attempt:인증시도 횟수 조회에 관련테스트 코드작성을 완료 하였다. 추후 DB 추가를 통하여 DB 에서 횟수를 저장하고 조회하는 기능을 구현할 것이다.
- POST /cert/reset:인증시도 횟수 초기화
- GET /cert/인증내역 리스트
위의 내용 또한, TDD 를 이용하여 구현 후 다음 장에서 프론트 페이지 적용 및 DB 연결을 진행 한뒤, 승인 MSA 서비스를 하나 추가 하며 서로다른 마이크로 서비스를 연결해 보도록 하겠다.
'MSA' 카테고리의 다른 글
타사 MSA 분석하기 (0) | 2023.04.02 |
---|---|
Kafka 를 이용한 msa 서비스 통신 (0) | 2023.02.26 |
Kafka 로컬 연동 및 테스트 (0) | 2023.02.25 |
스프링부트를 활용한 마이크로 서비스 개발_1 (0) | 2023.01.28 |
- Total
- Today
- Yesterday
- 웹개발
- kafka
- 켄트 백
- SpringBoot
- MQ
- 테스트주도개발
- Python
- 웹서비스
- mongodb
- 테스트
- 분산처리
- nodejs
- GateWayApi
- MSA
- data crawling
- AWS
- 테스트 주도 개발
- fastapi
- EC2
- 퀜트백
- Python #FastAPI
- TDD
- data mining
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |