Presentation Layer
- 외부 세계의 요청을 가장 먼저 받는 계층
- 파라미터에 대한 최소한의 검증을 수행한다.
Mock
가짜
MockMvc
- Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 테스트 프레임워크
요구사항 추가
- 관리자 페이지에서 신규 상품을 등록할 수 있다.
- 상품명, 상품타입, 판매 상태, 가격 등을 입력받는다.
// productNumber
// 001 002 003 004
// DB 에서 마지막 저장된 Product의 상품 번호를 읽어와서 +1
// 009 -> 010
구현코드
https://github.com/beginner0107/cafekiosk/commit/4e32f9b28d8b430fc82501c6c6bf96d65216df92
- 실무에서 이런 경우가 많이 있다.
- 예를 들어 제품코드인 경우 저런 예시처럼 001, 002 String Type으로 저장되기도 한다.
- 물론 저 경우에는 999개의 제품밖에 저장을 못하기에 테스트에 집중하기 위한~
- 만약 변화가 없는 코드번호에서는 유용하게 쓰인다.
- C001, D001 이런 카테고리가 있다고 치자.
- 의류 코드라고 가정 C001(상의) -> C002(스웨터) , C003(반팔) 이런식으로 부모 자식의 형태로도 사용이 가능하다.
@Query(value = "select p.product_number from product p order by id desc limit 1", nativeQuery = true)
String findLatestProductNumber();
- 현재 "001", "002"의 상품 번호를 부여받고, 제일 최근의 제품 번호 + 1이 다음 상품번호가 된다.
- 마지막에 등록된 상품 번호를 가져오는 쿼리이다.
ProductRepositoryTest - 단위테스트
@DisplayName("가장 마지막으로 저장한 상품의 상품번호를 읽어온다.")
@Test
void findLatestProduct() {
// given
String targetProductNumber = "003";
Product product1 = createProduct("001", BAKERY, SELLING, "아메리카노", 4000);
Product product2 = createProduct("002", BAKERY, HOLD, "카페라떼", 4500);
Product product3 = createProduct("003", HANDMADE, STOP_SELLING, "팥빙수", 7000);
productRepository.saveAll(List.of(product1, product2, product3));
// when
String latestProductNumber = productRepository.findLatestProductNumber();
// then
assertThat(latestProductNumber).isEqualTo(targetProductNumber);
}
@DisplayName("가장 먼저 저장한 상품의 상품번호를 읽어올 때, 상품이 하나도 없는 경우에는 null을 반환한다.")
@Test
void findLatestProductNumberWhenProductIsEmpty() {
// when
String latestProductNumber = productRepository.findLatestProductNumber();
// then
assertThat(latestProductNumber).isNull();
}
}
ProductServiceTest - 통합테스트
@Transactional
public ProductResponse createProduct(ProductCreateRequest request) {
String newProductNumber = createNewProductNumber();
Product product = request.toEntity(newProductNumber);
Product savedProduct = productRepository.save(product);
return ProductResponse.of(savedProduct);
}
@DisplayName("신규 상품을 등록한다. 상품번호는 가장 최근 상품의 상품번호에서 1증가한 값이다.")
@Test
void createProduct() {
// given
Product product1 = createProduct("001", BAKERY, SELLING, "아메리카노", 4000);
productRepository.saveAll(List.of(product1));
ProductCreateRequest request = ProductCreateRequest.builder()
.type(HANDMADE)
.sellingStatus(SELLING)
.name("카푸치노")
.price(5000)
.build();
// when
ProductResponse productResponse = productService.createProduct(request);
// then
assertThat(productResponse)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.contains("002", HANDMADE, SELLING, "카푸치노", 5000);
List<Product> products = productRepository.findAll();
assertThat(products).hasSize(2)
.extracting("productNumber", "type", "sellingStatus", "name", "price")
.containsExactlyInAnyOrder(
tuple("001", BAKERY, SELLING, "아메리카노", 4000),
tuple("002", HANDMADE, SELLING, "카푸치노", 5000)
);
}
- 간단하다고 볼 수 있겠지만...
유의사항
동시성 문제가 발생할 수 있다.
- "001" 상품이 존재하고 있을 때 관리자가 10명이 와서 정확히 동시에(가정) 상품 등록 버튼을 클릭 했을 때(트래픽이 많다고 가정해도 동일), "002"의 상품번호만 계속 나올 수 있다.
- UUID(고유한 값으로 상품 번호 지정)로 처리하거나, 이전의 비관적 락, 낙관적 락을 걸어 해결하는 것도 방법일 수 있다.
Transactional - CQRS
- Transactional 어노테이션은, 쿼리를 수행하고 문제가 생길 시 rollback을 담당한다.
- 혹은 테스트코드에 Transactional + SpringBootTest를 붙여주면 각각의 테스트가 하나씩 수행되고 rollback이 되는 것을 볼 수 있다. 그러나 테스트코드에 저 둘의 조합을 사용하는 것은 지양하라고 들었다.
- Service에 실제로 Transactional이 붙어있어야 하는데 Test코드에 Transactional을 붙여줘서 정상적으로 수행하는 줄 알았다가, 운영서버에 반영할 때 발견이 될 수 있다. 저장이 안됩니다.업데이트가 되지 않아!
- CQRS
- Transactional(readOnly=true) 와 Transactional(readOnly=false) -> Default
- Command / Query의 분리 : Command는 CUD를 뜻하고(create, update, delete), Query는 (read)를 뜻한다.
- 데이터를 생성하고 변경하고 삭제하는 Command 행위보다, Query(read)라는 행위의 빈도 수가 높다.
- Responsibiliy을 Seperate(분리)하자. 의도적으로!
- 예를들어
- Read에 트래픽이 쏟아진다 -> 시스템의 부하
- Read 때문에 장애가 생겼다고 가정 -> Command가 같이 동작을 안 해버리면 큰 장애가 일어날 수 있다.
- 반대의 상황도 마찬가지
- readOnly를 가지고 Query용 서비스 / Command용 서비스 나눌 수 있다.
- 애플리케이션 단에서 분리를 해서 관리하면 어떤 이점이 있을까?
- DB에 대한 엔드포인트를 구분을 할 수 있다.
- AWS의 Aurora db, MySQL사용하면 Read DB, WriteDB 나눠서 쓰는 편
- master(write권한) / slave(Read Replica)으로 쓰자. 하는 상황이 있음
- 이걸 readOnly = true, readOnly = false를 보고 나눠줄 수 있다.
- DB 엔드포인트를 구분하면서 장애 격리를 할 수 있는 좋은 포인트가 된다.!
- 또는 Aurora db AWS의 Aurora DB클러스트 모드같은 것을 쓰면 같은 엔드포인트로 보냈을 때 read-only 값을 보고 구분을 해준다. (데이터베이스마다 다르다)
- 읽기와 쓰기를 나누자는 말이다.
- 성능상의 이점을 누릴 수 있다.
- JPA는 1차캐시에 스냅샷을 저장해서, Transaction Commit flush하는 시점에 변경감지가 수행되면서 update쿼리가 나가게 된다.
- readOnly(ture)를 하게 되면 CUD 작업이 동작하지 않아 스냅샷, 저장, 변경 감지 이런 것들을 안 하게 되면서 성능이 향상된다.
그래서 이렇게 달자.!
@Transactional(readOnly = true) // -> 전체 readOnly = true 걸어주고 (read)
@RequiredArgsConstructor
@Service
public class ProductService {
private final ProductRepository productRepository;
@Transactional // (create,update,delete) -> 명시적으로 Transactional을 붙여주자.
public ProductResponse createProduct(ProductCreateRequest request) {
/* 코드 생략 */
}
// -> 자동으로 readOnly = true가 걸어진다.
public List<ProductResponse> getSellingProducts() {
/* 코드 생략 */
}
}
참고자료
https://clarkshim.tistory.com/37
'Spring관련 기술 > 테스트코드' 카테고리의 다른 글
Mockito로 Stubbing하기 (0) | 2023.12.24 |
---|---|
Presentation Layer 테스트 (2) (0) | 2023.12.22 |
Business Layer 테스트 (2) (0) | 2023.12.17 |
Business Layer 테스트 (1) (0) | 2023.12.14 |
Persistence Layer 테스트 (1) | 2023.12.14 |