[번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 2부

원본: https://msdn.microsoft.com/en-us/magazine/mt707524

개념만 보면, 이력을 기록하는 CRUD는 기존 CRUD에서 하나의 파라미터가 추가된 것입니다. 바로 시간입니다. 이력을 기록하는 CRUD는 데이터베이스 레코드의 추가, 갱신, 삭제를 수행한 특정 시점과 상태를 알 수 있습니다. 이를 통해 여러분의 응용 프로그램은 보다 나은 분석과 리포팅 기능으로 비즈니스 인텔리전스를 이룰 수 있습니다.

지난번 컬럼(원문: msdn.com/magazine/mt703431, 번역: youngjaekim.wordpress.com)에서는 이력을 기록하는 CRUD에 대한 이론적 기반을 설명했습니다. 이번 글에서는 구체적인 예시를 보여주고자 합니다.

예시 시나리오

이 문서에서는 간단한 회의실 예약 시스템을 예시로 들겠습니다. 이 회의실 예약 시스템은 사내에 직원들이 이용할 수 있다고 가정합시다. 이 소프트웨어는 단순한 CRUD 기반으로 구현되어 있으며 회의실 예약을 할 때 새 레코드를 생성합니다. 사용자가 해당 예약을 변경하거나 미팅을 취소했을 때는 동일한 레코드가 갱신됩니다.

일반적인 CRUD 기반으로 예약 시스템을 만들면 시스템의 마지막 상태는 알고 있지만 삭제되거나 갱신된 정보는 잃게 됩니다. 이게 정말 문제일까요? 뭐, 경우에 따라 다르겠지요. 실제 비즈니스 환경에서는 별 문제가 되지 않기도 합니다. 하지만 사용하는 직원들의 전반적인 능률 면에서는 이력을 기록하는 CRUD를 적용하면 지나치게 많은 미팅 취소와 변경은 비효율적인 행동이라고 판단하고 사내 프로세스를 개선할 수도 있습니다.

그림 1은 빈 회의실 예약 시스템의 UI 예시입니다. 데이터베이스로는 SQL을 사용했습니다. 데이터베이스 테이블은 Rooms와 Bookings가 연결된 형태입니다.The Front-End UI for a Booking System
그림 1 예약 시스템의 프론트엔드 UI

예시로 든 응용 프로그램은 ASP.NET MVC로 만들었습니다. 사용자가 “Place request (장소 예약)” 버튼을 클릭하면, 콘트롤러 메소드가 실행되고 관련 정보를 전송합니다. 아래 코드는 요청을 받는 서버 코드를 간소화한 것입니다:

[HttpPost]
public ActionResult Add(RoomRequest room)
{
  service.AddBooking(room); 
  return RedirectToAction("index", "home");
}

이 메소드는 BookingController 클래스에 있는 것이며, 인젝션된 워커 서비스 클래스를 실행합니다. 워커 서비스는 여러 작업의 묶음으로 이해하셔도 좋습니다. 이 메소드 구현에서 주목할 점은, 예약을 생성한 후에 그림 1에서 본 첫페이지로 리디력션하는 것입니다. 예약 추가 작업을 수행한 후에 별도로 뷰를 생성하지 않습니다. 이것은 CQRS(Command Query Responsibility Segregation) 구조를 택했기에 나온 영향입니다. 예약 추가 명령이 백엔드에 전송되면, 시스템에 상태를 변경하고 끝입니다. 예시 프로그램이 AJAX를 사용하여 전송하기 때문에 새로고침할 필요도 없습니다. 명령 그 자체는 별도의 작업이 아니기 때문에 어떤 명시적인 링크도 UI에 나오지 않습니다.

기존 CRUD와 이력을 기록하는 CRUD의 가장 중요한 차이점은, 이력을 기록하는 CRUD는 시스템의 시작부터 상태를 변경하는 모든 작업을 기록으로 가지고 있다는 것입니다. 이력을 기록하는 CRUD를 만들기 위해서는 우선 비즈니스 작업은 모두 명령 단위로 만들어야 한다는 것을 명심하세요. 그렇게 해야 각 명령마다 추적 가능한 매커니즘을 만들 수 있습니다. 시스템에 전달되는 각 명령은 상태를 바꾸고 이력을 기록하는 CRUD는 그 때마다 시스템의 상태를 저장합니다.  변경된 상태는 무조건 이벤트로 저장합니다. 이벤트란 단지 어떤 일이 일어났다는 불변의 정보입니다. 이렇게 쌓인 이벤트로 목록이 만들어지면, 이를 이용해서 여러 형태의 데이터 투사체(projection)를 만들 수 있습니다. 가장 흔한 투사체라면, 단순히 엔티티의 현재 상태를 보여주는게 있겠습니다. 보통 응용 프로그램에서 이벤트는 사용자가 직접 내린 명령이거나, 다른 명령이나 외부 입력이 만들어낸 간접적인 명령입니다. 이 글의 예시 시나리오에서는 사용자가 예약 요청 버튼을 클릭하는 행위라고 할 수 있습니다.

명령을 처리하기

AddBooking 메소드는 아래와 같이 구현할 수 있을 것입니다:

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  var saga = new BookingSaga();
  var response = saga.AddBooking(command);
  // Do something based on the outcome of the command
}

