package com.example.productorderservice.product;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
class ProductServiceTest {
private ProductService productService;
private ProductPort productPort;
private ProductRepository productRepository;
@BeforeEach
void setUp() {
productRepository = new ProductRepository(); //레파지토리는 레파지토리
productPort = new ProductAdapter(productRepository); // 인터페이스 = 구현체(어댑터)에 레파지토리받기
productService = new ProductService(productPort); // 서비스는 인터페이스 받기
}
@Test
void 상품등록() {
String name = "상품명";
final int price = 1000;
final DiscountPolicy discountPolicy = DiscountPolicy.NONE;
//테스트할 요청을 정의( 요청안에 상풍에 대한 정보가 들어있음)
final AddProductRequest request = new AddProductRequest(name, price, discountPolicy);
//비즈니스 로직실행(요청 add)
productService.addProduct(request);
}
//리퀘스트 객체는 테스트용이라 불변객체이므로 레코드가 더 적절
private record AddProductRequest(String name, int price, DiscountPolicy discountPolicy) {
private AddProductRequest {
Assert.hasText(name, "상품명은 필수입니다.");
Assert.isTrue(price > 0, "상품 가격은 0보다 커야 합니다.");
Assert.notNull(discountPolicy, "할인 정책은 필수입니다.");
}
}
//비즈니스 로직
private class ProductService {
// 인터페이스
private final ProductPort productPort;
// 서비스 생성자(서비스 생성시 인터페이스를 받아야함)
private ProductService(ProductPort productPort) {
this.productPort = productPort;
}
public void addProduct(AddProductRequest request) {
final Product product = new Product(request.name(), request.price(), request.discountPolicy());
//저장 로직 사용시 인터페이스 사용
productPort.save(product);
}
}
// 상품객체에대한 정의
private class Product {
private Long id;
public Product(String name, int price, DiscountPolicy discountPolicy) {
Assert.hasText(name, "상품명은 필수입니다.");
Assert.isTrue(price>0, "상품 가격은 0보다 커야 합니다.");
Assert.notNull(discountPolicy,"할인 정책은 필수입니다.");
}
public void assignId(Long aLong) {
this.id = id;
}
public Long getId() {
return id;
}
}
//저장 인터페이스=> "상품을 받아서 저장한다" 기능정의, 실제 구현은 구현체가함.
private interface ProductPort {
void save(Product product);
}
//저장 인터페이스 구현체(어댑터)
private class ProductAdapter implements ProductPort {
private final ProductRepository productRepository;
//어댑터에 레파지토리 받기
private ProductAdapter(final ProductRepository productRepository) {
this.productRepository = productRepository;
}
//실제 저장은 어댑터로 받은 레파지토리의 저장 메소드 사용
@Override
public void save(Product product) {
productRepository.save(product);
}
}
// 레파지토리는 실제 저장기능의 메소드를 사용하는 곳
private class ProductRepository {
private Map<Long, Product> persistence = new HashMap<>();
private Long sequence = 0L;
public void save(Product product) {
product.assignId(++sequence);
persistence.put(product.getId(),product);
}
}
private enum DiscountPolicy {
NONE
}
}
어댑터 패턴의 역할
코드에서 사용된 어댑터 (ProductAdapter)는 의존성 역전 원칙 (DIP)을 구현하고, 유연한 아키텍처 설계를 가능하게 하기 위해 사용됩니다. 포트-어댑터 패턴 (Hexagonal Architecture)의 일부로 동작하며, 주요 목적은 비즈니스 로직(ProductService)과 저장소(ProductRepository) 간의 결합도를 낮추는 것입니다.
어댑터의 용도
1. 비즈니스 로직과 데이터 저장소 분리
- ProductService는 비즈니스 로직을 담당하며, 데이터를 저장하거나 관리하는 방법에 대해 알 필요가 없습니다.
- ProductAdapter는 ProductService가 저장소의 구현 세부 사항을 알지 않아도 동작할 수 있게 합니다.
- 즉, ProductService는 ProductPort 인터페이스를 통해 저장소에 의존하며, 저장소의 구체적인 구현은 어댑터가 처리합니다.
2. 의존성 역전 원칙 (DIP)
- 의존성 역전 원칙은 다음과 같은 구조를 지향합니다:
- 고수준 모듈 (비즈니스 로직, ProductService)은 저수준 모듈 (저장소, ProductRepository)에 의존하지 말고, 추상화 (인터페이스, ProductPort)에 의존해야 한다.
- 저수준 모듈은 고수준 모듈이 정의한 인터페이스를 구현해야 한다.
ProductService --> ProductPort (추상화) <-- ProductAdapter --> ProductRepository (저수준 모듈)
- 위 구조를 통해, ProductRepository의 변경(예: 다른 저장소로 교체)이 발생해도 ProductService는 변경 없이 동작합니다.
3. 유연성과 테스트 용이성 증가
- 유연성:
- ProductAdapter를 통해 ProductRepository 외에 다른 저장소 구현체 (예: JPA, 외부 API 호출 등)를 쉽게 교체할 수 있습니다.
- 테스트 용이성:
- ProductPort 인터페이스를 모킹(mocking)하거나 더미 구현체를 사용해 테스트가 가능합니다.
- 예를 들어, ProductAdapter 없이 ProductRepository에 직접 의존하면 저장소를 테스트하는 동안 실제 데이터베이스 연결이 필요할 수 있습니다.
코드에서 어댑터의 역할
1. 인터페이스 구현
private class ProductAdapter implements ProductPort {
private final ProductRepository productRepository;
private ProductAdapter(final ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public void save(Product product) {
productRepository.save(product);
}
}
- ProductAdapter는 ProductPort 인터페이스를 구현하며, 저장소(ProductRepository)와 비즈니스 로직(ProductService) 사이에서 중재자 역할을 합니다.
2. ProductService에서 어댑터 사용
private class ProductService {
private final ProductPort productPort;
private ProductService(ProductPort productPort) {
this.productPort = productPort;
}
public void addProduct(AddProductRequest request) {
final Product product = new Product(request.name(), request.price(), request.discountPolicy());
productPort.save(product);
}
}
- ProductService는 ProductPort에만 의존하며, 저장소(ProductRepository)의 구현 세부 사항을 몰라도 됩니다.
- 이는 비즈니스 로직이 저장소의 변화에 영향을 받지 않도록 설계된 것입니다.
어댑터의 이점
- 결합도 감소:
- 비즈니스 로직과 저장소 간의 강한 결합을 방지합니다.
- 교체 가능성:
- 저장소(ProductRepository)를 JPA, NoSQL, 파일 시스템 등으로 쉽게 교체할 수 있습니다.
- 테스트 용이성:
- 저장소 대신 Mock 어댑터를 주입하여 테스트를 단순화할 수 있습니다.
- 확장성:
- 여러 어댑터(ProductAdapter)를 구현하여 다양한 데이터 소스와 통합할 수 있습니다.
결론
어댑터는 저수준 모듈(저장소)과 고수준 모듈(비즈니스 로직) 사이의 중재자로 작동하며, DIP와 헥사고날 아키텍처의 원칙에 따라 유연하고 확장 가능하며 테스트하기 쉬운 코드를 만듭니다.
ProductAdapter는 ProductService가 저장소 구현의 세부 사항에 의존하지 않도록 해줍니다.
반응형
'서버&백엔드 > 🔥 JAVA' 카테고리의 다른 글
JAVA | enum사용법 (0) | 2024.12.19 |
---|---|
의존성 역전 원칙 (DIP) (0) | 2024.12.19 |
JAVA | POJO로 개발한다는 무슨말일까? (0) | 2024.12.18 |
JAVA | Record란 무엇일까 (0) | 2024.12.18 |
Inner클래스는 언제 쓸까? (0) | 2024.12.17 |