Skip to main content

3 posts tagged with "message"

View All Tags

· 10 min read

예전에 개발된 모듈을 개선하는 작업을 진행하고 있는데 확장성 및 여러가지 측면에서 고민을 하다가 queue-worker (work queue) 구조를 생각하게 되었다. 코드만으로 어떻게 해볼수도 있겠지만 아래와 같은 이유이었던 것 같다.

1. 단순 반복작업을 수행하기만 하면 된다.

2. 빨리 처리될수록 좋지만 그렇다고 실시간 처리가 필요한 것은 아니다.

3. 특정 시점에 처리할 작업이 늘어날 수 있지만 일반적으로는 적은 수의 service instance만 필요하다.

4. 서비스할 곳이 많아지면 처리할 작업의 수도 늘어난다.

일단은 요청에 대한 처리를 동기로 맞출 필요는 없기 때문에 작업을 queue에 쌓아두고 여건이 되는대로 worker가 처리하면 될 듯 싶다. 상황에 따라 동일한 로직을 갖는 worker를 늘리거나 줄이면 확장도 편리해질 것 같고.

AWS SQS vs RabbitMQ

MQ나 message broker로 많이 언급되는건 RabbitMQ와 Kafka인데, 매우 빠른 처리속도가 필요한 것은 아니고 이미 어느정도 알고 있는 RabbitMQ를 써볼까 싶었다. 다시 구축하는게 귀찮아서 찾아보니 SQS가 눈에 들어왔는데 가격도 저렴하고 구축이나 관리 측면에서 편의성은 좋다는 생각이 들었지만 말그대로 너무 심플해서 polling 방식으로만 가능한데다가, worker가 여러개인 경우 message 분배의 문제, message를 가져가서 작업하다 잘못된 경우 message 처리 등을 직접 해야할 것 같아서 고려하지 않기로 했다.

보내고 받는다

Queue의 개념이 그러하듯이 RabbitMQ도 단순하게 보면 비슷한 기능을 수행한다.

'Queue로 message를 보내고 (쌓고) queue에서 message 받기 (꺼내기)'

Queue는 하나가 아닐 수 있으므로 '어디로' 보내고 '어디서' 받아오느냐를 지정해야 하는데 RabbitMQ에서는 routing key로 구분하게 된다.

Acknowledgement

Consumer가 message를 받아가면 잘 받았다고 RabbitMQ에게 알려주는게 ack인데 automatic과 manual 방식을 제공한다. Automatic인 경우 단어의 의미처럼 queue에서 message를 꺼내 consumer에게 전달하면 consumer 쪽에서의 처리와 관계없이 ack를 받은 것처럼 동작하고 message는 삭제할 것으로 분류된다. Manual 방식은 message를 제대로 받았으니 삭제해도 된다고 consumer가 RabbitMQ에게직접 ack를 전달하는 방법이다.

Worker가 죽는다면?

Worker (consumer)가 message를 받아서 정해진 작업을 수행하는 중에 무슨 문제가 생겨서 죽거나 연결이 끊어지면 어떻게 될까? Automatic ack를 사용하도록 되어있다면 message가 이미 전달된 상태이므로 queue에는 없고, worker에 다른 장치가 되어있지 않다면 message는 유실될 것이다. 이런 경우 worker가 전달받은 message에 대한 작업을 완료하고 ack를 전달하는 방식으로 worker를 구현하면 message 유실을 막을 수 있다. RabbitMQ는 ack를 받기 이전에 message를 전달했던 consumer와의 연결이 끊어지면 re-queue 하도록 되어있기 때문이다. 매우 좋은 기능이라고 생각한다.

<그림 : message가 전달되고 나서 ack 이전에 consumer가 끊어졌다가 다시 연결된 경우>

Message dispatch

