Corca Medium 아카이브

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사

2022-10-12한충환 (MLOps Engineer)

다들 식사하시나요?

밥에 진심인 코르카 medium.com 지난 포스팅에서 소개한 코르카의 bob봇을 기억하시나요?

코르카의 명물 bob봇의 탄생기와 변천사를 소개합니다.

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사 본문 이미지

#bob 채널 코르카 사람들은 밥에 진심이다.

삼성역 부근에 위치한 회사 근처의 맛집이 많을 뿐만 아니라, 15000원의 넉넉한 식대로 든든하게 배를 채울 수 있기 때문이다.

그리하여 슬랙에 채널을 만들어 메뉴를 의논하고 식사팀을 만들어 나가게 되었다

“#bob” .

문제는 매일 반복되는 “오늘 돈까스 드실 분 계신가요?”

“저요!”

“저두요~” 등의 과정이 지나치게 귀찮았다는 것이다.

슬랙웹훅 간단하게 정해진 인원을 가지고 랜덤으로 조를 나눠주는 봇 그래서 우리는 을 이용해, 을 만들게 되 었다.

파이썬 스크립트 한 장으로 이루어진 작고 귀여운 봇이었다.

초기의 밥봇 그러나 초기의 밥봇은 여러가지로 부족한 점이 많았다.

우선 자체적으로 DB가 있는 것이 아니기 때문에 팀원들의 이름을 하드코딩해야 하는 문제가 있었다.

하드코딩 된 전체 팀원에서, 점심을 먹지 않는다고 표시한 사람을 수동으로 제외하고 조를 짜는 방식이었다.

새로운 분이 입사하셨는데 미처 코드를 수정하지 않아 그 분이 아무 조에도 포함되지 못하는 민망한 상황이 종종 발생하곤 했다.

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사 본문 이미지

사실 내 얘기였다

또한 확장성이 부족했다.

OOP 보다는 스크립트 형식으로 작성되어 구조랄게 없었고, 새로운 기능을 작성할 때 재사용할 수 있는 코드 가 거의 없었다.

가장 큰 문제는 이 아닌 으로 구현되었다는 것이었다

bot webhook .

webhook을 이용하면 http를 쏴서 채널에 메세지를 아주 쉽게 보낼 수 있지만, 그 이상을 하기가 쉽지 않다.

때문에 우리의 밥봇은 인터렉션이 별로 없는 무뚝뚝한 봇이 되었다.

문제 정의 이로써 문제가 정의되었다.

(솔직히 이걸 처음에 만들 때는 이렇게까지 일을 크게 벌일 생각이 없었기 때문에 이런 식으로 문제를 정의하 고 UML을 그리고 테스트 케이스를 짜고 그런 프로세스를 거치진 않았지만 지금 돌이켜 생각해보니 보이는 문제들이다)

게 컨트리뷰션하실 수 있도록 해야한다.

본격적인 개발

NestJS

NestJS로 봇을 구현해야겠다는 결정을 내리기까지는 많은 시간이 필요하지 않았다.

가 꽤 크다보니 DB 연결을 위해 따로 작업할 것이 별로 많지 않다.

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사 본문 이미지

청은 일반적인 HTTP 요청과 결이 다르다. path가 하나로만 들어오고 모든 정보가 post body에 포함되어 있기 때문이다. 때문에 이를 좀 더 잘 핸들링하기 위해 커스텀 컨트롤러를 만드는 것이 좋을 것 같았다.

NestJS는 컨트롤러를 포함, 모든 것이 모듈과 프로바이더 단위로 관리되고, DI 패턴으로 결합이 매우 쉽 기 때문에 커스텀 컨트롤러를 제작하고 사용하기에 매우 적합해 보였다. 사실 이 부분이 NestJS를 선택한 가장 큰 이유이기도 하다.

기에도 적합하다. 팀원분들이 본인이 원하는 기능을 모듈로 만들어 봇에 추가할 수 있다면 재밌을 것 같 았다. (그리고 실제로 그렇게 되었다.)

다.

SlackEventListener

우선 슬랙 이벤트와 인터렉션을 subscribe할 컴포넌트를 만들어야 했다.

위에서 언급한 것처럼, 슬랙 이벤트/인터렉션 요청을 핸들링하는 컨트롤러를 더 자연스럽고 편하게 개발할 수 있도록 데코레이터 내지는 모듈을 만들어야 했다.

그렇지 않으면, 모든 이벤트를 하나의 컨트롤러 메소드로 받아서 그 안에 수많은 분기를 만들고 요청에 맞는 서비스로 넘겨야 한다.

이건 말이 안 된다고 생각했고, 그래서 SlackEventListener, SlackEventHandler와 같은 데코레이터를 만들게 되었다.

만들어 놓고 보니 맘에 들어서 npm 패키지로 퍼블리쉬했다. (컨트리뷰션 언제나 환영합니다.) 레포 목표와 결과 NestJS를 선택한 이유를 읽다보면 자연스럽게 알 수 있겠지만 슬랙 이벤트/인터렉션 용 커스텀 컨트롤러를 만드는 것이 가장 핵심적인 태스크였다.

이 커스텀 컨트롤러는 각각 SlackEventListener , SlackInteractivityListener 라는 이름의 데코레이터로 개 발되었다.

최대한 NestJS 기본 HTTP 컨트롤러처럼 쓸 수 있도록 구현하고 싶었기 때문에 데코레이터로 개발했다.

HTTP 컨트롤러를 클래스 데코레이터 Controller , 메소드 데코레이터 Get , Post 등으로 구현하는 것처럼, 봇의 슬랙 컨트롤러도 비슷한 방식으로 구현할 수 있었으면 좋겠다고 생각했다.

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사 본문 이미지