RoomRequest 클래스는 전송된 데이터가 ASP.NET MVC에 의해 바인딩 된 단순한 DTO (Data-Transfer Object; 데이터 전송 오브젝트) 입니다. 그에 비해 RequestBookingCommand 클래스는 명령을 실행하는데 필요한 파라미터입니다. 이런 단순한 시나리오에서는 두 클래스는 거의 동일합니다. 이제 명령을 어떻게 처리할까요? 그림 2는 명령을 실행하는 3단계를 보여줍니다.

The Chain of Core Steps to Process a Command

그림 2 명령을 처리하는 연쇄적인 단계

핸들러는 명령을 받고 수행하는 콤포넌트입니다. 아래의 코드와 같이, 핸들러는 워커 서비스의 코드에서 호출되어 인메모리에서 직접 실행될 수 있으며, 버스를 통해서 할 수도 있습니다.

public void AddBooking(RoomRequest request)
{
  var command = new RequestBookingCommand(request);
  // Place the command on the bus for
  // registered components to pick it up
  BookingApplication.Bus.Send(command);
}

버스를 사용하면 몇가지 장점이 있습니다. 하나는, 동일한 명령에 대해 여러 개의 핸들러가 관여할 때 보다 쉽게 제어할 수 있습니다. 또 하나는, 버스로 안정적인 메시징 도구를 사용하면 메시지 전송을 신뢰할 수 있고 접속 문제도 극복할 수 있습니다. 마지막으로, 버스는 그 자체로 명령을 로깅하는 콤포넌트가 될 수 있습니다. (역자주: 버스 구현에 대하여, 온프레미스는 RabbitMQ, 클라우드 기반은 Azure Service Bus를 고려할 수 있습니다)

핸들러는 보통 일회성 콤포넌트로 하나의 요청에 한 번 시작하고 종료합니다. 하지만, 몇시간에서 몇 일간 실행하는 아주 긴 워크플로우일 수도 있고 사람이 직접 승인해줄 때까지 기다리는 형태일 수도 있습니다. 단순히 일회성 콤포넌트가 아닌 경우는 saga(긴 이야기)라고 불리기도 합니다.

통상적으로 버스나 큐를 사용하는건 확장성이나 신뢰성을 도모하고자 할 때입니다. 단지 기존의 CRUD 대신 이력을 기록하는 CRUD를 구현하기 위해 버스를 사용할 필요는 없습니다. 버스의 사용 여부와는 상관 없이, 어쨌거나 명령은 일회성 핸들러나 긴 작업 핸들러에 도달할테고 어떤 형태의 작업을 수행할 것이고, 이런 대부분의 작업은 데이터베이스의 주요 작업의 집합일 것입니다.

