다층 아키텍처 : 애플리케이션을 여러 계층으로 나눈 아키텍처
package microservices.book.multiplication.service;
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 java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RandomGeneratorServiceTest {
@Autowired
private RandomGeneratorService randomGeneratorService;
@Test
public void generateRandomFactorIsBetweenExpectedLimits() throws Exception {
// 무작위 숫자를 생성
List<Integer> randomFactors = IntStream.range(0, 1000)
.map(i -> randomGeneratorService.generateRandomFactor())
.boxed().collect(Collectors.toList());
// 적당히 어려운 계산을 만들기 위해서
// 생성한 인수가 11~99 범위에 있는지 확인
assertThat(randomFactors).containsOnlyElementsOf(IntStream.range(11, 100)
.boxed().collect(Collectors.toList()));
}
}
-> @SpringBootTest를 남용하는 문제 발생 (지금처럼 단순히 클래스 하나의 기능을 테스트하기 위해서라면 종속성 주입이나 애플리케이션 컨텍스트가 필요없음 -> 클래스의 구현체를 테스트하는 게 나음)
package microservices.book.multiplication.service;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
public class RandomGeneratorServiceImplTest {
private RandomGeneratorServiceImpl randomGeneratorServiceImpl;
@Before
public void setUp() {
randomGeneratorServiceImpl = new RandomGeneratorServiceImpl();
}
@Test
public void generateRandomFactorIsBetweenExpectedLimits() throws Exception {
// 무작위 숫자를 생성
List<Integer> randomFactors = IntStream.range(0, 1000)
.map(i -> randomGeneratorServiceImpl.generateRandomFactor())
.boxed().collect(Collectors.toList());
// 적당히 어려운 계산을 만들기 위해서
// 생성한 인수가 11~99 범위에 있는지 확인
assertThat(randomFactors).containsOnlyElementsOf(IntStream.range(11, 100)
.boxed().collect(Collectors.toList()));
}
}
-> RandomGeneratorServiceImpl 작성 필요
package microservices.book.multiplication.service;
import org.springframework.stereotype.Service;
@Service
class RandomGeneratorServiceImpl implements RandomGeneratorService {
@Override
public int generateRandomFactor() {
return 0;
}
}
package microservices.book.multiplication.service;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
class RandomGeneratorServiceImpl implements RandomGeneratorService {
final static int MINIMUM_FACTOR = 11;
final static int MAXIMUM_FACTOR = 99;
@Override
public int generateRandomFactor() {
return new Random().nextInt((MAXIMUM_FACTOR - MINIMUM_FACTOR) + 1) + MINIMUM_FACTOR;
}
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
public class MultiplicationServiceImplTest {
private MultiplicationServiceImpl multiplicationServiceImpl;
@Mock
private RandomGeneratorService randomGeneratorService;
@Before
public void setUp() {
// 목 객체를 초기화합니다.
MockitoAnnotations.initMocks(this);
multiplicationServiceImpl = new MultiplicationServiceImpl(randomGeneratorService);
}
@Test
public void createRandomMultiplicationTest() {
// given (randomGeneratorService 가 처음에 50, 나중에 30을 반환하도록 설정)
given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when
Multiplication multiplication = multiplicationServiceImpl.createRandomMultiplication();
// assert
assertThat(multiplication.getFactorA()).isEqualTo(50);
assertThat(multiplication.getFactorB()).isEqualTo(30);
assertThat(multiplication.getResult()).isEqualTo(1500);
}
}
-> @MockBean 대신 @Mock을 사용해 목 객체를 생성하고 객체의 생성자로 넘겨줌
dependencies {
...
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 애플리케이션에서 곱셈을 나타내는 클래스 (a * b)
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class Multiplication {
// 두 인수
private final int factorA;
private final int factorB;
// JSON (역)직렬화를 위한 빈 생성자
Multiplication() {
this(0, 0);
}
}
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 사용자 정보를 저장하는 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class User {
private final String alias;
// JSON (역)직렬화를 위한 빈 생성자
protected User() {
alias = null;
}
}
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* {@link User}가 {@link Multiplication}을 계산한 답안을 정의한 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class MultiplicationResultAttempt {
private final User user;
private final Multiplication multiplication;
private final int resultAttempt;
// JSON (역)직렬화를 위한 빈 생성자
MultiplicationResultAttempt() {
user = null;
multiplication = null;
resultAttempt = -1;
}
}
[어노테이션 정리]
1. @RequiredArgsConstructor : 모든 상수 필드를 갖는 생성자를 만듦
2. @Getter : 모든 필드의 getter를 만듦
3. @ToString : 해당 클래스의 toString() 메서드를 읽기 쉽게 만듦
4. @EqualsAndHashCode : equals()와 hashCode() 메서드를 만듦
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
public interface MultiplicationService {
/**
* 두 개의 무작위 인수(11~99)를 담은 {@link Multiplication} 객체를 생성
*
* @return 무작위 인수를 담은 {@link Multiplication} 객체
*/
Multiplication createRandomMultiplication();
/**
* @return 곱셈 계산 결과가 맞으면 true, 아니면 false
*/
boolean checkAttempt(final MultiplicationResultAttempt resultAttempt);
}
@Override
public boolean checkAttempt(MultiplicationResultAttempt resultAttempt) {
return false;
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
public class MultiplicationServiceImplTest {
private MultiplicationServiceImpl multiplicationServiceImpl;
@Mock
private RandomGeneratorService randomGeneratorService;
@Before
public void setUp() {
// 목 객체를 초기화합니다.
MockitoAnnotations.initMocks(this);
multiplicationServiceImpl = new MultiplicationServiceImpl(randomGeneratorService);
}
@Test
public void createRandomMultiplicationTest() {
// given (randomGeneratorService 가 처음에 50, 나중에 30을 반환하도록 설정)
given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when
Multiplication multiplication = multiplicationServiceImpl.createRandomMultiplication();
// assert
assertThat(multiplication.getFactorA()).isEqualTo(50);
assertThat(multiplication.getFactorB()).isEqualTo(30);
}
@Test
public void checkCorrectAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(user, multiplication, 3000);
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isTrue();
}
@Test
public void checkWrongAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(user, multiplication, 3010);
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isFalse();
}
}
@Override
public boolean checkAttempt(final MultiplicationResultAttempt resultAttempt) {
return resultAttempt.getResultAttempt() ==
resultAttempt.getMultiplication().getFactorA() *
resultAttempt.getMultiplication().getFactorB();
}
package microservices.book.multiplication.controller;
import microservices.book.multiplication.service.MultiplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
/**
* 곱셈 애플리케이션의 REST API 를 구현한 클래스
*/
@RestController
final class MultiplicationController {
private final MultiplicationService multiplicationService;
@Autowired
public MultiplicationController(final MultiplicationService multiplicationService) {
this.multiplicationService = multiplicationService;
}
}
package microservices.book.multiplication.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.service.MultiplicationService;
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.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.junit4.SpringRunner;
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.test.web.servlet.request.MockMvcRequestBuilders.get;
@RunWith(SpringRunner.class)
@WebMvcTest(MultiplicationController.class)
public class MultiplicationControllerTest {
@MockBean
private MultiplicationService multiplicationService;
@Autowired
private MockMvc mvc;
// 이 객체는 initFields() 메소드를 이용해 자동으로 초기화
private JacksonTester<Multiplication> json;
@Before
public void setup() {
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void getRandomMultiplicationTest() throws Exception {
// given
given(multiplicationService.createRandomMultiplication())
.willReturn(new Multiplication(70, 20));
// when
MockHttpServletResponse response = mvc.perform(
get("/multiplications/random")
.accept(MediaType.APPLICATION_JSON))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString())
.isEqualTo(json.write(new Multiplication(70, 20)).getJson());
}
}
[이전 테스트와 다른 점]
1. @WebMvcTest는 스프링의 웹 애플리케이션 컨텍스트를 초기화. MVC 레이어와 관련된 설정만 불러옴 -> 이 애너테이션은 MockMvc빈도 불러옴
2. @MockBean을 사용 -> 스프링이 진짜 빈 대신 목 객체를 주입해야 하기 때문. 목 객체는 given() 메서드에서 지정한대로 값을 반환
3. JacksonTester 객체를 사용해 JSON의 내용을 쉽게 확인 가능
package microservices.book.multiplication.controller;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.service.MultiplicationService;
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;
/**
* 곱셈 애플리케이션의 REST API 를 구현한 클래스
*/
@RestController
@RequestMapping("/multiplications")
final class MultiplicationController {
private final MultiplicationService multiplicationService;
@Autowired
public MultiplicationController(final MultiplicationService multiplicationService) {
this.multiplicationService = multiplicationService;
}
@GetMapping("/random")
Multiplication getRandomMultiplication() {
return multiplicationService.createRandomMultiplication();
}
}
package microservices.book.multiplication.controller;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import microservices.book.multiplication.service.MultiplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 사용자가 POST 로 답안을 전송하도록 REST API 를 제공하는 클래스
*/
@RestController
@RequestMapping("/results")
final class MultiplicationResultAttemptController {
private final MultiplicationService multiplicationService;
@Autowired
MultiplicationResultAttemptController(final MultiplicationService multiplicationService) {
this.multiplicationService = multiplicationService;
}
// 나중에 여기에 POST 구현체를 추가
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
@Getter
static final class ResultResponse {
private final boolean correct;
}
}
package microservices.book.multiplication.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import microservices.book.multiplication.service.MultiplicationService;
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.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.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static microservices.book.multiplication.controller.MultiplicationResultAttemptController.ResultResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@RunWith(SpringRunner.class)
@WebMvcTest(MultiplicationResultAttemptController.class)
public class MultiplicationResultAttemptControllerTest {
@MockBean
private MultiplicationService multiplicationService;
@Autowired
private MockMvc mvc;
// 이 객체는 initFields() 메소드를 이용해 자동으로 초기화
private JacksonTester<MultiplicationResultAttempt> jsonResult;
private JacksonTester<ResultResponse> jsonResponse;
@Before
public void setup() {
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void postResultReturnCorrect() throws Exception {
genericParameterizedTest(true);
}
@Test
public void postResultReturnNotCorrect() throws Exception {
genericParameterizedTest(false);
}
void genericParameterizedTest(final boolean correct) throws Exception {
// given (지금 서비스를 테스트하는 것이 아님)
given(multiplicationService
.checkAttempt(any(MultiplicationResultAttempt.class)))
.willReturn(correct);
User user = new User("john");
Multiplication multiplication = new Multiplication(50, 70);
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3500);
// when
MockHttpServletResponse response = mvc.perform(
post("/results").contentType(MediaType.APPLICATION_JSON)
.content(jsonResult.write(attempt).getJson()))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonResponse.write(new ResultResponse(correct)).getJson());
}
}
package microservices.book.multiplication.controller;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.service.MultiplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 사용자가 POST 로 답안을 전송하도록 REST API 를 제공하는 클래스
*/
@RestController
@RequestMapping("/results")
final class MultiplicationResultAttemptController {
private final MultiplicationService multiplicationService;
@Autowired
MultiplicationResultAttemptController(final MultiplicationService multiplicationService) {
this.multiplicationService = multiplicationService;
}
@PostMapping
ResponseEntity<ResultResponse> postResult(@RequestBody MultiplicationResultAttempt multiplicationResultAttempt) {
return ResponseEntity.ok(
new ResultResponse(multiplicationService
.checkAttempt(multiplicationResultAttempt)));
}
@RequiredArgsConstructor
@NoArgsConstructor(force = true)
@Getter
static final class ResultResponse {
private final boolean correct;
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Multiplication v1</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="multiplication-client.js"></script>
</head>
<body>
<div>
<h1>안녕하세요, 소셜 곱셈입니다!</h1>
<h2>오늘의 문제:</h2>
<h1>
<span class="multiplication-a"></span> x <span class="multiplication-b"></span> =
</h1>
<p>
<form id="attempt-form">
답은? <input type="text" name="result-attempt"><br>
닉네임: <input type="text" name="user-alias"><br>
<input type="submit" value="확인">
</form>
</p>
<h2><span class="result-message"></span></h2>
</div>
</body>
</html>
html, body {
height: 100%;
}
html {
display: table;
margin: auto;
}
body {
display: table-cell;
vertical-align: middle;
}
function updateMultiplication() {
$.ajax({
url: "http://localhost:8080/multiplications/random"
}).then(function (data) {
// 폼 비우기
$("#attempt-form").find("input[name='result-attempt']").val("");
$("#attempt-form").find("input[name='user-alias']").val("");
// 무작위 문제를 API로 가져와서 추가하기
$('.multiplication-a').empty().append(data.factorA);
$('.multiplication-b').empty().append(data.factorB);
});
}
$(document).ready(function () {
updateMultiplication();
$("#attempt-form").submit(function (event) {
// 폼 기본 제출 막기
event.preventDefault();
// 페이지에서 값 가져오기
var a = $('.multiplication-a').text();
var b = $('.multiplication-b').text();
var $form = $(this),
attempt = $form.find("input[name='result-attempt']").val(),
userAlias = $form.find("input[name='user-alias']").val();
// API 에 맞게 데이터를 조합하기
var data = {user: {alias: userAlias}, multiplication: {factorA: a, factorB: b}, resultAttempt: attempt};
// POST를 이용해서 데이터 보내기
$.ajax({
url: '/results',
type: 'POST',
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
if (result.correct) {
$('.result-message').empty().append("정답입니다! 축하드려요!");
} else {
$('.result-message').empty().append("오답입니다! 그래도 포기하지 마세요!");
}
}
});
updateMultiplication();
});
});
[기능]
1. 페이지가 로드된 후에 REST API로 무작위 곱셈 문제를 가져와 보여줌
2. 폼 제출 이벤트 리스너를 등록. 폼 제출의 기본 동작 대신 폼에서 데이터를 가져와 채점 결과를 확인하는 API를 호출하고 결과를 사용자에게 보여줌
최근에 제출한 답안을 보고 싶음. 시간이 지나면서 내가 얼마나 잘하고 있는지 또는 못하고 있는지를 알 수 있기 때문에
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* {@link User}가 {@link Multiplication}을 계산한 답안을 정의한 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
public final class MultiplicationResultAttempt {
private final User user;
private final Multiplication multiplication;
private final int resultAttempt;
private final boolean correct;
// JSON/JPA 를 위한 빈 생성자
MultiplicationResultAttempt() {
user = null;
multiplication = null;
resultAttempt = -1;
correct = false;
}
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
public class MultiplicationServiceImplTest {
private MultiplicationServiceImpl multiplicationServiceImpl;
@Mock
private RandomGeneratorService randomGeneratorService;
@Before
public void setUp() {
// initMocks 를 호출해 Mockito 가 어노테이션을 처리하도록 지시
MockitoAnnotations.initMocks(this);
multiplicationServiceImpl = new MultiplicationServiceImpl(randomGeneratorService);
}
@Test
public void createRandomMultiplicationTest() {
// given (randomGeneratorService 가 처음에 50, 나중에 30을 반환하도록 설정)
given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when
Multiplication multiplication = multiplicationServiceImpl.createRandomMultiplication();
// then
assertThat(multiplication.getFactorA()).isEqualTo(50);
assertThat(multiplication.getFactorB()).isEqualTo(30);
}
@Test
public void checkCorrectAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3000, false);
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isTrue();
}
@Test
public void checkWrongAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3010, false);
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isFalse();
}
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
@Service
class MultiplicationServiceImpl implements MultiplicationService {
private RandomGeneratorService randomGeneratorService;
@Autowired
public MultiplicationServiceImpl(RandomGeneratorService randomGeneratorService) {
this.randomGeneratorService = randomGeneratorService;
}
@Override
public Multiplication createRandomMultiplication() {
int factorA = randomGeneratorService.generateRandomFactor();
int factorB = randomGeneratorService.generateRandomFactor();
return new Multiplication(factorA, factorB);
}
@Override
public boolean checkAttempt(final MultiplicationResultAttempt attempt) {
// 답안을 채점
boolean correct = attempt.getResultAttempt() ==
attempt.getMultiplication().getFactorA() *
attempt.getMultiplication().getFactorB();
// 조작된 답안을 방지
Assert.isTrue(!attempt.isCorrect(), "채점한 상태로 보낼 수 없습니다!!");
MultiplicationResultAttempt checkedAttempt = new MultiplicationResultAttempt(
attempt.getUser(),
attempt.getMultiplication(),
attempt.getResultAttempt(),
correct
);
return correct;
}
}
@PostMapping
ResponseEntity<MultiplicationResultAttempt> postResult(@RequestBody MultiplicationResultAttempt multiplicationResultAttempt) {
boolean isCorrect = multiplicationService.checkAttempt(multiplicationResultAttempt);
MultiplicationResultAttempt attemptCopy = new MultiplicationResultAttempt(
multiplicationResultAttempt.getUser(),
multiplicationResultAttempt.getMultiplication(),
multiplicationResultAttempt.getResultAttempt(),
isCorrect
);
return ResponseEntity.ok(attemptCopy);
}
void genericParameterizedTest(final boolean correct) throws Exception {
// given (지금 서비스를 테스트하는 것이 아님)
given(multiplicationService
.checkAttempt(any(MultiplicationResultAttempt.class)))
.willReturn(correct);
User user = new User("john");
Multiplication multiplication = new Multiplication(50, 70);
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3500, correct);
// when
MockHttpServletResponse response = mvc.perform(
post("/results").contentType(MediaType.APPLICATION_JSON)
.content(jsonResult.write(attempt).getJson()))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonResult.write(
new MultiplicationResultAttempt(attempt.getUser(),
attempt.getMultiplication(),
attempt.getResultAttempt(),
correct)
).getJson());
}
// POST를 이용해서 데이터 보내기
$.ajax({
url: '/results',
type: 'POST',
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
async: false,
success: function (result) {
if (result.correct) {
$('.result-message').empty().append("정답입니다! 축하드려요!");
} else {
$('.result-message').empty().append("오답입니다! 그래도 포기하지 마세요!");
}
}
});
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'junit:junit:4.13.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
# H2 데이터베이스 웹 콘솔에 접속
spring.h2.console.enabled=true
# 데이터베이스가 '없는 경우에만' 데이터베이스를 생성
spring.jpa.hibernate.ddl-auto=update
# 파일로 데이터베이스를 생성
spring.datasource.url=jdbc:h2:file:~/social-multiplication;DB_CLOSE_ON_EXIT=FALSE;
# 학습 목적으로 콘솔에 SQL을 출력
spring.jpa.properties.hibernate.show_sql=true
User 개체 : 사용자는 여러 답안을 제출할 수 있음. 동시에 같은 숫자로 이뤄진 곱셈 문제에 여러 사용자가 답안을 제출할 수 있음.
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* 애플리케이션에서 곱셈을 나타내는 클래스 (a * b)
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
@Entity
public final class Multiplication {
@Id
@GeneratedValue
@Column(name = "MULTIPLICATION_ID")
private Long id;
// 두 인수
private final int factorA;
private final int factorB;
// JSON/JPA 를 위한 빈 생성자
Multiplication() {
this(0, 0);
}
}
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* 사용자 정보를 저장하는 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
@Entity
public final class User {
@Id
@GeneratedValue
@Column(name = "USER_ID")
private Long id;
private final String alias;
// JSON/JPA 를 위한 빈 생성자
protected User() {
alias = null;
}
}
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
/**
* {@link User}가 {@link Multiplication}을 계산한 답안을 정의한 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
@Entity
public final class MultiplicationResultAttempt {
@Id
@GeneratedValue
private Long id;
@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "USER_ID")
private final User user;
@ManyToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "MULTIPLICATION_ID")
private final Multiplication multiplication;
private final int resultAttempt;
private final boolean correct;
// JSON/JPA 를 위한 빈 생성자
MultiplicationResultAttempt() {
user = null;
multiplication = null;
resultAttempt = -1;
correct = false;
}
}
-> 애너테이션으로 관계에 대해 상세한 내용을 정의
package microservices.book.multiplication.repository;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
/**
* 답안을 저장하고 조회하기 위한 인터페이스
*/
public interface MultiplicationResultAttemptRepository
extends CrudRepository<MultiplicationResultAttempt, Long> {
/**
* @return 닉네임에 해당하는 사용자의 최근 답안 5개
*/
List<MultiplicationResultAttempt> findTop5ByUserAliasOrderByIdDesc(String userAlias);
}
package microservices.book.multiplication.repository;
import microservices.book.multiplication.domain.User;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
/**
* {@link User}를 저장하고 조회하기 위한 인터페이스
*/
public interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByAlias(final String alias);
}
package microservices.book.multiplication.repository;
import microservices.book.multiplication.domain.Multiplication;
import org.springframework.data.repository.CrudRepository;
/**
* {@link Multiplication}을 저장하고 조회하기 위한 인터페이스
*/
public interface MultiplicationRepository extends CrudRepository<Multiplication, Long> {
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import microservices.book.multiplication.repository.MultiplicationResultAttemptRepository;
import microservices.book.multiplication.repository.UserRepository;
import org.assertj.core.util.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
public class MultiplicationServiceImplTest {
private MultiplicationServiceImpl multiplicationServiceImpl;
@Mock
private RandomGeneratorService randomGeneratorService;
@Mock
private MultiplicationResultAttemptRepository attemptRepository;
@Mock
private UserRepository userRepository;
@Before
public void setUp() {
// initMocks 를 호출해 Mockito 가 어노테이션을 처리하도록 지시
MockitoAnnotations.initMocks(this);
multiplicationServiceImpl = new MultiplicationServiceImpl(randomGeneratorService, attemptRepository, userRepository);
}
@Test
public void createRandomMultiplicationTest() {
// given (randomGeneratorService 가 처음에 50, 나중에 30을 반환하도록 설정)
given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when
Multiplication multiplication = multiplicationServiceImpl.createRandomMultiplication();
// then
assertThat(multiplication.getFactorA()).isEqualTo(50);
assertThat(multiplication.getFactorB()).isEqualTo(30);
}
@Test
public void checkCorrectAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3000, false);
MultiplicationResultAttempt verifiedAttempt = new MultiplicationResultAttempt(
user, multiplication, 3000, true);
given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty());
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isTrue();
verify(attemptRepository).save(verifiedAttempt);
}
@Test
public void checkWrongAttemptTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3010, false);
given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty());
// when
boolean attemptResult = multiplicationServiceImpl.checkAttempt(attempt);
// then
assertThat(attemptResult).isFalse();
verify(attemptRepository).save(attempt);
}
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import microservices.book.multiplication.repository.MultiplicationResultAttemptRepository;
import microservices.book.multiplication.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@Service
class MultiplicationServiceImpl implements MultiplicationService {
private RandomGeneratorService randomGeneratorService;
private MultiplicationResultAttemptRepository attemptRepository;
private UserRepository userRepository;
@Autowired
public MultiplicationServiceImpl(final RandomGeneratorService randomGeneratorService,
final MultiplicationResultAttemptRepository attemptRepository,
final UserRepository userRepository) {
this.randomGeneratorService = randomGeneratorService;
this.attemptRepository = attemptRepository;
this.userRepository = userRepository;
}
@Override
public Multiplication createRandomMultiplication() {
int factorA = randomGeneratorService.generateRandomFactor();
int factorB = randomGeneratorService.generateRandomFactor();
return new Multiplication(factorA, factorB);
}
@Transactional
@Override
public boolean checkAttempt(final MultiplicationResultAttempt attempt) {
// 해당 닉네임의 사용자가 존재하는지 확인
Optional<User> user = userRepository.findByAlias(attempt.getUser().getAlias());
// 조작된 답안을 방지
Assert.isTrue(!attempt.isCorrect(), "채점한 상태로 보낼 수 없습니다!!");
// 답안을 채점
boolean isCorrect = attempt.getResultAttempt() ==
attempt.getMultiplication().getFactorA() *
attempt.getMultiplication().getFactorB();
MultiplicationResultAttempt checkedAttempt = new MultiplicationResultAttempt(
user.orElse(attempt.getUser()),
attempt.getMultiplication(),
attempt.getResultAttempt(),
isCorrect
);
// 답안을 저장
attemptRepository.save(checkedAttempt);
return isCorrect;
}
}
package microservices.book.multiplication.service;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import java.util.List;
public interface MultiplicationService {
/**
* 두 개의 무작위 인수(11~99)를 담은 {@link Multiplication} 객체를 생성
*
* @return 무작위 인수를 담은 {@link Multiplication} 객체
*/
Multiplication createRandomMultiplication();
/**
* @return 곱셈 계산 결과가 맞으면 true, 아니면 false
*/
boolean checkAttempt(final MultiplicationResultAttempt resultAttempt);
/**
* 해당 사용자의 통계 정보를 조회한다.
*
* @param userAlias 해당 사용자의 닉네임
* @return 해당 사용자가 전에 제출한 답안 객체 {@link MultiplicationResultAttempt}의 리스트
*/
List<MultiplicationResultAttempt> getStatsForUser(final String userAlias);
}
@Override
public List<MultiplicationResultAttempt> getStatsForUser(String userAlias) {
return attemptRepository.findTop5ByUserAliasOrderByIdDesc(userAlias);
}
@Test
public void retrieveStatsTest() {
// given
Multiplication multiplication = new Multiplication(50, 60);
User user = new User("john_doe");
MultiplicationResultAttempt attempt1 = new MultiplicationResultAttempt(
user, multiplication, 3010, false);
MultiplicationResultAttempt attempt2 = new MultiplicationResultAttempt(
user, multiplication, 3051, false);
List<MultiplicationResultAttempt> latestAttempts = Lists.newArrayList(attempt1, attempt2);
given(userRepository.findByAlias("john_doe")).willReturn(Optional.empty());
given(attemptRepository.findTop5ByUserAliasOrderByIdDesc("john_doe"))
.willReturn(latestAttempts);
// when
List<MultiplicationResultAttempt> latestAttemptsResult =
multiplicationServiceImpl.getStatsForUser("john_doe");
// then
assertThat(latestAttemptsResult).isEqualTo(latestAttempts);
}
package microservices.book.multiplication.controller;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.service.MultiplicationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 사용자가 POST 로 답안을 전송하도록 REST API 를 제공하는 클래스
*/
@RestController
@RequestMapping("/results")
final class MultiplicationResultAttemptController {
private final MultiplicationService multiplicationService;
@Autowired
MultiplicationResultAttemptController(final MultiplicationService multiplicationService) {
this.multiplicationService = multiplicationService;
}
@PostMapping
ResponseEntity<MultiplicationResultAttempt> postResult(@RequestBody MultiplicationResultAttempt multiplicationResultAttempt) {
boolean isCorrect = multiplicationService.checkAttempt(multiplicationResultAttempt);
MultiplicationResultAttempt attemptCopy = new MultiplicationResultAttempt(
multiplicationResultAttempt.getUser(),
multiplicationResultAttempt.getMultiplication(),
multiplicationResultAttempt.getResultAttempt(),
isCorrect
);
return ResponseEntity.ok(attemptCopy);
}
@GetMapping
ResponseEntity<List<MultiplicationResultAttempt>> getStatistics(@RequestParam("alias") String alias) {
return ResponseEntity.ok(
multiplicationService.getStatsForUser(alias)
);
}
}
package microservices.book.multiplication.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import microservices.book.multiplication.domain.Multiplication;
import microservices.book.multiplication.domain.MultiplicationResultAttempt;
import microservices.book.multiplication.domain.User;
import microservices.book.multiplication.service.MultiplicationService;
import org.assertj.core.util.Lists;
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.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.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@RunWith(SpringRunner.class)
@WebMvcTest(MultiplicationResultAttemptController.class)
public class MultiplicationResultAttemptControllerTest {
@MockBean
private MultiplicationService multiplicationService;
@Autowired
private MockMvc mvc;
// 이 객체는 initFields() 메소드를 이용해 자동으로 초기화
private JacksonTester<MultiplicationResultAttempt> jsonResultAttempt;
private JacksonTester<List<MultiplicationResultAttempt>> jsonResultAttemptList;
@Before
public void setup() {
JacksonTester.initFields(this, new ObjectMapper());
}
@Test
public void postResultReturnCorrect() throws Exception {
genericParameterizedTest(true);
}
@Test
public void postResultReturnNotCorrect() throws Exception {
genericParameterizedTest(false);
}
void genericParameterizedTest(final boolean correct) throws Exception {
// given (지금 서비스를 테스트하는 것이 아님)
given(multiplicationService
.checkAttempt(any(MultiplicationResultAttempt.class)))
.willReturn(correct);
User user = new User("john");
Multiplication multiplication = new Multiplication(50, 70);
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3500, correct);
// when
MockHttpServletResponse response = mvc.perform(
post("/results").contentType(MediaType.APPLICATION_JSON)
.content(jsonResultAttempt.write(attempt).getJson()))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonResultAttempt.write(
new MultiplicationResultAttempt(attempt.getUser(),
attempt.getMultiplication(),
attempt.getResultAttempt(),
correct)
).getJson());
}
@Test
public void getUserStats() throws Exception {
// given
User user = new User("john_doe");
Multiplication multiplication = new Multiplication(50, 70);
MultiplicationResultAttempt attempt = new MultiplicationResultAttempt(
user, multiplication, 3500, true);
List<MultiplicationResultAttempt> recentAttempts = Lists.newArrayList(attempt, attempt);
given(multiplicationService
.getStatsForUser("john_doe"))
.willReturn(recentAttempts);
// when
MockHttpServletResponse response = mvc.perform(
get("/results").param("alias", "john_doe"))
.andReturn().getResponse();
// then
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo(
jsonResultAttemptList.write(
recentAttempts
).getJson());
}
}
function updateStats(alias) {
$.ajax({
url: "http://localhost:8080/results?alias=" + alias,
}).then(function (data) {
$('#stats-body').empty();
data.forEach(function (row) {
$('#stats-body').append('<tr><td>' + row.id + '</td>' +
'<td>' + row.multiplication.factorA + ' x ' + row.multiplication.factorB + '</td>' +
'<td>' + row.resultAttempt + '</td>' +
'<td>' + (row.correct === true ? 'YES' : 'NO') + '</td></tr>');
});
});
}
$(document).ready(function () {
updateMultiplication();
$("#attempt-form").submit(function (event) {
// 폼 기본 제출 막기
event.preventDefault();
// 페이지에서 값 가져오기
var a = $('.multiplication-a').text();
var b = $('.multiplication-b').text();
var $form = $(this),
attempt = $form.find("input[name='result-attempt']").val(),
userAlias = $form.find("input[name='user-alias']").val();
// API에 맞게 데이터를 조합하기
var data = {user: {alias: userAlias}, multiplication: {factorA: a, factorB: b}, resultAttempt: attempt};
// POST 를 이용해서 데이터 보내기
$.ajax({
url: '/results',
type: 'POST',
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
async: false,
success: function (result) {
if (result.correct) {
$('.result-message').empty().append("정답입니다! 축하드려요!");
} else {
$('.result-message').empty().append("오답입니다! 그래도 포기하지 마세요!");
}
}
});
updateMultiplication();
updateStats(userAlias);
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Multiplication v1</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="multiplication-client.js"></script>
</head>
<body>
<div>
<h1>안녕하세요, 소셜 곱셈입니다!</h1>
<h2>오늘의 문제:</h2>
<h1>
<span class="multiplication-a"></span> x <span class="multiplication-b"></span> =
</h1>
<p>
<form id="attempt-form">
답은? <input type="text" name="result-attempt"><br>
닉네임: <input type="text" name="user-alias"><br>
<input type="submit" value="확인">
</form>
</p>
<h2><span class="result-message"></span></h2>
<h2>통계</h2>
<table id="stats" style="width:100%">
<tr>
<th>답안 ID</th>
<th>곱셈</th>
<th>입력한 값</th>
<th>정답?</th>
</tr>
<tbody id="stats-body"></tbody>
</table>
</div>
</body>
</html>
package microservices.book.multiplication.domain;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import javax.persistence.*;
/**
* 사용자 정보를 저장하는 클래스
*/
@RequiredArgsConstructor
@Getter
@ToString
@EqualsAndHashCode
@Entity
@Table(name = "user_table")
public final class User {
@Id
@GeneratedValue
@Column(name = "USER_ID")
private Long id;
private final String alias;
// JSON/JPA 를 위한 빈 생성자
protected User() {
alias = null;
}
}
그랬더니 성공