Windows 앱 데이터 저장 위치를 고르는 방법 ── SQLite / JSON / 레지스트리 / Access 판단표
· 小村 豪 · SQLite, Windows, .NET, C#, 데이터 저장, 레지스트리, Access, 설계, 판단표, 기술 상담
「설정은 INI 파일이면 되는가」「이력 데이터가 늘어나서 Access에 넣고 싶다」「레지스트리와 설정 파일은 어떻게 구분해서 써야 하는가」. Windows 업무 앱을 만들다 보면 데이터 저장 위치 선택은 반드시 거치게 되는 길입니다. 하지만 이 선택은 처음에 별생각 없이 정해진 채로 다시 검토되지 않는 경우가 많아, 몇 년 후 「JSON 파일이 수십 MB로 커져서 시작이 느리다」「공유 폴더의 Access 파일이 일주일에 한 번씩 손상된다」「Program Files 바로 아래에 쓰고 있어서 Windows 11 환경에서 동작하지 않는다」같은 형태로 문제가 드러납니다.
이 글에서는 업무용 Windows 앱의 로컬 데이터 저장에 대해 「어디에 둘 것인가」(폴더 선택)와 「무엇으로 저장할 것인가」(형식・엔진 선택)를 나누어 정리합니다. 이 블로그에서 여러 번 사용한 판단표 형식으로, SQLite / JSON / 레지스트리 / Access 각각의 강점과 함정을 정리합니다.
1. 먼저 결론
- 저장 위치를 고르는 것은 「어디에 둘 것인가」와 「무엇으로 저장할 것인가」라는 두 가지 독립적인 판단입니다. 전자를 잘못 고르면 권한・멀티유저 문제가 생기고, 후자를 잘못 고르면 손상・성능・유지보수 문제가 생깁니다.
- 저장 위치의 기본은, 사용자별 설정・데이터라면
%LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData), 모든 사용자가 공유한다면%PROGRAMDATA%이며, exe와 같은 폴더(Program Files 아래)에는 쓰지 않는 것입니다.1 - 형식의 첫 번째 선택지는 단순히 두 가지입니다. 구조화된 작은 설정은 JSON 파일, 계속 늘어나는 업무 데이터・이력・검색하고 싶은 데이터는 SQLite. 이 두 가지로 업무 앱의 로컬 저장 대부분을 커버할 수 있습니다.2
- 레지스트리는 「작은 플래그나 Windows와의 연동 정보를 두는 곳」이지, 앱의 데이터 저장소가 아닙니다. 32bit/64bit 레지스트리 리다이렉션(
Wow6432Node)을 이해하지 못한 채 사용하면 「분명히 썼는데 값이 보이지 않는다」는 문제를 겪게 됩니다.3 - Access(.accdb)를 신규 개발의 데이터 저장소로 선택할 이유는 거의 없어졌습니다. 기존 자산과의 연동으로 사용하는 경우에도, ACE 공급자의 비트 수를 맞춰야 한다는 배포상의 제약이 항상 따라옵니다.4
- 어떤 형식을 쓰더라도, 기밀 정보(비밀번호・API 키)만큼은 별도로 취급해야 합니다. 평문으로 JSON이나 레지스트리에 두지 말고 DPAPI로 보호하세요. 자세한 내용은 다른 글 「Windows 앱에서 설정 파일에 기밀 정보를 평문으로 저장하지 않기 위한 베스트 프랙티스」를 참고하세요.
2. 저장할 데이터를 4가지로 분류한다
무엇으로 저장할지 정하기 전에, 저장하려는 데이터의 성질을 분류합니다. 업무 앱의 로컬 데이터는 대략 다음 4가지로 나뉩니다.
| 분류 | 예 | 특징 |
|---|---|---|
| 설정 | 접속 대상, 화면 레이아웃, 마지막으로 연 폴더 | 작다. 시작 시 전부 읽는다. 사용자가 직접 편집하고 싶어 하는 경우도 있다 |
| 업무 데이터・이력 | 측정 결과, 처리 이력, 마스터의 로컬 사본 | 계속 늘어난다. 검색・집계하고 싶다. 손상되면 업무에 큰 영향을 준다 |
| 캐시 | 썸네일, 다운로드한 리소스 | 사라져도 다시 만들 수 있다. 용량 관리가 필요하다 |
| 기밀 정보 | 저장된 비밀번호, 토큰 | 소량. 평문으로 두어서는 안 된다 |
이 분류마다 적절한 저장 위치와 형식이 다르다는 것이 이 글의 핵심입니다. 「설정도 이력도 전부 하나의 XML에 들어 있는」 앱이라면, 이 분류를 다시 하는 것이 개선의 첫걸음이 됩니다.
3. 어디에 둘 것인가 ── 폴더 선택의 기본
.NET이라면 Environment.GetFolderPath로 가져올 수 있는 위치를 기준으로 합니다.5
| 위치 | 가져오는 방법 | 용도 |
|---|---|---|
%LOCALAPPDATA%\회사명\앱 이름 |
SpecialFolder.LocalApplicationData |
사용자별 데이터의 기본값. 우선 여기 |
%APPDATA%\회사명\앱 이름(Roaming) |
SpecialFolder.ApplicationData |
이동 사용자 프로필 환경에서 사용자를 따라가게 하고 싶은 설정에만 |
%PROGRAMDATA%\회사명\앱 이름 |
SpecialFolder.CommonApplicationData |
모든 사용자가 공유하는 데이터. ACL 설계가 필요 |
| 문서 폴더 아래 | SpecialFolder.MyDocuments |
사용자가 자신의 파일로 다루는 결과물(내보낸 보고서 등)에만 |
코드로 만들면 이 정도지만, 「회사명\앱 이름」 계층을 끼우는 것, 처음 실행 시 폴더를 만드는 것까지 포함해서 공통 처리로 만들어 두면, 저장 위치가 그때그때 되는 대로 늘어나는 것을 막을 수 있습니다.
public static class AppPaths
{
public static string DataDir { get; } = CreateDir(
Environment.SpecialFolder.LocalApplicationData);
private static string CreateDir(Environment.SpecialFolder root)
{
var dir = Path.Combine(
Environment.GetFolderPath(root), "KomuraSoft", "MyApp");
Directory.CreateDirectory(dir); // 이미 있으면 아무것도 하지 않음
return dir;
}
}
환경 변수 %LOCALAPPDATA%를 문자열 연결로 구성하는 대신 Environment.GetFolderPath를 쓰는 이유는, 서비스 계정이나 다른 사용자로 실행할 때, 폴더 리다이렉션이 구성된 환경에서도 올바른 위치를 반환해 주기 때문입니다. 작업 스케줄러에서 다른 계정으로 돌리는 순간 경로가 바뀌어 버리는 사고(작업 스케줄러 글의 5장에서 다룬 「수동이면 된다」 문제의 일종입니다)도 이렇게 하면 피할 수 있습니다.
함정을 3가지 짚어 보겠습니다.
- exe가 있는 폴더에 쓰지 않는다. Program Files 아래는 표준 사용자가 쓸 수 없습니다. 오래된 32bit 앱에서는 UAC의 호환성 기능 때문에 VirtualStore로 조용히 리다이렉트되는 경우가 있어, 「관리자로 실행했을 때와 표준 사용자로 실행했을 때 설정 파일의 내용이 다르다」는 이해하기 어려운 증상의 원인이 됩니다.
- ProgramData는 「쓸 수는 있지만 안전하지는 않다」. 기본 ACL로는 한 사용자가 만든 파일을 다른 사용자가 변경할 수 없는 구조가 될 수 있습니다. 모든 사용자가 공유해서 읽고 쓰게 하려면, 설치 프로그램에서 폴더를 만들고 ACL을 명시적으로 설정합니다.
- Roaming을 기본값으로 하지 않는다. 도메인의 이동 사용자 프로필 환경에서는 Roaming 아래가 로그온・로그오프 시에 동기화됩니다. 크기가 큰 데이터나 머신 고유 데이터(캐시, 하드웨어 설정)를 Roaming에 두면 동기화 지연이나 다른 머신으로의 「오염」 원인이 됩니다. 애매하면 Local로 하세요.
4. 무엇으로 저장할 것인가 ── 4가지 선택지의 본질
4.1 JSON 파일 ── 설정의 첫 번째 선택지
System.Text.Json으로 간단하게 읽고 쓸 수 있고, 사람이 읽을 수 있으며, Git 관리나 diff 비교가 쉽다는 점에서 설정 용도로는 장점이 잘 갖춰져 있습니다. 주의할 점은 두 가지입니다.
손상 대책을 세울 것. 쓰는 중에 전원이 꺼지면 어중간한 파일이 남아 다음 실행 시 읽을 수 없게 됩니다. 「임시 파일에 쓴 다음 교체하는」 것이 정석이며, .NET이라면 File.Replace가 백업을 포함한 교체 기능을 제공합니다.
var json = JsonSerializer.Serialize(settings, options);
var tmp = path + ".tmp";
File.WriteAllText(tmp, json);
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak");
else
File.Move(tmp, path);
읽는 쪽에서도 「손상되어 있으면 .bak을 시도하고, 그것도 안 되면 기본값으로 시작하고 경고를 표시한다」는 축소 동작을 처음부터 넣어 두면, 설정 파일 손상이 지원 문의로 이어지지 않게 됩니다.
데이터 저장소로 쓰지 않을 것. 「시작할 때 전부 읽고, 종료할 때 전부 쓰는」 것이 성립하는 크기(기준으로 수백 KB까지)가 JSON의 적용 범위입니다. 계속 추가되는 이력이나 레코드 검색이 필요한 데이터를 JSON에 넣기 시작했다면, 그것은 다음 SQLite로 넘어가야 한다는 신호입니다.
4.2 SQLite ── 늘어나는 데이터・검색하는 데이터의 첫 번째 선택지
SQLite는 서버가 필요 없고, 파일 하나로 되어 있으며, 퍼블릭 도메인인 임베디드 데이터베이스로, .NET에서는 Microsoft가 유지 관리하는 ADO.NET 공급자인 Microsoft.Data.Sqlite, 또는 EF Core의 SQLite 공급자로 다룰 수 있습니다.2 Microsoft 스스로가 Windows 앱의 로컬 데이터 저장 수단으로 권장하고 있으며6, 「로컬에서 계속 늘어나는 구조화된 데이터」에는 우선 SQLite를 떠올려도 되는 상황입니다.
먼저, 얼마나 간단한지 코드로 보여드립니다. NuGet으로 Microsoft.Data.Sqlite를 추가하면 서버 설정도 연결 문자열 관리 화면도 필요 없이, 파일 경로만 지정하면 바로 쓰기 시작할 수 있습니다.
using Microsoft.Data.Sqlite;
var dbPath = Path.Combine(AppPaths.DataDir, "app.db");
using var conn = new SqliteConnection($"Data Source={dbPath}");
conn.Open();
// 최초 1회만: WAL 모드 활성화와 테이블 생성
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = """
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS measurement (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
value REAL NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS ix_measurement_device
ON measurement(device_id, created_at);
""";
cmd.ExecuteNonQuery();
}
// 삽입에는 파라미터가 필수(문자열 연결로 SQL을 구성하지 않는다)
using (var cmd = conn.CreateCommand())
{
cmd.CommandText =
"INSERT INTO measurement (device_id, value) VALUES ($device, $value)";
cmd.Parameters.AddWithValue("$device", "CAM-01");
cmd.Parameters.AddWithValue("$value", 23.5);
cmd.ExecuteNonQuery();
}
「JSON 파일에 계속 추가하는」 것과 크게 다르지 않은 수고로, 인덱스가 있는 검색・집계・건수 제한 없는 이력을 손에 넣을 수 있다는 걸 알 수 있을 것입니다. ORM을 끼우고 싶다면 EF Core의 SQLite 공급자가 이 라이브러리 위에 올라갑니다.
그 위에서, 실무 포인트를 추려서 짚어 보겠습니다.
- WAL 모드를 활성화한다. 위 코드의
PRAGMA journal_mode=WAL;입니다. 읽기와 쓰기의 동시성이 올라가서, UI 스레드와 백그라운드 처리가 같은 DB를 다루는 구조에서도 막히기 어려워집니다. WAL 설정은 데이터베이스 파일 자체에 영구적으로 저장되므로 연결할 때마다 실행할 필요는 없습니다. - 쓰기는 1 프로세스 1개로 몰아준다. SQLite의 쓰기는 데이터베이스 단위로 배타적입니다. 여러 스레드에서 쓴다면, 큐를 거쳐서 쓰기 담당을 하나로 모으는 설계가 안전합니다. 또한 작은 INSERT를 대량으로 수행할 때는 명시적인 트랜잭션으로 묶으면 한 건씩 커밋하는 것보다 훨씬 빨라집니다.
- 네트워크 공유에 두지 않는다. SMB를 통한 파일 잠금은 환경에 따른 문제가 많고, SQLite 공식 문서에서도 손상 원인의 1순위로 네트워크 파일 시스템 상에서의 공유를 꼽고 있습니다.7 여러 대・여러 사용자가 동시에 쓰고 싶어졌다면, 그건 이미 클라이언트・서버형 DB(SQL Server Express 등)의 영역입니다.
- 타입은 4종류뿐이라는 것을 알아 둔다. SQLite의 실체는 INTEGER/REAL/TEXT/BLOB이며, 날짜나 GUID는 TEXT로 저장됩니다.
Microsoft.Data.Sqlite의 타입 매핑 규칙을 한 번 확인해 두면 날짜 비교나 정렬에서 헤매지 않게 됩니다.8 위 예제에서created_at을datetime('now')(UTC)로 하는 것도, 로컬 시각을 섞으면 정렬과 서머타임 전환에서 문제가 생기기 때문입니다. 표시할 때 로컬 시각으로 변환하는 방침이 안전합니다. - 백업은 「파일 복사」가 아니라
VACUUM INTO나 Backup API로 한다. 실행 중인 DB 파일을 단순히 복사하면 WAL과 본체의 불일치를 주울 수 있습니다(자세한 내용은 6장).
4.3 레지스트리 ── 작은 플래그와 Windows 연동 정보만
레지스트리가 적절한 것은 「설치되어 있는지 여부」「시작 프로그램 등록」「파일 연결」 등 Windows 자체와의 연동 정보와, 아주 작은 사용자 설정까지입니다. 앱이 스스로 사용하는 설정은 HKCU 아래, 머신 공통 정보는 설치 프로그램이 HKLM에 쓰는 것이 원칙입니다(실행 중에 HKLM에 쓰는 설계는 관리자 권한을 요구하게 되므로 피합니다).
가장 큰 함정은 비트 수입니다. 64bit Windows에서는 32bit 프로세스에서 보는 HKLM\Software가 HKLM\Software\Wow6432Node로 리다이렉트됩니다.3 「레지스트리 편집기에서는 값이 있는데 앱에서는 읽을 수 없다」「32bit 앱으로 쓴 값이 64bit 유지보수 도구에서 보이지 않는다」는 증상은 거의 이것입니다. AnyCPU로 전환하거나 64bit화하는 시점에 드러나므로, COM이나 ActiveX의 32bit/64bit 문제(「COM 컴포넌트나 OCX / ActiveX 개발에서 빠지기 쉬운 것 - Visual Studio의 32bit / 64bit, 등록, 관리자 권한의 덫을 정리」)와 같은 맥락에서 알아 두세요.
.NET에서 아무래도 다른 비트의 뷰를 읽어야 하는 경우(32bit로 유지보수 중인 앱이 64bit 쪽에 등록된 값을 읽는 등)에는 RegistryView로 뷰를 명시적으로 지정할 수 있습니다.
using Microsoft.Win32;
// 32bit 프로세스에서 64bit 뷰의 HKLM을 읽는다
using var hklm64 = RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine, RegistryView.Registry64);
using var key = hklm64.OpenSubKey(@"SOFTWARE\KomuraSoft\MyApp");
var installDir = key?.GetValue("InstallDir") as string;
반대로 말하면, 이 지정이 필요해진 시점에서 이미 「32bit와 64bit 중 어느 쪽에 쓰는 것이 맞는가」라는 설계 판단을 미루고 있다는 신호이기도 합니다. 쓰는 쪽과 읽는 쪽의 비트 수를 맞추는 것이 정석입니다.
몇 KB를 넘는 데이터나 배열 형태의 데이터를 레지스트리에 넣는 것은 백업・이전・진단 어느 측면에서도 불리합니다. 그런 용도는 파일(JSON/SQLite)에 맡기세요.
4.4 Access(.accdb) ── 신규 채택은 거의 없음, 기존 연동은 선을 긋고
과거 업무 앱의 로컬 DB라면 Access(JET/ACE)였지만, 신규 개발에서 선택할 이유는 지금은 거의 없습니다. 이유는 주로 배포입니다. .accdb에 코드로 접근하려면 ACE(Access Database Engine) 공급자가 필요하며, 앱의 비트 수와 ACE의 비트 수가 일치하지 않으면 연결할 수 없습니다.4 Office의 비트 수와의 공존 문제도 있어서, 「개발 머신에서는 동작하는데 고객사에서는 Microsoft.ACE.OLEDB.12.0 공급자가 로컬 컴퓨터에 등록되어 있지 않습니다가 발생한다」는 흔한 지원 사례입니다. 재배포 가능 패키지(Access Database Engine 2016 Redistributable) 설치가 필요해지는 점도 배포물을 늘립니다.9
그래도 Access가 관련되는 상황은 실제로 있습니다. 기존 Access 업무 시스템과의 데이터 연동, Access로 만들어진 마스터 데이터 읽기 등입니다. 그런 경우에는,
- 읽고 쓰는 프로세스의 비트 수를 고정하고(x86 고정이 현실적인 경우가 많음), 설치 프로그램에서 해당 ACE의 존재를 확인한다
- 공유 폴더에 둔 .accdb에 여러 사람이 동시에 쓰는 것은 설계상 피한다(손상되었을 때 복구 비용이 맞지 않습니다)
- 장기적으로는 SQLite나 서버형 DB로의 이전 경로를 확보해 둔다
라는 선 긋기를 추천합니다. Excel/VBA 자산을 포함한 기존 자산을 다루는 방법은 「VBA란 무엇인가 - 제약, 장래성, 교체해야 할 장면과 현실적인 이행 패턴」에서도 정리했습니다.
5. 판단표
| 관점 | JSON 파일 | SQLite | 레지스트리 | Access (.accdb) |
|---|---|---|---|---|
| 잘하는 데이터 | 작은 설정 | 늘어나는 구조화 데이터, 검색・집계 | 작은 플래그, Windows 연동 | 기존 Access 자산과의 연동 |
| 데이터량 기준 | ~수백 KB | ~수십 GB | ~수 KB | ~2GB(사양 상한) |
| 검색・집계 | ✕(전체 읽기 필요) | ◎(SQL) | ✕ | ○(SQL) |
| 사람이 직접 읽을 수 있음 | ◎ | △(도구 필요) | △ | △(Access 필요) |
| 손상 내구성 | △(직접 대책) | ○(트랜잭션) | ○ | △ |
| 여러 프로세스 동시 접근 | ✕ | ○(같은 머신 내) | ○ | △ |
| 여러 머신에서 공유 | ✕ | ✕ | ✕ | ✕(사실상) |
| 추가 배포물 | 없음 | 없음(NuGet에 포함) | 없음 | ACE 공급자 필수 |
마지막 행이 보여주듯, 로컬 저장 기술은 어느 것도 「여러 머신에서 공유」에는 적합하지 않습니다. 공유 폴더에 두면 공유할 수 있을 것처럼 보이지만, JSON은 배타 제어가 없고, SQLite는 SMB 상의 잠금을 신뢰할 수 없으며, Access는 손상 위험과 함께 한계가 옵니다. 여러 거점・여러 사용자가 같은 데이터를 다뤄야 하는 요건이 생긴다면, SQL Server Express 같은 서버형 DB나 Web API를 세우는 것을 판단 기준으로 삼으세요.
6. 손상되지 않는다・이전할 수 있다・되돌릴 수 있다 ── 형식과 무관한 공통 설계
어떤 형식을 선택하든, 몇 년간 운영한다면 반드시 필요해지는 설계가 3가지 있습니다. 첫 릴리스에 넣어 두는지 여부로 이후 유지보수 비용이 크게 달라지는 부분입니다.
6.1 스키마・형식에 버전 번호를 붙인다
앱을 업데이트하면 저장하는 데이터의 형태도 바뀝니다. 「오래된 버전이 쓴 데이터를 새 버전이 읽는」 순간은 반드시 오므로, 데이터 쪽에 형식 버전을 갖게 해 둡니다.
SQLite라면 PRAGMA user_version이 바로 이 용도를 위해 준비되어 있습니다.
int GetVersion(SqliteConnection conn)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "PRAGMA user_version";
return Convert.ToInt32(cmd.ExecuteScalar());
}
void Migrate(SqliteConnection conn)
{
void Exec(string sql)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
}
var v = GetVersion(conn);
if (v > 2)
// 새 버전 앱이 만든 DB를 예전 앱으로 연 경우.
// 모르는 스키마를 건드리지 않고 여기서 멈추는 것이 안전
throw new InvalidOperationException(
$"이 데이터베이스(버전 {v})는 더 새로운 앱에서 생성되었습니다.");
using var tx = conn.BeginTransaction();
if (v < 1) Exec("ALTER TABLE measurement ADD COLUMN unit TEXT");
if (v < 2) Exec("CREATE TABLE operator (id INTEGER PRIMARY KEY, name TEXT)");
Exec("PRAGMA user_version = 2");
tx.Commit();
}
시작할 때 버전을 확인해서 차이만 적용하는, 이른바 마이그레이션의 최소 형태입니다. 앞부분에서 「자기보다 새로운 버전」을 거부하는 것은, 앱을 예전 버전으로 되돌렸을(롤백했을) 때 예전 코드가 모르는 스키마에 써서 망가뜨리는 사고를 막기 위해서입니다. JSON에서도 사고방식은 같아서, 루트에 "version": 2를 두고, 읽을 때 예전 형식에서의 변환을 끼워 넣으며, 너무 새로운 형식은 읽기를 거부합니다. 「버전 번호가 없는 데이터 형식을 세상에 내놓지 않는다」, 이것만 지키면 미래의 자신이 구원받습니다.
6.2 손상 시의 축소 동작을 정해 둔다
4장에서 형식별 손상 대책(JSON의 원자적 쓰기, SQLite의 트랜잭션)을 다뤘지만, 그래도 「읽을 수 없는 데이터」는 만나게 됩니다. 디스크 장애, 백신 소프트웨어의 오탐으로 인한 격리, 사용자의 직접 편집. 그럴 때 앱이 어떻게 동작할지 정해 두지 않으면, 시작조차 되지 않는 앱이 됩니다.
- 설정을 읽을 수 없음 → 기본값으로 시작하고 그 사실을 사용자에게 알린다(조용히 기본값으로 하면 「설정이 사라졌다」는 문의로 이어집니다)
- 업무 데이터를 읽을 수 없음 → 읽기 전용 모드나 오류 화면에서 「어느 파일이 손상되었는지」를 제시한다. 자동으로 덮어써서 복구하지 않는다(증거가 사라집니다)
- 백업이 있음 → 복원을 제안한다. 다만 자동 복원은 「손상되었다고 오탐해서 오래된 데이터로 되돌리는」 위험과 뗄 수 없으므로, 원칙적으로 사용자 조작을 거치게 한다
6.3 백업은 「되어 있는가」보다 「되돌릴 수 있는가」
로컬 데이터는 서버의 데이터베이스와 달리 아무도 백업해 주지 않습니다. 앱 쪽에서 챙기려면 다음 3가지를 정합니다.
- 무엇을: 업무 데이터는 대상, 캐시는 대상 외, 기밀 정보는 DPAPI의 특성상 같은 사용자・같은 머신에서만 복호화할 수 있다는 점을 고려(다른 머신으로 이전하는 절차가 별도로 필요)
- 언제・어디로: 시작할 때나 매일,
%LOCALAPPDATA%안의backup폴더로 세대를 붙여서. 추가로 공유 폴더나 기존 PC 백업 대상 폴더에 올릴지는 운영과 상담 - 어떻게: SQLite는 실행 중에 단순 파일 복사 금지.
VACUUM INTO 'backup.db'라면 일관성 있는 스냅샷을 한 문장으로 얻을 수 있습니다
// VACUUM INTO는 상위 폴더를 만들지 않으며, 출력 대상이 이미 있으면 오류가 된다.
// 폴더 생성과 중복되지 않는 파일명 결정을 먼저 끝내 둔다
var backupDir = Path.Combine(AppPaths.DataDir, "backup");
Directory.CreateDirectory(backupDir);
var backupPath = Path.Combine(backupDir, $"app-{DateTime.Now:yyyyMMdd-HHmmss}.db");
using var cmd = conn.CreateCommand();
cmd.CommandText = "VACUUM INTO $path";
cmd.Parameters.AddWithValue("$path", backupPath);
cmd.ExecuteNonQuery();
세대가 계속 쌓이므로, 백업 후에 「최근 N세대만 남기고 오래된 것을 지우는」 처리도 함께 넣어 둡니다.
그리고 한 번은 복원 리허설을 해 보세요. 백업 파일은 있는데 되돌리는 방법을 아무도 모르고, 시도해 본 적도 없다는 것은 업무 시스템에서 「흔히 있는 일」입니다. PC를 교체할 때 데이터를 새 단말로 옮기는 절차서를 써 보면, 백업 설계의 구멍(DPAPI로 보호한 자격 증명이 옮겨지지 않는다, 경로에 사용자 이름이 포함되어 있어서 다른 사용자에서는 깨진다 등)이 대체로 발견됩니다. PC를 폐기할 때 데이터를 지우는 방법은 「Windows PC를 폐기하기 전에 해 두어야 할 것」도 참고하세요.
7. 헷갈리기 쉬운 경우의 지침
- 「설정인데 앞으로 늘어날 것 같다」 ── 시작할 때 전부 읽는 방식이 무너질 가능성이 있다면, 처음부터 SQLite로 합니다. SQLite에 「settings 테이블」을 만드는 것은 전혀 나쁜 일이 아닙니다.
- 「INI/XML에서 이전」 ── 형식만 바꾸는 거라면 JSON으로, 그 타이밍에 이력 계열 데이터가 섞여 있다면 분리해서 SQLite로. 읽는 쪽에 예전 형식으로의 폴백을 1~2버전 남겨 두면 이전이 안전합니다.
- 「Excel로 보고 싶다는 요청을 받는다」 ── 데이터 저장소를 Excel/Access로 하는 것이 아니라, SQLite에 저장하고 CSV/Excel로 내보내는 기능을 붙이는 쪽이 데이터의 신뢰성과 요청 양쪽을 모두 만족시킬 수 있습니다. 보고서 출력을 만드는 방법은 「Excel 장표 출력을 어떻게 만들까 - COM 자동화 / Open XML / 템플릿 방식의 판단표」를 참고하세요.
- 「여러 프로세스에서 같은 파일을 읽고 쓰고 싶다」 ── 같은 머신 안이라면 SQLite(WAL)로 꽤 버틸 수 있지만, 쓰기 경합에 대한 설계가 필요합니다. 파일 기반으로 연동한다면 「파일 연계의 배타 제어 기초 지식 - 파일 락과 원자적 claim의 베스트 프랙티스」의 배타 패턴을 사용하세요.
- 「여러 머신에서 공유하고 싶다」 ── 로컬 저장의 졸업입니다. 첫 번째 선택지는 SQL Server Express(무료, 데이터베이스 10GB까지)를 파일 서버급 머신에 세우는 클라이언트・서버 구성입니다. 참고로 SQL Server 「LocalDB」는 이름과 달리 개발 용도를 위한 단일 사용자 환경이므로 공유 목적으로는 선택하지 마세요. 거점을 넘나든다거나 회사 밖에서도 쓴다면 Web API를 끼우는 구성을 검토할 시점입니다.
8. 정리
저장 위치를 고르는 것은, 「어디에 둘 것인가」(LocalAppData/ProgramData, 그리고 Program Files에는 쓰지 않는다)와 「무엇으로 저장할 것인가」(설정은 JSON, 늘어나는 데이터는 SQLite, 레지스트리는 최소한으로, Access는 기존 연동에만)로 나누어 생각하면, 대부분의 경우 헤매지 않고 정할 수 있습니다.
그 위에서, 형식과 무관하게 6장의 3종 세트──형식 버전 번호, 손상 시의 축소 동작, 되돌릴 수 있는 백업──를 첫 릴리스에 넣어 둘 것. 기밀 정보만큼은 항상 별도로 DPAPI로. 이 글의 판단표와 공통 설계를 잡아 두면, 「수십 MB로 커진 JSON」「일주일에 한 번씩 손상되는 공유 Access」 같은, 나중에 비싸지는 구성은 거의 피할 수 있습니다. 기존 앱의 저장 방식이 불안하다면, 먼저 「무엇이 어디에 저장되어 있는지 파악하는 것」부터 시작하는 것을 추천합니다.
관련 글
- Windows 앱에서 설정 파일에 기밀 정보를 평문으로 저장하지 않기 위한 베스트 프랙티스
- 파일 연계의 배타 제어 기초 지식 - 파일 락과 원자적 claim의 베스트 프랙티스
- Excel 장표 출력을 어떻게 만들까 - COM 자동화 / Open XML / 템플릿 방식의 판단표
- 작업 스케줄러로 예약 작업을 안전하게 운영하는 방법
관련 상담 분야
합동회사 코무라소프트에서는 업무 앱의 데이터 저장 방식 재검토(INI/XML/Access로부터의 이전 설계를 포함)와, 데이터 손상・성능 저하의 원인 조사를 다루고 있습니다.
참고 자료
-
Microsoft Learn, KNOWNFOLDERID. LocalAppData, RoamingAppData, ProgramData 등 Windows의 알려진 폴더(Known Folder) 정의에 대해. ↩
-
Microsoft Learn, Microsoft.Data.Sqlite overview. Microsoft가 유지 관리하는 SQLite용 ADO.NET 공급자의 개요와, EF Core SQLite 공급자의 기반이 된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Registry Redirector. 64bit Windows에서 32bit 프로세스의 레지스트리 접근이 Wow6432Node로 리다이렉트되는 원리에 대해. ↩ ↩2
-
Microsoft Learn, Can’t establish a connection to Access Database Engine OLE DB. ACE OLE DB 공급자는 접근하는 프로세스와 비트 수가 일치해야 한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Environment.SpecialFolder Enum. .NET에서 알려진 폴더를 가져오기 위한 열거형에 대해. ↩
-
Microsoft Learn, Use a SQLite database in a Windows app. Windows 앱의 로컬 데이터 저장에 SQLite와 Microsoft.Data.Sqlite / EF Core를 권장하는 공식 튜토리얼. ↩
-
SQLite, How To Corrupt An SQLite Database File. 네트워크 파일 시스템 상에서의 잠금 불량이 데이터베이스 손상의 주요 원인이라는 점에 대해. ↩
-
Microsoft Learn, Data types (Microsoft.Data.Sqlite). SQLite의 4가지 기본 타입과, DateTime이나 Guid가 TEXT로 매핑되는 규칙에 대해. ↩
-
Microsoft, Microsoft Access Database Engine 2016 Redistributable. .accdb / .mdb에 접근하기 위한 ACE 재배포 가능 패키지(32bit/64bit)에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows의 프로세스 간 통신을 어떻게 선택할까 ── 네임드 파이프 / TCP / gRPC / 공유 메모리 / COM 판단표
Windows 앱 사이의 연동 수단을 어떻게 선택할지 정리합니다. 네임드 파이프, 로컬 TCP, gRPC, 공유 메모리, 파일 연동, COM 각각의 강점과 함정을 판단표로 정리하고, UI+서비스 분리・32bit/64bit 브리지・권한 경계 같은 ...
C#에서 SQLite를 업무 앱에 쓰는 법 ── WAL 모드・배타 제어・손상 대책・EF Core와의 구분
Microsoft.Data.Sqlite로 SQLite를 업무용 Windows 앱에 통합하는 실무 지식을 정리합니다. 연결 문자열과 풀링, WAL 모드의 원리, SQLITE_BUSY와 쓰기 경로 단일화, 트랜잭션을 이용한 고속화, 타입 매핑의 함정...
Windows의 세션 분리를 어떻게 이해할까 ── Session 0・RDP・다중 사용자 동시 실행
Windows 앱 개발자가 혼란스러워하기 쉬운 '세션' 개념을 정리합니다. 서비스가 UI를 표시할 수 없는 Session 0 분리의 이유, RDP 접속 시 세션의 동작, 명명된 개체의 세션 분리, 공유 PC・RDS 환경에서 흔히 발생하는 설계 실...
Windows 서비스 만드는 방법과 운영 ── 태스크 스케줄러와의 구분 사용부터 BackgroundService의 서비스화까지
상주 처리를 Windows 서비스로 만들어야 할지, 태스크 스케줄러로 충분한지를 판단표로 정리하고, .NET Worker Service(UseWindowsService)를 이용한 서비스 제작 방법, 실행 계정・복구 옵션・이벤트 로그・안전한 정지 ...
Windows 앱의 중복 실행 방지 ── 네임드 Mutex와 재실행 시 창 활성화
업무용 Windows 앱의 정석 요건인 「같은 앱을 두 번 실행시키지 않는다」를 네임드 Mutex로 구현하는 방법을 정리합니다. Global\ 과 Local\ 네임스페이스의 차이로 인한 RDP 환경의 함정, 소유 스레드 제약과 AbandonedM...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Windows 앱의 설정 파일은 어디에 저장해야 하나요?
- 사용자별 설정・데이터라면 %LOCALAPPDATA%(Environment.SpecialFolder.LocalApplicationData) 아래에 「회사명\앱 이름」 계층으로 두는 것이 기본입니다. 모든 사용자가 공유한다면 %PROGRAMDATA%이지만, 기본 ACL로는 다른 사용자가 변경할 수 없는 구조가 될 수 있으므로 설치 프로그램에서 ACL을 명시적으로 설정합니다. exe와 같은 폴더(Program Files 아래)에는 쓰면 안 됩니다. 표준 사용자는 쓸 수 없고, 오래된 32bit 앱에서는 VirtualStore로 조용히 리다이렉트되어 이해하기 어려운 증상의 원인이 됩니다.
- 설정은 JSON과 SQLite 중 어느 쪽에 저장해야 하나요?
- 구조화된 작은 설정은 JSON 파일, 계속 늘어나는 업무 데이터・이력・검색하고 싶은 데이터는 SQLite가 첫 번째 선택지이며, 이 두 가지로 업무 앱의 로컬 저장 대부분을 커버할 수 있습니다. JSON의 적용 범위는 「시작 시 전부 읽고 종료 시 전부 쓰기」가 성립하는 수백 KB까지가 기준입니다. 계속 추가되는 이력이나 레코드 검색이 필요한 데이터를 JSON에 넣기 시작했다면, SQLite로 옮겨야 한다는 신호입니다. 설정이라도 앞으로 늘어날 가능성이 있다면, 처음부터 SQLite에 settings 테이블을 만드는 형태로 해도 문제없습니다.
- SQLite 데이터베이스를 네트워크 공유 폴더에 두어도 되나요?
- 피해야 합니다. SMB를 통한 파일 잠금은 환경에 따른 문제가 많고, SQLite 공식 문서에서도 네트워크 파일 시스템 상에서의 공유를 손상 원인 1순위로 꼽고 있습니다. JSON은 배타 제어가 없고, Access도 손상 위험과 함께 한계가 오므로, 로컬 저장 기술은 어느 것도 여러 대의 컴퓨터에서 공유하는 데는 적합하지 않습니다. 여러 거점・여러 사용자가 같은 데이터를 다뤄야 하는 요건이 생긴다면, SQL Server Express(무료, 데이터베이스 10GB까지) 같은 서버형 DB나 Web API를 세우는 것을 판단 기준으로 삼으세요.
- 레지스트리에 앱 데이터를 저장해도 되나요?
- 레지스트리가 적합한 것은 시작 프로그램 등록이나 파일 연결처럼 Windows 자체와의 연동 정보, 그리고 아주 작은 사용자 설정까지입니다. 몇 KB를 넘는 데이터나 배열 형태의 데이터는 백업・이전・진단 어느 측면에서도 불리하므로 JSON이나 SQLite에 맡깁니다. 또한 64bit Windows에서는 32bit 프로세스에서 보는 HKLM\Software가 Wow6432Node로 리다이렉트되기 때문에, 「레지스트리 편집기에서는 값이 보이는데 앱에서는 읽을 수 없다」는 증상의 원인이 됩니다. 쓰는 쪽과 읽는 쪽의 비트 수를 맞추는 것이 정석입니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크