서비스를 구체화 하는 방법
(Reducing Complexity in Service)
이 아티클에서는 “에너지 현황 리포트”라는 요구사항에 대해서
서비스를 구체화하는 과정을 공유합니다.요구사항에 따라 구현된 제품은 동일한 기능을 서비스합니다.
하지만 고민과 철학에 따라 만들어진 제품의 모습이 달라지게 됩니다.
Background
환경과 사회공원, 기업의 투명 경영을 강조한 ESG 경영이 전 세계적인 화두로 부각되고 있는 가운데 에너지 사용량 절감은 ESG 경영 강화 일환 중 하나 입니다.
사물 인터넷(IoT, Internet of Things)을 이용하여 불필요하게 사용하는 에너지의 소비를 감시하고 줄이는 기술은 스마트 에너지 세이빙 중에 하나로 이를 통해 에너지 사용량을 확인하고 절감할 수 있습니다.야놀자클라우드의 GRMS(Guest Room Management System) 솔루션은 호텔 객실에 설치된 IoT 장치를 제어하고 관리하는 서비스입니다.
야놀자클라우드의 GRMS 솔루션을 이용하여 에너지의 사용량을 확인할 수 있는 “에너지 현황 리포트”를 개발해야 합니다.
GOAL
에너지 현황 리포트는 다음의 기능을 제공해야 합니다.
- 에너지(전등, 온도조절기 등)를 일별 사용량을 확인할 수 있으며, 기간 및 객실 별 사용 현황을 비교할 수 있다.
- 재실 센서를 통해 재실 상태를 구분할 수 있으며, 공실시 불필요한 에너지가 발생하는 지 확인할 수 있다.
- 객실의 체크인/아웃 상태를 통해 고객의 점유 상태를 확인할 수 있으며, 불필요한 에너지가 발생하는 지 확인할 수 있다.
REQUIREMENT DESIGN
도메인 전문가에 의해서 요구사항이 정의되고 구현 여부에 대한 검토 뒤에
설계와 개발이 진행됩니다. 이 과정에서 요구사항과 엔지니어마다 구체화해가는 과정과 방식은 다양합니다.
저는 “에너지 현황 리포트”에 대해서 다음과 같이 구체화 하였습니다.
구조 — 프로토콜 — 처리방식 — 동작 — 저장소 — 구현 — 테스트 — 배포
구체화해가는 과정에서의 중요한 점은
목표에 대한 일관된 방향성과 문제의 범위를 줄여나가는데 있습니다.
ARCHITECTURE
서비스가 구현될 GRMS 서비스는 다음과 같이 구분할 수 있습니다.
- 장치를 제어하고 장치의 상태를 관리하는 Device 관련된 영역
- 객실에 대한 정보와 고객에 의해 체크인/체크아웃을 처리하는 Space에 관련된 영역
리포트를 작성하기 위해서는 에너지 사용 이벤트와 재실 센서와 같은 이벤트를 Device로부터 받아야합니다. 그리고 객실의 체크인/아웃 상태를 확인하기 위해 Space로부터 객실의 이벤트를 받아야 합니다.
Protocol
HTTP 와 같은 동기 프로토콜을 사용할 경우 아래와 같이 구성할 수 있습니다.
Report 에서는 Device Event와 Space Event를 처리하기 위한 엔드포인트를 노출하고 발생하는 서비스들에서 호출되기를 기대합니다.
Device와 Space에서는 Report 에서 제공하는 API 포맷에 맞춰서 호출하도록 추가함으로써 동작합니다.
하지만 이로써 Device와 Space는 Report와 의존 관계가 발생하게 되고, Report의 API가 변경되거나 장애가 발생할 때마다 영향을 받게 됩니다.
Device와 Service가 Report에 “명령”을 전달하는 형태입니다.
이러한 서비스간에 발생하는 의존성에 대해서 Event-Driven을 통해서 의존성의 레벨을 낮출 수 있습니다.
Event-Driven은 서비스간 연결을 “이벤트”로 처리함으로써 느슨한 연결 구조로 처리하는 것을 의미합니다. 이벤트는 다른 서비스에 전달되는 명령이 아닌 “완료된 결과”로만 사용합니다.
“명령”과 “결과”의 차이는 이벤트의 소유권을 결정하게 됩니다.이 구조의 특징은 Message Queue와 결합하여 “비동기적”이고 “내결함성”을 갖게 되며, 일반적인 Http나 gRPC와 같은 프로토콜과 다르게 의존성이 역전되는 형태를 볼 수 있습니다.
Event 구조 일 경우 다음과 같이 구성할 수 있습니다.
Device와 Space는 처리 상태에 따라 Event를 발행합니다.
이 이벤트들을 Report가 구독함으로써 구현할 수 있습니다.
각 서비스는 자신의 이벤트 발행에만 관여하기 때문에 의존 관계는 느슨하며, Report의 변경이나 서비스들의 장애는 영향을 끼치지 않습니다.
Device나 Service에서 발행하는 Event에 Report가 의존되기 때문에 Http 프로토콜과 반대로 의존성이 역전됩니다.
Report에서 Device와 Space의 이벤트를 “구독”하여 처리하는 형태로 정의할 수 있습니다.
물론 이러한 Event-Driven 방식 또한 Message Queue에 대한 높은 의존성을 갖는 문제와 상대적으로 개발 난이도가 높고 전체적인 확인이 어려운 단점등이 존재합니다.
이번에는 AWS SQS를 통해 Event를 사용하도록 결정하였습니다.
Processing
전체적인 구조에 대해 결정됨에 따라 세부 구성에 대해서 고민을 해봅시다.
Device와 Space로부터 변경된 정보를 Event로 받고 있습니다.
Report를 만들기 위한 기본 재료는 갖춰진 상태입니다.
이러한 형태의 작업은 크게 배치 작업(Batch)과 실시간 처리(Real-Time) 방식 중에 고민해볼 수 있을 것입니다.
배치는 비교적 단순한 대신에 사용량의 증가에 따라 처리 시간이 리니어하게 증가하는 문제와 배치가 실패했을때에 대한 관리 문제가 있을 것입니다.
실시간은 사용량의 증가에도 크게 영향을 받지 않은 대신 상대적으로 복잡한 구현 문제가 있을 것입니다.
집계 결과가 IoT 장치의 사용시간이 재실 센서와 같은 다른 IoT 기기의 감지 시간을 참조하여야 하기 때문에 배치와 실시간 두가지 형태를 혼합하여 장치 별로 사용 시간은 실시간으로 관리하고, 이러한 사용 이력은 배치형태로 관리하도록 하였습니다.
각 IoT 기기 별로 사용시간을 관리하기 위해서는 시작 이벤트와 종료 이벤트 한쌍이 필요합니다.
각 변경사항에 대한 정보는Event로 전달받기 때문에 비순차적으로 동작할 수 있다는 특성이 존재합니다. 짧은 시간에 시작과 종료가 발생할 때 서버에 종료 이벤트가 시작 이벤트보다 먼저 도달할 수 있음을 의미합니다.
환경적인 특성으로 시작 또는 종료 이벤트가 유실 될 수도 있습니다.
ON → ON → OFF 와 같이 종료 이벤트가 유실되거나,
OFF → ON → OFF 와 같이 시작 이벤트가 유실 될 수 도 있을 것입니다.
이러한 문제를 해결하고 나면 IoT 기기 별로 기록된 사용시간을 통해 하루단위로 모아서 집계한다면 원하는 리포트를 생성할 수 있을 것입니다.
리포트를 생성을 위한 정보가 이력 정보로 단순화되었습니다.
이러한 과정을 통해 배치 작업을 단순화 할 수 있고, Validation이나 다양한 Exception을 처리할 수 있을 것입니다.
다른 문제가 있는지 더 살펴봅시다.
객실의 상태를 관리하기 위해 체크인과 체크아웃을 객실 이벤트로 구독하고 있지만, 연박의 경우 체크인 상태이지만 해당일에는 객실 이벤트가 발생하지 않기 때문에 이력으로만 조회하기 위해서는 별도로 처리가 필요합니다.
사용중인 IoT 기기 또한 일단위로 사용량을 관리하기 위해서는 자정을 기준으로 시간을 구분하여야 합니다.
이를 위해 일 마감이라는 개념을 추가하였으며, 일 마감을 통해 전일의 객실 상태와 사용중인 IoT 기기의 사용량을 관리할 수 있도록 하였습니다.
리포트를 생성하는 시점과 일 마감 시점이 같기 때문에
배치 프로세스에 리포트 생성 전에 일마감을 진행하도록 하였습니다.
각각에 대한 슈도코드를 작성해봅시다.
슈도코드(pseudocode)는 설계한 내용의 문제를 확인할 수 있는 간단한 방법중에 하나입니다.
설계상의 논리적인 문제를 미리 확인해볼수 있을 뿐만 아니라 미리 짜여진 슈도코드를 통해 좋은 코드가 만들어질 수 있도록 합니다. 테스트 케이스를 작성하기에도 더 쉬워지게 되면서 튼튼한 코드를 작성할 수 있습니다.
실시간 이벤트 처리
장치나 객실 이벤트가 발생하면 요약 정보에 정보를 기록하고, 시작시간-종료시간 쌍을 완성한 경우 사용 시간으로 남기도록 하였습니다
배치
- 체크인과 같은 연속된 상태나 사용중인 IoT 기기에 대해서 자정을 기준으로 사용시간을 정리함으로써 이력이 일단위로 관리될 수 있게 합니다.
- 리포트는 이력을 통해 작성하여 이력을 통해 역 추적 하거나 장애가 관리될 수 있도록 합니다.
- 객실마다 일별로 생성되는 리포트는 데이터로서 이를 가공해서 정보를 제공하게 됩니다.
Store
서비스에 대한 설계가 마무리가 되었습니다.
그럼 이러한 데이터를 보관할 저장소에 대해서 고민을 해봅시다.
처음 시작하는 서비스라면 자신의 서비스에 맞는 저장소에 대한 고민을 하겠지만, 비용적인 측면으로 인하여 사용중인 자원에서 선택하게 됩니다.
서비스가 커지기 전까지는 논리적으로는 분리하더라도 기존의 저장소의 일부 공간을 이용하여 물리적으로 공유하여 비용을 줄일 수 있을 것입니다.
야놀자클라우드의 GRMS는 AWS 위에서 서비스하고 있으며, RDS(AuroraDB)와 DynamoDB, ElasticSearch를 저장소로 사용하고 있습니다.
장치 또는 객실의 이벤트는 비동기적으로 발생할 수 있기 때문에 동시성 문제에 대해서 고민해야합니다.
또한 생성된 리포트와 이력은 각각 일정 기간의 데이터만 보관하고 삭제될 수 있어야 합니다.
RDS(MySQL)을 사용한다면 RDBMS에서 제공하는 Transaction을 통해 동시에 처리되는 이벤트들의 동시성을 관리할 수 있을 것입니다. 다만 검색을 위해 RDS보다는 ElasticSearch를 사용하는 게 더 나은 선택 일 수 있습니다.
Document DB(MongoDB)를 사용한다면 Transaction을 대신할 방안이 필요합니다. RDS + ElasticSearch를 사용하는 대신 Document DB 만으로 관리할 수 있을 것입니다.
최종적으로 관리의 편의성과 AWS 사용 비용을 고려하여 새로운 리소스를 사용하지만 Document DB를 사용하도록 결정하였습니다.
4.x부터는 MongoDB에서도 Transaction을 제공하고 있지만, 사용하는 Document DB가 3.6임에 따라 동시성 제어를 위해 Lock을 구현해봅시다.
별도의 로직을 구성하지 않더라도 동시성 제어가 필요한 부분은 유의하시는게 좋습니다.
동시성 제어가 필요한 부분은 장치와 객실 이벤트가 동시에 발생하더라도 순차적으로 처리를 위한 부분과 배치작업이 다수의 노드중에 하나만 실행되도록 하는 부분입니다.
순차적 처리를 위해 요약정보(Summary) 도큐먼트를 이용 SpinLock을 통해 구현할 수 있을 것입니다.
MongoDB의 findOneAndUpdate의 Atomic 특성을 이용하여 Lock을 구현할 수 있습니다.
다수의 노드 중에 하나의 노드에서만 배치를 처리하기 위해서 DistLock을 구현해봅시다. 순차적으로 증가하는 JobNo를 기준으로 항상 큰 JobNo만 수행할 수 있도록 하여, 동일한 JobNo에 대해서 한번만 수행할 수 있도록 합니다.
작업이 실패했다면 SpinLock과 다르게 endTransaction이 아닌 Rollback으로 다음 주기에 완료되지 않은 작업이 다시 시작될 수 있도록 하였습니다.
스케쥴러의 간격을 조정하고 같은 JobNo를 사용하도록 한다면, 배치 작업이 실패하더라도 다음 주기에 재 시도 될 수 있을 것입니다.
각 데이터의 생명 주기는 MongoDB에서 제공하는 TTL Index를 통해 관리하도록 하였습니다.
어떤 언어를 사용하는지에 따라 관련된 패키지가 달라질 수 있을 것입니다.
물론, 높은 성능이 필요하거나 제공하려는 기능이 언어적 특성이 필요하다면
어떤 언어를 선택할지와 사용할 프레임워크(Framework)에 대해서 고민할 필요가 있습니다.GRMS를 구성하는 서비스들은 대다수가 Nodejs로 작성되어 있으며,
hapiJs와 TypeScript기반의 Nestjs로 구성되어 있습니다.Module 단위로 관리가 쉬운 Nestjs(Typescript)로 구현함에 따라
Nodejs(Typescript)를 사용하였습니다.
ORM
새로운 저장소가 추가되었으니 어떤 ORM을 사용할지 고민이 필요합니다.
총 5개의 ORM을 검토하였으며, 최종적으로 TypeORM과 Prisma2 중에 안정성에 조금 더 나은 TypeORM을 선택하게 되었습니다.
레퍼런스에 유리하고 Prisma1에서 Prisma2로 전환되었던 상황등을 고려하게 되었습니다.
다만 공식문서에서 제공하는 0.2가 아닌 최신 릴리즈 버전인 0.36을 사용하고자 했습니다. 개발 시점에서는 공식적으로 지원하지 않았지만 지금은 nestjs/typeorm 8.1.0이 release 되어 TypeOrm 0.3+를 공식적으로 제공하고 있습니다.
CODE & TEST
어떻게 서비스가 흘러갈지 알고 있고, 대다수의 로직은 슈도코드로 검증이 되었기 때문에 코드를 작성하는데 한결 빠르고 쉬워집니다.
테스트 코드 또한 어떤 Unit을 테스트 해야할지 Controller는 어떤 처리를 해야할지 알기 때문에 UnitTest와 E2E(End To End) Test 또한 쉽게 작성할 수 있습니다.
AWS SQS는 NestJS에서 제공하는 MicroService로 추가하여 각 Module의 Controller에서 이벤트 패턴에 따라 구독할 수 있도록 추가합니다.
Controller에서는 Event라고 해서 특별하게 다르지 않습니다.
Post 데코레이터가 MessagePattern 데코레이터로 바꼈을 뿐 동일합니다.
언젠가 Http 나 gRPC와 같은 프로토콜로 변경되더라도 Controller만 추가해주면 문제가 없을 것입니다. Layered Architecture가 좋은 점 입니다.
물론, Controller에 Service 로직이 구현되면 안되겠죠.
중요한 로직이나 우려스러운 케이스들은 유닛 테스트를 통해 관리할 수 있을 것입니다. 다양한 유닛 테스트는 개발 과정에서의 테스트 해볼수 있다는 점도 있지만, 앞으로 코드의 변경사항에 대해서 문제를 사전에 방지하는데 큰 도움이 될 것입니다. 전체적인 커버리지는 크게 관리하지 않지만 가급적이면 70% 이상은 유지할 수 있도록 하고 있습니다.
테스트를 위한 API를 별도로 제공하고 이를 통해 시나리오를 검증할 수도 있을 것입니다.
서비스가 완성되었습니다. :)
끝 맺으며
에너지 세이빙 리포트 작업에 대해서 기술 블로그 작성을 제안 받았을 때 어떤 내용을 전달해드리는게 좋을지 고민이 있었습니다.
많은 아티클에서 구조적인 고민이나 해결, 코드 레벨의 팁이나 모범 사례들을 다루고 있지만 실제 업무에서의 요구사항의 정의로부터 시작하여 구조적인 고민부터 코드 레벨의 고민까지 요구사항을 구체화해가는 과정도 의미가 있다고 생각했습니다.
긴 글을 읽어주셔서 감사합니다.