MSSQL LAB
MSSQL 6.4 · 6부. 운영과 실전

데이터베이스 프로젝트

SSDT(.sqlproj)로 스크립트를 형상 관리에 둡니다.

예상 학습 시간 20분 난이도 중급
문제

지금 스키마가 어디에 있습니까

여기까지 만든 표·프로시저·뷰는 데이터베이스 안에만 있습니다. 그것이 문제입니다.

묻는 것데이터베이스만 있으면
이 프로시저를 누가 언제 왜 고쳤습니까알 수 없습니다
개발 서버와 운영 서버가 같습니까하나씩 열어 봐야 합니다
지난달 상태로 되돌릴 수 있습니까백업을 복원하는 수밖에 없습니다
새 서버에 똑같이 만들 수 있습니까손으로 옮겨야 합니다

스키마도 코드입니다. C# 파일과 똑같이 파일로 두고 형상 관리에 넣어야 위 질문에 답할 수 있습니다.

개념 설명

SSDT 프로젝트

SSDT(SQL Server Data Tools) 는 데이터베이스를 Visual Studio 프로젝트로 다루게 해 줍니다. 확장자는 .sqlproj 입니다.

핵심은 개체 하나에 파일 하나입니다. 표 Board.POSTSBoard.POSTS.sqlCREATE TABLE 문 하나로 들어 있습니다. ALTER TABLE 을 적지 않습니다.

폴더 구조 — 실제로 돌고 있는 프로젝트입니다
MAYYANET/
├─ MAYYANET.sqlproj              -- 프로젝트 파일
├─ MAYYANET.publish.xml          -- 운영 게시 프로파일
├─ TEST.publish.xml              -- 시험 게시 프로파일
├─ Schema Objects/
│  ├─ Database Level Objects/
│  │  └─ Security/Schemas/       -- Blog.sql · Member.sql …
│  └─ Schemas/
│     ├─ Blog/
│     │  ├─ Tables/              -- Blog.POST.sql …
│     │  └─ Programmability/
│     │     ├─ Stored Procedures/
│     │     │  ├─ 목록/          -- Blog.P_POST_LIST.sql …
│     │     │  ├─ 조회/
│     │     │  ├─ 저장/
│     │     │  └─ 삭제/
│     │     └─ Sequence/
│     ├─ Member/
│     └─ …
└─ Scripts/
   ├─ Pre-Deployment/            -- 배포 전에 한 번
   ├─ Post-Deployment/           -- 배포 뒤에 한 번
   └─ Migration/                 -- 손으로 한 번만 돌리는 것
규모
무엇개수
스키마 개체 파일341
데이터베이스 수준 개체8
.sql 파일 전체579
스키마별로 나누고 그 안에서 개체 종류로 나눕니다. 프로시저는 하는 일로 한 번 더 나눴습니다.

폴더 이름을 한글로 두어도 됩니다. 위 프로젝트는 프로시저를 목록 · 조회 · 저장 · 삭제로 나눠 두었습니다. 개체가 수백 개가 되면 이름만으로 찾기 어려워지므로 하는 일로 나누는 편이 낫습니다.

핵심

차이를 스스로 셈합니다

이것이 SSDT 의 핵심이자 가장 큰 차이점입니다. 배포할 때 "무엇을 바꿔라" 를 적지 않습니다. "이렇게 되어 있어야 한다" 만 적으면 도구가 지금 서버와 견주어 차이를 셈하고 필요한 문장을 만듭니다.

마이그레이션 방식SSDT (차이 기반)
적는 것바꾸는 문장(ALTER)최종 모습(CREATE)
파일이 쌓이는 방향변경마다 새 파일개체 파일 하나를 고침
지금 스키마를 보려면전부 순서대로 읽어야그 파일을 열면 됩니다
차이 문장사람이 적습니다도구가 만듭니다
되돌리기되돌리는 스크립트를 적어 둠파일을 되돌리고 다시 게시
파일에는 이렇게만 적습니다
-- Board.POSTS.sql — 열을 하나 더하고 싶다면
-- ALTER TABLE 을 적는 것이 아니라 이 CREATE 문을 고칩니다.
CREATE TABLE Board.POSTS (
    num         int IDENTITY(1,1) NOT NULL,
    board_num   int NOT NULL,
    title       nvarchar(200) NOT NULL,
    view_count  int NULL,          -- ← 이 줄을 더합니다
    CONSTRAINT PK_POSTS PRIMARY KEY CLUSTERED (num)
);
도구가 만드는 배포 문장
ALTER TABLE [Board].[POSTS] ADD [view_count] INT NULL;
사람은 최종 모습만 적고, ALTER 는 도구가 만듭니다.