이제 확장에 대해서 생각을 해봐야 한다. 처음에 기술했듯이 특정 시점에 동일한 형상의 worker instance를 추가할 필요가 있는데 이 경우 message는 여러개의 worker에 어떤 방식으로 전달되는 걸까? RabbitMQ는 기본적으로 round robin 방식으로 message를 worker에 순차적으로 돌리도록 되어있다. 일반적으로는 round robin 방식으로도 충분할 수 있는데 worker 형태이기 때문에 특정 작업은 시간이 좀 더 걸릴수도 있고, 다른 worker는 놀고 있는데도 불구하고 작업중인 worker에 message가 전달되는 불상사가 생길수도 있다. 이래서 줄을 잘 서야 하는것인가... 이런 비효율을 막기 위해 RabbitMQ에는 QOS 설정이 가능하도록 되어있다. Message가 특정 consumer에만 몰리지 않게 잘 분배해서 전체적으로는 성능을 올리겠다는 의미로 이해하면 되겠다. QOS 설정시 prefetch count를 알려줘야 하는데 (여기서 prefetch count란 ack가 RabbitMQ에 전달되기 전에 - worker 입장에서 보면 message가 다 처리되기 전 - consumer가 받을 수 있는 message의 갯수를 의미), prefetch count를 1로 설정하게 되면 ack 이전에 받을 수 있는 message는 1이므로 현재 작업중인 worker는 건너뛰고 다른 worker에 message 전달을 시도하게 될 것이다. 합리적이다.

조건에 따라 여러 개의 queue에 message를 보내고 싶다면?

나의 경우에는 한 번에 queue를 여러개 사용하지 않아도 될 것 같지만 혹시 그럴 필요가 생길지도 모른다. (여러 개의 queue를 사용한다는 것은 consumer를 여러 종류로 구분해서 다른 처리를 하겠다라는 의미로 생각하고 있다. 물론 동일한 consumer 여러개라도 queue를 여럿으로 분리해서 쓰지 못하는 것은 아닐뿐더러 성능 측면에서는 혹시 어떤 이점이 있을지도 모르겠다.) Routing key로 queue를 구분해서 message를 전달하는 방식을 사용하게 되면, 조건에 따라 queue 여기 저기로 보낼 필요가 있을 때 producer의 구현 코드가 좀 지저분해질 수 있을 것 같다. (물론 구조적으로 좀 유연하게 만들수는 있겠지만) 이런 경우 고민없이 exchange를 활용하면 될 것 같다.

Exchange

RabbitMQ는 기본적으로 no named exchange를 사용하도록 되어있다. 사실 routing key로 queue를 직접 지정해서 message를 전달하고 받는 방식도 그렇다. (direct exchange) RabbitMQ에서 제공하는 exchange는 종류가 direct, topic, headers, fanout 네가지.

Direct exchange는 그림처럼 routing key (명칭은 동일하지만 위에서처럼 queue 이름이 아니라 binding key)에 따라 바인딩 되어있는 queue에 message가 전달된다.

<출처 : https://www.rabbitmq.com/tutorials/tutorial-four-python.html\>

