3장 3계층 스프링 부트 애플리케이션

sua·2022년 9월 2일
0
post-thumbnail

시작하기

다층 아키텍처 : 애플리케이션을 여러 계층으로 나눈 아키텍처

3계층 구조

  1. 클라이언트 계층 : 사용자 인터페이스를 제공하는 계층. 보통 프런트엔드라고 부름
  2. 애플리케이션 계층 : 비즈니스 로직, 상호작용을 위한 인터페이스, 데이터를 저장하는 인터페이스를 포함하는 계층. 보통 백엔드라고 부름
  3. 데이터 저장 계층 : 애플리케이션의 데이터를 보관하는 계층. 데이터베이스, 파일시스템 등

애플리케이션 계층 3개의 레이어

  1. 비즈니스 레이어 : 도메인과 비즈니스 명세를 모델링한 클래스가 있음. 애플리케이션의 두뇌 역할. 해당 레이어를 도메인(개체)와 애플리케이션(서비스)로 나누기도 함
  2. 프레젠테이션 레이어 : 우리가 만들 애플리케이션에서는 웹 클라이언트에 기능을 제공하는 컨트롤러 클래스가 프레젠테이션 레이어에 해당. 컨트롤러에 REST API를 구현함
  3. 데이터 레이어 : 개체들을 데이터 스토리지나 데이터베이스에 보관. 보통 데이터 액세스 객체(DA)또는 저장소 클래스를 포함. DAO는 데이터베이스 모델을 다루고, 저장소 클래스는 도메인을 데이터베이스 레이어로 변환하는 클래스

사용할 아키텍처 패턴


기초 마무리하기

  1. RandomGeneratorServiceTest 작성
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를 남용하는 문제 발생 (지금처럼 단순히 클래스 하나의 기능을 테스트하기 위해서라면 종속성 주입이나 애플리케이션 컨텍스트가 필요없음 -> 클래스의 구현체를 테스트하는 게 나음)


  1. 구현체를 직접 테스트하는 케이스 만들기
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 작성 필요


  1. RandomGeneratorServiceImpl 작성 (0을 반환하도록)
package microservices.book.multiplication.service;

import org.springframework.stereotype.Service;

@Service
class RandomGeneratorServiceImpl implements RandomGeneratorService {

    @Override
    public int generateRandomFactor() {
        return 0;
    }
}
  1. 테스트 실행 (실패)

  1. 테스트 통과하도록 로직 수정
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;
  }
}

  1. 테스트 다시 실행 (성공)

  1. MultiplicationService 테스트에도 해당 방식 적용해보기
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을 사용해 목 객체를 생성하고 객체의 생성자로 넘겨줌


  1. 테스트 실행 (성공)

도메인 설계

요구사항에 따라 객체 정의해보기

  1. Multiplication : 곱셈의 인수와 연산을 포함
  2. User : 곱셈 문제를 푸는 사용자를 식별
  3. MultiplicationResultAttemp : Multiplication과 User의 참조를 포함하고 사용자가 제출한 값과 채점 결과를 포함

롬복 사용하기

  1. build.gradle에 롬복 종속성 추가
dependencies {
	...
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

  1. 롬복으로 Multiplication 도메인 개체 만들기
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);
  }
}

  1. 롬복으로 User 도메인 개체 만들기
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;
  }
}

  1. 롬복으로 MultiplicationResultAttempt 도메인 개체 만들기
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() 메서드를 만듦


비즈니스 로직 레이어

요구사항

  1. 제출한 답안의 정답 여부 확인
  2. 적당히 어려운 곱셈 만들어내기

새로운 로직 적용하기

  1. 계산 내용이 맞는지 검증하는 checkAttempt 메서드 추가

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);
}

  1. TDD 방식을 따라 임시 메서드 만들기 (항상 실패한 결과를 반환)
@Override
    public boolean checkAttempt(MultiplicationResultAttempt resultAttempt) {
        return false;
    }

  1. 테스트 메서드 작성
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();
    }
}

  1. 메서드 수정해서 진짜 로직 작성
@Override
  public boolean checkAttempt(final MultiplicationResultAttempt resultAttempt) {
    return resultAttempt.getResultAttempt() ==
            resultAttempt.getMultiplication().getFactorA() *
                    resultAttempt.getMultiplication().getFactorB();
  }

  1. 테스트 실행 (성공)

프레젠테이션 레이어(REST API)

인터페이스 제공 요구사항

  1. 적당히 어렵고, 무작위로 생성된 곱셈 문제를 제공하는 API
  2. 주어진 곱셈을 계산한 결과와 누가 풀었는지 알 수 있도록 사용자의 닉네임을 제출하는 API

REST API 설계

  1. GET /multiplications/random : 무작위로 생성한 곱셈을 반환
  2. POST /results/ : 결과를 전송하는 엔드포인트
  3. GET /results/?user=[user_alias] : 특정 사용자의 계산 결과를 검색

