이전글에서 이어지는 내용
[이커머스 프로젝트] 주문 로직 - 전략 패턴 적용(리팩토링)
[이커머스 프로젝트] 주문 로직 - 전략 패턴 적용(리팩토링)
초기 코드현재 주문로직은 상품을 직접 주문하는경우와 장바구니의 상품을 주문하는 경우로 나뉘어있다.상품을 직접 주문할때는 수량을 선택하여 주문하지만장바구니에 담긴 상품을 주문할때
mrxx.tistory.com
문제점 발견
이전에 전략패턴을 적용해보았는데 뭔가 잘못됨을 느꼇다
public Order createOrderByProduct(OrderByProductRequest request, Member member) throws CustomException {
Address address = addressQueryService.getAddress(request.getAddressId());
OrderStrategy<OrderByProductRequest> orderStrategy =
(OrderStrategy<OrderByProductRequest>) orderStrategyMap.get("productOrderStrategy");
Order order = orderStrategy.createOrder(request, member, address);
orderRepository.save(order);
return order;
}
public Order createOrderByCart(OrderByCartRequest request, Member member) throws CustomException {
Address address = addressQueryService.getAddress(request.getAddressId());
OrderStrategy<OrderByCartRequest> orderStrategy =
(OrderStrategy<OrderByCartRequest>) orderStrategyMap.get("cartOrderStrategy");
Order order = orderStrategy.createOrder(request, member, address);
orderRepository.save(order);
return order;
}
유연성과 확장성을 고려하여 전략 패턴을 도입하고 리팩토링 하였는데
주문을 생성하는 메서드는 주문 방식에 따라 하나씩 작성되고 있다. 이전에 뭔가 이상함을 느꼇던게 이것인거같다
이렇게 놔둔다면 다른 주문 방식이 추가됐을시 메서드를 계속 작성해야한다
그래서 이 부분을 다시 수정해보려고 한다
리팩토링 - 1
OrderType
@Getter
@AllArgsConstructor
public enum OrderType {
PRODUCT("orderByProductRequest"),
CART("orderByCartRequest")
;
private final String beanName;
}
먼저 OrderType이라는 Enum 클래스를 만들어주고
값으로 해당 빈의 이름을 명시적으로 적어주었다
OrderService.createOrder()
public <T extends CreateOrderRequest> Order createOrder(T request, Member member, OrderType orderType) throws CustomException {
OrderStrategy<T> orderStrategy = (OrderStrategy<T>) orderStrategyMap.get(orderType.getBeanName());
Address address = addressQueryService.getAddress(request.getAddressId());
Order order = orderStrategy.createOrder(request, member, address);
orderRepository.save(order);
return order;
}
그리고 주문 생성 메서드를 제네릭 메서드로 만들고
기존 orderStrategyMap에서 OrderType의 BaenName을 찾아 주입시키면 되지않을까 생각하여 작성해보았다
이렇게 한다면 새로운 주문이 방식이 추가됐을때 OrderType만 추가적으로 작성하면되서 꽤나 괜찮다고 보여진다
더 해보자
OrderType 클래스도 없애고 request에 따라 자동으로 빈을 주입시킬수있으면 더 좋겠다 하지만 방법을 모르겠다orderStrategyMap의 Key를 Bean 이름이 아닌 해당 Baen의 제네릭 타입으로 받을 수 있다면 할 수 있을 것 같다
예를 들어 상품을 직접 주문하는 경우의 ProductOrderStrategy를 사용하게된다public class ProductOrderStrategy implements OrderStrategy<OrderByProductRequest>
여기서 OrderByProductReqeust.class
를 strategyMap의 Key로 받아올수있다면
OrderType을 사용하지 않고 request.getClass()만으로 원하는 전략을 주입시킬수있다
하지만 방법을 모르기때문에 GPT의 도움을 받아보았다
GPT는 자바 리플렉션을 적용하여 해결할 수 있다고 답했다
자바 리플렉션
리플렉션을 들어만봤지 뭔지는 모르는 상태이다
간단하게 찾아보니
구체적인 클래스 타입을 몰라도 해당 클래스의 타입, 메서드, 변수에 접근할수있도록 해주는 API
런타임시에 동적으로 특정 클래스의 정보를 추출할 수 있다
라고 한다 제대로 사용하기위해선 구체적으로 공부를 해야겠지만 일단 한번 적용해본다
리팩토링 - 2
@Component
public class OrderStrategyProvider {
private final Map<Class<?>, OrderStrategy<?>> strategyMap = new HashMap<>();
public OrderStrategyProvider(List<OrderStrategy<?> strategies) {
for (OrderStrategy<?> strategy : strategies) {
Class<?> genericType = getGenericType(strategy);
if (genericType != null)
strategyMap.put(genericType, strategy);
}
}
private Class<?> getGenericType(OrderStrategy<?> strategy) {
return (Class<?>) ((ParameterizedType) strategy.getClass().getGenericInterfaces()[0])
.getActualTypeArguments()[0];
}
@SuppressWarnings("unchecked")
public <T extends CreateOrderRequest> OrderStrategy<T> getStrategy(Class<T> requestType) {
return (OrderStrategy<T>) strategyMap.get(requestType);
}
}
OrderStrategyProvider
라는 클래스를 만들어주었다
이 클래스에선 ApplicationContext에서 Orderstrategy 타입의 빈들을 가져온뒤
Generic 타입을 추출하여 strategyMap에 저장하는 역활을한다
getGenericType(OrderStrategy<?> strategy)
메서드에서 리플렉션이 사용되었는데
이 메서드의 리턴값은 OrderStrategy의 제네릭 타입이된다
예) OrderByProductReqeust.class
이러한 수순으로 가져오게된다
strategy.getClass() -> ProductOrderStrategy.class
해당 strategy의 클래스 정보를 먼저 가져온다
getGenericInterfaces()[0] -> [OrderStrategy<OrderByProductRequest>]
getGenericInterfaces() 메서드는 해당 클래스가 구현한 인터페이스를 모두 가져온다 (배열로)
지금은 OrderStrategy 인터페이스만 구현되어있기 때문에 하나만 가져온다
(ParameterizedType) -> OrderStrategy<OrderByProductRequest>
Java는 제네릭 타입을 런타임 시에 타입 정보를 소거해 일반적인 방법으론 조회할 수 없다고한다
하지만 ParmeterizedType을 사용하면 런타임에도 제네릭 타입을 조회할 수 있다
getActualTypeArguments()[0] -> OrderByProductRequest
getActualTypeArguments() 메서드는 제네릭 타입 매개변수를 가져온다 (배열로)
이렇게 자바의 리플렉션을 활용해서 어떤 request에 맞는 Strategy 구현체를 가져올수있다
그래서 결과적으로 strategyMap에 내가 원하던대로<OrderByProductReqeust.class, ProductOrderStrategy>
와<OrderByCartRequest.class, CartOrderStrategy>
가 저장되게된다
최종
private final OrderRepository orderRepository;
private final AddressQueryService addressQueryService;
private final OrderStrategyProvider orderStrategyProvider;
public <T extends CreateOrderRequest> Order createOrder(T request, Member member) throws CustomException {
@SuppressWarnings("unchecked")
OrderStrategy<T> strategy = (OrderStrategy<T>) orderStrategyProvider.getStrategy(request.getClass());
Address address = addressQueryService.getAddress(request.getAddressId());
Order order = strategy.createOrder(request, member, address);
orderRepository.save(order);
return order;
}
최종적으로 이렇게 하나의 createOrder() 메서드로 어떤 주문 방식이던 그에 맞는 Strategy만 구현해주면 된다
리플렉션을 들어만봤지 어디에 쓰이는지 몰랐는데 이번 기회에 알게되었다
활용도가 무궁무진할것같아서 꼭 시간내서 구체적으로 학습을 해야겠다