덕분에 "지금 스키마가 무엇인가" 를 한 파일에서 볼 수 있습니다. 마이그레이션 방식은 변경 파일이 백 개 쌓이면 지금 모습을 알려고 백 개를 순서대로 읽어야 합니다.

차이 기반이라 조심할 것

도구가 만든 문장을 그대로 믿으면 안 됩니다. 6.3 에서 본 대로 비슷하게 생긴 변경이 값은 수백 배 다릅니다.

생기는 일까닭
표를 통째로 다시 만듭니다열 순서나 형식이 바뀌면 임시 표로 옮겼다 되돌립니다
자료를 잃을 수 있습니다열을 지우거나 형식을 좁히면 그렇습니다
기본값을 채우느라 오래 걸립니다NOT NULL 열을 더하면 그렇습니다(6.3)

그래서 배포 전에 스크립트를 만들어 읽어야 합니다. 게시 프로파일에 자료를 잃을 수 있으면 멈추도록 설정해 둘 수도 있습니다.

게시 프로파일에 두는 것들
<!-- 자료를 잃을 수 있으면 배포하지 않습니다 -->
<BlockOnPossibleDataLoss>True</BlockOnPossibleDataLoss>

<!-- 프로젝트에 없는 개체를 지울지 -->
<DropObjectsNotInSource>False</DropObjectsNotInSource>

<!-- 배포 전 백업을 뜰지 -->
<BackupDatabaseBeforeChanges>True</BackupDatabaseBeforeChanges>

<!-- 한 트랜잭션으로 묶을지 (6.3 의 Sch-M 을 떠올리십시오) -->
<IncludeTransactionalScripts>True</IncludeTransactionalScripts>

DropObjectsNotInSource 를 켤 때는 특히 조심하십시오. 프로젝트에 없는 것을 전부 지웁니다 — 누군가 운영에서 직접 만든 인덱스나 프로시저가 말없이 사라집니다. 반대로 끄면 지운 개체가 서버에 남습니다. 어느 쪽이든 배포 스크립트를 읽고 확인해야 합니다.

스크립트

차이로 안 되는 것들

자료는 차이로 셈할 수 없습니다. 코드 목록을 채우거나 옛 자료를 옮기는 일은 사람이 적어야 합니다. 그래서 자리가 셋 있습니다.

자리언제 돕니까담는 것
배포 전(Pre-Deployment)차이 문장보다 먼저미리 치워야 할 것
배포 후(Post-Deployment)차이 문장보다 나중코드 목록·기본 자료
마이그레이션사람이 한 번만되돌릴 수 없는 자료 이동
배포 후 스크립트 — 실제 프로젝트에서
PRINT N'마이그레이션을 시작합니다.';
GO

-- 전화번호 입력의 국가번호 목록(멱등 — 이미 있으면 건너뜁니다).
:r .\Common\Common.CODE_PHONECOUNTRY.sql

-- 회원 탈퇴 사유 목록(멱등 — 이미 있으면 건너뜁니다).
:r .\Common\Common.CODE_WITHDRAWREASON.sql

-- 판매 제품·에디션·버전 첫 한 벌(멱등 — 이미 있으면 건너뜁니다).
-- 그 뒤로는 관리자 화면이 원천이라 여기서 덮어쓰지 않습니다.
:r .\Product\Product.PRODUCT.sql
읽을 것
:r 은 SQLCMD 문법입니다. 그 파일을 여기에 펼쳐 넣습니다. 스크립트 하나가 길어지지 않게 나눠 두는 방법입니다. 주석에 "멱등" 이라고 적어 둔 것에 주목하십시오. 배포는 몇 번이든 다시 돕니다(6.3).
배포 후 스크립트는 배포할 때마다 실행됩니다. 반드시 몇 번을 돌려도 같아야 합니다.
멱등하게 적는 법
-- 없으면 넣고 있으면 고칩니다(2.10 의 MERGE).
MERGE Common.CODE_WITHDRAWREASON AS t
USING (VALUES
    (1, N'서비스를 사용하지 않습니다'),
    (2, N'다른 서비스를 사용합니다'),
    (3, N'개인 정보가 걱정됩니다')
) AS s (num, name) ON t.num = s.num
WHEN MATCHED AND t.name <> s.name THEN UPDATE SET name = s.name
WHEN NOT MATCHED BY TARGET THEN INSERT (num, name) VALUES (s.num, s.name);

