인메모리 데이터베이스 프로그래밍 맛보기

  • 35 minutes to read

현대적인 응용 프로그램(앱)들은 대부분 앱에서 사용하는 데이터를 데이터베이스에 저장합니다. 사실, 업무용 앱에서 DBMS(데이터베이스를 조작하는 별도의 소프트웨어인 '데이터베이스 관리 시스템'을 말합니다.)를 사용하지 않는 앱은 거의 없다고 봐도 무관합니다. C#에서는 이러한 DBMS와 데이터를 주고 받는 클래스를 다수 제공합니다. 이러한 클래스들의 집합을 다른 말로 ADO.NET이라고 합니다. ADO.NET을 학습하려면 미리 선행 학습으로 SQL Server, Postgres와 같은 관계형 데이터베이스에 대한 경험이 있어야 합니다. 하지만, 이 강의에서는 관계형 데이터베이스 경험없이도 데이터베이스 프로그래밍을 이해할 수 있도록 인메모리(In-Memory) 데이터베이스 프로그래밍을 다루도록 하겠습니다. C#을 사용한 실제 DBMS와 ADO.NET 관련 클래스들에 대한 학습을 원한다면 필자가 집필한 <ASP.NET & Core를 다루는 기술>(길벗, 2016)을 참고하세요.

> // CRUD: 컬렉션, 파일, 데이터베이스 등의 저장소에 데이터를 저장, 조회, 수정, 삭제하는 작업

인-메모리 데이터베이스(In-Memory Database)

인-메모리(In-Memory) 데이터베이스는 사실 특별한 개념이 아니고 지금까지 우리가 사용해 온 변수, 배열, 구조체, 클래스 등에 저장되는 데이터를 다루는 기술이라고 생각하면 됩니다. 인메모리 기술이기에 프로그램을 실행하고 메모리에 저장된 데이터는 프로그램이 종료되면 자동으로 소멸되는 형태를 띄게 됩니다. 이렇게 메모리에서 임시로 저장된 데이터들은 파일 또는 데이터베이스에 저장하여 영구적으로 보관할 수 있게 됩니다. 이번 강의에서는 메모리 상에 데이터를 입력, 조회, 수정, 삭제, 검색 등의 작업을 진행하는 방법을 C# 코드로 살펴보겠습니다.

CRUD 작업

데이터베이스를 다룰 때 Create(입력), Read(출력) 또는 Retrieve(검색), Update(수정), Delete(삭제) 등의 기능을 줄여서 CRUD 또는 CRUD 작업이라고 합니다. 업무용 앱의 대부분의 로직은 이러한 CRUD 작업으로 표현할 수 있습니다. 참고로, 많은 영어권 개발자들은 CRUD를 '크러드'로 읽습니다.

CRUD와 연관된 메서드 이름

CRUD 관련 메서드 이름을 지을 때에는 Add, Get, Update 또는 Edit, Remove 또는 Delete 등의 단어를 많이 사용합니다. 이러한 단어를 접두사 또는 접미사로 사용하는 것은 가이드라인(권장 사항)이지 필수 사항은 아닙니다.

  • Add(): 데이터 입력 관련 메서드 이름 지을 때 사용
    • AddHero()
  • Get(): 데이터 전체 조회할 때 사용
    • GetAll()
    • GetHeroes()
  • GetById(): 단일 데이터를 조회할 때 사용
    • GetHeroById()
    • FindHeroById()
  • Update(), Edit(): 데이터를 수정할 때 사용
    • UpdateHero()
    • EditHero()
  • Delete(), Remove(): 데이터를 삭제할 때 사용
    • RemoveHero()
    • DeleteHero()