결과적으로 커스텀 컨트롤러를 아래처럼 구현할 수 있도록 데코레이터들이 개발되었다.

@Controller('on-boarding') @SlackEventListener() @SlackEventHandler('team_join') async onTeamJoin({ event: { user } }: IncomingSlackEvent<TeamJoinEvent>) { this.onboardingService.startOnBoarding({ user }); } } 데코레이터의 파라미터로 path를 넣듯이, 이벤트 혹은 인터렉션을 필터링할 수 있는 함수 등을 파라미터로

받을 수 있도록 구현했다.

@Controller('memo') @SlackEventHandler({ eventType: 'message', }) async onMessage({ event: { user } }: IncomingSlackEvent<MessageEvent>) { this.memoService.takeMemo({ message }); } }

구현 과정

데코레이터 개발은 쉽지 않았다. NestJS의 Controller 데코레이터 등 소스코드를 많이 참고하기도 했지만 레 퍼런스를 찾기가 어려웠다.

결론적으로는,

드를 싹 긁어와서 핸들러 Array로 쌓아놓고,

다.

1번 과정이 구현하기 정말 어려웠는데, NestJS 소스코드를 참고해 데코레이터로 메소드에 Metadata를 넣고 Explorer(Scanner)로 메소드를 훑어가며 이 Metadata를 확인하는 방법을 사용했다. 설명은 SlackEventHandler , SlackEventListener 만 가지고 하겠다. interactivity에 대한 데코레이터도 거의 동일한 방 법으로 개발되었다.

params: | { eventType?: SlackEventType;

} | SlackEventType, ) { return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { // ...

target[SUB_SLACK_EVENT_HANDLER].set(propertyKey, { eventType, filter, target, propertyKey, descriptor, }); }; } 메소드 데코레이터를 통해, 이 메소드가 slack event handler 메소드임을 표시한다.

listenerFilter: (event: IncomingSlackEvent) => boolean = () => true, ) { return function _<T extends { new (...args: any[]): any }>(Base: T) { constructor(...args: any[]) { // ...
if (subMethods) { subMethods.forEach( SetMetadata<string, SlackEventHandlerConfig>( SLACK_EVENT_HANDLER, { eventType: config.eventType, listenerFilter(event) && }, }, ); } } }; }; } 클래스 데코레이터를 통해 해당 클래스의 메소드들 중 slack event handler로 표시된 메소드를 가져와

metadata를 설정한다.

이걸 만들다보니 NestJS의 HTTP 컨트롤러 클래스 데코레이터의 존재 이유를 분명히 알 수 있었다. 어떤 메 소드가 Get , Post 로 표시되어 있더라도, 그 메소드를 포함하는 클래스가 메소드의 메타데이터를 설정해주 어야 한다. 이러한 작업을 하려면 Controller 데코레이터가 필요해진다.

그리고 항상 잊지 말아야 할 것, 메소드들은 static하지 않으므로 this가 필요하다.

@Injectable()

constructor( ) {} public explore(): { eventHandlers: SlackEventHandlerConfig[]; interactivityHandlers: SlackInteractivityHandlerConfig[]; } { // ...
return { eventHandlers: instanceWrappers .map(({ instance }) => { return this.metadataScanner.scanFromPrototype( instance, instancePrototype, (method) => ); }) .reduce((prev, curr) => { return prev.concat(curr); }), // ...
}; } public exploreEventHandler( instance: object, instancePrototype: Controller, methodKey: string, ): SlackEventHandlerConfig | null { return null; } return handler; } } 이제 모듈에 존재하는 모든 컨트롤러를 돌면서, slack event handler 의 metadata를 스캔해서 handler array

를 만들어주는 SlackHandlerExplorer 를 만들었다.

마지막으로 application 초기화 시에 모듈에서 이 explorer를 invoke하고 만들어진 handler array를 저장한 다.

@Module({ }) // ...

constructor( ) {} onModuleInit() { this.slackHandler.addEventHandler(eventHandler); }

this.slackHandler.addInteractivityHandler(interactivityHandler); } } } 거의 다 왔다. 실제 슬랙에서 오는 HTTP POST 요청을 받을 단일 path 컨트롤러를 만든다.

@Controller('slack') @Post(`events`) //...
return this.slackHandler.handleEvent(event); } //...

} 이 이벤트를 받아 handler array를 돌며 filtering해서 알맞은 핸들러 메소드를 call하는 SlackHandler 프로바 이더를 만들었다.

@Injectable() //...

async handleSingleEvent( event: IncomingSlackEvent, handlerConfig: SlackEventHandlerConfig, ) { // ...

if (filter) { try { if (!filter(event)) { return; } } catch (e) { return; } } return handler(event); } return Promise.all( this._eventHandlers.map( async (handlerConfig) => ), ); } } 완성! 간단히 메세지 다이어그램으로 표현해보면 아래처럼 돌아간다.

이제 만들어진 데코레이터를 이용해 우리가 원하는 비즈니스 로직의 어플리케이션을 만드는 재미있는 부분 만 남았다.

글이 너무 길어진 것 같으니, 이 파트는 다음에 이어서 작성될 예정이다.

우리가 살아가는 세상을 AI 기술로 변화시키는 팀 Corca는 고도화된 기술력과 기획력을 토대로 새로운 가치 를 창출하고 있습니다.

Corca의 여정에 함께하실 분들은 코르카 채용페이지를 확인해주세요!

기술 발전의 혜택을 모두가 누리게 하여 인류 문명의 발전에 기여하는 코르카

오르카봇 개발기: 개발자의 시선 (1편). 코르카 bob봇의 탄생과 변천사 본문 이미지