프로젝트를 처음 구성할 때, 패키지를 어떻게 구성해야 할지 많은 고민을 하게 됩니다.
인터넷에 있는 수많은 프로젝트가 있지만 어째서 이 클래스는 이 패키지에 있는 걸까? 라는 명확한 이유를 몰랐고 어떠한 기준으로 패키지를 구성하면 될지 개인적인 생각과 정말 간단한 시나리오로 구상해봤습니다.
예제로 사용될 시나리오는 다음과 같습니다.
"사용자는 원하는 제품을 구매 요청 후 결제를 하면 배송이 완료 된다." //마법의 물품 배송;;
코드를 작성하기 전에 먼저 계층형 아키텍쳐에 대해 알아보겠습니다.
계층형 아키텍쳐
Layered Architecture라고 불리며 각 레이어에 특정 관심사와 관련된 객체만을 포함하게 만들어 관심사가 분리된 코드 구성을 목표로 하는 아키텍처입니다.
각 레이어는 밑에 있는 레이어에 종속되며, 반대로 아래에 있는 레이어는 상위 계층에 어떠한 연관도 있어서는 안 됩니다.
각 레이어의 관심사는 다음과 같습니다.
- Presentation Layer
사용자나 다른 시스템을 위한 인터페이스로, Controller가 이 계층에 해당함.
- Application Layer
애플리케이션의 요구사항에 해당함. 실제 비즈니스 로직을 담고 있지는 않음. Service가 이 계층에 해당
- Domain Layer
핵심 비즈니스 로직이 담긴 계층으로, 이 layer는 어떤 외부 관심사도 포함하지 않고 순수한 비즈니스 로직만을 담아야 함. Domain 모델
- Infrastructure Layer
이메일 발송 등 인프라 관련 계층
계층형 아키텍처가 말하는 '종속', '연관' 은 의존성을 뜻 합니다.
즉, 객체가 어떤 역할이나 책임이 필요한지, 의존성에 따라 설계의 모양이 바뀌게 됩니다.
이를 코드로 알아보겠습니다.
실제 동작하는 코드가 아닌 대략 컨셉을 위한 코드입니다.
@SpringBootApplication
public class DemoApplication implements ApplicationRunner {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
// OrderController
@Autowired
OrderService orderService;
@Override
public void run(ApplicationArguments args) throws Exception {
// Controller에서 주문 요청이 들어옴
// OrderRequest는 상품 번호를 갖고 있음
OrderRequest orderRequest = new OrderRequest(1L);
orderService.placeOrder(orderRequest);
// Controller에서 해당 주문번호가 결제 되었다고 Service로 전달
orderService.payOrder(1L);
}
}
사용자는 구매할 제품을 선택해 OrderController로 요청을 한다.
OrderController 코드에 다음과 같은 의존성이 생겼습니다. 그럼 이제 OrderService 코드를 확인해보겠습니다.
@Service
public class OrderService {
private OrderRepository orderRepository = OrderRepository.getInstance();
private ProductRepository productRepository = ProductRepository.getInstance();
@Transactional
public void placeOrder(OrderRequest orderRequest) {
long productId = orderRequest.getProductId();
Product product = productRepository.findById(productId)
.orElseThrow(IllegalStateException::new);
OrderItem orderItem = new OrderItem(product);
Order order = new Order(orderItem);
order.place();
orderRepository.save(order);
}
@Transactional
public void payOrder(long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow(IllegalStateException::new);
order.pay();
//결제와 함께 배송이 완료되는 마법
order.delivery();
}
}
주문 요청이 들어오면 요청 정보에 있는 상품 정보로 DB에서 상품을 가져오고 주문 정보를 만든 후 주문 로직 수행 후 저장한다.
결제 요청이 들어오면 주문번호에 해당하는 주문 정보를 가져와 결제 로직을 수행한다.
결제가 완료되면 배송 요청을 하고, 주문번호에 해당하는 주문 정보를 가져와 배송 로직을 수행한다.
이제 두 의존성을 레이어 별로 나누어 보겠습니다.
가장 먼저 Persistence 계층의 Repository가 상위 레이어인 Domain을 가지고 있는 게 보입니다.
아직 DI로 주입받지 않고 임의로 만든 Dao 객체입니다. 현재 연관관계로 만들어져 있지만 의존관계 화살표로 표현해두었습니다.
의존성의 방향을 바꾸기 위해 Spring Jpa에서 제공하는 JpaRepository를 상속받은 Repository 인터페이스를 만들어 의존 방향을 역전시킵니다.
또 어느 레이어에 속할지 정해지지 않은 OrderRequest와 OrderItem을 의존성의 방향에 맞게 위치해줍니다.
만약 OrderRequest가 Controller와 같은 패키지에 위치하게 된다면, Application 계층에서 Presentation계층으로 의존이 생김
최종적으로 상위 계층에서 아래로만 의존성을 가지고 아키텍처가 되었습니다.
프로젝트를 설계하며 객체 간 의존성의 방향이 순환하거나, 레이어 간에 의존성의 방향이 반대가 되지 않도록 주의하며, 설계를 하면 될 거 같습니다.
'Spring' 카테고리의 다른 글
OAuth2.0 + JWT를 사용한 토큰 기반 서버 인증 구현하기 (0) | 2021.04.12 |
---|---|
TDD.. 테스트 코드는 습관이에요! (0) | 2021.04.02 |
뭐? MSA? 그렇다면 Zuul로 Gateway를 구현해보자. - 01 (0) | 2021.03.30 |
헥사고날 아키텍처(Hexagonal Architecture) (1) | 2021.03.27 |
Event를 발행해 느슨한 결합을 가져보자 (0) | 2021.03.27 |