여러 이유가 있지만 가장 중요한 점은 TDD로 요구사항에 대해 더 깊이 있게 생각할 수 있다는 점입니다. 테스트 코드를 먼저 작성하면 특정 상황에서 코드가 어떻게 동작할지 생각하게 됩니다. 이런 과정에서 애매한 요구사항은 명확히 하고, 유효하지 않은 요구사항은 거부할 수 있습니다.
@Test publicvoidcreateRandomMultiplicationTest() { // given (randomGeneratorService가 처음에 50, 나중에 30을 반환하도록 설정) given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when Multiplicationmultiplication= multiplicationService.createRandomMultiplication();
@Test publicvoidcreateRandomMultiplicationTest() { // given (randomGeneratorService가 처음에 50, 나중에 30을 반환하도록 설정) given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when Multiplicationmultiplication= multiplicationService.createRandomMultiplication();
테스트를 작성하면서 요구사항을 코드로 바꿀 수 있습니다. 요구사항을 살펴보면서 필요한 것과 필요하지 않은 것에 대해 생각해볼 수 있습니다. 지금까진 우리의 첫 번째 요구사항인 무작위로 곱셈을 생성하는 서비스만 있으면 됩니다.
테스트 가능한 코드를 만들게 됩니다. 구현하는 클래스를 먼저 작성하게 된다면 무작위 생성 로직을 그 안에 넣었을 가능성이 높고 그렇게 되면 그 이후에 테스트하기가 굉장히 어려워진다. 테스트 코드를 미리 작성한 덕에 테스트 코드를 더 작성하기 쉬운 구조로 구성할 수 있습니다.
중요한 로직에 초점을 맞추고 나머지는 나중에 구현할 수가 있습니다. 무작위 숫자 생성을 개발하고 테스트할 때 RandomGeneratorService 의 구현체를 작성할 필요가 없습니다.
@SpringBootTest 를 남용하지 말자.
SpringRunner 와 @SpringBootTest 어노테이션은 애플리케이션 컨텍스트를 초기화하고 필요한 객체를 주입합니다. 다행이 컨텍스트는 캐싱이 되어 재사용이 가능해 테스트 당 한 번만 로딩됩니다. 그러나 여기 테스트 처럼 하나의 테스트만 필요할 때는 어플리케이션 컨텍스트를 초기화해서 사용하는 것 보다 그냥 해당 객체를 직접 구현해서 테스트하는 것이 보다 효율적입니다.
@Test publicvoidgenerateRandomFactorIsBetweenExpectedLimits()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 은 어플리케이션 컨텍스트를 불러오지 않고 바로 객체를 구현해서 테스트를 진행하였다. 위 테스트가 통과될 수 있도록 실제 소스 코드를 작성하자.
@Test publicvoidcreateRandomMultiplicationTest() { // given (목 객체가 처음에 50, 나중에 30 을 반환하도록 설정) given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when Multiplicationmultiplication= multiplicationServiceImpl.createRandomMultiplication();
@MockBean 대신 @Mock 을 사용해 목 객체를 생성하였다. 어플리케이션 컨텍스트를 띄우지 않으니 빈을 사용하지 않기 때문이다.
도메인은 설계하자
Multiplication : 곱셈의 인수와 연산을 포함
User : 곱셈 문제를 푸는 사용자를 식별
MultiplicationResultAttempt : Multiplication 과 User 의 참조를 포함하고 사용자가 제출한 값과 채점 결과를 포함.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/** * 애플리케이션에서 곱셈을 나타내는 클래스 (a * b) */ @RequiredArgsConstructor @Getter @ToString @EqualsAndHashCode publicclassMultiplication { // 두 인수 privatefinalint factorA; privatefinalint factorB;
// JSON (역)직렬화를 위한 빈 생성자 Multiplication() { this(0, 0); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/** * 사용자 정보를 저장하는 클래스 */ @RequiredArgsConstructor @Getter @ToString @EqualsAndHashCode publicfinalclassUser { privatefinal String alias;
// JSON (역)직렬화를 위한 빈 생성자 protectedUser() { alias = null; } }
@Test publicvoidgetRandomMultiplicationTest()throws Exception { // given given(multiplicationService.createRandomMultiplication()) .willReturn(newMultiplication(70, 20));
// when MockHttpServletResponseresponse= mvc.perform( get("/multiplication/random") .accept(MediaType.APPLICATION_JSON)) .andReturn().getResponse();
// then assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value()); assertThat(response.getContentAsString()) .isEqualTo(json.write(newMultiplication(70, 20)).getJson()); } }
아직 소스 코드 구현을 하지 않았기 때문에 테스트를 돌리면 실패할 것이다. 위 테스트 코드를 간단하게 살펴보면
@WebMvcTest 는 @SpringBootTest 어노테이션과 동일하게 스프링 웹 어플리케이션 컨텍스트를 초기화합니다. 그러나 다른 점은 오직 MVC 레이어 (컨트롤러) 와 관련된 설정만 불러옵니다. 이 어노테이션은 MockMvc 빈도 불러옵니다.
JacksonTester 객체를 사용해 JSON 의 내용을 쉽게 확인할 수 있습니다. JacksonTester 객체는 자동으로 설정할 수 있고 @JsonTest 어노테이션을 이용해 자동으로 주입할 수 있습니다. 예제에서는 @WebMvcTest 어노테이션을 사용하기 때문에 수동으로 설정해야 합니다. (@Before 메서드 안에서 설정하고 있습니다)
@WebMvcTest 와 @SpringBootTest 의 차이
@WebMvcTest 는 컨트롤러를 테스트하는 어노테이션입니다. HTTP 요청과 응답은 Mock 을 이용해 가짜로 이뤄지고 실제 연결은 생성되지 않습니다. 반면 @SpringBootTest 는 웹 어플리케이션 컨텍스트와 설정을 모두 불러와서 실제 웹 서버 연결을 시도합니다. 이런 경우에는 MockMvc 가 아니라 RestTemplate 혹은 TestRestTemplate 을 대신 사용하면 된다.
보통 @WebMvcTest 는 서버에서 컨트롤러만 테스트할 때 사용하고, @SpringBootTest 는 클라이언트부터 상호작용을 확인하는 통합 테스트에서 사용하는 것이 좋습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13
/** * 곱셈 어플리케이션의 REST API 를 구현한 클래스 */ @RestController publicclassMultiplicationController {
// 페이지에서 값 가져오기 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(); }); });
화면작업 마무리 되었으면 어플리케이션을 실행해서 확인해보자.
새로운 요구사항
최근에 제출한 답안을 보고 싶어요. 그러면 시간이 지나면서 내가 얼마나 잘하고 있는지 또는 못하고 있는지 알 수 있어요.
MultiplicationResultAttempt 클래스의 인스턴스를 모두 저장합니다. 그렇게 하면 나중에 추출해서 사용할 수 있습니다.
특정 사용자의 최근 답안을 가져오는 새로운 REST 엔드포인트를 만듭니다.
답안을 검색하는 새로운 서비스(비지니스 로직)을 만듭니다.
사용자가 답안을 제출하면 답안 내역을 보여주는 웹 페이지를 만듭니다.
애자일과 리팩토링
애자일 방법론에 따라 일하려면 리팩토링을 일의 일부분으로 받아들여야 한다. 프로젝트 초기 단계에서 설계에 많은 시간을 투자하는 것은 잘못됐다는 것을 의미한다.
균형을 찾는 것이 핵심이다. 실제 비지니스는 사업이고 시간과의 싸움이기 때문에 기술적으로 완벽함을 찾는 것이나, 완벽한 설계도를 그리는 것에 시간을 쏟지 말아라는 의미 같다. 리팩토링은 필수적인 것이며, 요구사항이 바뀌어 가면서 소스도 바뀌어 가는 것. 그것이 애자일 방법론의 생각인 것 같다.
요구사항이 변경되어 소스에 점점 문제가 보이는 것을 방치한다면 그것은 기술부채가 되며 나중에 점점 큰 자원이 들어가게 되는 부분이 된다. 그렇기 때문에 이 애자일 방법론을 따를 때 중요한 것은 기술부채가 발생하지 않도록 문제가 발생했다면 바로바로 리팩토링하는 것이 중요하다.
불필요한 계산을 피하기 위해 코드를 수정합니다. 답안에 불린 값을 저장하고 데이터베이스에서 쿼리를 이용해 사용자가 맞춘 답안을 읽어올 수 있습니다.
// when booleanattemptResult= multiplicationServiceImpl.checkAttempt(attempt);
// then assertThat(attemptResult).isFalse(); } }
실제 답안을 체크하는 비지니스 로직 checkAttemp() 함수를 수정하자 여기 조금 의아한 부분이 객체지향 설계가 안되어 있다. MultiplicationResultAttempt 도메인이 가져야할 로직들이 이 서비스 로직에 드러나 있다. 이 글의 요점이 아니라 그런건가..
// 조작된 답안을 방지 Assert.isTrue(!attempt.isCorrect(), "채점한 상태로 보낼 수 없습니다!!");
// 복사본을 만들고 crrect 필드를 상황에 맞게 설정 MultiplicationResultAttemptcheckAttempt= newMultiplicationResultAttempt(attempt.getUser(), attempt.getMultiplication(), attempt.getResultAttempt(), correct); // 결과를 반환 return correct; } }
기존에는 ResultResponse 객체를 사용해서 응답을 내렸는데, 이번에는 DTO 아예 없애고 MultiplicationResultAttempt 도메인 객체로 내린다. 관련해서 컨트롤러 쪽도 수정하자.
# 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
/** * 사용자 정보를 저장하는 클래스 */ @RequiredArgsConstructor @Getter @ToString @EqualsAndHashCode @Entity publicfinalclassUser {
@Id @GeneratedValue @Column(name = "USER_ID") private Long id;
privatefinal String alias;
// JSON (역)직렬화를 위한 빈 생성자 protectedUser() { alias = null; } }
Repository 쪽을 만들어보자.
1 2 3 4 5 6 7 8 9
/** * 답안을 저장하고 조회하기 위한 인터페이스 */ publicinterfaceMultiplicationResultAttemptRepositoryextendsCrudRepository<MultiplicationResultAttempt, Long> { /** * @return 닉네임에 해당하는 사용자의 최근 답안 5개 */ List<MultiplicationResultAttempt> findTop5ByUserAliasOrderByIdDesc(String userAlias); }
1 2 3 4 5
/** * {@link microservices.book.multiplication.multiplication.domain.Multiplication} 을 저장하고 조회하기 위한 인터페이스 */ publicinterfaceMultiplicationRepositoryextendsCrudRepository<Multiplication, Long> { }
1 2 3 4 5 6
/** * {@link microservices.book.multiplication.multiplication.domain.User} 를 저장하고 조회하기 위한 인터페이스 */ publicinterfaceUserRepositoryextendsCrudRepository<User, Long> { Optional<User> findByAlias(final String alias); }
리포지토리를 만들때에는 왜 TDD를 사용하지 않는가? 간단합니다. 이건 새로운 코드가 아니라 스프링에서 제공하는 코드이기 때문에 믿을 수 있고 따로 단위 테스트를 작성할 필요가 없습니다
TDD로 돌아가서 위 Repository를 사용하는 Service 코드를 검증하는 단위 테스트 코드를 추가하자.
@Before publicvoidsetUp() { // initMocks 를 호출해 Mockito 가 어노테이션을 처리하도록 지시 MockitoAnnotations.initMocks(this); multiplicationServiceImpl = newMultiplicationServiceImpl(randomGeneratorService, attemptRepository, userRepository); }
@Test publicvoidcreateRandomMultiplicationTest() { // given (목 객체가 처음에 50, 나중에 30 을 반환하도록 설정) given(randomGeneratorService.generateRandomFactor()).willReturn(50, 30);
// when Multiplicationmultiplication= multiplicationServiceImpl.createRandomMultiplication();
// when booleanattemptResult= multiplicationServiceImpl.checkAttempt(attempt);
// then assertThat(attemptResult).isFalse(); verify(attemptRepository).save(attempt); } }
서비스 계층의 단위 테스트에 집중하기 위해 MultiplicationResultAttemptRepository 와 UserRepository 의 목 객체가 필요합니다. MultiplicationServiceImpl 생성자에 인자로 넘겨주고 있는데, 나중에 해당 클래스를 수정할 때 수정하거나 아니면 지금 새로운 생성자를 만들 수도 있습니다.
checkCorrectAttemptTest() 메서드는 실제처럼 하기 위해 attempt 의 복사본(verifiedAttempt)을 만듭니다. 사용자가 보낸 답안은 correct 필드가 false 여야만 합니다. 그리고 마지막 줄에서 Mockito 로 correct 가 true 인 답안을 저장하는 경우를 verify 합니다.
checkWrongAttemptTest() 메서드는 틀린 답을 검증합니다. 호출이 실제로 실행되는 것은 아닙니다. 목 객체가 해당 인자를 가지고 동작하는지 테스트하는 것뿐입니다.
Layer Connection
To show the list registed recently, let’s create new API and service unit.
We should create the message at ‘MultiplicationService’ interface for getting the list graded recently.
microservice isn’t fit with creating the prototype. because the prototype usually should be created as soon as possible. but building the microservice isn’t easier than the monolithic one.
so when you launch the new service, I recommend you to build it as monolithic system.
Refactoring is always needed in the agile progamming paradigm so Don’t be avoid it.
Scalability
let’s suppose that the one of the microservices takes traffics a lot, then we gotta expand the microservice without anothers.
but it’s impossible to control each service at monolithic system.
Server and Infra are always the cost so Software enginner should consider it.
Connecting each microservices
How can do we connect between each microservices?
Sharing the same database. This solution is not proper with microservice essentially. because the one of purpose of microservice is separating the business context, but this solution is not keep this rule.
Building the batch service pushing the updated list between each one. but this way couldn’t offer the real time service.
…
Event Driven Architecture
Most of companies with microservice structure use Event to notify any actions, changes for each microservices.
Each microservices take the published event through Event Bus and they can publish the event when they want.
The benfit and disadvantage of Event Driven Architecture
Loose coupling
Each microservices can have own context area. It should be independent, then the changes of their process are not affect to other one.
Transaction
Event Driven Architecture can’t keep ACID Transaction principle. if one transaction needs multiple services, we have to do lots of things to keep the ACID principle like Rollback processing.
Fault Tolerance
When the one of microservices is shuted down, another microservices depended it should have the policy how can handle this problem.
Difficulty to trace log.
Event Driven Architecture is really hard to trace the log because it is built separately. so We should have the occastration layer to check it like a gateway.
// 이벤트로 결과를 전송 eventDispatcher.send( newMultiplicationSolvedEvent(checkAttempt.getId(), checkAttempt.getUser().getId(), checkAttempt.isCorrect()) ); return isCorrect; } }
after this code, you should fix MultiplicationServiceImplTest class. because the existing one doesn’t have the code to check whether event message is sent well or not.
/** * 점수와 답안을 연결하는 클래스 * 사용자와 점수가 등록된 시간의 타임스탬프를 포함 */ @RequiredArgsConstructor @Getter @ToString @EqualsAndHashCode @Entity publicfinalclassScoreCard { // 명시되지 않은 경우 이 카드에 할당되는 기본 점수 publicstaticfinalintDEFAULT_SCORE=10;
@Id @GeneratedValue @Column(name = "CARD_ID") privatefinal Long cardId;
@Column(name = "USER_ID") privatefinal Long userId;
@Column(name = "ATTEMPT_ID") privatefinal Long attemptId;
@Query("SELECT SUM(s.score) FROM microservices.book.gamification.domain.ScoreCard s WHERE s.userId = :userId GROUP BY s.userId") intgetTotalScoreForUser(@Param("userId")final Long userId);
@Query("SELECT NEW microservices.book.gamification.domain.LeaderBoardRow(s.userId, SUM(s.score)) " + "FROM microservices.book.gamification.domain.ScoreCard s " + "GROUP BY s.userId ORDER BY SUM(s.score) DESC") List<LeaderBoardRow> findFirst10();
List<ScoreCard> findByUserIdOrderByScoreTimestampDesc(final Long userId); }
ScoreCardRepository is built by JPQL(Java Persistence Query Language). and JPQL keeps the abstraction rules to use itself for any database not for specific database.
Business Logics
This system is required two business logics. the one is GameService and the other is LeaderBoardService.
GameService : It’s for calculating the scores and the badges based on grading what is taken.
LeaderBoardService : It’s for Looking up the 10 users who takes highest the scores.
this RabbitMQ configuration bind with Exchange and Routing Key to Queue. and consumerJackson2MessageConverter(), messageHandlerMethodFactory(), configureRabbitListeners() methods are for setting Json Deserialization.
Data Communication between each microservices.
Reactive pattern means communication based on Event and unlike that pattern, Request/Response pattern means communication based on REST API.
Event only should have immutable data. and the reason was already refer the above.
Keeping the domain as isolaed status.
if the specific domain can be accessed all of microservices, then Each microservices have dependency between themselves because of this domain. It means that all of microservices using the domain have to be considered whenever the domain is changed.
@JsonDeserialize annotation can offer the customization of Json type deserialization when RestTemplate is used. and that is MultiplicationResutAttemptDeserializer object
How can solve the problem what docker is not operated will with showing the error message ‘Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?’
this situation is occured when your docker daemon process is stopped. in my case, i just solved this problem as re-installing docker program.
Solving the error ‘homebrew-core is a shallow clone’ when you use brew command.
the solution in already in the message what you taken in terminal with brew command. you just fetch the code from the below two repositories related with brew.
if the above properties is configured in your h2 db, then you can access h2-console with the below information.
1 2 3 4 5 6 7
http://localhost:8081/h2-console
jdbc:h2:file:~/gamification
user : myuser
password : mypass
furthermore, you have to know one thing that you gotta delete the h2 file db when you want to change the user or password
the location is almostly C:/User/USER/
The components of Service Discovery
Service Registry
Register Agent
Service Discovery Client
The version Error between Spring boot and Spring Cloud
The latest version of Spring boot is 2.6.1 but i don’t know the way add the spring cloud dependencies with this spring boot version. someday, I should solve this problems.. actually i learned one thing about this.
1
https://spring.io/projects/spring-cloud
You can get the version information of spring cloud.