티스토리 뷰

이번엔, 앞에서 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
링크
«   2024/11   »
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
글 보관함