일반적으로 CRUD 관련해서 메서드 등의 이름 지을 때 데이터 출력은 Get을 많이 사용합니다. 입력은 Create, Add, New 등을 사용하며 수정은 Update, Modify, Edit, Change 그리고 삭제는 Delete, Remove 중 하나를 사용합니다. 또는 기억하기 편하게 BREAD로 표현할 수도 있습니다. BREAD는 Browse(상세보기), Read(읽기), Edit(편집), Add(추가), Delete(삭제)의 앞자를 따서 기억하면 됩니다. 참고로, 오랜기간 박용준 강사가 가장 많이 사용한 단어는 입력(Write), 출력(List), 상세(View), 수정(Modify), 삭제(Delete), 검색(Search)입니다. 추가적으로 상태를 저장하는 개념으로 SaveStore도 많이 사용됩니다.

2.2. BREAD SHOP 패턴: CRUD 관련 개체 이름 짓기 패턴

데이터 저장소 관련 이름 짓기가 고민일 때에는 박용준 강사가 고안한 제과점(BREAD SHOP)을 생각하면 좋습니다. Browse, Read, Edit, Add, Delete, Search, Has, Ordering, Paging의 앞자만 기억해서 BREAD SHOP으로 기억하면 좋습니다. 다음은 CRUD 작업 관련 이름 짓기의 예입니다.

  • Browse: 상세
    • Browse()
    • BrowseCategory()
  • Read: 출력
    • Read()
    • ReadAll()
    • ReadCategories()
  • Edit: 수정
    • Edit()
    • EditCategory()
  • Add: 입력
    • Add()
    • AddCategory()
  • Delete: 삭제
    • Delete()
    • DeleteCategory()
  • Search: 검색
    • Search()
    • SearchCategory()
  • Has: 건수
    • Has()
    • HasValue: 여부
  • Ordering: 정렬
    • Ordering()
    • OrderingCategory()
  • Paging: 페이징
    • Paging()
    • PagingCategory()

참고로, 건수를 구하는 메서드는 Has 보다는 Count 단어가 더 맞지만 BREAD SHOP으로 기억하기 위해서 Has를 사용했습니다.

이 내용은 박용준 강사가 학습용으로 정리한 것이며, 실제 현업에서는 각 단체와 회사의 정책을 따릅니다.

CRUD 관련 파일 이름

CRUD와 관련된 페이지나 파일 이름으로는 아래와 같은 명칭이 자주 사용됩니다:

  • Create: 입력
  • Index: 출력
  • Details: 상세
  • Edit: 수정(삭제 포함 가능)
  • Delete: 삭제
  • Manage: 관리

리포지토리 패턴

프로그래밍에서 자주 사용하는 설계 유형을 패턴(Pattern)이라고 합니다. 데이터베이스 프로그래밍에서는 리포지토리 패턴(Repository Pattern)이 가장 일반적으로 활용됩니다.

리포지토리 패턴은 데이터가 저장되는 방식이나 위치와 관계없이 데이터 처리 코드를 추상화하는 개념입니다. 이전에 학습한 상속을 예로 들면, 리포지토리 인터페이스는 부모 역할을 하며, 이를 구현하는 리포지토리 클래스는 자식 역할을 수행합니다.

이 개념이 다소 어려울 수 있지만, C# 기초 입문에서는 추상화된 인터페이스를 정의하고, 이를 구현한 클래스를 통해 코드를 작성하는 정도로 이해하고 넘어가면 충분합니다.

박용준 강사의 강의 기준으로 보면, 리포지토리 패턴은 데이터베이스 프로그래밍 영역에서 가장 우선적으로 사용되는 패턴 중 하나입니다.

모델 클래스, 리포지토리 클래스, 컨텍스트 클래스

이번 장에서는 일반적인 업무용 프로그램에서 자주 사용되는 패턴을 살펴보겠습니다.

  • 모델 클래스(Model Class): 데이터의 구조를 정의합니다.
  • 리포지토리 클래스(Repository Class): 데이터의 입력, 출력, 수정, 삭제 등 데이터 처리 로직을 담당합니다. 경우에 따라 서비스 클래스(Service Class)라고도 부릅니다.
  • 컨텍스트 클래스(Context Class): 모델과 리포지토리를 묶어 하나의 업무 단위를 관리합니다.