-- 없을 때만 넣는 것이 나을 때도 있습니다.
-- 관리자 화면에서 고칠 수 있는 자료라면 덮어쓰면 안 됩니다.
IF NOT EXISTS (SELECT 1 FROM Product.PRODUCT)
    INSERT INTO Product.PRODUCT (…) VALUES (…);

배포 후 스크립트가 덮어써도 되는 자료인지 먼저 정하십시오. 코드 목록처럼 개발이 정하는 것은 MERGE 로 맞추고, 관리자 화면에서 고치는 자료는 처음 한 번만 넣고 그 뒤로는 건드리지 않습니다.

한 번만 돌려야 하는 것

이미 쌓인 자료를 고치는 일은 배포 스크립트에 넣지 않습니다. 따로 두고 사람이 한 번만 실행합니다.

Scripts/Migration/ — 날짜와 내용으로 이름을 붙입니다
2026.08.09_IDS_소셜통합.sql
2026.08.09_PHONE_E164.sql
2026.08.16_IBANK_CATE.sql
2026.08.19_EDITION_표기정리.sql
2026.08.19_LICENSE_키지문.sql
2026.08.19_LICENSE_폐기단계.sql
파일 머리에 적어 두는 것
1. 작성일 2026.08.19 2. 작성자 MAYYA 3. 설명 이미 발급한 라이선스 키의 지문(key_hash) 채우기 4. 사용처 운영 데이터베이스에 한 번만 실행합니다. 스키마 게시를 마친 뒤에 실행하십시오 — key_hash 열이 있어야 합니다. 5. 사용방법 SSMS 에서 연결한 뒤 통째로 실행하십시오. 여러 번 돌려도 안전합니다 — 이미 채워진 행은 건드리지 않습니다. 6. 주의사항 셈하는 식이 저장 프로시저와 같아야 합니다. …
언제·왜·어떤 순서로 돌려야 하는지를 파일 안에 적어 둡니다. 몇 달 뒤에 보는 사람이 알 수 있어야 합니다.

"한 번만" 이라도 멱등하게 적으십시오. 중간에 끊겨 다시 돌리는 일이 반드시 생깁니다(6.3). 위 파일도 "이미 채워진 행은 건드리지 않습니다" 라고 적혀 있습니다.

참조

다른 데이터베이스를 가리킬 때

프로시저가 LOGS.Common.ADMIN 처럼 다른 데이터베이스를 가리키면, 프로젝트는 그 이름을 알지 못해 빌드 오류를 냅니다.

그 데이터베이스도 프로젝트로 두고 참조를 걸어 이름을 변수로 둡니다.

.sqlproj — 실제 선언
<ProjectReference Include="..\LOGS\LOGS.sqlproj">
  <Name>LOGS</Name>
  <SuppressMissingDependenciesErrors>True</SuppressMissingDependenciesErrors>
  <DatabaseSqlCmdVariable>LOGS</DatabaseSqlCmdVariable>
</ProjectReference>

<SqlCmdVariable Include="LOGS">
  <DefaultValue>LOGS</DefaultValue>
</SqlCmdVariable>
그러면 SQL 에는 이렇게 적습니다
INSERT INTO [$(LOGS)].Common.ADMIN (…) VALUES (…);
이름을 직접 적지 않고 변수로 둡니다.

변수로 두면 환경마다 다른 이름을 넣을 수 있습니다. 실제 게시 프로파일에는 이렇게 들어 있습니다.

프로파일TargetDatabaseName$(LOGS)
MAYYANET.publish.xmlMAYYANETLOGS
TEST.publish.xmlMAYYANET_TESTLOGS_TEST

같은 프로젝트로 운영과 시험에 게시할 수 있습니다. 프로파일만 갈아 끼우면 됩니다. 연결 문자열도 프로파일에 들어 있으니 운영 프로파일은 아무나 열지 못하게 두십시오.

