18 min read

메시지 유효성 검사에 대한 다양한 관점

Justin Yoo

대부분의 정보 시스템은 크게 사용자의 입력을 받는 부분과, 그 입력을 처리하는 부분으로 구분할 수 있다. 이 개념은 서로 다른 시스템 사이에 메시지를 주고 받는 형태로도 확장할 수 있다. 이렇게 시스템 사이에 메시지를 주고 받을 때, 이 메시지가 우리가 원하는 형태의 것인지 아닌지를 검증하는 절차가 반드시 필요하다. 만약 메시지에 대한 검증을 하지 않으면 검증되지 않은 메시지로 인해 시스템 전체가 엉망이 될 수도 있고, 이는 곧 이 시스템을 이용하는 회사에 엄청난 손실을 가져올 수도 있기 때문이다.

그렇다면, 메시지의 유효성에 대한 검증은 어떤 식으로 이루어질까? 아니, 메시지 유효성 검증이란 것의 의미는 무엇일까? 이 포스트에서는 메시지 검증 혹은 유효성 검사에 대한 몇 가지 관점에 대해 논의해 보도록 하자.

메시지 본문 유효성 검사

우선 메시지 본문에 대한 얘기부터 시작해 보자. 예를 들어 온라인으로 핏자를 주문하는 시스템이 있다고 가정해 보자. 나는 파인애플이 올라간 핏자를 세상에서 가장 좋아하기 때문에 하와이안 핏자를 한 판 주문할 것이고 동시에 건강을 고려해서 탄산음료 대신 탄산수를 하나 주문하려고 한다. 주문 내역은 대략 아래와 같다.

  • 하와이안 핏자 (대) 한 판
  • 탄산수 600mL 한 병

하와이안 핏자

이 시스템은 JSON 객체를 이용해 이 주문 내역을 전송한다. 따라서 대략의 JSON 요청 객체 본문은 아래와 같이 생겼을 것이다. 물론 결제 정보라든가 배송 정보라든가 하는 다른 내용들도 있겠지만, 여기서는 고려하지 않는다.

{
"orderId": 123,
"items": [
{
"itemId": "pizza-hwaiian-large",
"amount": 1
},
{
"itemId": "water-sparkling-medium",
"amount": 1
}
]
}
view raw order.json hosted with ❤ by GitHub

일반적으로 사용자 화면에서 입력 받은 주문 내역은 위와 비슷한 형태의 데이터로 구성이 되어 시스템으로 보내지는데, 시스템은 이 데이터를 바탕으로 주문을 처리하게 된다. 이 때 시스템은 요청 본문의 데이터가 유효한지 검증을 해야 한다. 이렇게 작은 데이터라고 할 지라도 아래와 같은 기준으로 데이터를 검증할 수 있다.

  1. orderId: 필드는 숫자 형태인지 검증해야 한다.
  2. itemId: 필드는 문자열을 받는데, 이 모양이 항상 카테고리-서브카테고리-크기와 같은 포맷이라면 이런 형태를 갖추었는지 데이터를 검증해야 한다.
  3. amount: 필드는 수량이 비정상적으로 많은 것인지 아닌지를 검증해야 한다. 예를 들어 한 번에 핏자 100판 이상 주문을 할 수 없다라는 가정을 한다면, 이 amount 필드의 값은 반드시 1에서 100 사이의 값이어야 한다.

만약 위 항목 중 어느 하나라도 유효성 검증에 실패한다면, 이 메시지는 처리하지 말고 오류를 반환해야 하거나 별도의 오류 처리를 거쳐야 할 것이다.

닷넷 기반의 애플리케이션을 개발한다면 여러 유명한 라이브러리들이 있어 이러한 데이터 유효성 검사에 도움을 준다. 그 중 FluentValidation이라는 라이브러리를 사용하면 정말로 손쉽게 데이터 유효성 검사를 할 수 있다. 위에 예시로 언급한 JSON 객체는 아래와 같은 형태로 유효성 검증을 하면 된다.

public class OrderItem
{
public string ItemId { get; set; }
public int Amount { get; set; }
}
public class OrderItemValidator : AbstractValidator<OrderItem>
{
public OrderItemValidator()
{
RuleFor(item => item.ItemId).Matches(@"\w+\-\w+\-\w+");
RuleFor(item => item.Amount).GreaterThan(0).LessThanOrEqualTo(100);
}
}
var orderItem = new OrderItem();
var validator = new OrderItemValidator();
var result = validator.Validate(orderItem);

그런데, 이 예제 코드에서 보이듯이 이 메시지 본문을 검사하는 것은 기본적으로 시스템이 메시지가 어떤 구조를 갖고 있는지를 알고 있다는 것을 전제로 한다. 여기서는 이미 시스템이 OrderItem 이라는 메시지 구조를 알고 정의를 해 놓았다. 만약에 메시지가 들어오는데 아예 시스템이 이해할 수 없는 구조가 들어온다면 어떻게 해야 할까?

메시지 구조 유효성 검사

