hexagonal architecture라는 거를 왜 쓰는걸까?

연쇄코딩마·2023년 7월 19일
0
post-thumbnail

서론

개발에 대한 무지로 인해 아키텍쳐의 소중함(?)을 잘 알지 못했다. 대충 mvc나 Layered로 잘 활용하면서 될껄 이벤트 기반 아키텍처니 헥사고날 아키텍처니 머리아프다고만 생각 했었다. 그러다 팀원들이 많아지고 TDD에 중요함과 유지보수라는 큰 문제들을 겪게 되면서 마음한켠에 불편함을 안고 개발을 했었드랬는데 어떤 연사의 헥사고날 아키텍처에서 유닛테스트 적용하는 모습을 보면서 오와 저거다하는 눈이 트이는 것을 경험했다.

(역시 사람은 경험에서 많은 것을 꺠닫는다)

hexagonal architecture가 뭘까?

hexagonal architecture란 소프트웨어 시스템을 설계하는 방법 중 하나로, 소프트웨어 시스템의 핵심 비즈니스 로직과 외부 시스템 또는 인터페이스 간의 결합을 제어하여 유연하고 테스트 가능한 시스템을 구축하는 것을 목표한다.

이게 무슨 말일까? 나도 처음엔 무슨 개풀뜯어먹는 소리하고 앉았네 했는데 쉽게 말하면 이렇다. 우리가 실제로 많은 시간을 쏟아야할 비즈니스 로직을 외부의 의존을 최소화한다. 뭘로? 인터페이스로... 예를 들자면 아래와 같다.

내가 생각하는 hexagonal architecture의 최대의 장점

//먼저 우리의 비즈니스 로직을 보자!!!!

export class PostProductService implements PostProductInboundPort {
  constructor(
    @Inject(PRODUCT_ENUM_DI_TOKEN.POST_OUTBOUND_PORT)
    private readonly postProductOutboundPort: PostProductOutboundPort,
  ) {}
  async execute(reqBody: PostProductInboundPortDto): Promise<string> {
  /**
  *우리가 시간을 쏟아야할 로직
  */
    //....
      await this.postProductOutboundPort.execute(
        {
          ...reqBody,
          selecters,
          productTypes,
        },
        session,
      );
    //....
  }
}


//class PostProductOutboundPort 이것이 외부 세계와 단절시켜줄 인터페이스이다.
import { ClientSession } from 'mongoose';
import { ProductEntity } from '../repository/entities/product.entity';
import { PostProductOutboundPortDto } from '../dtos/post-product.outbound.port.dto';

export interface PostProductOutboundPort {
  execute(
    reqBody: PostProductOutboundPortDto,
    session?: ClientSession,
  ): Promise<ProductEntity>;
}
//실제 구현체가 아니다. 인터페이스다. 비밀은 아래 modules에 있다.


const imports = [
  //...
];
const controllers = [
  //...
];
const providers: Provider[] = [
  
//....
  
  {
    provide: PRODUCT_ENUM_DI_TOKEN.POST_OUTBOUND_PORT,
    useClass: PostProductRepository,
  },
  
//....
  
];
@Module({
  imports,
  controllers,
  providers,
  exports: [],
})
export class ProductV1Module implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(JsonBodyParserMiddleware).forRoutes('*');
  }
}
//실제 구현체는 PostProductRepository 이다. 

이렇게 관심사를 분리 하므로써 비즈니스로직에 집중하게 만들고 유연성과 확장성을 향상시키고 테스트 용이성을 제공한다. 뭐 여기에서 끝나면 별로 놀랍지도 않은 아키텍처인듯 싶지만 PostProductService 테스트 파일을 한번보자.

class MockPostProductOutboundPort implements PostProductOutboundPort {
  private readonly result: ProductEntity;
  
  constructor(result: ProductEntity) {
    this.result = result;
  }
  
  execute
  (
  reqBody: PostProductOutboundPortDto,
  session?: ClientSession
  ): Promise<ProductEntity> {
    return Promise.resolve(this.result);
  }
  // 항상 같은 결과를 리턴하는 목을 만들고 
}


describe('findOne-product 서비스', () => {
  let service: PostProductService;
  let product;
  beforeAll(() => {
    service = new PostProductService(
      new MockFindOneProductOutBoundPort(productDummy),
    );
  });
  it('findOneProductOutbound에서 리턴해준 값 그대로 리턴', async () => {
    expect(await service.execute(reqBody)).toBe(product);
  });
});
// 이렇게 간단하게 테스트 파일을 만들어 줄 수 있다.

이렇게 다른 의존성을 전부 배제하고 비즈니스 로직만 단위 테스트를 할수 있는 장점이 있다. 나는 테스트의 용이성 때문에 이 아키텍처에 눈을 뜬 케이스다.

또한 인터페이스만 갖다 바꿔주면 되기 때문에 확장성 측면이나 유지보수측면에서는 말할 나위가 없다.

물론 단점도 있다. 이 아키텍처는 개발자의 정확한 이해가 수반되어야 되기 때문에 초기 설계가 복잡하고 타이핑이 많기 때문에 느린 개발속도, 성능상 오버헤드등 극복해야할 단점들이 있다. 그럼에도 불구하고 장점이 워낙 명확하기 때문에 개발시에 고려해봐도 좋은 아키텍처인거 같다.

profile
只要功夫深,铁杵磨成针

0개의 댓글