참고: 리포지토리 패턴 관련 클래스 파일 이름

예를 들어, 게시판 관련 모델 클래스는 다음과 같은 이름을 많이 사용합니다:

  • Board.cs
  • BoardModel.cs
  • BoardViewModel.cs
  • BoardEntity.cs
  • BoardObject.cs
  • BoardDto.cs

데이터 저장소와 관련된 CRUD(Create, Read, Update, Delete) 로직을 포함하는 클래스에는 주로 Component, Repository, Service, Controller와 같은 접미사를 사용합니다.
예를 들어, 웹 프로그래밍에서 게시판 관련 클래스 파일 이름은 다음과 같이 표현할 수 있습니다:

  • BoardComponent.cs
  • BoardRepository.cs
  • BoardService.cs
  • BoardController.cs

이러한 명명 규칙은 코드의 역할과 책임을 명확히 구분하는 데 도움이 됩니다.

모델, 리포지토리, 컨텍스트 클래스 구현 및 사용

다음 예제는 모델 클래스를 정의하고, 이 모델 클래스를 사용하여 데이터를 제공하는 리포지토리 클래스를 생성한 뒤, 이를 호출하는 컨텍스트 클래스를 작성하는 단계별 과정을 보여줍니다. 마지막으로 Main() 메서드에서 이 모든 클래스를 테스트하여 전체 흐름을 확인합니다.

코드: SignBaseSignRepository.cs

using System;
using System.Collections.Generic;

/// <summary>
/// 모델 클래스
/// </summary>
public class SignBase
{
    public int SignId { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

/// <summary>
/// 리포지토리 클래스
/// </summary>
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; 
    }
}

/// <summary>
/// 컨텍스트 클래스 
/// </summary>
public class SignContext
{
    public List<SignBase> Signs
    {
        get
        {
            return (new SignRepository()).GetAll(); 
        }
    }
}

/// <summary>
/// 테스트 클래스
/// </summary>
class SignBaseSignRepository
{
    static void Main()
    {
        var signs = (new SignContext()).Signs;

        foreach (var sign in signs)
        {
            Console.WriteLine($"{sign.SignId}, {sign.Email}, {sign.Password}");
        }
    }
}
1, a@a.com, 1234
2, b@b.com, 2345
3, c@c.com, 3456

C# 학습 후 데이터베이스 프로그래밍을 진행할 때, 이 예제에서 사용된 모델 클래스, 리포지토리 클래스, 컨텍스트 클래스는 매우 자주 접하게 되는 개념입니다. 이번 예제는 이러한 내용을 가장 간단한 코드로 표현한 것입니다.

리포지토리 패턴(Repository Pattern) 사용하기

이번에는 리포지토리 인터페이스를 정의하고, 이를 상속받는 세 가지 리포지토리 클래스를 구현하는 리포지토리 패턴 예제를 만들어보겠습니다.

코드: RepositoryPatternDemo.cs

//[!] 리포지토리(Repository) 패턴(저장소 패턴): 저장소를 쉽게 관리 
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());
    }
}
SQL Server 데이터베이스 사용
  • string repo 변수의 값에 따라 ITableRepository 인터페이스를 구현한 각기 다른 클래스의 인스턴스가 생성됩니다.
    • "SQL": TableSqlRepository 클래스 사용
    • "InMemory": TableInMemoryRepository 클래스 사용
    • "XML": TableXmlRepository 클래스 사용
  • 이번 예제는 종속성 주입(Dependency Injection)의 개념으로 확장될 수 있는 기초를 보여줍니다.
  • switch 문을 사용하여 조건 분기를 간결하게 처리했습니다.

[실습] 인메모리 데이터베이스 만들고 CRUD 작업 수행하기

소개