이 때 필요한 것이 바로 메시지 구조에 대한 유효성 검사이다. 이 유효성 검사는 다시 두 가지로 구분할 수 있다. 유효한 메시지 구조를 이용하고 있는가(데이터 계약)에 대한 것이 첫번째가 될 것이고, 유효한 메시지 인터페이스를 이용하고 있는가(서비스 계약)에 대한 것이 두번째가 될 것이다.

악수하는 두 사람

일단 메시지를 주고 받는 시스템 사이에서 데이터 계약을 통한 공통의 메시지 구조를 이용하는 것이 당연할 것이다. 즉, 메시지를 보내는 시스템과 받는 시스템이 서로 다른 메시지 구조를 이용한다면 당연히 메시지 처리가 되지 않고 무시하거나, 오류를 반환하거나 별도의 오류 처리 프로세스를 거칠 것이다. 또한, 서비스 계약을 통해 메시지를 지정한 방법을 통해 보내고 있는지도 판단해야 한다. 서비스 계약에 지정되지 않은 방법으로 메시지를 보낸다면 당연히 받는 시스템에서는 이를 처리할 수가 없을 것이다.

그렇다면 현재 널리 쓰이는 메시지 구조 유효성 검사를 위한 것들에는 무엇이 있을까?

WSDL

SOAP을 이용한 XML 웹 서비스를 구현할 때 사용하는 것이 바로 이 WSDL이다. 이 WSDL의 구조를 보면 바로 이 서비스 계약(interface)과 데이터 계약(types)에 대한 정의가 있다. 여기서는 WSDL 2.0 스펙을 기준으로 얘기한다.

<description>
...
<types>
// Definition of data contract
</types>
...
<interface>
// Definition of service contract
</interface>
...
<service>
// Definition of service endpoint
</service>
...
</description>
view raw wsdl.xml hosted with ❤ by GitHub

즉 WSDL은 위와 같은 구조로 서비스 계약과 데이터 계약을 정의해 놓았기 때문에 메시지를 보내고 받는 시스템 모두 이 계약을 통해 데이터를 주고 받게 된다. 따라서 이 계약에 따르지 않는 메시지 전송은 원천적으로 이루어질 수 없다. 또한 이렇게 정의된 계약을 따라 SDK도 손쉽게 만들 수 있다. dotnet-svcutil은 이 SDK를 만들어주는 도구의 좋은 예라고 할 수 있다.

Open API

레거시 시스템들이 SOAP을 이용한 XML 웹 서비스를 이용했다면, 요즘에는 주로 RESTful 웹 API를 이용해서 메시지를 전송한다. 이럴 때 사실상 표준으로 쓰이는 규약이 바로 Open API이다. Open API 스펙 최신 버전인 3.0.2에 보면 Path를 통해 서비스 계약을, Schema를 통해 데이터 계약을 정의한다. 또한 AutoRest와 같은 도구를 이용하면 SDK도 손쉽게 만들 수 있다.

...
paths:
# Path definition
/pets:
get:
description: Returns all pets from the system that the user has access to
responses:
'200':
description: A list of pets.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/pet'
...
components:
...
schemas:
# Schema definition
pet:
type: object
required:
- name
properties:
name:
type: string
address:
$ref: '#/components/schemas/Address'
age:
type: integer
format: int32
minimum: 0
...
view raw openapi.yaml hosted with ❤ by GitHub

이렇게 WSDL 혹은 Open API 스펙을 통해 메시지 구조에 대한 시스템 상호간 계약을 정의하면 기본적인 메시지 유효성 검증을 수행할 수 있다.

그런데, 여기서 또 한가지 문제라면 문제가 있다. WSDL을 사용하건 Open API 스펙을 사용하건 두 시스템은 서로 동기식으로 메시지를 주고 받는다. 물론, 메시지를 받는 시스템이 내부적으로는 비동기식으로 메시지를 처리할 수 있지만, 메시지를 보낸 시스템에게는 동기식으로 내가 받았다는 응답을 보내야 한다. 예를 들어 HTTP 상태 코드 201 (Created) 혹은 202 (Accepted) 같은 것을 보내야 한다.

다른 말로 하자면, 두 시스템은 서로 의존성을 갖고 있어서 어느 시스템 하나가 임시적으로 사용할 수 없는 상태가 될 때 이 메시지를 처리할 방법이 없게 된다. 이 경우에는 메시지 구조 검증 자체가 성립하지 않는다. 또한 이러한 상호 의존성 때문에 보내는 시스템과 받는 시스템 사이에 계약이 바뀔 경우 이 변경사항에 대응하기 위한 비용이 굉장히 커진다.

스키마 저장소

클라우드 기반의 애플리케이션을 구현하다 보면 가장 흔히 접하는 아키텍처 패턴이 바로 게시자(Publisher)/구독자(Subscriber) 패턴이다. 두 시스템 사이에 메시지를 실시간으로 전송해서 처리하는 대신 중간에 브로커를 하나 두고 게시자 시스템은 브로커로 메시지를 보낸다. 같은 방식으로 구독자 시스템은 브로커에서 메시지를 받아 본다. 즉, 게시자와 구독자 시스템 사이에 있었던 의존성을 완전히 없앨 수 있다.

