프로젝트 설계에 대해
개요
코드를 구조화하는 건 생각보다 어렵지 않다고 생각합니다. 하지만 생각보다 어렵기도 하죠. 그래서 사실 이걸 어떻게 표현해야할 지는 모르겠는데, 그냥 편하게 아키텍처를 만드는 것에서 프로젝트 구조를 짜는 것, 코드를 작성하는 것까지의 제 나름대로의 룰을 정리해보려고 합니다.
룰
모듈 혹은 서비스를 분리하세요.
모듈을 서비스와 동치해서 서술합니다.
아키텍처나 프로젝트 전반에서 한번에 거대한 문제를 해결하려고 하지 않습니다.
흔히 말하는 분할-정복 방식을 적용합니다. 각 문제를 분할하여 모듈화하고, 각 모듈을 독립적으로 개발 후 통합합니다.
이런 구조를 가지면 여러 레이어로 나누어진 모듈의 역할이 분리된 만큼 많은 이점이 생깁니다.
- 각 모듈의 R&R을 이해하기 쉽습니다.
- 각 모듈의 역할이 분리되어 있기 때문에, 각 모듈의 테스트가 용이합니다.
- 각 모듈에 추가 기능을 구현할 때, 다른 모듈에 영향을 미치지 않습니다.
- 각 모듈 간의 의존성이 낮아져, 모듈 간의 결합도가 낮아집니다.
- 필요에 따라 모듈을 재사용할 수 있습니다.
- 각 모듈을 독립적으로 배포할 수 있습니다.
- 장애가 발생했을 때, 특정 모듈의 문제임을 빠르게 파악할 수 있습니다.
- 각 모듈의 문제가 전체 시스템에 영향을 미치지 않습니다.
서비스 간 통신에 메시지 버스를 적극적으로 활용하세요.
각 서비스 간 통신에 여전히 메시지 버스는 유용한 도구이며 아키텍처입니다.
메시지 버스를 활용하면 다음 2가지 방법의 정보 교환이 가능합니다.
- 특정 토픽(혹은 서브젝트)를 통한 데이터 교환
- 특정 토픽(혹은 서브젝트)를 통한 시그널 교환 및 직접 연결
이 중 첫번째는 당연히 메시지 버스에 직접 큰 페이로드를 전달하는 방식입니다.
그리고 두번째 방식은 메시지 버스를 통해 각 서비스는 자신의 연결 정보를 전달하는 방식입니다.
이 방식은 다음 과정을 거칩니다.
- A 서비스 군은 특정 토픽(혹은 서브젝트)를 구독합니다.
- B 서비스 군은 A 서비스에 연결을 해야할 때, 해당 토픽(혹은 서브젝트)에 메시지를 요청합니다.
- 구독 중이던 A 서비스 중 하나가 메시지를 받아, B 서비스에 연결 정보를 전달합니다.
- B 서비스는 A 서비스에 직접 연결합니다. (TCP나 HTTP, gRPC 등)
이런 방식은 각 서비스 간의 결합도를 낮추고, 서비스 간의 통신을 효율적으로 할 수 있습니다.
모듈을 작성할 때 레이어를 구분하세요.
모듈을 작성할 때, 데이터의 흐름에 따른 레이어를 구분합니다.
레이어를 분리하게 되면, 다음 이점이 생깁니다.
- 각 레이어의 역할이 분리되어 있기 때문에, 각 레이어의 테스트가 용이합니다.
- 각 레이어의 R&R이 명확해지기 때문에, 각 레이어의 역할을 이해하기 쉽습니다.
- 각 레이어 간의 결합도가 낮아집니다. 하지만 필요에 따라 응집성은 높아집니다.
- 언제라도 같은 레이어 내의 다른 모듈로 교체할 수 있습니다.
- 데이터의 흐름에 따라 레이어를 구분하기에, 각 모듈의 선후 관계를 명확히 할 수 있습니다.
- 데이터의 흐름을 역행하는 참조를 미연에 방지하여 더 안전한 구조를 가질 수 있습니다.
하나의 모듈과 서비스는 가급적 하나의 역할을 수행하세요.
모듈과 서비스는 하나의 역할을 수행하는 것이 가장 이상적입니다.
만약 단일 모듈이나 서비스가 여러 역할을 수행하게 될 경우에, 다음과 같은 문제가 발생할 수 있습니다.
- 각 역할의 R&R이 명확하지 않아, 각 역할의 테스트가 어려워집니다.
- 각 역할의 의존성이 높아져, 각 역할 간의 결합도가 높아집니다. 이는 한 역할의 문제가 다른 역할에 영향을 미칠 수 있습니다.
- 각 역할의 역할이 분리되어 있지 않아, 각 역할의 역할을 이해하기 어려워집니다.
- 하나의 역할이 다른 역할에 영향을 미치지 않는다는 보장이 없어집니다. 장애가 전파될 수 있습니다.
어그리게이터를 적극적으로 활용하세요.
어그리게이터는 여러 모듈과 서비스에서 데이터를 수집하여, 하나의 데이터로 만드는 역할을 수행합니다.
이러한 어그리게이터는 위의 하나의 모듈과 서비스는 하나의 역할을 수행한다는 원칙을 지키게 해줍니다.
어그리게이터는 그 자체로 데이터를 수집하고 하나의 데이터로 만드는 역할을 수행하기 때문입니다.
이는 레이어를 나눈다는 개념에서도 우수한 이점을 가집니다.
같은 곳에서의 입력과 출력을 담당하는 기능은 같은 모듈에 작성하세요.
같은 곳에서의 입력과 출력을 담당하는 기능은 같은 모듈에 작성하는 것은 불필요한 실수와 소통의 오류를 줄일 수 있습니다.
예를 들어 DB나 레디스에 데이터를 저장하고, 그 데이터를 읽어오는 기능을 생각해봅시다.
만약 서로 다른 모듈에서 쓰기 기능과 읽기 기능을 작성한다면, 다음과 같은 문제가 발생할 수 있습니다.
- 쓰기 기능과 읽기 기능이 서로 다른 모듈에 작성되어 있기 때문에, 서로 다른 모듈 간의 스키마 혹은 데이터 형식에 대한 일치가 필요합니다.
- 쓰기 기능과 읽기 기능이 서로 다른 모듈에 작성되어 있기 때문에, 서로 다른 모듈 간의 테스트가 어려워집니다.
- 쓰기 기능이나 읽기 기능 중 하나가 추가 및 삭제, 변경 사항이 발생했을 때, 다른 곳에 영향을 미칠 수 있습니다.
이러한 문제를 방지하기 위해, 같은 곳에서의 입력과 출력을 담당하는 기능은 같은 모듈에 작성합니다.
패러미터는 가급적 구조체나 클래스로 전달하세요.
패러미터는 가급적 구조체나 클래스로 전달하는 것이 좋습니다.
만약 전통적으로 함수의 인자에 여러 데이터 타입을 나열하는 방식으로 전달한다면, 다음과 같은 문제가 발생할 수 있습니다.
- 함수의 인자가 많아질수록, 함수의 호출이 복잡해집니다.
- 함수의 시그니처가 불가피하게 변경되면, 함수를 호출하는 모든 곳에서 변경이 필요합니다.
이러한 문제를 방지하기 위해, 패러미터는 가급적 구조체나 클래스로 전달합니다.
이는 주로 환경 설정이나, 컴파일 타임 의존성 주입같은 부분에서 유용합니다.
반응형 프로그래밍을 적극적으로 활용하세요.
반응형 프로그래밍은 데이터의 흐름을 이벤트 스트림으로 처리하는 프로그래밍 패러다임입니다.
하지만 이는 단순히 프로그래밍 패러다임을 넘어, 아키텍처에도 적용할 수 있습니다.
- 데이터의 흐름을 이벤트 스트림으로 처리하기 때문에, 데이터의 흐름을 추적하기 쉽습니다.
- 필요할 때만 데이터를 처리하기 때문에, 불필요한 데이터 처리를 줄일 수 있습니다.
- 그때그때 데이터를 처리하기 때문에, 데이터의 처리 속도가 빨라집니다.
이를 효율적으로 사용하는 좋은 방법은 채널(혹은 메일박스), 메시지큐, 이벤트 버스 등을 사용하는 것입니다.
TTL을 적극적으로 활용하세요.
TTL은 Time To Live의 약자로, 데이터의 유효 시간을 의미합니다.
모든 데이터는 영원히 유효할 수는 없습니다.
데이터의 유효 시간을 설정하면, 다음과 같은 이점이 생깁니다.
- 불필요한 데이터를 줄일 수 있습니다.
- 피치 못할 사유로 인해 데이터가 무한정 쌓이는 것을 방지할 수 있습니다.
- 불필요한 데이터를 처리하지 않아도 되기 때문에, 전반적인 성능 향상을 기대할 수 있습니다.
모든 동작에 timeout과 delay를 적용하세요.
모든 동작에 timeout과 delay를 적용하는 것은 매우 중요합니다.
만약 timeout과 delay를 적용하지 않는다면, 다음과 같은 문제가 발생할 수 있습니다.
- 네트워크나 I/O 등의 문제로 인해, 동작이 무한정 지연될 수 있습니다.
- 필요 이상으로 오래 걸리는 연산으로 인해 데드락이 발생할 수 있습니다.
- 불필요한 요청을 지속적으로 보내는 것을 방지할 수 있습니다.
기능을 수정하기보다 추가하세요.
기능을 수정하는 것보다 추가하는 것이 더 좋습니다.
기능을 수정하게 되면, 기존에 작성된 코드에 영향을 미칠 수 있습니다.
하지만 기능을 추가하게 되면, 기존에 작성된 코드에 영향을 미치지 않습니다.
이는 다음과 같은 이점을 가집니다.
- 기존 코드의 안정성을 보장할 수 있습니다.
- 기존에 검증된 테스트를 다시 검증할 필요가 없습니다.
- 기존에 작성된 코드를 수정할 필요가 없습니다.
함수나 변수 이름이 정책을 나타내도록 작성하세요.
함수나 변수 이름이 정책을 나타내도록 작성하는 것은 매우 중요합니다.
만약 함수나 변수 이름이 정책을 나타내지 않는다면, 우리는 그것을 파악하기 위해 히스토리를 찾아야 합니다.
하지만 함수나 변수 이름이 정책을 나타내도록 작성한다면, 우리는 그것을 파악하기 위해 이름만 보면 됩니다.
예를 들어, ageUnder18
이라는 변수가 있다고 가정해봅시다.
이 변수는 18세 미만의 나이를 나타내는 변수일 것입니다.
이름을 보고 18세 미만의 나이를 나타낸다는 것을 알 수 있습니다만, 왜 존재하는 지는 알 수 없습니다.
하지만 이름이 isAdult
라면, 성인인지 아닌지를 파악하기 위한 함수임을 알 수 있습니다.