이번 실습에서는 메모리 상에 제네릭 클래스 형태의 정적(static) 데이터 저장 공간을 생성하고, 이를 활용해 데이터 저장, 조회, 수정, 삭제와 같은 CRUD 작업을 단계별로 진행합니다. 이 과정에서 카테고리 관리 앱 제작에 필요한 기본 로직을 연습할 예정입니다.

이번 실습은 지금까지 배운 C#의 주요 개념이 거의 모두 포함된 긴 예제입니다. 먼저 이 강의의 완성된 소스를 실행해 본 뒤, 단계별 따라 하기를 진행하는 것도 좋은 방법입니다.

전체 완성된 소스 코드는 아래 GitHub 경로에서 다운로드할 수 있습니다. 해당 소스는 DotNet 솔루션의 InMemoryDatabase 프로젝트에 포함되어 있습니다:

따라하기 1: InMemoryDatabase 콘솔 프로젝트 만들기

(1) Visual Studio를 실행하고 InMemoryDatabase 이름으로 .NET Core 기반 콘솔 응용 프로그램 프로젝트를 생성합니다. 기본으로 생성된 Program.cs 파일을 InMemoryDatabase.cs로 이름 및 클래스 이름을 변경합니다.

(2) InMemoryDatabase 솔루션에 마우스 오른쪽 버튼 클릭 후 [추가 > 새 프로젝트 > .NET Standard]를 선택하여 Dul.Data 이름으로 닷넷 스탠다드 프로젝트를 추가합니다. 기본 생성되는 Class1.cs 파일은 제거합니다.

(3) 2개의 프로젝트가 생성된 후의 솔루션 구성은 다음 그림과 같습니다. Dul.Data 프로젝트는 InMemoryDatabase 프로젝트에서 참조되어 사용됩니다. 참고로, Dul.Data 프로젝트의 내용은 앞선 장에서 만들어본 Dul.dll 이름의 NuGet 패키지에 이미 포함되어 있습니다.

그림: 인메모리 데이터베이스 연습용 솔루션 구성

인메모리 데이터베이스 연습용 솔루션 구성

(4) 따라하기 2에서 따라하기 5까지를 모두 수행한 후의 솔루션의 모습은 다음 그림과 같습니다.

그림: 인메모리 데이터베이스 연습용 솔루션 완성 모습

인메모리 데이터베이스 연습용 솔루션 완성 모습

따라하기 2: 공통 사용을 위한 열거형과 제네릭 인터페이스 만들기

(1) 먼저, Dul.Data 프로젝트에 전체 솔루션에서 공통으로 사용할 열거형 파일인 OrderOption.cs를 생성하고, 아래 코드를 작성합니다. 이 열거형은 데이터 정렬 시 오름차순, 내림차순, 또는 기본값을 지정할 때 사용됩니다.

코드: OrderOption.cs

namespace Dul.Data
{
    /// <summary>
    /// SortOrder 열거형: 행의 데이터 정렬 방법을 지정합니다. 
    /// </summary>
    public enum OrderOption
    {
        /// <summary>
        /// 오름차순
        /// </summary>
        Ascending,

        /// <summary>
        /// 내림차순
        /// </summary>
        Descending,

        /// <summary>
        /// 기본값
        /// </summary>
        None
    }
}

OrderOption 열거형은 일반적인 데이터베이스 프로그래밍에서 데이터 정렬에 대한 정보를 표현할 때 사용됩니다.

(2) Dul.Data 프로젝트에 전체 솔루션에서 공통으로 사용할 제네릭 인터페이스인 IBreadShop.cs 파일을 생성하고 다음과 같이 코드를 작성합니다.

코드: IBreadShop.cs

using System.Collections.Generic;

namespace Dul.Data
{
    /// <summary>
    /// 제네릭 인터페이스: 공통 CRUD 코드 => BREAD SHOP 패턴 사용
    /// </summary>
    /// <typeparam name="T">모델 클래스</typeparam>
    public interface IBreadShop<T> where T : class
    {
        /// <summary>
        /// 상세
        /// </summary>
        T Browse(int id);