이 브로커는 시스템에 대해 독립적으로 운영이 되기 때문에 어떤 형태의 메시지이건 일정 조건만 만족시킨다면 모든 메시지를 받아들인다. 즉 브로커 자체는 들어오는 메시지에 대한 검증을 하지 않고 이를 메시지 게시자와 구독자의 책임으로 규정한다. 아래 그림을 한 번 보자. 애저 로직 앱서비스 버스를 이용해서 게시자/구독자 패턴을 구현한 전형적인 아키텍처이다.

애저 로직 앱과 서비스 버스를 이용한 게시자/구독자 패턴 다이어그램

위 그림에서 푸른색 화살표는 메시지의 흐름을 나타낸다. 소스 시스템에서 나온 메시지가 게시자 로직 앱을 거쳐 서비스 버스, 구독자 로직 앱으로 가서 최종적으로 타겟 시스템으로 들어간다. 그런데 이 때, 게시자 쪽 로직 앱에서 서비스 버스로 메시지를 보낼 때 사용한 메시지 구조가 구독자 쪽 로직 앱이 서비스 버스에서 받아온 메시지 구조와 동일할 것이라는 가정을 할 수 없다. 서비스 버스에 저장되어 있는 메시지는 문자 그대로 어떤 형식이든 가능하기 때문이다.

따라서 아파치 Kafka와 같은 이벤트 브로커는 이러한 문제를 해결하기 위해 별도로 스키마 저장소를 운영한다. 즉 스키마 저장소를 별도로 두고 게시자가 이벤트 브로커로 메시지를 보내기 전에 저장소에 있는 스키마를 이용해 메시지 구조 유효성 검증을 한다. 반대로 구독자 역시 이벤트 브로커에서 메시지를 받은 후 저장소에 있는 스키마를 이용해 유효성 검증을 한 후 메시지를 처리한다.

애저 서비스 버스를 이용할 때에도 비슷한 개념으로 스키마 저장소를 운영할 수 있다. 아래 그림을 살펴 보자. 이전 그림과 동일한 게시자/구독자 패턴이지만, 애저 스토리지를 스키마 저장소로 활용하고 애저 펑션을 이용해서 스키마 유효성 검사를 수행하는 추가적인 단계를 구현한 모습이다.

스키마 저장소와 펑션앱을 추가한 게시자/구독자 패턴 다이어그램

푸른색 화살표는 이전 그림과 동일한 메시지의 흐름을 나타낸다. 그런데, 게시자 쪽 로직 앱에서 서비스 버스로 메시지를 보내기 전에 오렌지색의 워크플로우를 타고 펑션 앱으로 메시지를 보낸다. 그러면 펑션 앱은 스키마 저장소에 있는 스키마를 호출해서 메시지 구조 유효성을 검증하고 다시 결과를 로직 앱으로 보낸다. 구독자 쪽 로직 앱에서도 마찬가지 방식으로 스키마 저장소를 활용한다.

이 스키마 저장소 방식을 이용하게 되면 전체적인 시스템의 효율이 향상되는 점이 몇 가지가 있다.

  1. 게시자 쪽 시스템과 구독자 쪽 시스템 사이에 의존성이 완전히 없어진다. 즉, 어느 한쪽이 시스템을 변경해도 다른쪽 시스템에 전혀 영향을 주지 않는다는 점이다.
  2. 이 의존성 해소는 스키마 버전 변경, 즉 계약 버전 변경에도 의존성을 없애준다. 즉, 시스템이 변경사항을 고려하는 것이 아니라 게시자 로직 앱과 구독자 로직 앱 안의 워크플로우만 변경하면 된다.
  3. 로직 앱 내부적으로 스키마 유효성 검증과 관련한 부분을 구현하는 대신 스키마 저장소로 유효성 검사 요청을 호출하게끔 간소화 시킬 수 있다.
  4. 서비스 계약과 관련한 유효성 검사는 더이상 필요하지 않다. 오로지 스키마를 이용한 데이터 계약 유효성 검사만 수행하면 된다.

지금까지 메시지 유효성 검사에 대한 여러 가지 관점에 대해 살펴 보았다. 메시지 본문에 대한 유효성 검사는 반드시 필요한 부분이고, 이에 더해 메시지 구조, 즉 스키마에 대해서도 유효성 검사가 반드시 이루어져야 한다. 이를 위해 이미 예전부터 WSDL, Open API 등으로 스키마 유효성 검증을 해 왔고, 이벤트/메시지 기반으로 아키텍처를 구성할 때에는 스키마 저장소를 이용하면 된다.

사실 대부분의 내용들이 이미 시스템을 구축하면서 한 번쯤은 고민해 봤을만한 내용들인데, 이 포스트는 한데 모아서 정리를 해 본 것에 불과하다. 이제 클라우드 기반 시스템 아키텍처를 구축하면서 이러한 메시지 구조 검증을 위한 스키마 저장소에 대해 고민해 보는 것도 나쁘지 않을 것이다.

다음 포스트에서는 실제로 애저 서비스 버스를 위해 스키마 저장소를 구현하는 방법에 대해 알아보기로 한다.