스키마 우선 접근 방식 with ThingsDB
Source: Dev.to
무대 설정: 백지 상태
우리는 애플리케이션을 위해 새로운 컬렉션을 생성하는 것으로 시작합니다.
new_collection('OrderApp');
타입된 컬렉션 이해하기 (핵심 방어)
보호를 위한 첫 번째 단계는 스키마를 설정하는 것입니다. 기본적으로 새로운 ThingsDB 컬렉션은 루트에 빈, 제한 없는 객체(기본 thing)를 가지고 시작합니다. 쓰기 권한이 있는 모든 사용자는 여기에 무엇이든 추가할 수 있습니다.
우리는 App이라는 타입을 만들어 컬렉션 전체의 구조적 청사진으로 사용함으로써 이를 잠급니다.
// 1. Create a type for the root of my collection
new_type('App');
// 2. Convert the collection root to the 'App' type
.to_type('App');
왜 중요한가: 이제 컬렉션은 App 타입에 의해 제한됩니다. App 타입 정의에 맞는 변경이 이루어지지 않으면 데이터는 추가되거나 수정될 수 없습니다. 이것이 기본 스키마 방어입니다.
데이터 모델 정의
다음으로 저장하려는 데이터 구조인 Order 객체와 이를 App 루트 안에 어디에 둘지 정의합니다.
// Create an enumerator for consistent order status values
set_enum('OrderStatus', {
OPEN: 'Open',
PENDING: 'Pending',
CLOSED: 'Closed',
});
// Define the Order structure
set_type('Order', {
id: '#', // Return ID as `id` when wrapped
name: 'str',
price: 'float',
status: 'OrderStatus', // Enforcing status consistency
created: 'datetime',
});
// Update the App type: Add a property 'orders' as an array of Order objects
mod_type('App', 'add', 'orders', '[Order]');
mod_type(..) 호출은 컬렉션 루트에 빈 리스트 형태의 orders 속성을 자동으로 생성하여 데이터를 보관할 준비를 합니다.
이벤트 채널 구축
현대 마이크로서비스나 컴포넌트 기반 애플리케이션에서는 데이터 변화를 폴링하는 것이 비효율적입니다. 우리는 Room을 사용해 실시간 통신을 위한 이벤트 채널을 설정합니다.
// Add a room property to the App type for event handling
mod_type('App', 'add', 'order_events', 'room');
// For easy identification, we give the room a fixed name
.order_events.set_name('api_order_events');
이점: 이제 어떤 컴포넌트든 이 방에 참여해 new-order나 change-order-status와 같은 이벤트를 즉시 받아볼 수 있으며, 컬렉션을 지속적으로 조회할 필요가 없습니다.
로직을 절차(Procedure)로 캡슐화 (API 레이어)
컬렉션에 유효한 변경만 이루어지도록 모든 동작을 Procedures를 통해서만 노출합니다. 나중에 보안 화이트리스트를 쉽게 적용할 수 있도록 이 절차들의 이름 앞에 api_를 붙입니다.
// 1. Add a new order (with validation on input types)
new_procedure('api_order_add', |name, price| {
order = Order{
name:,
price:,
};
.orders.push(order);
// Emit the 'new-order' event for all listeners
.order_events.emit('new-order', order.wrap());
nil;
});
// 2. Change order status
new_procedure('api_order_set_status', |order_id, status| {
// Input validation: ensures the new status is a valid enum value
new_status = OrderStatus{||status.upper()};
// Access and update the specific Order object
Order(order_id).status = new_status;
// Emit the 'change-order-status' event
.order_events.emit('change-order-status', order_id, new_status.value());
nil;
});
// 3. Retrieve orders by status
new_procedure('api_orders_by_status', |status| {
order_status = OrderStatus{||status.upper()};
.orders.filter(|order| order.status == order_status).map_wrap();
});
엄격한 보안: 잠긴 API 사용자
마지막이자 가장 중요한 단계는 오직 이 절차들만 실행할 수 있는 사용자를 만드는 것입니다. 이를 통해 외부 서비스가 정의된 비즈니스 로직을 우회하는 것을 방지합니다.
// Create a new user for microservice access
new_user('api');
// Grant access to the collection:
// CHANGE (to modify data), JOIN (to listen to events), RUN (to execute procedures)
grant('//OrderApi', 'api', CHANGE|JOIN|RUN);
// Whitelist: This is the security lock.
// The user can only execute procedures and join rooms starting with 'api_'
whitelist_add('api', 'procedures', /^api_.*/);
whitelist_add('api', 'rooms', /^api_.*/);
// Generate and return the authentication token
new_token('api');
이 설정으로 api 사용자는 완전히 제한됩니다. 컬렉션을 임의로 삭제하거나 구조를 바꾸거나, 검증되지 않은 데이터를 직접 수정할 수 없으며, 반드시 api_ 절차를 통해서만 작업이 이루어집니다. 이제 컬렉션은 코드에 의해 진정으로 보호됩니다!
다음 글에서는 클라이언트 측으로 전환하여, 마이크로서비스가 생성된 토큰을 사용해 OrderApp 컬렉션에 연결하고, 인증된 api_ 쿼리를 수행하며, api_order_events 방을 통해 실시간 이벤트 변화를 듣는 방법을 시연합니다.