비즈니스 계층에서 간단한 데이터 캐시 구현하기

  • Post author:
  • Post category:
  • Post comments:0 Comments
  • Post last modified:February 8, 2020

[2004년 12월 7일] 11월 27일에 쓴 글이지만, 예제 소스코드의 패턴이 마음에 들지 않아서 수정했다. 더구나 이전 소스코드에는 버그도 있었다. 혹시라도 예전 글을 참고한 이들에겐 미안하다.

실제 사례에 비추어 이야기를 진행해보자. 물론 정책상 이야기의 구체적인 내용은 약간 비틀었다. 우선 예제 소스 코드는 여기에서 받으면 된다.

— 요구사항

개발자 피곤해씨에게 고객사측으로 신용카드결제 정보를 전달하는 서버를 개발하는 임무가 주어졌다. 명세를 간단하게 요약하자면, 매우 간단하게도 데이터베이스에 저장되어 있는 결제 데이터를 읽어서 네트워크로 전달하는 것이다. 결제정보 테이블의 스키마는 다음과 같다.

[그림 1 – 테이블 Payment]

002

각 신용카드 회사마다 고유의 에러코드를 정해놓았다. 이를테면 결제완료코드가 BB 카드의 경우에는 0, CC 카드의 경우에는 1 이다. 서로 다른 상태코드를 하나로 정리하여 보내주기 위해 매핑 테이블이 존재한다.

[그림 2 – 테이블 StatusCodesMapping]

003

제시된 두 테이블은 관계제약조건을 갖고 있다.

[그림 3 – 관계제약조건]

001

네트워크로 보내는 정보는 payment_idstatuscode 조합이다.

— 요구사항 분석

즉, 그림1과 그림2의 테이블을 조인해서 결과를 얻어야 한다. 분당 트랜잭션이 100건이 넘기 때문에 매번 조인하는 것은 비효율적이다. 이 경우에는 에러코드가 자주 변하지 않기 때문에 소스코드에 매핑 테이블의 내용을 수작업으로 복사할 수 있다. 그러나 이 방법은 에러코드가 수정될 때마다 데이터베이스와 소스코드 모두 수정해야 하기 때문에 유지관리비가 증가할 것이다. 매핑 테이블의 내용을 담는 별도의 Flat file을 만드는 것도 별도움이 되지 않는다. 이전 방법과는 달리 소스코드를 수정하지 않아도 된다는 점은 훌륭하지만, 여전히 데이터베이스와 flat file 모두를 수정해야 한다. 결국 매핑 테이블에서 직접 정보를 읽어오되, 그 회수를 최소한으로 줄이는 것이 정답이다.

— 데이터 계층 구현

우선 테이블 StatusCodesMapping의 데이터를 읽어오는 데이터 계층 클래스부터 구현한다. 예제에서는 DataRetriever.cs 파일에 구현되어 있다. 보통은 데이터 계층을 별도의 어셈블리에 구현하지만, 여기서는 글쓴이가 귀찮으므로 대충 클래스 하나로 흉내만 냈다. 클래스 DataRetriever의 멤버함수 StatusCodes()는 데이터베이스에서 테이블 StatusCodesMapping의 레코드를 모두 읽어서 Dataset에 저장한 후, Dataset을 반환한다. 데이터계층이므로 최소의 기능만 구현한다.

— 비즈니스 계층 구현

public sealed class StatusCodesCache
{
	private const string statusCodesReadingMinutesKeyName = "StatusCodesReadingMinutes";
	private StatusCodesDataset statusCodes = null;
	private TimeSpan statusCodesReadingMinutes;
	private DateTime previousStatusCodesReadTime;

	StatusCodesCache()
	{
		int minutes = int.Parse( ConfigurationSettings.AppSettings[statusCodesReadingMinutesKeyName] );
		statusCodesReadingMinutes = new TimeSpan(0,minutes,0);
		previousStatusCodesReadTime = DateTime.Now;
	}

	public StatusCodesDataset StatusCodes
	{
		get
		{
			if(statusCodes == null
				|| DateTime.Now - previousStatusCodesReadTime > statusCodesReadingMinutes)
			{
				previousStatusCodesReadTime = DateTime.Now;
				DataRetriever retriever = new DataRetriever();
				statusCodes = retriever.StatusCodes();
				Console.WriteLine("Refresh!");
			}
			return statusCodes;
		}
	}

	public static StatusCodesCache Instance
	{
		get
		{
			return Nested.instance;
		}
	}

	class Nested
	{
		static Nested()
		{
		}

		internal static readonly StatusCodesCache instance = new StatusCodesCache();
	}
}

클래스 StatusCodesCache의 멤버함수 StatusCodes를 살펴보자. 몇가지 점에서 차이는 있지만, 기본적으로 Singleton 패턴의 응용이라고 할 수 있다. Application Domain 내에서 멤버함수 StatusCodes가 처음 호출될 때, 즉 멤버변수 statusCodes가 null 값을 가질 때 데이터 계층에 요청하여 테이블 StatusCodesMapping의 레코드를 가져온다. 이 점에서는 거의 Singleton 패턴과 차이가 없다. 하지만 여기에 조건 하나가 추가되어 있음을 확인할 수 있다.

if(statusCodes == null
	|| DateTime.Now - previousStatusCodesReadTime > statusCodesReadingMinutes)

이전에 함수가 호출된 시각과 현재 시각 간의 차이가 정해진 시간을 넘겼을 때, 다시 한번 데이터베이스에서 레코드를 읽어온다.

— 결론

여태까지 설명한 패턴은 사실 별로 새로울 것도 없다. 하지만 이런 약간의
노력만으로도 얻는 것이 많다. 데이터베이스에 존재하는 데이터를 flat file이나 소스코드에 직접 써넣지 않아도 되므로 유지관리하기 쉽다. 데이터베이스의 데이터만 바꾸어주면, 어플리케이션에도 해당 사항이 적용될 것이다. 예제에서 보인 하나의 어플리케이션만 생각하면, 이 방법이 왜 더 나은지 이해하기 힘들 수도 있다. 그러나 예제에서 보인 StatusCode와 같은 데이터는 보통 여러 어플리케이션에서 사용하게 된다. 예를 들면, 결제정보를 웹 상에서 확인할 수도 있다. 이때 하나의 데이터 소스를 유지하는 것의 장점이 부각되는 것이다. 물론 이 방법은 거의 변경되지 않고, 변경된 후 바로 적용되지 않아도 되는 데이터에 한해서만 사용해야 한다.

Author Details
Kubernetes, DevSecOps, AWS, 클라우드 보안, 클라우드 비용관리, SaaS 의 활용과 내재화 등 소프트웨어 개발 전반에 도움이 필요하다면 도움을 요청하세요. 지인이라면 가볍게 도와드리겠습니다. 전문적인 도움이 필요하다면 저의 현업에 방해가 되지 않는 선에서 협의가능합니다.
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments