이 글은 우리FISA 1기 굿프렌즈팀의 기술 블로그에 게시된 글 입니다.
굿프렌즈팀은 프로젝트를 진행하면서 어떠한 패키지를 구성하면 좋을지 고민했습니다. 보통 패키지를 구조를 나누는 방법으로 대표적인 패키지 구조인 계층별
, 기능별
이 있습니다.
계층형 패키지 구조
계층형 구조는 각 계층을 대표하는 디렉터리를 기준으로 코드들이 구성됩니다. 계층형 구조의 장점은 해당 프로젝트에 이해가 상대적으로 낮아도 전체적인 구조를 빠르게 파악할 수 있다는 장점이 있습니다.
하지만 에플리케이션에 새로운 기능이 추가되고 고도화될수록 클래스들이 너무 많아져서 구분이 어려워지는 단점이 있습니다.
기능별 패키지 구조
기능별로 패키지를 나눠서 구성합니다. 기능별 패키지 구조의 장점은 해당 도메인에 관련된 코드들이 응집되어 있고, 이는 지역성의 원칙
을 잘 지킬 수 있습니다.
컴퓨터 과학에서, 참조의 지역성, 또는 지역성의 원칙 이란 프로세서가 짧은 시간 동안 동일한 메모리 공간에 반복적으로 접근하는 경향을 의미한다. 참조 지역성엔 두 종류가 있는데, 시간적 지역성과 공간적 지역성이다. 시간적 지역성이란 특정 데이터 또는 리소스가 짧은 시간 내에 반복적으로 사용되는 것을 가리킨다.
공간적 지역성이란 상대적으로 가까운 저장 공간에 있는 데이터 요소들이 사용되는 것을 가리킨다.
공간적 지역성의 특수한 경우인 순차적 지역성은 배열의 요소를 순회할 때와 같이 데이터 요소들이 선형적으로 배열되어 있고 접근될 때 발생한다.
기능별 패키지 구조의 또다른 장점은 연관된 코드가 모두 같은 폴더에 존재하므로 추후 기능을 간단하게 삭제, 또는 별도 패키지로 추출할 수 있습니다. 그뿐만 아니라, 각 기능 폴더의 최상단을 기능 내부를 추상화하는 인터페이스처럼 사용해, 기능 내에서만 쓰일 코드는 감추고 외부서 접근해야 할 코드만 노출하는 식의 추상화를 구현하기도 훨씬 쉽습니다. 물론 기능별 패키지의 단점도 존재합니다. 각 계층이 기능별로 모여있기 때문에 프로젝트에 대한 이해도가 낮으면 전체 구조를 파악하는데 시간이 오래 걸립니다.
굿프렌즈팀은 프론트엔드 2명, 백엔드 2명으로 구성되었으며, 프로젝트 기간 동안 매일 다 같이 모여있는 시간이 많습니다. 그리고 프로젝트의 서비스 기획부터 개발까지 팀원 모두가 참여하기 때문에 프로젝트에 대한 이해도가 높아서 기능별 패키지 구조
를 선택하게 되었습니다.
DDD 계층 구조
DDD의 계층 구조(Layered Archtecture), 말 그대로 계층이 나뉘어 있는 아키텍처를 활용하여 기능별 패키지를 구성했습니다. Layered Architecture의 주된 목표는 “각각의 Layer는 하나의 관심사에만 집중”할 수 있도록 하는 것입니다.
일반적으로 3 계층 또는 4 계층으로 나누어 사용합니다.
Layered Architecture에서 지켜야 할 두 가지 규칙은 아래와 같습니다.
- 위의 계층에서 아래 계층에는 접근이 가능하지만 아래에서 위로는 불가능한 것을 기본으로 한다.
- 한 계층의 관심사와 관련된 그 어떤 것도 다른 계층에 배치되어서는 안 된다.
결론적으로 말하면, 굿프렌즈팀은 DDD의 계층 구조에 있는 Layer들을 활용하여 기능별로 패키지를 구성했습니다.
DDD의 계층 구조를 완벽히 이해하고 적용하는 것도 좋지만, 팀원 모두가 이해를 해야 것이 중요했고 기간 안에 주어진 요구사항들을 모두 구현하는 것이 더 중요했습니다.
그래서 팀원들과 논의한 결과, ‘DDD의 계층 구조에 있는 Layer들을 팀에 맞게 적용하면서 기능별 패키지로 가져가보자‘라는 결론이 나왔습니다.
굿프렌즈팀의 기능별 패키지 구조
백엔드 패키지 구조를 기능별로 분리했고, DDD의 4 계층 구조를 아래와 같이 활용했습니다. (Presentation-Application-Domain-Infrastructure)
Application(애플리케이션)
비즈니스 로직을 정의하고 정상적으로 수행될 수 있도록 domain 패키지와 infrastructure 패키지를 연결해 주는 역할을 하는 계층입니다.
많은 정보를 가지고 있지 않게 유지하는 것이 중요하며, 실질적인 데이터의 상태 변화 등의 처리는 도메인 계층에서 진행할 수 있도록 위임합니다.
Presentation(표현)(=Controller)
사용자 요청에 대해 해석하고 응답하는 일을 책임지는 계층입니다.
사용자에게 UI를 제공하거나 클라이언트에 응답을 다시 보내는 역할을 하는 모든 클래스가 포함됩니다.
주로 클라이언트로부터 request를 받고 response를 return 하는 API를 정의하는 클래스가 있습니다.
Domain(도메인)(=Model)
비즈니스 규칙, 정보에 대한 실질적인 도메인에 대한 정보를 가지고 있으며 이 모든 것을 책임지는 계층입니다.
Entity를 활용하여 실제 애플리케이션의 핵심이 되는 도메인 로직이 모여 있습니다.
그렇기 때문에 외부와의 의존성을 최소화해야 합니다. 외부의 변경에 의해 도메인 내부가 변경되는 것을 막아야 한다는 것을 인지해야 합니다.
Infrastructure Layer(인프라 계층)(=Repository)
infrastructure
패키지는 외부와의 통신을 담당하는 로직들이 담겨 있습니다.
이번 프로젝트에서는 OAuth
를 활용한 회원 관리(소셜 로그인/회원가입)를 진행하기 때문에 Google의 인증 서버와 통신이 필요합니다.
따라서 이 패키지에는 저희 팀의 의지와 다르게 외부의 변화에 따라 변경될 여지를 가지고 있습니다. 변화에 매우 취약한 구조이며 외부 서버에 의존적이기 때문에 항시 변화에 대응할 수 있도록 대비해야 합니다.
즉, 도메인 패키지에서 인프라 패키지을 직접적으로 의존하는 것이 도메인 로직을 안전하게 지킬 수 없다는 의미를 내포합니다.
그 밖에 굿프렌즈팀의 기능별 패키지 구조에 필요한 패키지들이 있습니다.
Global
global
패키지는 프로젝트 전반에 사용하는 객체로 구성됩니다.
공통적으로 사용하는 dto나 error, config, common(BaseTimeEntity)와 같은 것들이 모여있습니다.
Exception
exception
패키지는 각 기능과 관련된 예외에 대해 커스텀한 클래스들이 모여있습니다.
Dto
application
또는 presentation
패키지에서 사용할 때 필요한 객체들이 모여있습니다.
Data Transfer Object의 약자로 계층 간 데이터 전송을 위해 도메인 모델 대신 사용되는 객체입니다. dto는 어떠한 비즈니스 로직을 가져서는 안 되며 저장, 검색, 직렬화, 역직렬화 로직만을 가지도록 했습니다.
결론
계층형, 기능형 패키지 구조 중 어떤 것이든 사실 장단점은 존재하고 정답은 없다고 생각합니다. 현재 프로젝트의 규모와 요구사항을 모두 고려해서 선택했고, 무엇보다 팀원 모두가 이해해야 한다는 점이 중요했습니다.다만, 선택한 패키지 구조에 대해서 객체와 패키지 사이의 의존성에 대해 충분히 고민해봐야 합니다.
현재 굿프렌즈의 주어진 기간과 요구사항만 봤을 때는 기능별 패키지가 더 적합하다고 생각합니다. 지금은 문제가 없지만, 추후에 새로운 기능이 추가되어 패키지 구조를 바꿔야 하는 상황이 오거나 이보다 더 나은 방법이 있다면, 그때 가서 포스팅을 적도록 하겠습니다.
지금까지 읽어주셔서 감사합니다.