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

  • 34 minutes to read

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

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

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

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

57.2. CRUD 작업

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

57.2.1. CRUD와 연관된 메서드 이름

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

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

일반적으로 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도 많이 사용됩니다.

57.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를 사용했습니다.

그리고, 이 내용은 박용준 강사가 학습용으로 정리해 둔 내용이지 실제 현업에서는 해당 단체외 회사의 정책을 따릅니다.

57.2.3. CRUD와 관련된 파일 이름

CRUD 관련 페이지 또는 파일 이름은 다음을 많이 사용합니다.

  • Create: 입력
  • Index: 출력
  • Details: 상세
  • Edit: 수정 또는 삭제
  • Manage: 관리

57.3. 리포지토리 패턴(Repository Pattern)

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

리포지토리 패턴은 데이터가 영구적으로 저장되는 저장소와 상관없이 데이터 처리에 대한 코드를 추상화하는 개념입니다. 우리가 앞서 배운 상속에서 부모 역할을 하는게 리포지토리 인터페이스고 이를 실제 코드로 구현하는 자식 역할을 하는 부분이 리포지토리 클래스입니다.

이 부분의 말이 조금 어려운데요. C# 기초 입문에서는 추상화를 인터페이스에 두고, 실제 구현 코드를 해당 인터페이스를 상속하여 구현하는 클래스에 두고 프로그래밍하는 정도로 요약하고 넘어가겠습니다.

박용준 강사의 강의를 기준으로하면 C# 기초 보다는 데이터베이스 프로그래밍 영역에서 최우선적으로 사용하고 있는 패턴이 리포지토리 패턴입니다.

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

이번에 다뤄볼 내용은 일반적인 업무용 프로그램 작성할 때 많이 나오는 패턴을 살펴보겠습니다.

  • 모델 클래스(Model Class)는 데이터의 구조를 나타냅니다.
  • 리포지토리 클래스(Repository Class)는 데이터에 대한 입력, 출력, 수정, 삭제 등을 담당하는 클래스를 의미합니다. 리포지토리 클래스는 다른 표현 방법으로 서비스 클래스(Service Class)로도 부릅니다.
  • 컨텍스트 클래스(Context Class)는 모델과 리포지토리를 사용하여 하나의 업무를 묶어서 관리하는 역할을 합니다.

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

예를 들어, 게시판에 대한 모델 클래스는 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 등으로 표현될 수 있습니다.

57.3.3. 모델, 리포지토리, 컨텍스트 클래스 만들고 사용하기

다음 내용은 모델 클래스를 만들고 모델 클래스를 사용하여 데이터를 채워 넘겨주는 리포지토리 클래스를 만들고 이를 호출해주는 컨텍스트 클래스를 만들고 최종적으로 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#을 학습하고나서 데이터베이스 프로그래밍을 진행한다면, 일반적으로 이번 예제에서 다룬 모델 클래스, 리포지토리 클래스, 컨텍스트 클래스 등의 단어를 많이 접하게됩니다. 그러한 내용을 가장 최소한의 코드로 표현해보았습니다.

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

이번에는 리포지토리 인터페이스를 사용하여 3개의 리포지토리 클래스에서 상속해서 사용하는 리포지토리 패턴 예제를 만들어보겠습니다.

코드: 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 변수에 저장된 값이 "SQL", "InMemory", "XML"에 따라서 repository 개체가 서로 다른 클래스의 인스턴스로 생성됩니다.

이번 예제의 개념은 나중에 종속성 주입(의존성 주입, Dependency Injection)의 개념으로 확장됩니다.

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

57.4.1. 소개

메모리 상에 제네릭 클래스 형태의 정적(static)인 데이터 저장 공간을 만들고 이 곳에 데이터를 저장, 조회, 수정, 삭제 등의 CRUD 작업을 수행하는 예제를 단계별로 진행해 보겠습니다. 카테고리를 관리하기 위한 앱을 제작할 때 필요한 로직을 연습할 예정입니다. 지금까지 배운 C#의 주요 개념이 거의 다 들어가는만큼 굉장히 긴 실습 예제입니다. 이 강의의 완성된 소스를 먼저 실행해 본 후 아래 따라하기를 진행해도 됩니다.

참고로, 전체 완성된 소스는 다음 경로의 DotNet 솔루션의 InMemoryDatabase 프로젝트에서 다운로드 받을 수 있습니다.

57.4.2. 따라하기 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까지를 모두 수행한 후의 솔루션의 모습은 다음 그림과 같습니다.

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

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

57.4.3. 따라하기 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의 단어를 미리 인터페이스로 정의해 놓은 형태입니다. 간단한 구조의 데이터를 다룰 때에는 이 인터페이스를 상속한 후 기본 코드 구현 후 추가적인 로직을 구현해가는 식으로 사용될 수 있습니다.

참고로 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)을 적용해 더 이상 사용하지 않도록 권장한 후 다음 버전에서 제거하는 식으로 프로그램을 작성 및 유지보수 할 수 있습니다.

57.4.4. 따라하기 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를 정의해서 사용할 수 있습니다.

57.4.5. 따라하기 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();
        }
    }
}

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

57.4.6. 따라하기 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 - 가전용품

57.4.7. 마무리

이번 실습은 단계도 많고 지금까지 C#으로 공부해 본 여러 개념들 중에서 실제 업무용 앱 작성시 가장 많이 사용하는 내용들을 다루었습니다. 인메모리 데이터베이스를 다루었지만 영구적인 저장소인 실제 데이터베이스를 사용하는 부분도 이와 동일한 패턴으로 진행이 됩니다. 이번 실습의 내용은 여러 번 반복하여 따라하기를 권장합니다.

57.5. 장 요약

인-메모리 데이터베이스 또는 인-메모리 컬렉션으로 표현되는 부분에 대한 내용을 학습했습니다. 이 책이 C#에 대한 내용을 다루고 따로 물리적인 데이터베이스를 다루지 않는 부분에서 휘발성이 있는 메모리 상에 데이터 구조를 만들고 이곳에 데이터를 입력, 출력, 수정, 삭제 등의 작업을 진행해보았습니다. 이번 강의의 내용을 바탕으로 다음 장에서는 물리적인 파일에 데이터를 저장하는 내용에 대해 학습해 나가겠습니다.

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