Multiplication 컨트롤러

  1. 컨트롤러 클래스 작성
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;
    }
}

  1. MultiplicationController의 단위 테스트 작성
    GET 방식의 /multiplications/random 요청에 컨트롤러가 무작위로 작성된 곱셈을 반환하는지 확인하는 테스트
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의 내용을 쉽게 확인 가능


  1. 테스트 실행 (실패) -> 로직이 없기 때문에 404 코드가 출력됨

  1. 컨트롤러에 로직 추가
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();
  }

}

  1. 테스트 실행 (성공)

Result 컨트롤러

  1. 내용이 없는 컨트롤러 만들기
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;
    }
}

  1. MultiplicationResultAttemptController의 단위 테스트 작성
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());
    }

}

  1. 컨트롤러 POST 매핑 구현
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;
  }
}

  1. 테스트 실행 (성공)

프런트엔드(웹 클라이언트)

  1. index.html 만들기
<!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>

  1. CSS 만들기
html, body {
  height: 100%;
}

html {
  display: table;
  margin: auto;
}

body {
  display: table-cell;
  vertical-align: middle;
}

  1. js 만들기
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를 호출하고 결과를 사용자에게 보여줌


  1. localhost:8080/index.html 접속


새로운 요구사항(데이터 저장)

사용자 스토리 2

최근에 제출한 답안을 보고 싶음. 시간이 지나면서 내가 얼마나 잘하고 있는지 또는 못하고 있는지를 알 수 있기 때문에

요구사항

  1. MultiplicationResultAttempt 클래스의 인스턴스를 모두 저장. 그렇게 하면 나중에 추출해서 사용할 수 있음
  2. 특정 사용자의 최근 답안을 가져오는 새로운 REST 엔드포인트를 만듦
  3. 답안을 검색하는 새로운 서비스(비즈니스 로직)을 만듦
  4. 사용자가 답안을 제출하면 답안 내역을 보여주는 웹 페이지를 만듦

리팩터링 작업 정리

  1. 사용자가 제출한 답안(MultiplicationResultAttempt)에 정답인지 오답인지를 나타내는 불린 값을 추가. 클래스에 값을 담았다가 데이터베이스에 저장
  2. 서비스(MultiplicationServiceImpl)에서는 해당 결과를 바로 반환하는 대신 답안 내에 저장
  3. 클라이언트가 답안을 채점해선 안됨. 따라서 해당 필드는 RST API에서 읽는 대신 내부적으로 계산
  4. 새로운 환경을 반영하기 위해 테스트를 수정

리팩터링

  1. MultiplicationResultAttempt 클래스 수정
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;
    }

}

  1. MultiplicationServiceImplTest 클래스 수정
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();
    }
}

  1. MultiplicationServiceImpl에 새로운 로직 추가
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;
    }
}

  1. MultiplicationResultAttemptController 수정
@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);
  }

  1. MultiplicationResultAttemptControllerTest 수정
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());
    }

  1. multiplication-client.js 수정
// 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("오답입니다! 그래도 포기하지 마세요!");
        }
      }
    });

데이터 레이어

종속성 추가

  1. build.gradle 추가
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'
}

  1. application.properties 설정 추가

# 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 개체 : 사용자는 여러 답안을 제출할 수 있음. 동시에 같은 숫자로 이뤄진 곱셈 문제에 여러 사용자가 답안을 제출할 수 있음.

  1. Multiplication JPA로 모델링
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);
  }
}

  1. User 클래스 JPA로 모델링
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;
  }
}

  1. MultiplicationResultAttempt 클래스 JPA로 모델링
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;
  }

}

-> 애너테이션으로 관계에 대해 상세한 내용을 정의


리포지토리

  1. MultiplicationResultAttempt를 저장할 저장소 만들기
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);
}

  1. UserRepository 작성

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);

}

  1. MultiplicationRepository 작성
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> {
}

  1. 단위 테스트에 저장 로직을 추가 (MultiplicationServiceImplTest)
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);
    }
}

  1. MultiplicationServiceImpl에 리포지토리와 답안을 저장하는 로직을 추가
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;
  }

}

  1. 테스트 실행 (성공)

레이어 연결

  1. MultiplicationService 수정

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);
}

  1. MultiplicationServiceImpl 수정
@Override
  public List<MultiplicationResultAttempt> getStatsForUser(String userAlias) {
    return attemptRepository.findTop5ByUserAliasOrderByIdDesc(userAlias);
  }

  1. MultiplicationServiceImplTest 수정
@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);
    }

  1. MultiplicationResultAttemptController 수정
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)
    );
  }

}

  1. MultiplicationResultAttemptControllerTest 수정
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());
    }

}

  1. multiplication-client.js 로직 추가
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);
  });
});

  1. HTML에 간단한 표를 만드는 코드 추가
<!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>

  1. http://localhost:8080/index.html에 접속
    그런데, caused by org.h2.jdbc.jdbcsqlsyntaxerrorexception syntax error in sql statement create table 에러가 났음
    -> 만들려는 테이블명이 예약어라서 안된다는 뜻이므로 User 도메인의 테이블명을 어노테이션으로 user_table로 지정해줌
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;
    }
}

그랬더니 성공

profile
가보자고

0개의 댓글