        /// <summary>
        /// 출력
        /// </summary>
        List<T> Read();

        /// <summary>
        /// 수정
        /// </summary>
        bool Edit(T model);

        /// <summary>
        /// 입력
        /// </summary>
        T Add(T model);

        /// <summary>
        /// 삭제
        /// </summary>
        bool Delete(int id);

        /// <summary>
        /// 검색
        /// </summary>
        List<T> Search(string query);

        /// <summary>
        /// 건수
        /// </summary>
        int Has();

        /// <summary>
        /// 정렬
        /// </summary>
        IEnumerable<T> Ordering(OrderOption orderOption);

        /// <summary>
        /// 페이징
        /// </summary>
        List<T> Paging(int pageNumber, int pageSize);
    }
}

IBreadShop 제네릭 인터페이스는 앞서 소개한 CRUD 관련 개체 이름 짓기 패턴 중 "BREAD SHOP"이라는 단어를 기반으로 설계된 인터페이스입니다. 간단한 구조의 데이터를 다룰 때, 이 인터페이스를 상속받아 기본적인 CRUD 코드 구현을 완료한 후, 필요에 따라 추가적인 로직을 덧붙여 확장하는 방식으로 활용할 수 있습니다.

참고로, Ordering() 메서드는 학습 목적으로 읽기/쓰기 가능한 List<T> 대신 **읽기 전용인 IEnumerable<T>**를 사용했습니다. 이 부분은 List<T> 형태를 사용해도 문제가 없으며, 요구사항에 따라 적절히 선택하면 됩니다.

또한, 모든 메서드는 오버로드를 활용해 기능을 확장할 수 있습니다. 예를 들어, Search() 메서드는 다음과 같은 형태로 다양하게 구현할 수 있습니다:

  • List<T> Search(string query);
  • List<T> Search(string query, string field);
  • List<T> Search(string query, int pageNumber, int pageSize);
  • ...

이와 같은 방식으로 메서드의 기능을 확장하여 다양한 시나리오에 맞게 활용할 수 있습니다.

(3) InMemoryDatabase 프로젝트에 CategoryNameOrder.cs 열거형을 만들고 다음과 같이 코드를 작성합니다. 이 코드에서 만든 열거형은 앞서 작성한 OrderOption 열거형으로 대체되어 사용하지는 않는 코드입니다. 이러한 코드에는 [Obsolete] 특성을 붙여서 관리합니다.

코드: CategoryNameOrder.cs

using System;

namespace InMemoryDatabase
{
    // 한국식 발음: 오브솔릿, 오브설리트
    [Obsolete("OrderOption 열거형을 사용하세요.")]
    /// <summary>
    /// 열거형: 카테고리 이름 정렬 방법
    /// </summary>
    public enum CategoryNameOrder
    {
        /// <summary>
        /// 오름차순
        /// </summary>
        Asc,

        /// <summary>
        /// 내림차순
        /// </summary>
        Desc
    }
}

CategoryNameOrder 열거형은 현재 사용되지 않는 코드입니다. 리팩터링 과정에서 새로운 열거형을 만들어 이를 대체할 경우, 기존 코드를 바로 삭제하면 해당 열거형을 참조하던 코드에서 오류가 발생할 수 있습니다. 이를 방지하기 위해 [Obsolete] 특성(Attribute)을 사용하여 해당 열거형의 사용을 권장하지 않도록 표시한 후, 점진적으로 업데이트를 진행할 수 있습니다. 이후, 충분한 이행 기간을 거친 뒤 다음 버전에서 해당 코드를 완전히 제거하는 방식으로 프로그램을 작성하고 유지보수할 수 있습니다.

따라하기 3: 모델 클래스와 리포지토리 인터페이스 만들기

