개발 일기

[객체 지향적 설계] 디자인 패턴과 레이어드 아키텍처 적용하기 본문

Tech/배워서 남주기

[객체 지향적 설계] 디자인 패턴과 레이어드 아키텍처 적용하기

flow123 2022. 9. 29. 09:42

객체 지향을 고민하게 된 계기

 

작년에 스프링 입문을 위한 자바 객체 지향의 원리와 이해 책을 읽었습니다. 

(이 책을 읽고 나서 스프링을 좀 더 개발 의도에 맞게 (== 객체 지향을 생각하며) 바라볼 수 있었습니다. 

디자인 패턴, SOLID 등을 소개하면서 어떻게 객체 지향을 적용할 수 있을지, 좋은 가이드가 되어준 책 이었는데요. 스프링 개발자라면 꼭 읽어보기를 추천합니다. )

 

객체 지향은 인간이 사물을 인지하는 방식대로 프로그래밍 하는 방식입니다. 

즉 기계에 맞춰 사고하던 방식이 아니라, 인간이 현실세계를 인지하는 방식으로 프로그램을 만들자는 의도에서 출발합니다.

객체 지향 원칙을 지키다 보면, 결합도는 낮추고 응집도는 높이는 방향으로 설계하게 됩니다. 그리고 이는 재사용성, 유지보수성을 용이하게 해줍니다. 

 

스프링은 객체 지향의 정수를 반영하고 있는 프레임워크입니다. 책을 읽으면서, 이전에 여러 스프링 프로젝트를 만들어보면서도 SOLID, 디자인 패턴, 프로젝트 구조 등 설계적 고민을 깊게 하지 못한 것이 아쉬움으로 남았습니다. 

본 포스팅에서는 제가 프로젝트에서 레이어드 아키텍처, 어댑터 패턴, DIP 등을 어떻게 활용하였는지 전반적으로 소개 드리고자 합니다. 


레이어드 아키텍처

 

레이어드 아키텍처에서 클래스는 클래스의 역할에 따라 Layer 로 분리합니다.

Presentation은 Presentation 끼리, Application 은 Application 끼리 묶입니다. 

따라서 레이어드 아키텍처를 도입하면, 관심사의 분리하고 단방향으로 의존성을 제한할 수 있습니다. 

이렇게 특정 레이어의 변경이 미치는 영향을 최소화 함으로써, 결합도를 낮추고 유지 보수성을 높일 수 있습니다.

함께 변경될 가능성이 높은 클래스끼리 묶어놓기 때문에,  코드 수정 등도 보다 편리 합니다. 

구성

- Presentation: 외부와 상호작용하는 계층입니다. ex. HTTP 를 통하는 Controller 클래스 

- Application: 비즈니스 로직이 다뤄집니다. ex) 주문 로직이 담긴 OrderService 클래스 

- Domain : 애플리케이션의 중심과 같습니다. ex) Entity, Interface 등 잘 변하지 않는 클래스.

*Application, Domain  모두 가급적 의존성 없이 순수한 객체여야 합니다.

이를 Infrastructure 레이어 등 외부에서 결정하게 해야 합니다. 그래야 구조 등을 바꿀 때, 두 레이어의 변경이 없이 고칠 수 있습니다

- Infrastructure: 구현 상세 ex) MyBatisRepository

 

기능(Feature) 별로 4개 레이어 구조를 적용

 

레이어드 아키텍처는 크게 Feature By Layer , Feature By Package 가 있습니다. 

저는 패키지 내에 응집도를 높이고자 Feature By Package 를 사용했습니다. Feature를 한 패키지로 만들고, 해당 패키지 내에서 레이어를 나누기 때문에, 주로 참조 되는 레이어들을 패키지 내에 둘 수 있게 됩니다. 유지 보수 할 때도, 특정 Feature 의 패키지만 열어보면 된다는 점에서 명료하고 편리합니다. 

 

  • 회원 가입, 제품, 주문 등의 기능(feature) 별로 패키지를 두고, 패키지 내에 레이어드 아키텍처를 구성했습니다
  • 모든 패키지에서 참고하는 클래스는 global 에 두었습니다. 
  • 인증/인가 기능에서 쓰이는 인터셉터와 어노테이션을 authentication 에 두었습니다.

 