명령 로깅하기

전통적인 CRUD에서 데이터베이스에 정보를 기록하는 것은 입력 값을 포장한 후에 새 레코드로 추가하는 작업을 의미했습니다. 이력을 기록하는 CRUD의 관점에서 새 레코드는 새 이벤트의 생성을 의미합니다. 즉, 우리 예시에서는 새 예약 이벤트입니다. 예약 이벤트는 독립적이면서도 불변의 정보 조각으로 이벤트의 고유 ID, 타임스탬프, 이벤트 명, 이벤트 관련 변수를 포함합니다. 여기서 새 예약 이벤트 관련 변수는 전통적인 CRUD의 Bookings 테이블에 새 예약 레코드를 추가할 때 사용하는 모든 컬럼의 값입니다. 갱신 이벤트에 대한 변수는 갱신하는 필드만 해당됩니다. 그러므로, 모든 갱신 이벤트에 같은 필드가 있지는 않을 것입니다. 끝으로, 삭제 이벤트는 예약 ID 값만 있어도 충분합니다.

이력을 기록하는 CRUD는 두 단계로 동작합니다.

  1. 이벤트와 그 관련 정보를 로깅
  2. 현재 시스템 상태가 빠르게 쿼리할 수 있는지 확인

이 방법이면 시스템의 현재 상태는 언제나 최신 정보이며, 이로 인한 다음 작업도 확신을 가지고 할 수 있습니다. 참고로 기존의 전통적인 CRUD에서는 ‘시스템의 현재 상태’만 있었고 ‘시스템의 과거 상태’ 는 없었습니다. 기존 CRUD 시스템을 이력을 기록하는 CRUD로 발전시킬 때, 이벤트 로깅 단계와 시스템 상태 업데이트를 일관되게 처리하려면 하나의 트랜젝션으로 묶어야 하며, 그 결과는 그림 3과 같습니다.

using (var tx = new TransactionScope())
{
  // Create the "regular" booking in the Bookings table   
  var booking = _bookingRepository.AddBooking(
    command.RoomId, ...);
  if (booking == null)
  {
    tx.Dispose();   
    return CommandResponse.Fail;
  }
  // Track that a booking was created
  var eventToLog = command.ToEvent(booking.Id);
    eventRepository.Store(eventToLog);
  tx.Complete();
  return CommandResponse.Ok;
}
그림 3 이벤트 로깅과 시스템 업데이트

매번 예약 기록을 추가/수정/삭제할 때마다 현재 상태를 정확한 순서대로 알면서 예약 목록을 최신으로 유지할 수 있습니다. 그림 4는 예시 시나리오에 사용된 두 SQL 서버 테이블이며, 추가와 업데이트 과정을 겪은 후의 모습입니다.

Bookings and LoggedEvents Tables Side by Side
그림 4 Bookings와 LoggedEvents 테이블 비교

Bookings 테이블은 개별 예약 목록을 가지고 있으며, 각각은 현재 상태를 가지고 있습니다. LoggedEvents 테이블에는 모든 예약에서 발생한 모든 이벤트가 시간 순서대로 기록되어 있습니다. 예를 들어, 예약 #54는 예약을 만든 다음 몇 일 후에 수정했음을 알 수 있습니다. 그림의 예시에서, Cargo 열은 실행한 명령의 JSON 문자열을 그대로 직렬화해서 저장하고 있습니다.

UI로 로깅된 이벤트 보기

인증된 사용자가 예약에 대한 상세 정보를 보고 싶다고 가정해봅시다. 아마도 사용자는 달력에서 예약 정보를 불러오거나 기간 설정을 하여 쿼리할 것입니다. 두 경우 모두 기본적으로는 사용자가 언제/얼마나/누가 예약을 했는지는 알고 있으므로, 이런 상세 정보는 별로 안쓰일 것입니다. 그 대신 그림 5와 같이 예약에 대한 전체 히스토리를 보여준다면 꽤 도움이 될 것입니다.