시스템 개체(sys.databases 같은 것)를 가리키면 master.dacpac 을 참조로 걸어야 합니다. 위 프로젝트에도 ArtifactReference 로 들어 있습니다.

배포

빌드하고 게시합니다

명령
-- 빌드하면 .dacpac 하나가 나옵니다. 스키마 전체가 그 안에 들어 있습니다.
msbuild MAYYANET.sqlproj /p:Configuration=Release

-- 배포 스크립트만 만들어 봅니다. 실행하지는 않습니다.
sqlpackage /Action:Script ^
    /SourceFile:bin\Release\MAYYANET.dacpac ^
    /Profile:TEST.publish.xml ^
    /OutputPath:deploy.sql

-- 읽어 보고 이상이 없으면 게시합니다.
sqlpackage /Action:Publish ^
    /SourceFile:bin\Release\MAYYANET.dacpac ^
    /Profile:TEST.publish.xml

-- 무엇이 다른지만 보고 싶을 때
sqlpackage /Action:DeployReport ^
    /SourceFile:bin\Release\MAYYANET.dacpac ^
    /Profile:TEST.publish.xml ^
    /OutputPath:report.xml
배포 보고서 — 실제로 나온 것
** 강조 마이그레이션된 데이터로 다시 만들 테이블 없음 데이터 문제가 있는 것 같습니다. 없음 ** 사용자 작업 삭제 [Blog].[CK_CATEGORY_usable] (CHECK 제약 조건) [Blog].[CK_COMMENT_usable] (CHECK 제약 조건) …
"다시 만들 테이블" 과 "데이터 문제" 를 먼저 보십시오. 여기에 이름이 있으면 시간이 오래 걸리거나 자료를 잃습니다.

반드시 /Action:Script 로 먼저 만들어 읽으십시오. 6.3 에서 본 위험한 변경이 섞여 있는지 그 자리에서 드러납니다. 읽지 않고 게시하는 것이 이 방식의 가장 큰 위험입니다.

차이 기반이라 "지금 서버가 어떤 상태인가" 에 따라 결과가 달라집니다. 개발 서버에서 만든 스크립트가 운영에서도 같으리라는 보장이 없습니다. 게시할 서버에서 스크립트를 다시 만들어 읽으십시오.

연습

직접 해보기

1. 실습 데이터베이스를 프로젝트로 만듭니다 난이도 하

MssqlLab 을 SSDT 프로젝트로 가져와 보세요. 이미 있는 데이터베이스에서 시작하는 방법을 적으십시오.

-- 방법 (가) Visual Studio 에서 -- 1. 새 프로젝트 → SQL Server 데이터베이스 프로젝트 -- 2. 프로젝트에서 오른쪽 클릭 → 가져오기 → 데이터베이스 -- 3. MssqlLab 에 연결하고 폴더 구조를 고릅니다. -- "스키마와 개체 종류별" 을 고르면 위에서 본 구조가 나옵니다. -- 방법 (나) 명령으로 dacpac 을 뽑아 가져오기 sqlpackage /Action:Extract ^ /SourceServerName:"(localdb)\MSSQLLocalDB" ^ /SourceDatabaseName:MssqlLab ^ /TargetFile:MssqlLab.dacpac -- 가져온 뒤 할 일 -- 1. 빌드해 봅니다. 오류가 나는 것부터 고칩니다. -- 크로스 데이터베이스 참조가 있으면 위에서 본 대로 변수로 바꿉니다. -- 2. 게시 프로파일을 환경마다 만듭니다. -- 3. 형상 관리에 넣습니다. bin · obj 는 제외합니다. -- 4. 자료가 필요한 것을 배포 후 스크립트로 옮깁니다. -- lab-setup.sql 의 INSERT 들이 그 자리에 해당합니다. -- 확인 -- 프로젝트를 빌드한 것과 지금 데이터베이스를 견주어 -- 차이가 없어야 제대로 가져온 것입니다. sqlpackage /Action:DeployReport ^ /SourceFile:bin\Debug\MssqlLab.dacpac ^ /TargetServerName:"(localdb)\MSSQLLocalDB" ^ /TargetDatabaseName:MssqlLab ^ /OutputPath:report.xml -- 보고서가 비어 있으면 같은 상태입니다.
2. 배포가 표를 다시 만들려고 합니다 난이도 중