(1) 카테고리 관리를 위한 모델 클래스인 Category.cs 파일을 생성한 후 다음과 같이 코드를 작성합니다.

코드: Category.cs

namespace InMemoryDatabase
{
    /// <summary>
    /// 모델 클래스: 카테고리 모델 클래스
    /// </summary>
    public class Category
    {
        /// <summary>
        /// 카테고리 고유 일련번호
        /// </summary>
        public int CategoryId { get; set; }

        /// <summary>
        /// 카테고리 이름
        /// </summary>
        public string CategoryName { get; set; }
    }
}

모델 클래스 또는 뷰모델 클래스는 일반적으로 데이터가 저장되는 그릇의 구조를 표현합니다. Category 모델 클래스는 다음 불릿 리스트와 같이 일련번호와 카테고리 이름의 2가지 필드만을 관리하는 간단한 구조입니다.

  • 1 – 책
  • 2 – 강의
  • 3 – 컴퓨터

(2) 카테고리 관리를 위한 저장소 인터페이스를 ICategoryRepository.cs 파일로 생성하고, 아래와 같이 코드를 작성합니다.
IBreadShop<T> 인터페이스의 기능을 상속받아 사용하므로, ICategoryRepository 인터페이스는 간결하게 유지할 수 있습니다.
IBreadShop<T>에 정의되지 않은 새로운 기능이 필요할 경우, 이곳에 메서드 시그니처를 추가하면 됩니다.
예를 들어, 로그를 기록하는 기능을 설계하려면 void Log(string message); 형태로 정의할 수 있습니다.

코드: ICategoryRepository.cs

using Dul.Data;

namespace InMemoryDatabase
{
    /// <summary>
    /// 리포지토리 인터페이스 => BREAD SHOP 패턴 사용
    /// </summary>
    public interface ICategoryRepository : IBreadShop<Category>
    {
        // Empty
    }
}

리포지토리 인터페이스는 리포지토리 클래스에서 사용할 멤버의 시그니처를 정의하는 역할을 합니다. 이렇게 설계된 리포지토리 인터페이스는 실제로 구현되는 하나 이상의 리포지토리 클래스에서 상속받아 사용됩니다. 이번 실습에서는 IBreadShop<T> 제네릭 인터페이스에 미리 정의된 Browse 메서드부터 Paging 메서드까지 그대로 활용하므로, 추가적인 내용을 구현하지 않았습니다. 대신, IBreadShop<T>Category 클래스를 전달하여 Category 모델 클래스에 대한 CRUD 기능을 정의하고 사용할 수 있습니다.

따라하기 4: 데이터 저장소를 위한 리포지토리 클래스 만들기

(1) 이번 실습에서의 핵심 코드가 들어오는 CategoryRepositoryInMemory.cs 파일을 만들고 다음과 같이 코드를 작성합니다. 실제 데이터베이스를 사용하지 않고 List<T> 형태의 _categories 컬렉션 개체를 생성한 후 이곳에 데이터를 입력 및 조회, 수정, 삭제 등의 기능을 구현한 클래스입니다.

참고로, Visual Studio에서는 특정 인터페이스를 상속하는 코드를 작성하면 자동으로 빨간 밑줄이 생기고 Ctrl + .을 누르거나 왼쪽의 노랑 전구와 같은 아이콘을 누르면 자동으로 인터페이스 구현 코드를 생성합니다.

그림: Visual Studio의 인터페이스 구현 가이드 사용하기

Visual Studio의 인터페이스 구현 가이드 사용하기