Consuming Logged Events in the UI
그림 5 UI로 로깅된 이벤트 보기

로깅된 이벤트를 쭉 불러오면 한 엔티티(Booking 54)에 대한 상태의 목록을 표시하는 뷰 모델을 만들 수 있습니다. 예시에서, 사용자가 예약 상세 정보를 보려고 클릭하면  JSON이 백그라운드에서 다운로드되고 모달(modal) 팝업이 열립니다. 이 때 JSON을 주는 메소드는 아래와 같습니다.

public JsonResult BookingHistory(int id)
{
  var history = _service.History(id);
  var dto = history.ToJavaScriptSlotHistory();
  return Json(dto, JsonRequestBehavior.AllowGet);
}

서비스 내에 History 메소드를 실행하는 것이 전부입니다. 이 동작은 특정 예약 ID에 대한 이벤트를 쿼리해서 모두 가져옵니다.

var events = new EventRepository().All(aggregateId);
foreach (var e in events)
{
  var slot = new SlotInfo();
  switch (e.Action)
  {
    :
  }
  history.Changelist.Add(slot);
}

로깅된 이벤트를 하나씩 넘기면서, 적절한 오브젝트를 DTO에 덧붙여서 반환합니다. 그림 5에서 보이는 ToJavaScriptSlotHistory(팝업창)에는 두 상태의 차이점을 신속하게 보여주기 위한 변환 작업이 있습니다.

주목할 점은, 이벤트를 CRUD만 이용해서 로깅해도 UI가 더 나아질 수 있다는 것입니다. 여러분은 이제 시스템에서 일어나는 모든 상황을 알 수 있으며 언제든 원하는 형태, 원하는 시점으로 데이터를 투사해서 볼 수 있습니다. 예를 들어, 갱신과 삭제 작업에 대한 통계를 만들어서 애널리스트에게 회사의 회의실 예약이 얼마나 비효율적인지를 파악하게 할 수 있습니다. 또는, 특정 날짜의 예약 정보를 쿼리하고 연관된 이벤트를 산출할 수도 있습니다. 한 마디로, 이력을 기록하는 CRUD는 응용 프로그램에 완전히 새로운 가능성을 열어줍니다.

정리

이력을 기록하는 CRUD는 기존의 단순한 CRUD 응용 프로그램을 보다 똑똑하게 발전시키는 방법입니다. 이 글에서는 최근에 유행하는 CQRS, 이벤트소싱, 버스와 큐, 메시지 기반 비즈니스 로직 등 몇 가지 단어와 패턴도 언급했습니다. 이 글이 도움이 됐다면, 저의 이전 글인 2015년 7월의 글(msdn.com/magazine/mt238399)과 2015년 8월 글(msdn.com/magazine/mt185569)도 한 번 읽어보세요. 이번 예시에 이어서 더 많은 영감을 줄 것입니다!


Dino Esposito “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014), “Modern Web Applications with ASP.NET” (Microsoft Press, 2016)의 저자입니다. JetBrains에서 .NET과 안드로이드 분야의 기술 에반젤리스트이며, 세계 곳곳의 여러 행사에서 연사로 활동하고 있습니다. Esposito의 소프트웨어에 대한 비전은 다음 링크에서 볼 수 있습니다.  software2cents.wordpress.com, 트위터: @despos.

이 문서의 리뷰를 한 Microsoft 기술 전문가 Jon Arne Saeteras에게 감사드립니다.

이 문서를 번역한 김영재 교육서비스 바로풀기의 개발사 Bapul의 CTO로서 기술로 교육에 새로운 시각을 주기 위해 열심히 개발하고 있습니다.

MSDN Magazine 포럼에서 이 문서에 대한 토론 보기 (영문)

[번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 2부”에 대한 답글 1개

[번역] 최신 기술 – 이력을 기록하는 CRUD 구현하기 1부 | youngjae.kim님에게 덧글 달기 응답 취소