Log4KJS
Community - Push 기반 소셜 네트워크 서비스 본문
[사용 기술]
- JAVA, Spring Boot, Spring Cloud, Spring Eureka, Kafka, MySql, JPA, Redis, Grpc, Cassandra
마이크로서비스간 통신에 구글에서 개발한 고성능 RPC 시스템인 gRPC 를 사용하였습니다.
MySQL 은 read-heavy 한 oltp 애플리케이션에서 뛰어난 성능을 보여주는 관계형 데이터베이스로, social, post, auth 서비스에서 엔티티를 저장하는 데에 JPA 와 함께 사용하였습니다.
Cassandra 는 고가용성을 보장하는 분산 데이터베이스로, 쓰기 부하에 아주 효과적입니다.
푸쉬 방식의 SNS 는 글이 하나 포스트되었을 때 많은 팔로워들의 피드에 게시글 아이디를 써야 합니다.
또, 이러한 피드는 업데이트 없이 INSERT 와 SELECT 만 이루어지므로 Cassandra 를 쓰기에 적합하다고 생각하였습니다.
Apache Kafka 는 로그 기반 메세지 브로커입니다. 파티션을 통한 분산처리, ISR 을 이용한 고가용성을 제공합니다.
MSA 간 비동기 메시지 교환에 사용하였습니다.
또, 네이버에서 개발한 오픈소스인 nGrinder 와 Pinpoint 를 사용하여 부하 테스트와 모니터링을 수행하였습니다.
[프로젝트 관심 사항]
- 스프링을 기반으로 다양한 기술 학습
- 먼저 공식 레퍼런스를 읽고 학습, 구현을 뜯어보며 이해
- Layered Architecture, 클린 아키텍처 등 소프트웨어 디자인 패턴 학습
- 지속적인 리팩토링을 통한 나쁜 냄새 제거
- ngrinder 와 pinpoint 를 이용한 부하 테스트
- Pipelining, 메세지 큐를 통한 비동기 처리 등 성능 개선
- MSA 에 대한 기본적인 지식 습득
- docker, kubernetes 의 기본적인 개념과 사용 방법 습득
- 멀티 모듈 프로젝트 구성 방법 학습
[구성]
소셜 네트워크 서비스의 구현 방법은 크게 Push 와 Pull 방식으로 나뉩니다. Pull 방식이란 유저가 피드를 조회 (읽기) 를 수행할 때마다 팔로워와 팔로워가 작성한 게시글들을 검색하는 방식이고, Push 방식은 반대로 유저가 게시글을 업로드할 때마다 팔로워들의 피드 저장소에 게시글을 발행하는 방법입니다. Pull 방식은 게시글 조회 시에 수행하는 연산이 많아 시간복잡도를 희생해야 하고, Push 방식은 유저별 타임라인 저장소에 게시글 인덱스를 저장해야 하므로 공간복잡도를 희생해야 합니다
이 중에서 저는 Push 아키텍처를 채택하여 프로젝트를 구성하였습니다.
-참조-
https://d2.naver.com/helloworld/551588
전체 아키텍처는 위와 같습니다.
[모듈 분리]
모듈을 어떻게 나눠야 하는지에 관한 문제는 항상 어렵습니다.
Community 에서는 Clean Architecture 에서 설명하는 의존성 규칙을 따르기 위해 노력하였습니다.
몇가지 예시를 들어 설명하겠습니다.
계층 결합 분리
관심사의 분리 측면에서 몇가지 분명한 것들이 있습니다.
인터페이스 (http, grpc 등) 과 업무 규칙 (domain rule) 은 서로 다른 이유로 변경됩니다.
또, 인프라스트럭쳐(외부 시스템과의 연결) 과 업무 규칙 또한 다른 이유로 변경됩니다.
따라서 인터페이스, 도메인, 인프라스트럭처 계층은 다른 모듈(또는 패키지) 로 분리되어야 합니다.
이를 Layered Architecture 라고 합니다.
여기서 한가지 더 주의깊게 보아야 할 것은 모듈간의 의존성의 방향입니다.
변경이 쉽지 않은, 변경으로부터 보호받아야 하는 컴포넌트가 변동이 예상되는 컴포넌트에 의존해서는 절대 안됩니다.
예를 들어, 만일 의존성의 방향이 Domain Layer -> Infrastructure Layer 라면 Infrastructure 의 변경이 Domain Layer 에 전파될 수 있습니다. Domain Layer 는 서비스의 업무 규칙, 즉 다른 두 계층과 비교했을 때 비교적 고수준의 정책을 포함하고 있습니다. 따라서 Domain Layer 는 다른 계층의 변경으로부터 보호받을 수 있어야 합니다.
또, 변경이 어려운 고수준 컴포넌트가 변동이 예상되는 저수준 컴포넌트에 의존하게 되면, 결국 저수준 컴포넌트도 변경이 어려워지게 되는 문제도 있습니다.
이런 문제를 해결하기 위해서는 도메인 계층에 인터페이스를 정의하고 InfraStructure Layer 의 구체 클래스가 이를 구현하게 하면 됩니다. 이렇게 하면 Domain Layer 는 Infrastructure Layer 를 알지 못하고, 따라서 Infrastructure Layer 의 변경이 Domain Layer 에 영향을 끼치지 못하게 됩니다. 따라서 도메인 계층은 저수준 계층의 변경으로부터 독립적일 수 있고, 저수준 계층은 고수준 계층에 영향을 끼치지 않고 자유롭게 구현을 변경할 수 있습니다. 즉, Infrastructure 계층이 도메인 계층의 플러그인이 되는 것입니다.
처리 흐름은 Domain Layer -> Infrastructure Layer 지만, 의존성의 방향은 그 반대입니다. 이렇듯 의존성의 방향을 처리 흐름과 반대 방향으로 뒤집는 것을 Dependency Inversion Principle (DIP) 라고 합니다.
의존성과 추상화 수준
Community 에서는 여러 api 모듈(http interface layer) 들이 공유하는 공통 관심사가 존재합니다.
바로 인가 (Authorization) 에 관한 관심사였습니다. 따라서 jwt 토큰을 이용해 인가를 수행하는
mvc-user-jwt 모듈을 만들었습니다.
처음에는 이러한 구조로 의존성을 구성하였습니다. 하지만, 이러한 구조에는 문제가 있었는데,
api 모듈들과 mvc-user-jwt 모듈 모두 변동성이 큰 모듈이라는 점입니다.
api -> mvc-user-jwt 로 의존성이 존재하므로, 예를 들어 토큰에서 세션으로 유저의 인증 방식이 변경되면 모든
api 모듈이 수정되어야 합니다.
그렇다고 해서 mvc-user-jwt -> api 로 의존성을 역전시켜도 문제는 사라지지 않습니다. api 모듈 역시 변동성이 크기 때문에 api 모듈의 변경이 인증 모듈에 영향을 끼칠 가능성이 높아집니다. 이는 우리가 원하는 바가 아닙니다.
또, 인증 모듈은 여러 api 모듈에서 재사용될 수 있어야 하는데,
위와 같은 의존성 방향에서는 새롭게 추가된 api 모듈에서 인증 모듈을 재사용하기 어렵습니다.
이러한 문제는 추상 모듈을 이용해 해결할 수 있었습니다.
추상 모듈은, 인터페이스와 추상클래스만을 (혹은 많은 부분을) 포함하는 모듈입니다.
이러한 모듈은 추상화 정도가 매우 높기 때문에 변동 가능성이 적어 안정적입니다. (당연하게도, 구현 클래스보다 인터페이스는 잘 바뀌지 않고, 바뀌어서는 안되기 때문입니다).
이런 안정적인 모듈은 다른 모듈에서 의존성을 가지기 매우 좋습니다. 높은 안정성과 낮은 변동 가능성은 변경을 의존하는 모듈로 전파할 가능성이 낮기 때문입니다.
더 나아가 정리해보면 여러 모듈들이 의존하는, 안정적인 모듈은 추상화 정도가 높아야 합니다. 그래야 해당 모듈에 의존하는 다른 모듈들에게 변경을 전파하지 않고도 구현을 자유롭게 바꿀 수 있습니다.
위 예시에서, mvc-user-api 라는 추상 모듈을 중간에 끼워 넣으면, 여러 api 모듈에서 추상 모듈에 의존성을 가지고 재사용할 수 있고, 유저의 인증 방식이 바뀌더라도 메인 모듈에서 구현체를 jwt <-> session 으로 바꾸어 주입해 주면 됩니다.
Github. https://github.com/IceMelon404/Community.git
'Project' 카테고리의 다른 글
Redis pipelining (feat. Spring + Lettuce) (0) | 2022.02.19 |
---|---|
Grpc NameResolver + Eureka Service Discovery (0) | 2022.02.17 |
Spring Kafka 의 Async ack 활용 (0) | 2022.02.11 |
Cachy 의 스레딩 모델과 성능 개선 (0) | 2022.02.05 |
Cachy - 로그 구조화 key-value 저장소 (1) | 2021.12.18 |