토비 왈 : 테스트를 만들지 않을 거면 스프링을 도대체 뭐하러 쓰는 거죠?
자동으로 수행되는 테스트(Automated Test)
수동 테스트의 한계
프린트 된 메세지를 수동으로 확인하는 방법은 불편하다.
사용자 웹 UI까지 개발한 뒤에 확이하는 방법은 테스트가 실패했을 때 확인할 코드가 많다.
테스트 할 대상이 많아질 수록 검증하는데 시간이 많이 걸리고 부정확한다.
(결국 사람이 직접 하는 일이기 때문에, 구멍이 생길 수 밖에 없다)
자동으로 수행되는 테스트 (작은 크기로!)
개발자가 만드는 테스트
개발한 코드에 대한 검증 기능을 코드로 작성한다.
테스팅 프레임워크를 활용한다. (ex, JUnit)
JUnit 테스트 작성
JUnit 5
@Test
테스트 메소드 : 테스트 메서드에 붙이는 에너테이션
@BeforeEach
테스트 : @Test
메서드가 실행 되기 전마다 @BeforeEach
메서드가 실행된다.
테스트 마다 새로운 인스턴스가 만들어진다.
각각의 테스트가 다른 테스트에 영향을 받지 않도록 하기 위해서 테스트마다 각각 새로운 인스턴스를 생성한다.
package spring.hellospringv5;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Sort {
public List<String> sortByLength(List<String> list){
list.sort((o1, o2) -> o1.length() - o2.length());
return list;
}
}
package spring.hellospring;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import spring.hellospringv5.Sort;
import java.util.Arrays;
import java.util.List;
public class SortTest {
Sort sort;
@BeforeEach
void beforeEach() {
// 준비 (given)
sort = new Sort();
System.out.println(this); // 각각의 테스트는 독립적으로 실행되어야 하기 때문에, 각각의 테스트를 실행할 때마다 새로운 인스턴스를 만든다.
}
@Test
void sort(){
// 실행 (when)
List<String> list = sort.sortByLength(Arrays.asList("aa", "b"));
// 검증 (then)
Assertions.assertThat(list).isEqualTo(List.of("b", "aa"));
}
@Test
void sort3Items(){
List<String> list = sort.sortByLength(Arrays.asList("aa", "ccc", "b"));
Assertions.assertThat(list).isEqualTo(List.of("b", "aa", "ccc"));
}
@Test
void sortAlreadySorted(){
List<String> list = sort.sortByLength(Arrays.asList("b", "aa", "ccc"));
Assertions.assertThat(list).isEqualTo(List.of("b", "aa", "ccc"));
}
}
PaymentService 테스트
우리가 만들어 낸 PaymentService
를 테스트 해보자.
PaymentService
의 요구사항에 맞추어서 테스트를 해보자.
package spring.hellospringv5.payment;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import spring.hellospringv5.exrate.WebApiExRateProvider;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.*;
class PaymentServiceTest {
@Test
@DisplayName("prepare 메서드가 요구사항 3가지를 잘 충족했는지 검증")
void prepare() throws IOException {
PaymentService paymentService = new PaymentService(new WebApiExRateProvider());
Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
// 환율정보 가져오기
assertThat(payment.getExRate()).isNotNull();
// 원화환산금액 계산
assertThat(payment.getConvertedAmount())
.isEqualByComparingTo(payment.getExRate().multiply(payment.getForeignCurrencyAmount()));
// 원화환산금액 유효시간 계산
assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}
}
문제점
우리가 제어할 수 없는 외부 시스템에 문제가 생긴다면?
환율 정보를 가져오는 외부 서버의 문제가 생겨버리면 테스트를 할 수 없다.
ExRateProvider
가 제공하는 환율 값으로 계산한 것인가?
prepare
메서드만 보고서 ExRateProvider
가 제공하는 메서드인지 확신할 수 없다.
테스트의 구성 요소
일반적으로 테스트는 다음의 구성 요소를 가지고 있다.
협력자의 경우 개발자가 관여할 수 없는 외부 서버에 의존하는 경우가 있는데, 이 경우 서버 상황에 따라 각각 다른 결과를 얻게 될 수 있다.
이런 경우를 위해서 고정된 결과를 리턴해주는 Mock 객체를 만들어 사용할 수 있다.
이 구조가 만들어지기 위해서는 테스트 대상이 특정 인터페이스를 상속받은 클래스를 사용하는 구조가 되어야 한다.
테스트와 DI
외부와 의존성이 없는 ExRateProviderStub
클래스를 만들고 해당 클래스 인스턴스를 테스트에서 사용하도록 변경하였다.
이로 인해 외부와 의존성이 없는 테스트를 만들 수 있게 되었다.
수동 DI 를 이용하는 테스트
테스트용 협력자(Colloborator) / 의존 오브젝트를 테스트 대상에 직접 주입하고 테스트
스프링이 원하는 방향
스프링은 기본적으로 DI 를 제공하면서, 외부와 종속적이지 않은 테스트를 만들기를 원한다.
아래 코드는 이러한 방향을 잘 따른 코드라고 볼 수 있다.
package spring.hellospringv5.payment;
import java.io.IOException;
import java.math.BigDecimal;
public class ExRateProviderStub implements ExRateProvider {
private BigDecimal exRate;
public ExRateProviderStub(BigDecimal exRate) {
this.exRate = exRate;
}
public BigDecimal getExRate() {
return exRate;
}
public void setExRate(BigDecimal exRate) {
this.exRate = exRate;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
return exRate;
}
}
package spring.hellospringv5.payment;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.math.BigDecimal;
import static java.math.BigDecimal.*;
import static org.assertj.core.api.Assertions.*;
class PaymentServiceTest {
@Test
@DisplayName("prepare 메서드가 요구사항 3가지를 잘 충족했는지 검증")
void convertedAmount() throws IOException {
testAmount(valueOf(500), valueOf(5_000));
testAmount(valueOf(1_000), valueOf(10_000));
testAmount(valueOf(3_000), valueOf(30_000));
// 원화환산금액 유효시간 계산
// assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
// assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}
private static void testAmount(BigDecimal exRate, BigDecimal convertedAmount) throws IOException {
PaymentService paymentService = new PaymentService(new ExRateProviderStub(exRate));
Payment payment = paymentService.prepare(1L, "USD", TEN);
// 환율정보 가져오기
assertThat(payment.getExRate()).isEqualByComparingTo(exRate);
// 원화환산금액 계산
assertThat(payment.getConvertedAmount()).isEqualTo(convertedAmount);
}
}
스프링 DI 를 이용하는 테스트
테스트용 협력자(Collborator) / 의존 오브젝트를 스프링의 구성 정보를 이용해서 지정하고, 컨테이너로부터 테스트 대상을 가져와 테스트
알아야 하는 어노테이션
@ExtendWith(SpringExtension.class)
: JUnit 이 스프링의 기능을 사용한다는 어노테인션, 기계적으로 사용하자.
@ContextConfiguration
: 테스트가 실행될 때, 해당 어노테이션으로 지정 된 클래스를 스프링 컨테이너에 등록해준다.
@Autowired
: 테스트가 실행될 때 해당 타입과 매칭되는 인스턴스가 스프링 컨테이너 내에 있다면 해당 인스턴스로 초기화 해준다.
스프링 컨테이너를 사용한 테스트
package spring.hellospringv5.payment;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import spring.hellospringv5.TestObjectFactory;
import java.io.IOException;
import static java.math.BigDecimal.TEN;
import static java.math.BigDecimal.valueOf;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestObjectFactory.class)
class PaymentServiceSpringTest {
@Autowired BeanFactory beanFactory;
@Test
void convertedAmount() throws IOException {
PaymentService paymentService = beanFactory.getBean(PaymentService.class);
Payment payment = paymentService.prepare(1L, "USD", TEN);
assertThat(payment.getExRate()).isEqualByComparingTo(valueOf(1_000));
assertThat(payment.getConvertedAmount()).isEqualTo(valueOf(10_000));
// 원화환산금액 유효시간 계산
// assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
// assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}
}
개선된 방식
생각해보면, 스프링 컨테이너에 paymentService
가 올라가 있을 것이기 때문에, BeanFactory
를 사용하지 않아도 된다.
추가적으로 ExRateProviderStub
클래스를 직접적으로 가져와 환율 정책을 바꾸어가면서 사용할 수도 있다.
(실 사용하는 클래스가 아니기 때문에 Setter
를 사용해 값을 변경하며 테스트를 할 수 있다)
package spring.hellospringv5.payment;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import spring.hellospringv5.TestObjectFactory;
import java.io.IOException;
import static java.math.BigDecimal.TEN;
import static java.math.BigDecimal.valueOf;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestObjectFactory.class)
class PaymentServiceSpringTest {
@Autowired PaymentService paymentService;
@Autowired ExRateProviderStub exRateProviderStub;
@Test
void convertedAmount() throws IOException {
// exRate : 1000
Payment payment1 = paymentService.prepare(1L, "USD", TEN);
assertThat(payment1.getExRate()).isEqualByComparingTo(valueOf(1_000));
assertThat(payment1.getConvertedAmount()).isEqualTo(valueOf(10_000));
// exRate : 500
exRateProviderStub.setExRate(valueOf(500));
Payment payment2 = paymentService.prepare(1L, "USD", TEN);
assertThat(payment2.getExRate()).isEqualByComparingTo(valueOf(500));
assertThat(payment2.getConvertedAmount()).isEqualTo(valueOf(5_000));
// 원화환산금액 유효시간 계산
// assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
// assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}
}
학습 테스트
언제 학습 테스트를 할까?
직접 만들지 않은 코드, 라이브러리, 레거시 시스템에 대한 테스트
학습 테스트의 목적은 사용할 API, 프레임워크의 기능을 테스트로 작성하고 실행해보면서 사용방법을 바르게 이해했는지 확인할 수 있다.
외부 기술, 서비스가 버전이 올라갔을 때, 이전과 동일하게 동작하는지 확인할 수도 있다.
(테스트를 촘촘하게 만드는 것이 중요하다!)
학습 테스트 예제 (Clock)
package spring.hellospringv5.learningtest;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
public class ClockTest {
// Clock 을 이용해서 LocalDateTime.now?
@Test
void Clock(){
Clock clock = Clock.systemDefaultZone();
LocalDateTime dt1 = LocalDateTime.now(clock);
LocalDateTime dt2 = LocalDateTime.now(clock);
Assertions.assertThat(dt2).isAfter(dt1);
}
// Clock 을 Test 에서 사용할 때 내가 원하는 시간을 지정해서 현재 시간을 가져오게 할 수 있는가?
@Test
void fixedClock() {
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
LocalDateTime dt1 = LocalDateTime.now(clock);
LocalDateTime dt2 = LocalDateTime.now(clock);
LocalDateTime dt3 = LocalDateTime.now(clock).plusHours(1);
Assertions.assertThat(dt1).isEqualTo(dt2);
Assertions.assertThat(dt3).isEqualTo(dt1.plusHours(1));
}
}
Clock 을 이용한 시간 테스트
스프링 컨테이너에 Clock
을 이용해 FixedClock
을 만들어 PaymentService
를 테스트해보자.
테스트를 위한 사전작업
테스트를 위해서 우리가 사용하던 ObjectFactory
를 도메인에 맞는 이름(PaymentConfig
) 로 바꾸어 보자.
그리고 PaymentService
에서 Clock
을 사용할 수 있도록 코드를 수정해주자.
package spring.hellospringv5.payment;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
@Component
public class PaymentService {
private final ExRateProvider exRateProvider;
private final Clock clock;
public PaymentService(ExRateProvider exRateProvider, Clock clock) {
this.exRateProvider = exRateProvider;
this.clock = clock;
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.getExRate(currency); // 1. 환율을 가져오고
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exRate); // 2. 원화 환산 금액을 계산한다.
LocalDateTime validUntil = LocalDateTime.now(clock).plusMinutes(30); // 3. 환율 유효시간을 계산한다.
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
}
package spring.hellospringv5;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import spring.hellospringv5.exrate.CachedExRateProvider;
import spring.hellospringv5.exrate.WebApiExRateProvider;
import spring.hellospringv5.payment.ExRateProvider;
import spring.hellospringv5.payment.PaymentService;
import java.time.Clock;
@Configuration
public class PaymentConfig {
@Bean
public PaymentService paymentService() {
return new PaymentService(exRateProvider(), clock());
}
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
@Bean
public Clock clock(){ return Clock.systemDefaultZone(); }
}
수동 DI 를 이용하는 테스트
수동 DI 테스트에서는 직접 의존성을 넣어주어야 한다.
package spring.hellospringv5.payment;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import static java.math.BigDecimal.*;
import static org.assertj.core.api.Assertions.*;
class PaymentServiceTest {
Clock clock;
@BeforeEach
void beforeEach() {
this.clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
}
@Test
@DisplayName("prepare 메서드가 요구사항 3가지를 잘 충족했는지 검증")
void convertedAmount() throws IOException {
testAmount(valueOf(500), valueOf(5_000), this.clock);
testAmount(valueOf(1_000), valueOf(10_000), this.clock);
testAmount(valueOf(3_000), valueOf(30_000), this.clock);
}
@Test
@DisplayName("valid until 이 prepare() 30분 뒤로 설정되었는가?")
void validUntil() throws IOException {
PaymentService paymentService = new PaymentService(new ExRateProviderStub(valueOf(1_000)), this.clock);
Payment payment = paymentService.prepare(1L, "USD", TEN);
LocalDateTime now = LocalDateTime.now(this.clock);
LocalDateTime expectedValidUntil = now.plusMinutes(30);
Assertions.assertThat(payment.getValidUntil()).isEqualTo(expectedValidUntil);
}
private static void testAmount(BigDecimal exRate, BigDecimal convertedAmount, Clock clock) throws IOException {
PaymentService paymentService = new PaymentService(new ExRateProviderStub(exRate), clock);
Payment payment = paymentService.prepare(1L, "USD", TEN);
// 환율정보 가져오기
assertThat(payment.getExRate()).isEqualByComparingTo(exRate);
// 원화환산금액 계산
assertThat(payment.getConvertedAmount()).isEqualTo(convertedAmount);
}
}
스프링 DI 를 이용하는 테스트
스프링 DI 테스트에서는 PaymentConfig
에서 이미 DI 설정이 이루어졌기 때문에, @Autowired
를 통해 직접 인스턴스를 가져와 사용하면된다.
package spring.hellospringv5.payment;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import spring.hellospringv5.TestPaymentConfig;
import java.io.IOException;
import java.time.Clock;
import java.time.LocalDateTime;
import static java.math.BigDecimal.TEN;
import static java.math.BigDecimal.valueOf;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestPaymentConfig.class)
class PaymentServiceSpringTest {
@Autowired PaymentService paymentService;
@Autowired Clock clock;
@Autowired ExRateProviderStub exRateProviderStub;
@Test
@DisplayName("prepare 메서드가 요구사항 3가지를 잘 충족했는지 검증")
void convertedAmount() throws IOException {
// exRate : 1000
Payment payment1 = paymentService.prepare(1L, "USD", TEN);
assertThat(payment1.getExRate()).isEqualByComparingTo(valueOf(1_000));
assertThat(payment1.getConvertedAmount()).isEqualTo(valueOf(10_000));
// exRate : 500
exRateProviderStub.setExRate(valueOf(500));
Payment payment2 = paymentService.prepare(1L, "USD", TEN);
assertThat(payment2.getExRate()).isEqualByComparingTo(valueOf(500));
assertThat(payment2.getConvertedAmount()).isEqualTo(valueOf(5_000));
}
@Test
@DisplayName("valid until 이 prepare() 30분 뒤로 설정되었는가?")
void validUntil() throws IOException {
PaymentService paymentService = new PaymentService(new ExRateProviderStub(valueOf(1_000)), this.clock);
Payment payment = paymentService.prepare(1L, "USD", TEN);
LocalDateTime now = LocalDateTime.now(this.clock);
LocalDateTime expectedValidUntil = now.plusMinutes(30);
Assertions.assertThat(payment.getValidUntil()).isEqualTo(expectedValidUntil);
}
}
도메인 오브젝트 테스트
도메인 모델 아키텍처 패턴이란?
도메인 로직, 비지니스 로직을 어디에 둘 것인가를 결정하는 패턴
일반적인 스프링의 아키텍처 패턴
트랜잭션 스크립트 - 서비스 메서드(PaymentService.prepare
)
현재 코드에서 사용하고 있는 방식
service
로직에서 모든 서비스 로직을 담당한다.
도메인 모델 - 도메인 모델 오브젝트(Payment
)
도메인 모델 오브젝트에서 도메인 모델이(Payment
) 서비스 로직을 가지고 있어야 한다.
현재 코드에서 Payment
는 값만 가지고 있다.
그렇기에 현재 코드는 도메인 모델 방식의 코드가 아니다.
도메인 모델 방식을 사용했을 때, 테스트에서 얻는 이점이 크다.
도메인 모델 안에 비지니스 로직이 모두 들어 있다면, 외부 의존성과 관계 없이 독립적으로 테스트 가능하다.
응집도가 높아지기 때문에, 테스트 대상이 되는 도메인의 경계를 명확하게 할 수 있다.
도메인 모델 아키텍처 패턴 예제
기존에는 PaymentService.prepare()
메서드 내부에서 모든 비지니스 로직을 담당하고 있었다.
하지만 도메인 오브젝트 모델에서는, 도메인 모델(Payment
)이 서비스 로직을 가지고 있어야 한다.
팩토리 메서드 패턴을 사용해, 외부 오브젝트에 영향을 받는 로직을 제외하고는 도메인 모델로 서비스 로직을 옮기자.
외부 영향을 받는 로직 : 환율을 가져오는 로직
구현 예제
package spring.hellospringv5.payment;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
@Component
public class PaymentService {
private final ExRateProvider exRateProvider;
private final Clock clock;
public PaymentService(ExRateProvider exRateProvider, Clock clock) {
this.exRateProvider = exRateProvider;
this.clock = clock;
}
public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
BigDecimal exRate = exRateProvider.getExRate(currency); // 1. 환율을 가져오고
return Payment.createPrepared(orderId, currency, foreignCurrencyAmount, exRate, LocalDateTime.now(clock));
}
}
package spring.hellospringv5.payment;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class Payment {
private Long orderId;
private String currency;
private BigDecimal foreignCurrencyAmount;
private BigDecimal exRate;
private BigDecimal convertedAmount;
private LocalDateTime validUntil;
public Payment(Long orderId, String currency, BigDecimal foreignCurrencyAmount, BigDecimal exRate, BigDecimal convertedAmount, LocalDateTime validUntil) {
this.orderId = orderId;
this.currency = currency;
this.foreignCurrencyAmount = foreignCurrencyAmount;
this.exRate = exRate;
this.convertedAmount = convertedAmount;
this.validUntil = validUntil;
}
public static Payment createPrepared(Long orderId, String currency, BigDecimal foreignCurrencyAmount, BigDecimal exRate,
LocalDateTime now) {
BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exRate); // 2. 원화 환산 금액을 계산한다.
LocalDateTime validUntil = now.plusMinutes(30); // 3. 환율 유효시간을 계산한다.
return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
}
public Long getOrderId() {
return orderId;
}
public String getCurrency() {
return currency;
}
public BigDecimal getForeignCurrencyAmount() {
return foreignCurrencyAmount;
}
public BigDecimal getExRate() {
return exRate;
}
public BigDecimal getConvertedAmount() {
return convertedAmount;
}
public LocalDateTime getValidUntil() {
return validUntil;
}
@Override
public String toString() {
return "Payment{" +
"orderId=" + orderId +
", currency='" + currency + '\'' +
", foreignCurrencyAmount=" + foreignCurrencyAmount +
", exRate=" + exRate +
", convertedAmount=" + convertedAmount +
", validUntil=" + validUntil +
'}';
}
}
테스트 예제
package spring.hellospringv5.payment;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.time.*;
import java.time.temporal.ChronoUnit;
import static java.math.BigDecimal.valueOf;
public class PaymentTest {
@Test
@DisplayName("Payment.createPrepared() 테스트")
void createPrepared() {
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
Payment payment = Payment.createPrepared(
1L, "USD", BigDecimal.TEN, valueOf(1_000), LocalDateTime.now(clock)
);
Assertions.assertThat(payment.getConvertedAmount()).isEqualTo(valueOf(10_000));
Assertions.assertThat(payment.getValidUntil()).isEqualTo(LocalDateTime.now(clock).plusMinutes(30));
}
@Test
@DisplayName("payment.isValid() 테스트")
void isValid() {
Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
Payment payment = Payment.createPrepared(
1L, "USD", BigDecimal.TEN, valueOf(1_000), LocalDateTime.now(clock)
);
Assertions.assertThat(payment.isValid(clock)).isTrue();
Assertions.assertThat(
payment.isValid(Clock.offset(clock, Duration.of(30, ChronoUnit.MINUTES)))).isFalse();
}
}
도메인 모델 아키텍처 패턴을 왜 사용하는가?
객체 지향을 이야기 할 때, 좋은 객체 지향 코드는 다음과 같다고 한다.
결합도가 낮고, 응집도가 높은 코드가 좋은 코드이다.
도메인 모델 아키텍처 패턴을 적용하기 전 코드를 생각해보면, 요구사항이 복잡해지고, 추가적인 클래스를 만드는 상황이 생겨난다면 PaymentService
클래스에 변화가 생겨나고, 결합도는 높아질 것이다.
이를 해결하기 위해서, 각 도메인의 기능들은 해당 메서드로 옮겨서, 결합도를 낮추고, 응집도를 높이는 설계를 할 필요가 있다.
Last updated