DTO를 Presentation과 Application 계층에서 분리

presentation 의 dto (ex. Cart 객체) 는 말 그대로 이동하는 데이터 객체입니다. 따라서 application 등 다른 계층에 넘겨주는 경우가 있는데,  이렇게 되면 의존 방향성이 어긋난다는 점에서 고민이 되었습니다. 저는 해당 클래스를 구성 요소는 같지만, 이름과 소속 계층이 다른 객체 (ex. cartCommand) 로 변환함으로써 의존성 문제를 해결했습니다. 

 

Cart.class - 주문 아이템을 담는 DTO 객체.

패키지: order/presentation

package com.example.freshcart.order.presentation.request;
import com.example.freshcart.order.application.command.CartCommand;

public class Cart {

  private String receiverName;
  private String receiverPhone;
  private String receiverAddress;
  private List<CartItem> cartItems;

  public CartCommand toCommand() {
    return new CartCommand(this.receiverName, this.receiverPhone, this.receiverAddress,
        this.cartItems.stream().map(CartItem::toCartItemCommand).collect(toList()));
  }

 

레이어를 철저히 분리하고자, Presentation 층의 요청 객체를 application 층의 CartCommand 객체로 변환하였습니다.

OrderManagerFacade - 주문과 관련된 서비스를 처리함 (order/application )

package com.example.freshcart.order.application;

public class OrderRegisterProcessor {

  @Transactional
  public void place(LoginUser user, CartCommand cart) {
    Order order = cartToOrderMapper.mapFrom(user, cart);
    orderValidator.validate(order);
    checkInventoryByOption(order.getOrderItemList()); 
    save(user, order);
  }

디자인 패턴 - 어댑터 패턴의 적용

 

상황 - 다양한 기술을 도입할 경우, 인터페이스 구현의 한계 

 

프로젝트에 사용된 모든 저장소는 구현체 변경 가능성을 고려하여 인터페이스로 만들었습니다.

초기 기능 구현 시, 아직 ORM 기술을 확정 하지 않아서 임시로 해시 맵 자료구조에 저장했습니다.

이후 MyBatis와 JPA 를 추가적으로 도입할 예정이었습니다. 

 

해시맵 구현체를 사용할 때는, 저장소 인터페이스를 바로 구현할 수 있었습니다.

하지만 MyBatis를 도입할 경우, Mapper 인터페이스를, Jpa는 JpaRepository를 상속 받아야 해서, 인터페이스를 바로 구현할 수 없었습니다. 어댑터 패턴이 적합해 보이는 상황이었습니다. 

 

어댑터 패턴이란? 

 

어댑터 패턴은 변환기라고 할 수 있습니다. 변환기라는 이름에서 추론할 수 있듯, 서로 다른 두 인터페이스 사이에 통신이 가능하게 하는 것입니다. 충전기를 예로 들어보겠습니다. 휴대폰 충전기는 [휴대폰 - 충전기 - 콘센트] 의 형태로, 휴대폰과 콘센트 사이에서 변환기 역할을 수행합니다. 

 

해결 

 

저장소 인터페이스를 구현하는 MyBatis/Jpa 전용 어댑터를 추가했습니다.

어댑터 클래스에는 mapper /Jpa 구현체를 지역 변수로 추가하고, 이를 오버라이드한 메서드 내에서 호출하였습니다.

public class JpaOrderRepositoryAdaptor implements OrderRepository {
  private final JpaOrderRepository jpaOrderRepository;

  @Override
  public Order save(LoginUser user, Order order) {
    jpaOrderRepository.save(order);
    return order;
  }

 

레이어드 아키텍처가 적용된 프로젝트 코드는 아래 깃헙에서 확인하실 수 있습니다. 

코드 개선에 대한 피드백 등은 언제든 환영이니 댓글 달아주시면 감사하겠습니다. 

 

https://github.com/f-lab-edu/FreshCart


참고 자료 

 

책: 스프링 입문을 위한 자바 객체 지향의 원리와 이해 

https://ademcatamak.medium.com/layers-in-ddd-projects-bd492aa2b8aa

https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html

https://medium.com/sahibinden-technology/package-by-layer-vs-package-by-feature-7e89cde2ae3a

https://msolo021015.medium.com/layered-architecture-deep-dive-c0a5f5a9aa37

Comments