본문 바로가기
Concepts/SW Architecture

아키텍처 - 프로젝트 패키지 구조

by ocwokocw 2021. 2. 10.

- 이 글은 로버트 C.마틴의 Clean Architecture를 기반으로 작성되었습니다. (가능하면 책을 읽어보는것을 추천한다.)

- 개요

각 프로젝트 마다 규모나 설계자의 생각에 따라 패키지 구조를 정한다. 패키지 구조는 프로그램이 단순히 동작하게 하는데에는 크게 중요하지 않을수도 있지만, 프로그램을 파악하는데 도움이 되며 설계의 사상을 반영한다.


- 계층 기반 패키지

가장 단순하고 전통적인 아키텍처다. 기술적인 관점의 계층에 기반하여 코드를 분할한다.

위의 다이어그램은 스프링 MVC 를 처음 시작하거나, 웹 프로그래밍에 경력이 좀 있다면 꽤 익숙할만한 패키지 구조이다. 각 클래스와 인터페이스는 다음과 같은 역할을 한다.

  • OrdersController: 웹 컨트롤러, 웹 요청 처리
  • OrdersService: 업무규칙 인터페이스
  • OrdersServiceImpl: OrdersService 구현체
  • OrdersRepository: 영속적인 Orders 정보에 대한 접근을 정의한 인터페이스
  • JdbcOrdersRepository: OrdersRepository 인터페이스 구현체

계층을 웹(Controller), 업무규칙(Service), 영속성(Repository)로 나누었다. 패키지는 항상 아래방향으로만 의존한다.

 

이런 패키지 구조는 웹, 업무규칙, 영속성 계층으로 나누어져있다는것과 스프링 MVC 일것 같다는 정보를 표현한다. 주문과 관련된 코드가 있다라는 도메인에 대한 정보를 제공하지 않는다. 만약 이런 도메인이 많을 때 주문정보를 고쳐야겠다면 어느 부분을 고쳐야할지 찾는데 수고가 든다.

 

하지만 무조건 이 계층구조가 오래됐다거나 도메인 계층을 표현하지 않았다고해서 무조건 옛날식 구조라거나 책도 읽어보지 않은 개발자라고 맹목적으로 비판할 필요는 없다는것이 개인적이 생각이다. 만약 프로젝트가 소규모이거나 복잡한 구조가 아니라면 이런 계층구조 또한 엄연히 공수를 적게 투입하여 웹 프로그램을 구현할 수 있는 패키지 구조이기 때문이다.


- 기능 기반 패키지

기능 기반 패키지 구조는 연관 개념이나, 도메인, Aggregate Root 에 기반하여 코드를 나누는 방식이다. 같은 도메인 개념내의 코드라면 하나의 패키지 구조로 몰아넣는다.

계층 기반 패키지와 달리 주문에 대한 코드들이 orders 하위의 패키지로 모두 포함되었다. 이 패키지 구조는 읽는 사람에게 "주문에 대한 정보를 포함하고 있다"라는 사실을 인지시켜서 도메인 개념을 표현한다.


- 포트와 어댑터

우리가 Controller, Service, Repository 를 나눈 이유는 세부사항으로부터 업무로직을 보호하기 위함이다. 그러려면 웹 계층이나 데이터베이스 계층으로 부터 업무로직이 분리되어야 한다. 도메인(업무규칙)을 '내부'라고 하고 나머지를 '외부'라고 한다면 의존성은 외부가 내부를 향해야 한다.

위의 다이어그램을 보면 외부(세부사항)인 Controller와 Jdbc 데이터베이스가 내부(도메인)에 의존하고 있다.

 

OrdersRepository도 Orders로 이름이 변경되었다. 이는 DDD(도메인 기반 디자인) 사상에 입각하여 도메인 영역에서의 이름은 반드시 유비쿼터스 도메인 언어를 사용해야 하며, 기술기반의 이름을 사용해서는 안된다는점을 반영한것이다.


- 컴포넌트 기반 패키지

계층형 아키텍처에서 영속성과 Service 인터페이스는 타 패키지에서 접근할 수 있어야하므로 public 으로 서언한다. 이렇게 선언된 public 은 가끔 문제를 야기하는데 다음과 같은 경우를 생각해보자.

 

신입 개발자에게 게시판 만들기를 시킨다. 신입은 소스를 둘러보다가. 화면의 요청이 Controller의 .do와 맵핑되는것을 알았고, 데이터를 가져올 때 Repository를 사용하면 된다는것을 알았다. 이제 조회 기능을 테스트해보았더니 화면에 잘 뿌려서 게시판 만들기를 마무리 지었다.

위의 다이어그램에서보면 위의 신입이 저지른 행동은 Danger 라고 Text로 표기된 경로로 조회기능을 구현한것이다. CQRS 패턴을 사용할 경우 Danger 경로의 의존성은 사용하는 경우도 있지만, 그것이 아니라면 바람직한 방법은 아니다. 특히 조회로직에서도 특별한 인증이나 접근제어를 하는 경우라면 더욱 그렇다.

 

물론 신입을 교육시키면 어차피 위와 같은 현상은 한두번이고 교육을 하면 Service를 통해서 접근하게 해야 한다는걸 알텐데 굳이 신경써야할 문제인가?라고 생각할수도 있지만 코드 품질은 개발자마다 차이가 너무 크고 예외적인 인력이란 항상 있기 마련이다. 또 경험에 비추어볼때 "돌아만가면 그만"이라는 생각으로 코딩하는 개발자는 제대로 돌아가게 코딩을 하거나 테스트하지도 않는다.

코드정책을 적용하는데 가장 좋은것은 컴파일러에 의한 정책이다. 위의 다이어그램은 컴포넌트 기반의 패키지구조를 표현한것인데 주문과 관련된 무엇인가를 해야할때에는 OrdersComponent 로만 접근한다. 업무로직과 영속성계층이 분리되어있고, 외부에서는 Orders의 영속성으로 직접 접근도 불가능하다.

 

public interface OrdersComponent {

	Order findOrder(OrderDto orderDto);
	
	List<Order> findOrders(OrderDto orderDto);
	
	void saveOrder(Order order);
}

interface OrdersRepository {

	Order findOrder();
	
	void saveOrder(Order order);
	
	void updateOrder(Order order);
	
	void removeOrder(Order order);
}

 

위의 예제코드에서 패키지 외부에 공개해야하는 OrdersComponent의 접근제어자를 public으로 하고, OrdersRepository 접근제어자를 package로 설정하면 아래와 같이 web 계층에서 접근 할 수 없다. 반면 같은 패키지내에 있는 OrdersComponentImpl 에서는 OrdersRepository에 접근이 가능하다.


- 결론

위에서 아래로 갈수록 점점 핵심업무규칙을 보호하는 패키지구조임을 나타낸다. 대규모 프로젝트에서는 이런 패키지구조를 결정할때에도 생각이 필요하다. 또한 상황에 따라 쓰는 도구가 다르듯이 무조건 옛날구조라서 해서 맹목적으로 무시할 필요는 없다. 듀토리얼이나 빠른 구현 및 확장가능성이 없는 간단한 프로그램이라면 계층기반구조로도 충분할 수 있기 때문이다.

 

또한 Interface 파일은 어차피 외부에 공개되는 경향이 있으니 접근제어자에 대해 생각해보지 않고 무조건 public 으로 하지 말고 정말 public 으로 설정을 해야하는지 생각을 해보길 바란다. 깊게 생각하다보면 더 좋은 패키지 구조를 생각하거나, 더 좋은 Class 리팩토링을 혹은 객체설계를 할 수 있을것이다.

댓글