개발꿈나무
[C# 교과서] 34. C# 확장 기능(9) - 인메모리 데이터베이스 프로그래밍 본문
인메모리 데이터베이스 프로그래밍
현대적인 응용 프로그램(앱)은 대부분 앱에서 사용하는 데이터를 데이터베이스에 저장한다.
C#에서는 이러한 DBMS와 데이틀 주고받는 클래스를 ㄷ수 제공하는데 이러한 클래스의 집합을 ADO.NET이라고 한다.
인메모리 데이터베이스
인메모리 데이터베이스(in-memory database)는 특별한 개념이 아니고 지금까지 우리가 사용한 변수, 배열, 구조체, 클래스 등에 저장되는 데이터를 다루는 기술이라고 생각하면 된다.
인메모리 기술이기에 프로그램을 실행하고 메모리에 저장된 데이터는 프로그램이 종료되면 자동으로 소멸하는데 이렇게 메모리에 임시로 저장된 데이터들을 파일 또는 데이터베이스에 저장하여 영구적으로 보관할 수 있다.
CRUD 작업하기
데이터베이스를 다룰 때 Create(입력), Read(출력), Retrieve(검색), Update(수정), Delete(삭제) 등 기능을 줄여 CURD 또는 CRUD 작업이라고 한다.
CRUD와 연관된 메서드
- Create: 입력
- Index: 출력
- Details: 상세
- Edit: 수정 또는 삭제
- Manage: 관리
리포지토리 패턴
프로그래밍을 할 때 자주 사용하는 유형을 패턴(pattern)이라고 하는데 데이터베이스 프로그래밍에서는 일반적으로 리포지토리 패턴(repository pattern)이 가장 맣이 사용된다.
- 모델 클래스(model class)는 데이터 구조를 나타낸다.
- 리포지토리 클래스(repository class)는 데이터에 대한 입력, 출력, 수정, 삭제 등을 담당하는 클래스를 의미한다.
- 컨텍스트 클래스(context class)는 모델과 리포지토리를 사용하여 업무 하나를 묶어 관리하는 역할을 한다.
모델, 리포지토리, 컨텍스트 클래스를 만들고 사용하기
모델 클래스를 만들고, 모델 클레스를 사용하여 데이터를 채워 넘겨주는 리포지토리 클래스를 만든다.
그리고 리포지토리 클래스를 호출하는 컨텍스트 클래스를 만들고 마지막으로 Main() 메서드에서 컨텍스트 클래스를 사용하여 테스트한다.
using System;
using System.Collections.Generic;
// 모델 클래스
public class SignBase
{
public int SignId { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
// 리포지토리 클래스
public class SignRepository
{
public List<SignBase> GetAll()
{
var signs = new List<SignBase>()
{
new SignBase() { SignId = 1, Email = "a@a.com", Password = "1234" },
new SignBase() { SignId = 2, Email = "b@b.com", Password = "2345" },
new SignBase() { SignId = 3, Email = "c@c.com", Password = "3456" },
};
return signs;
}
}
// 컨텍스트 클래스
public class SignContext
{
public List<SignBase> Signs
{
get
{
return (new SignRepository()).GetAll();
}
}
}
// 테스트 클래스
class SignBaseSignRepository
{
static void Main()
{
var signs = (new SignContext()).Signs;
foreach (var sign in signs)
{
Console.WriteLine($"{sign.SignId}, {sign.Email}, {sign.Password}");
}
}
}
리포지토리 패턴 사용하기
리포지토리 인터페이스를 사용하여 리포지토리 클래스 3개에서 상속하는 리포지토리 패턴 예제이다.
using System;
public interface ITableRepository
{
string GetAll();
}
public class TableInMemoryRepository : ITableRepository
{
public string GetAll()
{
return "인-메모리 데이터베이스 사용";
}
}
public class TableSqlRepository : ITableRepository
{
public string GetAll() => "SQL Server 데이터베이스 사용";
}
public class TableXmlRepository : ITableRepository
{
public string GetAll() => "XML 데이터베이스 사용";
}
class RepositoryPatternDemo
{
static void Main()
{
// SQL, InMemoy, XML 등 넘어오는 값에 따른 인스턴스 생성(저장소 결정)
string repo = "SQL"; // 여기 값을 SQL, InMemory, XML 중 하나로 변경
ITableRepository repository;
if (repo == "InMemoy")
{
repository = new TableInMemoryRepository();
}
else if (repo == "XML")
{
repository = new TableXmlRepository();
}
else
{
repository = new TableSqlRepository();
}
Console.WriteLine(repository.GetAll());
}
}
repo 변수의 값에 따라 repository 개체가 서로 다른 클래스의 인스턴스로 생성된다.
인메모리 데이터베이스를 만들고 CRUD 작업 수행하기
메모리상에 제네릭 클래스 형태의 정적(static)인 데이터 저장 공간을 만들고, 이 곳에 데이터의 CRUD 작업을 수행해보자.
<InMemoryDatabase 콘솔 프로젝트 만들기>
1. InMemoryDatabase라는 이름으로 닷넷 코어 기반 콘솔 앱 프로그램 프로젝트를 만들고 기본으로 생성된 Program.cs 파일을 InMemoryDatabase.cs 파일로 이름 및 클래스 이름을 변경한다.
2. InMemoryDatabase 솔루션에서 Dul.Data라는 클래스 라이브러리(.NET Standard)를 추가하고 기본으로 생성된 Class1.cs 파일은 제거한다.
3. InMemoryDatabase 프로젝트에서 Dul.Data 프로젝트를 참조 추가한다.

<공통으로 사용할 열거형과 제네릭 인터페이스 만들기>
1. Dul.Data 프로젝트에 전체 솔루션에서 공통으로 사용할 열거형인 OrderOption.cs 파일을 만들고
다음과 같이 코드를 작성한다.
namespace Dul.Data
{
public enum OrderOption
{
Ascending,
Descending,
None
}
}
OrderOption 열거형은 일반적인 데이터베이스 프로그래밍에서 데이터 정렬 정보를 표현한다.
2. Dul.Data 프로젝트에 전체 솔루션에서 공통으로 사용할 제네릭 인터페이스인 IBreadShop.cs 파일을 만들고
다음과 같이 코드를 작성한다.
using System.Collections.Generic;
namespace Dul.Data
{
public interface IBreadShop<T> where T : class
{
T Browse(int id);
List<T> Read();
bool Edit(T model);
T Add(T model);
bool Delete(int id);
List<T> Search(string query);
int Has();
IEnumerable<T> Ordering(OrderOption orderOption);
List<T> Paging(int pageNumber, int pageSize);
}
}
IBredShop 제네릭 인터페이스는 CRUD와 관련한 이름 짓기 패턴 중 자주 사용되는 단어를 미리 정의한 형태이다.
Ordering() 메서드는 학습 목적으로 읽고 쓰기가 가능한 List<T> 대신 읽기 전용인 IEnumerable<T>를 사용한 것으로 List<T>를 사용해도 무관하다.
3. InMemoryDatabase 프로젝트에 CategoryNameOrder.cs 열거형을 만들고 다음과 같이 코드를 작성한다.
using System;
namespace InMemoryDatabase
{
[Obsolete("SortOrder 열거형을 사용하세요.")]
public enum CategoryNameOrder
{
Asc,
Desc
}
}
CategoryNameOrder 열거형은 더 이상 사용하지 않는 코드로 [Obsolete] 특성을 적용하여 더 이상 사용하지 않도록
권장한다.
<모델 클래스와 리포지토리 인터페이스 만들기>
1. InMemoryDatabase 프로젝트에 카테고리를 관리하는 모델 클래스인 Catagory.cs 파일을 만들고
다음과 같이 코드를 작성한다.
namespace InMemoryDatabase
{
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
}
}
Category 모델 클래스는 일련번호와 카테고리 이름으로 된 두가지 필드만 관리하는 간단한 구조이다.
2. InMemoryDatabase 프로젝트에 카테고리를 관리할 수 있는 제장소의 인터페이스를 ICategoryRepository.cs 파일로 만들고, 다음과 같이 코드를 작성한다.
using Dul.Data;
namespace InMemoryDatabase
{
public interface ICategoryRepository : IBreadShop<Category>
{
// Empty
}
}
Dul.Data 프로젝트의 IBreadShop<T> 인터페이스 기능을 상속받아 사용한다.
IBreadShop<T>에 정의되지 않은 새로운 기능을 추가로 구현할 때는 이곳에 메서드 시그니처를 작성한다.
리포지토리 인터페이스는 리포지토리 클래스에서 사용할 멤버에 대한 시그니처를 담아 두는 곳이다.
<데이터 저장소를 위한 리포지토리 클래스 만들기>
InMemoryDatabase 프로젝트에 핵심 코드를 담은 CategoryRepositoryInMemory.cs 파일을 만들고 다음과 같이 코드를 작성한다.
using Dul.Data;
using System.Collections.Generic;
using System.Linq;
namespace InMemoryDatabase
{
public class CategoryRepositoryInMemory : ICategoryRepository
{
//[인메모리 데이터베이스 역할을 하는 정적 컬렉션 개체 생성
private static List<Category> _categories = new List<Category>();
public CategoryRepositoryInMemory()
{
// 생성자에서 컬렉션 이니셜라이저를 사용하여 3개의 데이터로 초기화
_categories = new List<Category>()
{
new Category() { CategoryId = 1, CategoryName = "책" },
new Category() { CategoryId = 2, CategoryName = "강의" },
new Category() { CategoryId = 3, CategoryName = "컴퓨터" }
};
}
// 입력: 데이터베이스에 데이터를 저장
public Category Add(Category model)
{
// 가장 큰 CategoryId에 1 더한 값으로 새로운 CategoryId 생성
// 실제 데이터베이스에서는 자동 증가값(시퀀스)을 사용
model.CategoryId = _categories.Max(c => c.CategoryId) + 1;
_categories.Add(model);
return model;
}
// 상세: 단일 데이터 출력
public Category Browse(int id)
{
return _categories.Where(c => c.CategoryId == id).SingleOrDefault();
}
// 삭제: 특정 키값(Id)에 해당하는 데이터 지우기
public bool Delete(int id)
{
int r = _categories.RemoveAll(c => c.CategoryId == id);
if (r > 0)
{
return true;
}
return false;
}
// 수정: 지정한 모델로 데이터 수정하기
public bool Edit(Category model)
{
// Select() 메서드에서 값을 수정하고 다시 반환하는 형태로 특정 속성의 값 변경
var result = _categories
.Where(c => c.CategoryId == model.CategoryId)
.Select(c => { c.CategoryName = model.CategoryName; return c; })
.FirstOrDefault(); // SingleOrDefault()도 가능
if (result != null)
{
return true;
}
return false;
}
// 개수, 건수
public int Has()
{
return _categories.Count;
}
// 정렬: OrderOption 열거형의 데이터에 따른 정렬 조건 처리
public IEnumerable<Category> Ordering(OrderOption orderOption)
{
IEnumerable<Category> categories;
switch (orderOption)
{
case OrderOption.Ascending:
//[a] 확장 메서드 사용
categories = _categories.OrderBy(c => c.CategoryName);
break;
case OrderOption.Descending:
//[b] 쿼리 식 사용
categories = (from category in _categories
orderby category.CategoryName descending
select category);
break;
default:
//[c] 기본 값
categories = _categories;
break;
}
return categories;
}
// 페이징: PageSize만큼 나눠서 필요한 페이지의 데이터 조회
public List<Category> Paging(int pageNumber = 1, int pageSize = 10)
{
return
_categories
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
}
// 출력: 전체 데이터 출력, GetAll() 메서드
public List<Category> Read()
{
return _categories;
}
// 검색: 지정한 매개 변수에 해당하는 데이터만 조회
public List<Category> Search(string query)
{
return _categories
.Where(category => category.CategoryName.Contains(query)).ToList();
}
}
}
<Main() 메서드에서 CRUD 테스트하기>
InMemoryDatabase.cs 파일을 열고 다음과 같이 코드를 작성한다.
using Dul.Data;
using System;
using System.Collections.Generic;
using System.Linq;
namespace InMemoryDatabase
{
class InMemoryDatabase
{
// 리포지토리 클래스 참조
static CategoryRepositoryInMemory _category;
#region Print
// [0] 카테고리 출력 공통 메서드
private static void PrintCategories(List<Category> categories)
{
foreach (var category in categories)
{
Console.WriteLine($"{category.CategoryId} - {category.CategoryName}");
}
Console.WriteLine();
}
#endregion
#region Has
// [1] 건수
private static void HasCategory()
{
if (_category.Has() > 0)
{
Console.WriteLine("기본 데이터가 있습니다.");
}
else
{
Console.WriteLine("기본 데이터가 없습니다.");
}
Console.WriteLine();
}
#endregion
#region Read
// [2] 출력
private static void ReadCategories()
{
var categories = _category.Read();
PrintCategories(categories);
}
#endregion
#region Add
// [3] 입력
private static void AddCategory()
{
var category = new Category() { CategoryName = "생활용품" };
_category.Add(category);
ReadCategories();
}
#endregion
#region Browse
// [4] 상세
private static void BrowseCategory()
{
int categoryId = 4;
var category = _category.Browse(categoryId);
if (category != null)
{
Console.WriteLine($"{category.CategoryId} - {category.CategoryName}");
}
else
{
Console.WriteLine($"{categoryId}번 카테고리가 없습니다.");
}
Console.WriteLine();
}
#endregion
#region Edit
// [5] 수정
private static void EditCategory()
{
_category.Edit(new Category { CategoryId = 4, CategoryName = "가전용품" });
ReadCategories();
}
#endregion
#region Delete
// [6] 삭제
private static void DeleteCategory()
{
int categoryId = 1;
_category.Delete(categoryId);
Console.WriteLine($"{categoryId}번 데이터를 삭제합니다.");
ReadCategories();
}
#endregion
#region Search
// [7] 검색
private static void SearchCategories()
{
var query = "강의";
var categories = _category.Search(query);
PrintCategories(categories);
}
#endregion
#region Paging
// [8] 페이징
private static void PagingCategories()
{
var categories = _category.Paging(2, 2);
if (categories.Count > 1)
{
categories.RemoveAt(0); // 0번째 인덱스 항목 지우기
}
PrintCategories(categories);
}
#endregion
#region Ordering
// [9] 정렬
private static void OrderingCategories()
{
var categories = _category.Ordering(OrderOption.Descending);
PrintCategories(categories.ToList());
}
#endregion
static void Main(string[] args)
{
_category = new CategoryRepositoryInMemory();
Console.WriteLine("[1] 기본값이 있는지 확인: ");
HasCategory();
Console.WriteLine("[2] 기본 데이터 출력: ");
ReadCategories();
Console.WriteLine("[3] 데이터 입력: ");
AddCategory();
Console.WriteLine("[4] 상세 보기: ");
BrowseCategory();
Console.WriteLine("[5] 데이터 수정: ");
EditCategory();
Console.WriteLine("[6] 데이터 삭제: ");
DeleteCategory();
Console.WriteLine("[7] 데이터 검색: ");
SearchCategories();
Console.WriteLine("[8] 페이징: ");
PagingCategories();
Console.WriteLine("[9] 정렬: ");
OrderingCategories();
}
}
}
최종적인 InMemoryDatabase 솔루션의 구조는 다음과 같다.

<Reference>
'C# 기초' 카테고리의 다른 글
[C# 교과서] 36. C# 확장 기능(11) - XML과 JSON (0) | 2022.01.26 |
---|---|
[C# 교과서] 35. C# 확장 기능(10) - 스트림과 파일 입출력 (0) | 2022.01.26 |
[C# 교과서] 33. C# 확장 기능(8) - 비동기 프로그래밍 (0) | 2022.01.26 |
[C# 교과서] 32. C# 확장 기능(7) - 스레드 (0) | 2022.01.25 |
[C# 교과서] 31. C# 확장 기능(6) - NuGet 패키지 (0) | 2022.01.25 |