배포 스크립트를 만들어 읽어 보니 5천만 행짜리 표를 임시 표로 옮겼다 되돌리는 문장이 들어 있습니다. 무엇 때문이고 어떻게 하시겠습니까.

도구는 파일에 적힌 최종 모습대로 맞추려 합니다. 무엇을 맞추려고 표를 다시 만들어야 합니까.
-- 나온 문장은 대개 이런 모양입니다. -- CREATE TABLE [Board].[tmp_ms_xx_POSTS] (…) -- INSERT INTO [Board].[tmp_ms_xx_POSTS] (…) SELECT … FROM [Board].[POSTS] -- DROP TABLE [Board].[POSTS] -- EXEC sp_rename N'[Board].[tmp_ms_xx_POSTS]', N'POSTS' -- → 5천만 행을 통째로 옮깁니다. 그동안 표가 잠깁니다(6.3). -- 왜 생깁니까 -- (가) 열 순서가 다릅니다. -- 파일에서는 세 번째 열인데 서버에서는 마지막입니다. -- 기능은 같지만 도구는 순서까지 맞추려 합니다. -- (다) 열 형식을 좁혔습니다. nvarchar(200) → nvarchar(100) -- (라) NOT NULL 로 조였는데 기본값이 없습니다. -- (마) 계산 열이나 데이터 정렬이 달라졌습니다. -- 무엇을 하겠습니까 -- 1. 무엇 때문인지 찾습니다. -- 스키마 비교(Schema Compare)로 그 표만 견주면 바로 드러납니다. -- 2. 순서 때문이라면 파일의 열 순서를 서버에 맞춥니다. -- 기능이 같으므로 파일을 고치는 편이 낫습니다. -- 새 열은 맨 뒤에 더하는 습관을 들이십시오. -- 3. 정말 필요한 변경이라면 배포에서 빼고 손으로 합니다. -- 프로젝트 파일은 최종 모습으로 고쳐 두고, -- 실제 변경은 6.3 의 넓히기·채우기·좁히기로 나눕니다. -- 그다음 배포에서는 차이가 없으므로 도구가 아무 문장도 만들지 않습니다. -- 4. 게시 프로파일에 안전 장치를 둡니다. -- True -- 이것이 켜져 있으면 자료를 잃을 수 있는 배포는 아예 멈춥니다. -- 배울 것 -- 스크립트를 읽지 않고 게시했다면 운영이 몇 분간 멈췄을 것입니다. -- /Action:Script 로 먼저 만들어 읽는 일을 건너뛰지 마십시오.
요약
  • 스키마도 코드입니다. 데이터베이스 안에만 있으면 누가 언제 왜 고쳤는지 알 수 없고, 되돌릴 수도 없습니다.
  • SSDT 프로젝트(.sqlproj)는 개체 하나에 파일 하나로 둡니다. 파일에는 CREATE 문 하나가 있고 ALTER 를 적지 않습니다.
  • 차이는 도구가 셈합니다. "이렇게 되어 있어야 한다" 만 적으면 지금 서버와 견주어 필요한 문장을 만듭니다.
  • 그래서 지금 스키마를 한 파일에서 볼 수 있습니다. 마이그레이션 방식은 변경 파일을 순서대로 다 읽어야 합니다.
  • 도구가 만든 문장을 그대로 믿지 마십시오. 열 순서 하나가 달라도 표를 통째로 다시 만들 수 있습니다(6.3).
  • 자료는 차이로 셈할 수 없어 배포 전 · 배포 후 · 마이그레이션 세 자리에 사람이 적습니다.
  • 배포 후 스크립트는 배포할 때마다 실행되므로 반드시 멱등해야 합니다. MERGE 로 맞추거나 없을 때만 넣습니다.
  • 다른 데이터베이스를 가리킬 때는 프로젝트 참조와 SqlCmd 변수로 둡니다. 환경마다 다른 이름을 게시 프로파일에서 넣습니다.
  • /Action:Script 로 먼저 만들어 읽으십시오. "다시 만들 테이블" 과 "데이터 문제" 에 이름이 있으면 그대로 게시하면 안 됩니다.
  • BlockOnPossibleDataLossDropObjectsNotInSource환경마다 다르게 두십시오. 운영에서는 막고, 시험에서는 열어 둘 수 있습니다.