Topic은 direct 보다는 더 다양한 조건(패턴)으로 queue를 바인딩해서 message를 전달하는 방식 (아래 그림의 패턴에서 *는 하나의 단어, #은 없거나 하나 이상의 단어와 매핑)

<출처 : https://www.rabbitmq.com/tutorials/tutorial-five-python.html\>

Headers는 message에 포함될 headers 속성값에 따라 message를 어디로 보낼지 결정하는 방식이다. 마지막으로 fanout exchange를 사용하게 되면, exchange에 binding 되어있는 모든 queue로 message가 전달된다.

RabbitMQ를 열면 마주치게 되는 부분들이기도 해서 몇가지 기본적인 내용에 대해서 살펴보고 그 의미들을 생각해봤다. 사용하면서 기술적인 부분에서 문제가 생기거나 운영상의 이슈가 생긴다면 (아마도 그렇겠지만) 또 정리할 예정.

· 3 min read

Raspberry pi를 가지고 놀다보니 불편한 점이 하나 있었다. 작아서 휴대성이 좋기는 한데 새로운 환경에서 전원을 켰을 때 접속 정보를 알기 위해서 번거로운 작업을 해야 한다는 것이다. HDMI 케이블, USB 혹은 무선 키보드/마우스를 들고 가서 다 연결한 후에야 사용 가능한데 주변기기들이 훨씬 크기 때문에 여간 불편한게 아니다. 특히 나의 경우엔 ssh로 붙어서 작업하는 빈도가 높고 연결된 모니터를 직접 보며 작업하는 일은 거의 없기 때문에 위의 과정이 좀 무의미했다.

그래서 인터넷 연결이 되면 IP 정보만 telegram으로 전달하는 모듈을 만들어보기로 했다.

1. Github

https://github.com/blurblah/tell_me_your_ip

위 저장소에 올려두고 대략적인 설명을 써넣었으니 참조.

2. Telegram bot의 제약사항

예전에 telegram bot을 만들어봤을 때 한가지 제약 혹은 유의할 점을 하나 알았는데 하나의 token을 가진 bot은 한 곳에서만 동작해야 한다는 점이다. 두 군데 이상 동작하려고 하면 하나는 오류를 뱉고 실행되지 않는다. 위 저장소에 있는 소스는 device에서 직접 동작하게 되어있기 때문에 telegram bot을 직접 생성해서 token을 넣게 되어있다. 여러개의 device를 대상으로 활용하기 위해서는 방식을 변경해야 할 것 같다. 특정 서버 한 곳에서 bot이 동작하게 하고 device를 등록한 후에 관리되게 만들어야 할 것 같은데 나중에 필요성이 생기면 작업해봐야겠다.

3. 동작 결과

Raspberry pi를 새로운 곳에서 LAN 케이블을 연결하고 전원을 올리면 곧 아래와 같은 메세지가 telegram으로 전달된다. 모든 network interface의 정보를 다 뿌리게 되어있는데 필요한 정보만 선별해서 다듬은 후에 전송할 필요가 있어보인다.

telegram

· 9 min read

Indirection에 이어 OOP에 대한 부분을 공부했다. Objective C 자체가 객체지향 때문에 C에서 small talk를 차용했다고 알려져 있기 때문에 얼마나 효율적인가가 궁금하긴 했다. 일단 C++ 과 비교했을 때 비슷하다는 느낌도 들었지만 가독성 면에서는 더 낫다는 생각이다. 또 java와 비슷하게 interface와 implementation 부분을 구분하기 좋다는 것도 장점이지 않을까 싶다. 내용 정리하면서 사용했던 예제는 보통 C++이나 java에서 OOP 설명에 많이 사용하는 도형 예제이다. (Circle, Rectangle, Triangle 을 처음엔 구조체로 작성했다가 클래스로 바꾸고, 나중엔 상속 개념을 도입하면서 추상화 클래스인 Shape 라는 걸 만들면서 refactoring 하는 그런...)

1. 용어

다른 언어에서는 볼 수 없었던 조금은 생소한 용어를 아래에 따로 정리했다. 사실 메세지의 경우엔 Objectvie C 코드에서 가장 특이하다고 생각했던 문법에 관련된 내용으로 이 부분만 책에서 봤는데도 Objective C 예제 코드들을 이해하기가 쉬웠다.

Message : Objective C에서 객체가 수행하는 액션.

Method dispatcher : Objective C의 코드에서 메세지가 보내지면 method dispatcher 가 메소드를 찾는다. 가장 먼저 해당 클래스에서 찾고, 없으면 상위 클래스로 올라가면서 찾는 방식을 취하며 찾아도 없으면 오류를 내보낸다.

2. Message

아래의 그림을 살펴보면,

이상한 부분이 하나 눈에 띈다.

[shape draw];

이게 뭔지 도대체 알 수 없었다. 배열도 아니고...Objective C 샘플들을 보면 대괄호로 묶인 저런 문장들이 자주 보인다. 메세지를 보낸다는 의미로 해석하면 된다고 하는데, 책 내용 그대로 보면 shape 라는 객체에 draw라는 메세지를 보낸다는 의미라고 한다. 근데 왠지 뭔가 뒤죽박죽 이상해서 내 나름대로 편하게 해석하기로 했다. shape에게 draw 라는 명령을 내리는 거라고 생각하면 훨씬 편한 듯 하다. 뭐 그게 그거일 수도 있다...ㅎㅎ

다시 정리하면 shape라는 객체가 존재하는데 그 객체는 멤버함수로 draw를 가지고 있어야 하고 그 draw를 호출하는 내용이라고 볼 수 있다. 그런데 그건 그렇다고 하더라도 또 이상한게 보인다. "id". 객체를 가리키는 포인터로 이해하면 쉽다. 난잡하게 생긴 포인터 대신 깔끔하게 id 라는 타입을 사용하는 듯 하다. 그러니까 [shape draw]의 shape가 id 타입의 변수이지만 어차피 객체를 가리키기 때문에 shape가 가리키고 있는 객체에게 draw 를 지시하는 내용.

헷갈릴 수 있으니 또 다른 예문을 보자.

첫번째 예제와 같은 프로젝트에 사용한 main 함수의 내용이다. 첫번째 예제와 비슷한 부분이 몇 군데 보인다. 일단 재미있는 부분은 shapes[0] = [Circle new]; 문장인데 처음에 다루었던 내용을 보면 같다. 단지 Circle 이라는 클래스에 new라는 메세지를 보내고 있을 뿐이다. 한마디로 Circle 객체를 새로 만들라는 의미.

두번째로, 비슷하지만 좀 이상한 부분이 보인다.

[shape[0] setBounds: rect0];

shape[0]는 객체를 가리키는 id 배열 중 하나이니 어차피 객체를 가리키는 거라 생각하면 되지만 뒷 부분의 콜론 좌우는 좀 어색하게 느껴진다. 알고보니 간단했는데, setBounds는 메세지. 그러니까 멤버함수인거고 콜론 이후의 rect0는 setBounds에 전달하고 싶은 parameter. 끝. 이상한거 없음.

3. Interface / Implementation

첫 부분에 언급했던 것처럼 Objective C에서는 interface 와 implementation이 구분하기 좋게 문법적으로 구성되어 있다. 아래는 상속개념까지 적용해서 refactoring 까지 마친 상태의 도형의 최상위 클래스 Shape이다.

클래스를 interface와 implementation으로 구분해 놓은 내용이고, @interface로 시작해 @end로 끝맺음을 하는 형태를 갖추어야 한다. Shape 옆의 콜론은 상속을 받는 의미로 해석하면 편할 듯 하고 extends 정도로 해석하면 되지 않을까? (나만의 생각) 그러니까 Shape는 NSObject (C++에서 Object 클래스 같은 최상위 클래스)를 상속받고 그 아래의 중괄호로 싼 내용은 멤버변수라고 생각하면 된다. 특이한 부분은 '-' 로 시작하는 3줄. 직관적으로 멤버함수를 의미한다는게 느껴진다. (void) 는 리턴타입을 의미하며, 위에 있는 main 함수 예문을 참고해서 보면 setFillColor 라는 method는 ShapeColor 타입의 fillColor를 인자로 받는 method 라는 걸 명시해주고 있다.

구현부인 implementation도 @로 시작해 @end로 끝맺음한다. Interface 부분과 동일한 형태로 method를 나열해 중괄호로 묶어 구현내용을 넣어두었다. 마지막에 있는 draw는 Shape를 상속받을 Circle, Rectangle 등의 sub class에서 overriding 해서 쓸 거라 빈 내용으로 두었다.

4. Inheritance

상속이다. OOP에서 가장 중요한 개념 중 하나. 역시나 예제 위주로 보는게 편하다. 아래의 그림.

Shape를 상속받는 Circle과 Rectangle 클래스의 interface와 implementation 부분이다. 대충보면 대부분 이해가 되는데 하나 이상한게 interface 안에 내용이 없다는 사실. 두 클래스 모두 Shape의 멤버변수와 멤버함수 이외에 추가할게 없는 경우엔 저렇게 빈 공간으로 두어도 무방하다. implementation 부분을 보면 Shape의 draw를 overriding 해서 쓰려고 재정의해 두었다. 두 클래스의 draw의 차이점은 NSLog에 들어가는 circle과 rect 문자열의 차이. ㅎㅎ

초반에 용어정리해 두었던 method dispatcher를 생각해보면, method dispatcher는 내가 사용한 예제에서 draw를 호출할 때 각 클래스(Circle, Rectangle)에 있는 draw를 먼저 호출하려 할거고 만약 재정의되어 있지 않다면 상위의 Shape로 올라가 draw를 찾아서 있다면 Shape의 draw를 호출하게 만든다.