코드: 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 = "컴퓨터" }
            };
        }

        /// <summary>
        /// 입력: 데이터베이스에 데이터를 저장
        /// </summary>
        public Category Add(Category model)
        {
            // 가장 큰 CategoryId에 1 더한 값으로 새로운 CategoryId 생성
            // 실제 데이터베이스에서는 자동 증가값(시퀀스)을 사용
            model.CategoryId = _categories.Max(c => c.CategoryId) + 1; 
            _categories.Add(model);
            return model;
        }

        /// <summary>
        /// 상세: 단일 데이터 출력
        /// </summary>
        public Category Browse(int id)
        {
            return _categories.Where(c => c.CategoryId == id).SingleOrDefault();
        }

        /// <summary>
        /// 삭제: 특정 키값(Id)에 해당하는 데이터 지우기 
        /// </summary>
        public bool Delete(int id)
        {
            int r = _categories.RemoveAll(c => c.CategoryId == id);
            if (r > 0)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// 수정: 지정한 모델로 데이터 수정하기 
        /// </summary>
        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;
        }

        /// <summary>
        /// 개수, 건수
        /// </summary>
        public int Has()
        {
            return _categories.Count;
        }

        /// <summary>
        /// 정렬: OrderOption 열거형의 데이터에 따른 정렬 조건 처리 
        /// </summary>
        /// <param name="orderOption">OrderOption 열거형</param>
        /// <returns>읽기전용(IEnumerable)으로 정렬된 레코드셋</returns>
        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;
        }

        /// <summary>
        /// 페이징: PageSize만큼 나눠서 필요한 페이지의 데이터 조회
        /// </summary>
        /// <param name="pageNumber">페이지 번호: 1, 2, 3, ...</param>
        /// <param name="pageSize">페이지 크기: 한 페이지 당 10개씩 표시</param>
        /// <returns>읽고 쓰기가 가능한(List) 페이징 처리된 레코드셋</returns>
        public List<Category> Paging(int pageNumber = 1, int pageSize = 10)
        {
            return
                _categories
                    .Skip((pageNumber - 1) * pageSize)
                    .Take(pageSize)
                    .ToList();
        }

        /// <summary>
        /// 출력: 전체 데이터 출력, GetAll() 메서드
        /// </summary>
        public List<Category> Read()
        {
            return _categories;
        }

        /// <summary>
        /// 검색: 지정한 매개 변수에 해당하는 데이터만 조회
        /// </summary>
        public List<Category> Search(string query)
        {
            return _categories
                .Where(category => category.CategoryName.Contains(query)).ToList();
        }
    }
}

앞서 지금까지 배운 내용들을 바탕으로 코드를 작성하였고 각각의 메서드에서 진행하는 코드는 간결하게 표현했기에 자세한 설명은 위 소스 코드의 내용 및 주석을 참고합니다.

따라하기 5: Main 메서드에서 CRUD 테스트

(1) 지금까지 작성한 내용을 최종적으로 메인 메서드에서 테스트를 해보겠습니다. InMemoryDatabase.cs 파일을 열고 다음과 같이 코드를 작성합니다. 전체 코드를 다 작성해도 되지만 필요한 부분만 순서대로 작성 후 실행해도 됩니다.

코드: InMemoryDatabase.cs

using Dul.Data;
using System;
using System.Collections.Generic;
using System.Linq;

namespace InMemoryDatabase
{
    class InMemoryDatabase
    {
        // 리포지토리 클래스 참조 
        static CategoryRepositoryInMemory _category;

        #region Print
        /// <summary>
        /// [0] 카테고리 출력 공통 메서드
        /// </summary>
        /// <param name="categories">카테고리 리스트</param>
        private static void PrintCategories(List<Category> categories)
        {
            foreach (var category in categories)
            {
                Console.WriteLine($"{category.CategoryId} - {category.CategoryName}");
            }
            Console.WriteLine();
        }
        #endregion

        #region Has
        /// <summary>
        /// [1] 건수
        /// </summary>
        private static void HasCategory()
        {
            if (_category.Has() > 0)
            {
                Console.WriteLine("기본 데이터가 있습니다.");
            }
            else
            {
                Console.WriteLine("기본 데이터가 없습니다.");
            }
            Console.WriteLine();
        }
        #endregion

