아님말고
헥사고날 아키텍처 본문
계층형 아키텍처
우리가 익숙한 계층형 아키텍처 이다.
표현 계층은 사용자 요청을 받아 응용영역에 전달하고 처리 결과를 다시 사용자에게 보여주는 역할을 하는 Controller이고,응용 계층과 도메인 계층은 비즈니스 로직을 구현하는 Service이며, 인프라스트럭처 계층은 DB 연동을 처리하고, 메시지 큐에 메시지를 전송하거나 수신하는 기능을 구현하고 SMTP을 이용하여 메일을 발송하기도 한다.
계층구조의 문제는 상위 계층에서 하위 계층으로 의존한다는 것이다. 예를 들어 표현 계층은 응용 계층에 의존하고 응용 계층은 인프라스트럭처 계층에 의존한다. 그래서, 개발 순서도 DB 쿼리 작성하고 Dao를 만들어 Service가 호출하고 controller를 만든다. 한 마디로 DB가 왕이다. DB 테이블이 변경되면 controller 까지 모두 수정해야한다.
헥사고날 아키텍처
헥사고날 아키텍처의 논리적인 구조를 보면 business core를 담당하는 도메인 영역이 있고 port라고 불리우는 응용 영역이 있다. 외부 adapter들은 port를 통해 통신하게 된다.
Port는 input port와 out port로 나뉘는데, 왼쪽의 어댑터는 도메인를 호출하는 어댑터로 input port로 통신하고, 오른쪽의 어댑터는 도메인에 의해 호출되는 어댑터로 output port이다.
헥사고날 아키텍처의 핵심은 비즈니스 로직이 있는 '도메인 계층' 이 외부 요소에 의존하지 않고 외부 요소가 '도메인 계층'에 의존하도록 하는 것이 핵심이다.
구현측면에서 계층형과 비교한다면
영역 | 계층형 | 헥사고날 |
표현영역 | controller | adapter.in |
응용영역 | service | application |
도메인영역 | domain | |
인프라스트럭처영역 | dao | adapter.out |
응용 계층은 외부에 제공해야 할 정보를 application.port.in 패키지 영역에 interface로 만들고 application.service 패키지 영역에 구현하여 제공한다. 또한 DB 조회 및 등록 같이 도메인 영역에서 이용할 정보는 application.port.out 패키지 영역에 interface로 만들어 놓으면 adapter.out 패키지 영역에서 구현한다.
예제
사용자가 주문취소를 하는 경우를 생각하자.
도메인영역
package 위치 : domain
주문번호와 주문상태를 가진 Order 라는 도메인 객체가 있다. 주문취소 메소드에서는 주문상태가 준비상태일때만 취소가 가능하다는 비즈니스 로직이 들어있다.
@Slf4j
@Data
public class Order {
private String orderNum;
private int orderState; //1:준비, 2:배송중, 3:배송완료, 4:취소
//주문취소하기
public void cancel() throws Exception {
if(isCancelable()) {
this.orderState = 4;
}else {
throw new BusinessException("ORD_0001"); //주문이 준비상태일때만 주문취소가 가능합니다.
}
}
//주문취소 가능한지 확인
public boolean isCancelable() {
return orderState == 1; //준비중일때
}
}
응용영역
pacakge 위치: application.port.in
주문취소의 사용자 케이스를 인터페이스로 제공하여 표현영역의 adapter 가 이용할 수 있도록 한다.
주의할 점은 표현영역의 request, dto 같은 객체가 들어가서는 안된다. 응용영역은 표현영역에 의존되면 안된다는 것을 명심하자.
@Service
public interface OrderUseCase {
//전체 조회
public List<Order> selectOrder() throws Exception;
//주문번호로 조회
public Order selectOrderByOrderNum(String orderNum) throws Exception;
//주문 취소
public boolean CancelOrder(String orderNum) throws Exception;
}
pacakge 위치: application.port.out
order 도메인 객체를 DB에서 가져오기 위한 인터페이스를 작성한다.
@Service
public interface OrderOutPort {
//전체 조회
public List<Order> selectOrder();
//주문번호로 조회
public Order selectOrderByOrderNum(String orderNum);
//주문 취소
public boolean cancelOrder(Order order);
}
package 위치 : application.service
응용영역은 표현영역과 도메인영역의 연결 그리고, 트렌젝션의 역할을 담당한다.
사용자 케이스를 실제 구현하는데, 사용자의 요청을 처리하기 위해서 DB에서 도메인 객체를 구하고, 도메인 객체의 메소드를 사용한다.
※ 주의 :
- 비즈니스 로직은 도메인 객체에서 구현한다는 것을 명심하자.
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService implements OrderUseCase {
private final OrderOutPort orderOutPort;
@Override
public List<Order> selectOrder() throws Exception {
return orderOutPort.selectOrder();
}
@Override
public Order selectOrderByOrderNum(String orderNum) throws Exception {
return orderOutPort.selectOrderByOrderNum(orderNum);
}
@Override
public boolean CancelOrder(String orderNum) throws Exception {
Order order = orderOutPort.selectOrderByOrderNum(orderNum);
order.cancel();
return orderOutPort.cancelOrder(order);
}
}
표현 영역
package 위치 : adapter.in
표현영역으로 응용영역의 useCase 을 사용하여 controller를 작성한다.
프런트와의 파라메터로 DTO 객체를 만들어 사용할 수도 있고, useCase로 전달할 Order객체로의 변환을 위해 modelMapper을 이용할 수 있다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sample/v1")
public class OrderController {
private final OrderUseCase orderUseCase;
private final ModelMapper modelMapper;
//전체 조회
@GetMapping("/orders")
public ResponseEntity<?> selectOrder() throws Exception {
List<Order> list = orderUseCase.selectOrder();
List<OrderDTO> retList = list.stream()
.map(e -> modelMapper.map(e, OrderDTO.class))
.collect(Collectors.toList());
return ResponseHandler.generateResponse("success", HttpStatus.OK, retList);
}
//주문번호로 조회
@GetMapping("/orders/{orderNum}")
public ResponseEntity<?> selectOrderByOrderNum(@PathVariable(required=true) final String orderNum) throws Exception {
Order domain = orderUseCase.selectOrderByOrderNum(orderNum);
OrderDTO dto = modelMapper.map(domain, OrderDTO.class);
return ResponseHandler.generateResponse("success", HttpStatus.OK, dto);
}
//주문 취소
@GetMapping("/orders/cancel/{orderNum}")
public ResponseEntity<?> CancelOrder(@PathVariable(required=true) final String orderNum) throws Exception {
orderUseCase.CancelOrder(orderNum);
return ResponseHandler.generateResponse("success", HttpStatus.OK, "");
}
}
인프라스트럭처 영역
package 위치 : adapter.out
application.영역의 outPort를 구현한다. DB에서 Order를 가져오고 있다.
@Repository
@RequiredArgsConstructor
public class OrderRepository implements OrderOutPort {
private static final String NAMESPACE = "com.skt.ssp.adapter.out.sample.mapper.OrderMapper";
private final SqlSession sqlSession;
private final ModelMapper modelMapper;
@Override
public List<Order> selectOrder() {
List<OrderVO> list = sqlSession.selectList(NAMESPACE + ".selectOrder");
return list.stream()
.map(e -> modelMapper.map(e, Order.class))
.collect(Collectors.toList());
}
@Override
public Order selectOrderByOrderNum(String orderNum) {
OrderVO vo = sqlSession.selectOne(NAMESPACE + ".selectOrderByOrderNum", orderNum);
Order domain = modelMapper.map(vo, Order.class);
return domain;
}
@Override
public boolean cancelOrder(Order order) {
int affectedRow = sqlSession.update(NAMESPACE + ".cancelOrder", modelMapper.map(order, OrderVO.class));
return affectedRow > 0 ? true : false;
}
}
pacakage 위치 : resource/mapper
<mapper namespace="com.skt.ssp.adapter.out.sample.mapper.OrderMapper">
<select id="selectOrder" resultType="com.skt.ssp.adapter.out.sample.vo.OrderVO">
<![CDATA[
SELECT *
FROM order_sample
]]>
</select>
<select id="selectOrderByOrderNum" parameterType="String" resultType="com.skt.ssp.adapter.out.sample.vo.OrderVO">
<![CDATA[
SELECT *
FROM order_sample
WHERE order_num = #{orderNum}
]]>
</select>
<update id="cancelOrder" parameterType="com.skt.ssp.adapter.out.sample.vo.OrderVO">
<![CDATA[
UPDATE order_sample
SET order_state = #{orderState}
WHERE order_num = #{orderNum}
]]>
</update>
</mapper>
'Java' 카테고리의 다른 글
[gradle] spring boot + XSS filter 설정 (0) | 2023.05.31 |
---|---|
[AWS] KMS을 이용한 DB 암복호화 (with Mybatis) (0) | 2023.05.24 |
[JWT] token secret key 생성 (0) | 2023.05.10 |
[gradle] Spring Cloud Gateway Header 변경 (0) | 2023.05.08 |
[gradle] Spring Cloud Gateway 설정 및 예제 (0) | 2023.05.04 |