        #region Read
        /// <summary>
        /// [2] 출력
        /// </summary>
        private static void ReadCategories()
        {
            var categories = _category.Read();
            PrintCategories(categories);
        }
        #endregion

        #region Add
        /// <summary>
        /// [3] 입력
        /// </summary>
        private static void AddCategory()
        {
            var category = new Category() { CategoryName = "생활용품" };
            _category.Add(category);
            ReadCategories();
        }
        #endregion

        #region Browse
        /// <summary>
        /// [4] 상세
        /// </summary>
        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
        /// <summary>
        /// [5] 수정
        /// </summary>
        private static void EditCategory()
        {
            _category.Edit(new Category { CategoryId = 4, CategoryName = "가전용품" });
            ReadCategories();
        }
        #endregion

        #region Delete
        /// <summary>
        /// [6] 삭제
        /// </summary>
        private static void DeleteCategory()
        {
            int categoryId = 1;
            _category.Delete(categoryId);
            Console.WriteLine($"{categoryId}번 데이터를 삭제합니다.");
            ReadCategories();
        }
        #endregion

        #region Search
        /// <summary>
        /// [7] 검색
        /// </summary>
        private static void SearchCategories()
        {
            var query = "강의";
            var categories = _category.Search(query);
            PrintCategories(categories);
        }
        #endregion

        #region Paging
        /// <summary>
        /// [8] 페이징
        /// </summary>
        private static void PagingCategories()
        {
            var categories = _category.Paging(2, 2);
            if (categories.Count > 1)
            {
                categories.RemoveAt(0); // 0번째 인덱스 항목 지우기
            }
            PrintCategories(categories);
        } 
        #endregion

        #region Ordering
        /// <summary>
        /// [9] 정렬
        /// </summary>
        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();
        }
    }
}

(2) InMemoryDatabase 프로젝트를 시작 프로젝트로 설정 후 Ctrl+F5를 눌러 실행합니다. 실행된 결과는 다음과 같습니다. 전체 결과가 길기 때문에 [1]번부터 [9]번 순서대로 하나씩 실행해보면 좋습니다.

[1] 기본값이 있는지 확인:
기본 데이터가 있습니다.

[2] 기본 데이터 출력:
1 - 책
2 - 강의
3 - 컴퓨터

[3] 데이터 입력:
1 - 책
2 - 강의
3 - 컴퓨터
4 - 생활용품

[4] 상세 보기:
4 - 생활용품

[5] 데이터 수정:
1 - 책
2 - 강의
3 - 컴퓨터
4 - 가전용품

[6] 데이터 삭제:
1번 데이터를 삭제합니다.
2 - 강의
3 - 컴퓨터
4 - 가전용품

[7] 데이터 검색:
2 - 강의

[8] 페이징:
4 - 가전용품

[9] 정렬:
3 - 컴퓨터
2 - 강의
4 - 가전용품

마무리

이번 실습은 단계가 많았으며, 지금까지 C#을 통해 배운 여러 개념들 중 실제 업무용 앱 작성 시 가장 자주 사용되는 내용을 다루었습니다. 비록 인메모리 데이터베이스를 활용했지만, 영구적인 저장소인 실제 데이터베이스를 사용하는 경우에도 동일한 패턴으로 작업이 진행됩니다. 이번 실습의 내용을 여러 번 반복하여 익히는 것을 권장합니다.

장 요약

이번 장에서는 인메모리 데이터베이스 또는 인메모리 컬렉션을 활용한 내용을 학습했습니다. 이 강의는 C#에 초점을 맞추고 있으며, 물리적인 데이터베이스 대신 휘발성 메모리를 활용하여 데이터 구조를 생성하고, 이를 통해 입력, 출력, 수정, 삭제 등 CRUD 작업을 수행해 보았습니다. 이번 강의를 바탕으로, 다음 장에서는 데이터를 물리적인 파일에 저장하는 방법을 학습할 예정입니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com