{"componentChunkName":"component---src-templates-blog-post-js","path":"/etc/Drizzle-ORM과-PostgreSQL-스키마-마이그레이션-운영-완전-가이드/","result":{"data":{"site":{"siteMetadata":{"title":"Bottlehs Tech Blog"}},"markdownRemark":{"id":"2610924a-d226-545a-9fbb-5d7498441232","excerpt":"Drizzle ORM 가이드 관계형 데이터베이스 위에 TypeScript 서비스를 올릴 때, 개발자는 오랫동안 두 갈래 사이에서 고민해 왔다. 한쪽은 객체–관계 매핑(ORM) 의 편의성이고, 다른 한쪽은 SQL의 예측 가능성과 성능이다. Prisma는 DX(Developer…","html":"<p><img src=\"/assets/etc.png\" alt=\"Drizzle ORM 가이드\" title=\"Drizzle ORM 가이드\"></p>\n<p>관계형 데이터베이스 위에 TypeScript 서비스를 올릴 때, 개발자는 오랫동안 두 갈래 사이에서 고민해 왔다. 한쪽은 <strong>객체–관계 매핑(ORM)</strong> 의 편의성이고, 다른 한쪽은 <strong>SQL의 예측 가능성과 성능</strong>이다. Prisma는 DX(Developer Experience)가 뛰어나지만 런타임과 쿼리 제어 방식이 팀의 취향과 맞지 않을 수 있고, 순수 SQL은 유지보수 부담이 크다. 그 사이에서 <strong>“SQL에 가깝되, 타입으로 끝까지 잡고 싶다”</strong>는 요구를 비교적 잘 만족시키는 선택지가 <strong>Drizzle ORM</strong>이다. 이 글은 Drizzle을 PostgreSQL과 함께 <strong>실제 서비스에 운영</strong>할 때 필요한 내용을 빠짐없이 담았다. 튜토리얼 수준의 짧은 예제가 아니라, <strong>스키마 설계부터 마이그레이션, 배포, 장애 대응</strong>까지 이어지는 흐름을 목표로 했다.</p>\n<h2 id=\"drizzle을-선택하는-이유와-한계\" style=\"position:relative;\"><a href=\"#drizzle%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%ED%95%9C%EA%B3%84\" aria-label=\"drizzle을 선택하는 이유와 한계 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Drizzle을 선택하는 이유와 한계</h2>\n<p>Drizzle의 철학은 단순하다. <strong>테이블 정의가<em>gold source</em>에 가깝고</strong>, 그로부터 TypeScript 타입이 파생된다. SQL과의 거리가 가깝기 때문에, 복잡한 조인·서브쿼리·윈도 함수를 쓸 때 “ORM이 막는 느낌”이 상대적으로 덜하다. 또한 번들 크기와 런타임 특성이 가벼운 편이라, <strong>엣지 런타임</strong>이나 <strong>서버리스</strong> 환경에서도 선택지로 자주 언급된다.</p>\n<p>반면 한계도 분명하다. 생태계가 Prisma만큼 거대하지 않을 수 있고, GUI 기반 스튜디오나 풍부한 플러그인을 기대했다면 실망할 수 있다. <strong>“모든 것을 추상화해 주는 마법”</strong>을 원한다면 Drizzle은 오히려 <strong>SQL을 함께 써야 한다는 전제</strong>를 요구한다. 팀이 SQL을 피하고 싶어 한다면 다른 도구가 나을 수 있다.</p>\n<h2 id=\"프로젝트-구조-권장안\" style=\"position:relative;\"><a href=\"#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EA%B6%8C%EC%9E%A5%EC%95%88\" aria-label=\"프로젝트 구조 권장안 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>프로젝트 구조 권장안</h2>\n<p>실무에서는 보통 다음과 같이 나눈다.</p>\n<ul>\n<li><code class=\"language-text\">src/db/schema/</code> : 테이블·열거형·관계 정의</li>\n<li><code class=\"language-text\">src/db/client.ts</code> : <code class=\"language-text\">drizzle()</code> 인스턴스와 풀</li>\n<li><code class=\"language-text\">drizzle/</code> 또는 <code class=\"language-text\">migrations/</code> : drizzle-kit이 생성한 SQL 마이그레이션</li>\n<li><code class=\"language-text\">drizzle.config.ts</code> : drizzle-kit 설정</li>\n</ul>\n<p>모노레포라면 <strong>패키지 단위로 스키마를 분리</strong>하고, 마이그레이션은 저장소 루트에서 한 번에 관리할지, 서비스별로 나눌지를 팀 규칙으로 정한다. 잘못 나누면 <strong>순환 의존</strong>이나 <strong>마이그레이션 순서</strong> 문제가 생긴다.</p>\n<h2 id=\"스키마-정의-pg-테이블과-타입-안전성\" style=\"position:relative;\"><a href=\"#%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%A0%95%EC%9D%98-pg-%ED%85%8C%EC%9D%B4%EB%B8%94%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84%EC%84%B1\" aria-label=\"스키마 정의 pg 테이블과 타입 안전성 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>스키마 정의: pg 테이블과 타입 안전성</h2>\n<p>Drizzle의 핵심은 <code class=\"language-text\">pgTable</code> 등으로 스키마를 코드로 표현하는 것이다. 예시는 개념 설명용이며, 실제 프로젝트에서는 네이밍·모듈 분리 규칙을 팀에 맞춘다.</p>\n<div class=\"gatsby-highlight\" data-language=\"typescript\"><pre class=\"language-typescript\"><code class=\"language-typescript\"><span class=\"token comment\">// 개념 예시 — 실제 import 경로·버전에 맞게 조정</span>\n<span class=\"token keyword\">import</span> <span class=\"token punctuation\">{</span> pgTable<span class=\"token punctuation\">,</span> serial<span class=\"token punctuation\">,</span> varchar<span class=\"token punctuation\">,</span> timestamp<span class=\"token punctuation\">,</span> uniqueIndex <span class=\"token punctuation\">}</span> <span class=\"token keyword\">from</span> <span class=\"token string\">\"drizzle-orm/pg-core\"</span><span class=\"token punctuation\">;</span>\n\n<span class=\"token keyword\">export</span> <span class=\"token keyword\">const</span> users <span class=\"token operator\">=</span> <span class=\"token function\">pgTable</span><span class=\"token punctuation\">(</span>\n  <span class=\"token string\">\"users\"</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">{</span>\n    id<span class=\"token operator\">:</span> <span class=\"token function\">serial</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"id\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">primaryKey</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    email<span class=\"token operator\">:</span> <span class=\"token function\">varchar</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"email\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> length<span class=\"token operator\">:</span> <span class=\"token number\">320</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">notNull</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n    createdAt<span class=\"token operator\">:</span> <span class=\"token function\">timestamp</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"created_at\"</span><span class=\"token punctuation\">,</span> <span class=\"token punctuation\">{</span> withTimezone<span class=\"token operator\">:</span> <span class=\"token boolean\">true</span> <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">defaultNow</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">notNull</span><span class=\"token punctuation\">(</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">(</span>t<span class=\"token punctuation\">)</span> <span class=\"token operator\">=></span> <span class=\"token punctuation\">(</span><span class=\"token punctuation\">{</span>\n    emailUnique<span class=\"token operator\">:</span> <span class=\"token function\">uniqueIndex</span><span class=\"token punctuation\">(</span><span class=\"token string\">\"users_email_unique\"</span><span class=\"token punctuation\">)</span><span class=\"token punctuation\">.</span><span class=\"token function\">on</span><span class=\"token punctuation\">(</span>t<span class=\"token punctuation\">.</span>email<span class=\"token punctuation\">)</span><span class=\"token punctuation\">,</span>\n  <span class=\"token punctuation\">}</span><span class=\"token punctuation\">)</span>\n<span class=\"token punctuation\">)</span><span class=\"token punctuation\">;</span></code></pre></div>\n<p>여기서 기억할 점은 다음과 같다.</p>\n<ol>\n<li><strong>DB 식별자와 코드 식별자 분리</strong>: 컬럼명을 스네이크 케이스로 두면 운영 DB와의 심리적 거리가 줄어든다.</li>\n<li><strong>시간대</strong>: <code class=\"language-text\">timestamp</code>에 <code class=\"language-text\">withTimezone: true</code> 여부는 서비스 전체 규칙과 맞춘다. 한 번 잘못 고치면 마이그레이션이 아프다.</li>\n<li><strong>인덱스를 스키마 코드에 같이</strong>: 나중에 “왜 느리지?”를 추적할 때, 코드 리뷰 단계에서 인덱스 논의가 가능해진다.</li>\n</ol>\n<h2 id=\"관계와-참조-무결성\" style=\"position:relative;\"><a href=\"#%EA%B4%80%EA%B3%84%EC%99%80-%EC%B0%B8%EC%A1%B0-%EB%AC%B4%EA%B2%B0%EC%84%B1\" aria-label=\"관계와 참조 무결성 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>관계와 참조 무결성</h2>\n<p><code class=\"language-text\">relations()</code>로 관계를 선언하면, 쿼리 빌더에서 <code class=\"language-text\">with</code>를 사용한 <strong>관계형 로딩</strong> 패턴을 쓸 수 있다. 다만 <strong>외래 키 제약</strong>을 DB에 둘지, 애플리케이션에서만 보장할지는 트레이드오프가 있다.</p>\n<ul>\n<li><strong>DB FK + ON DELETE 규칙</strong>: 데이터가 드물게 손상되는 대신, 마이그레이션 순서와 잠금 이슈를 이해해야 한다.</li>\n<li><strong>애플리케이션만</strong>: 마이그레이션은 단순해질 수 있으나, 배치·수동 SQL·다른 서비스가 같은 DB를 건드리면 무결성이 깨지기 쉽다.</li>\n</ul>\n<p>일반적으로 <strong>프로덕션 핵심 도메인</strong>에는 DB 수준 제약을 두는 편이 안전하다.</p>\n<h2 id=\"drizzle-kit과-마이그레이션-워크플로\" style=\"position:relative;\"><a href=\"#drizzle-kit%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C\" aria-label=\"drizzle kit과 마이그레이션 워크플로 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>drizzle-kit과 마이그레이션 워크플로</h2>\n<p>Drizzle의 운영 경험은 <strong>drizzle-kit</strong>과 함께 논의되어야 한다. 흔한 흐름은 다음과 같다.</p>\n<ol>\n<li>로컬에서 스키마 코드를 수정한다.</li>\n<li><code class=\"language-text\">drizzle-kit generate</code>로 마이그레이션 SQL을 생성한다(이름은 팀 규칙으로).</li>\n<li>리뷰어가 SQL을 읽는다. <strong>여기서 대부분의 사고를 막는다.</strong></li>\n<li>스테이징에 적용해 본 뒤 프로덕션에 반영한다.</li>\n</ol>\n<h3 id=\"왜-생성된-sql-리뷰가-중요한가\" style=\"position:relative;\"><a href=\"#%EC%99%9C-%EC%83%9D%EC%84%B1%EB%90%9C-sql-%EB%A6%AC%EB%B7%B0%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80\" aria-label=\"왜 생성된 sql 리뷰가 중요한가 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>왜 “생성된 SQL 리뷰”가 중요한가</h3>\n<p>ORM이 SQL을 만들어 주는 순간, 개발자는 “내가 짠 코드”라는 착각을 한다. 하지만 마이그레이션은 <strong>한 번만 실행</strong>되는 것이 아니라, 이후 모든 환경에 재현되어야 한다. 특히 다음을 반드시 확인한다.</p>\n<ul>\n<li><strong>LOCK</strong>: 긴 테이블 락이 걸리는지. 대용량 테이블에 <code class=\"language-text\">ALTER</code>가 오래 걸리면 서비스가 멈출 수 있다.</li>\n<li><strong>DEFAULT / NOT NULL 추가 순서</strong>: PostgreSQL 버전에 따라 전략이 달라진다. 기존 행이 있을 때 채우는 전략이 있었는가.</li>\n<li><strong>인덱스 생성 CONCURRENTLY</strong>: 프로덕션에서는 <code class=\"language-text\">CREATE INDEX CONCURRENTLY</code>가 필요한 경우가 많다. drizzle-kit 출력이 이를 항상 만족하지는 않는다. <strong>수동 SQL 보정</strong>이 빈번하다.</li>\n</ul>\n<p>즉, Drizzle은 편하지만 <strong>DBA 감각을 없애 주지는 않는다</strong>.</p>\n<h2 id=\"쿼리-빌더와-sql-원문의-혼합\" style=\"position:relative;\"><a href=\"#%EC%BF%BC%EB%A6%AC-%EB%B9%8C%EB%8D%94%EC%99%80-sql-%EC%9B%90%EB%AC%B8%EC%9D%98-%ED%98%BC%ED%95%A9\" aria-label=\"쿼리 빌더와 sql 원문의 혼합 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>쿼리 빌더와 SQL 원문의 혼합</h2>\n<p>Drizzle의 강점 중 하나는 <code class=\"language-text\">sql</code> 템플릿을 통해 <strong>검증된 파라미터 바인딩</strong>을 유지하면서도 복잡한 쿼리를 쓸 수 있다는 점이다. 리포트성 쿼리, 배치, 윈도 함수가 필요하면 과도하게 ORM 추상화에 얽매이지 말고 <strong>SQL을 밝히는 것</strong>이 오히려 유지보수에 유리할 때가 많다.</p>\n<p>팀 규칙 예시는 다음과 같다.</p>\n<ul>\n<li><strong>단순 CRUD</strong>: Drizzle 쿼리 빌더</li>\n<li><strong>복잡 집계·운영 스크립트</strong>: <code class=\"language-text\">sql</code> 또는 <code class=\"language-text\">.execute</code>로 raw에 가깝게</li>\n<li><strong>일회성 데이터 수정</strong>: 저장소에 스크립트를 두고 코드 리뷰 후 실행</li>\n</ul>\n<h2 id=\"트랜잭션과-재시도-정책\" style=\"position:relative;\"><a href=\"#%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EA%B3%BC-%EC%9E%AC%EC%8B%9C%EB%8F%84-%EC%A0%95%EC%B1%85\" aria-label=\"트랜잭션과 재시도 정책 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>트랜잭션과 재시도 정책</h2>\n<p>금융·재고·포인트처럼 <strong>경합이 심한 도메인</strong>에서는 트랜잭션 격리 수준과 재시도 전략이 중요하다. Drizzle은 보통 연결 풀에서 <code class=\"language-text\">db.transaction()</code> 콜백을 제공한다. 여기에 더해 애플리케이션 레벨에서:</p>\n<ul>\n<li><strong>데드락 감지 시 재시도</strong>(지수 백오프)</li>\n<li><strong>멱등 키</strong>(외부 결제 웹훅 등)</li>\n</ul>\n<p>을 조합한다. ORM이 아무리 좋아도 <strong>동시성 이슈는 비즈니스 로직</strong>과 함께 설계해야 한다.</p>\n<h2 id=\"연결-풀과-서버리스\" style=\"position:relative;\"><a href=\"#%EC%97%B0%EA%B2%B0-%ED%92%80%EA%B3%BC-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4\" aria-label=\"연결 풀과 서버리스 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>연결 풀과 서버리스</h2>\n<p>Node에서 <code class=\"language-text\">pg</code> 풀을 쓸 때 흔한 실수는 <strong>서버리스 함수마다 풀을 남발</strong>하는 것이다. Lambda 같은 환경에서는 연결 수가 급증해 DB에 도달한다. 대응 전략은 다음과 같다.</p>\n<ul>\n<li><strong>풀러 게이트웨이</strong>(PgBouncer 등) 도입</li>\n<li><strong>콜드 스타트마다 새 풀 생성 금지</strong> 패턴(모듈 스코프 재사용)</li>\n<li>가능하면 <strong>RDS 프록시</strong> 같은 관리형 풀링</li>\n</ul>\n<p>Drizzle 자체보다 <strong>배포 형태</strong>가 병목인 경우가 많다.</p>\n<h2 id=\"인덱스-전략과-explain\" style=\"position:relative;\"><a href=\"#%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%A0%84%EB%9E%B5%EA%B3%BC-explain\" aria-label=\"인덱스 전략과 explain permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>인덱스 전략과 EXPLAIN</h2>\n<p>스키마에 인덱스를 적었다고 끝이 아니다. 프로덕션에서는 주기적으로 <code class=\"language-text\">EXPLAIN (ANALYZE, BUFFERS)</code>로 확인한다. 특히:</p>\n<ul>\n<li><strong>복합 인덱스 컬럼 순서</strong></li>\n<li><strong>부분 인덱스</strong>가 필요한지</li>\n<li><strong>통계 정보</strong>가 낙후되지 않았는지</li>\n</ul>\n<p>Drizzle은 쿼리 작성을 돕지만, <strong>실행 계획은 PostgreSQL</strong>이 결정한다. 느린 쿼리 로그와 pganalyze 같은 도구를 함께 쓰는 팀이 유리하다.</p>\n<h2 id=\"마이그레이션-배포-전략\" style=\"position:relative;\"><a href=\"#%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5\" aria-label=\"마이그레이션 배포 전략 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>마이그레이션 배포 전략</h2>\n<p>무중단 배포를 목표로 한다면 <strong>확장/계약(expand/contract)</strong> 패턴을 고려한다.</p>\n<ol>\n<li><strong>확장</strong>: 새 컬럼·새 테이블을 추가하고, 기존 코드는 그대로 둔다.</li>\n<li><strong>이행</strong>: 배치나 듀얼 라이트로 데이터를 채운다.</li>\n<li><strong>계약</strong>: 구 스키마 의존 코드를 제거하고, 불필요한 컬럼을 삭제한다.</li>\n</ol>\n<p>Drizzle 마이그레이션 한 방에 “컬럼 삭제 + 코드 삭제”를 하고 싶은 유혹이 생기지만, <strong>배포 파이프라인이 블루–그린이 아닌 이상</strong> 위험하다.</p>\n<h2 id=\"관측-가능성-무엇을-로깅할-것인가\" style=\"position:relative;\"><a href=\"#%EA%B4%80%EC%B8%A1-%EA%B0%80%EB%8A%A5%EC%84%B1-%EB%AC%B4%EC%97%87%EC%9D%84-%EB%A1%9C%EA%B9%85%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80\" aria-label=\"관측 가능성 무엇을 로깅할 것인가 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>관측 가능성: 무엇을 로깅할 것인가</h2>\n<p>최소한 다음을 추천한다.</p>\n<ul>\n<li><strong>쿼리 지연</strong>: p95, p99. ORM 레벨이 아니라 DB 프록시나 APM에서 볼 수도 있다.</li>\n<li><strong>에러 코드</strong>: <code class=\"language-text\">23505</code> unique violation 같은 코드를 매핑해 알림.</li>\n<li><strong>마이그레이션 적용 기록</strong>: 어떤 버전이 어느 환경에 적용됐는지.</li>\n</ul>\n<h2 id=\"팀-협업-코드-리뷰-체크리스트\" style=\"position:relative;\"><a href=\"#%ED%8C%80-%ED%98%91%EC%97%85-%EC%BD%94%EB%93%9C-%EB%A6%AC%EB%B7%B0-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8\" aria-label=\"팀 협업 코드 리뷰 체크리스트 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>팀 협업: 코드 리뷰 체크리스트</h2>\n<ol>\n<li>스키마 변경에 <strong>비즈니스 맥락</strong>이 설명돼 있는가.</li>\n<li>마이그레이션 SQL에 <strong>락/시간</strong> 추정이 주석으로라도 있는가.</li>\n<li>롤백 전략이 현실적인가(되돌리기 SQL, 백업).</li>\n<li>데이터 백필이 필요한가.</li>\n</ol>\n<h2 id=\"prisma와의-비교--짧게\" style=\"position:relative;\"><a href=\"#prisma%EC%99%80%EC%9D%98-%EB%B9%84%EA%B5%90--%EC%A7%A7%EA%B2%8C\" aria-label=\"prisma와의 비교  짧게 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Prisma와의 비교 — 짧게</h2>\n<table>\n<thead>\n<tr>\n<th>항목</th>\n<th>Drizzle</th>\n<th>Prisma</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>SQL 표현력</td>\n<td>매우 가깝게 제어 가능</td>\n<td>제약이 느껴질 수 있음</td>\n</tr>\n<tr>\n<td>스키마 소스</td>\n<td>TS 코드 중심</td>\n<td>schema.prisma 중심</td>\n</tr>\n<tr>\n<td>마이그레이션 스토리</td>\n<td>팀에 따라 유연</td>\n<td>성숙한 워크플로 많음</td>\n</tr>\n<tr>\n<td>DX 도구</td>\n<td>성장 중</td>\n<td>풍부</td>\n</tr>\n</tbody>\n</table>\n<p>“누가 이겼다”가 아니라 <strong>팀의 SQL 문해력과 운영 성숙도</strong>에 맞추는 것이 맞다.</p>\n<h2 id=\"실패-사례-운영에서-자주-터지는-것들\" style=\"position:relative;\"><a href=\"#%EC%8B%A4%ED%8C%A8-%EC%82%AC%EB%A1%80-%EC%9A%B4%EC%98%81%EC%97%90%EC%84%9C-%EC%9E%90%EC%A3%BC-%ED%84%B0%EC%A7%80%EB%8A%94-%EA%B2%83%EB%93%A4\" aria-label=\"실패 사례 운영에서 자주 터지는 것들 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>실패 사례: 운영에서 자주 터지는 것들</h2>\n<ol>\n<li><strong>로컬에서는 빠른데 프로덕션만 느리다</strong>: 데이터량·캐시·인덱스 차이. 로컬 시드 데이터가 너무 작다.</li>\n<li><strong>마이그레이션 중 다운타임</strong>: <code class=\"language-text\">ALTER TABLE</code>이 테이블 전체를 잠근다.</li>\n<li><strong>환경 변수 불일치</strong>: 스테이징과 프로덕션의 <code class=\"language-text\">search_path</code>, 확장(extension) 설치 차이.</li>\n<li><strong>트랜잭션 범위 과대</strong>: 한 트랜잭션에 너무 많은 행을 잠근다.</li>\n</ol>\n<h2 id=\"정리\" style=\"position:relative;\"><a href=\"#%EC%A0%95%EB%A6%AC\" aria-label=\"정리 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>정리</h2>\n<p>Drizzle은 TypeScript 생태계에서 <strong>SQL과 타입 안전성 사이의 균형</strong>을 잡으려는 실용적인 ORM이다. drizzle-kit과 함께 쓸 때 비로소 <strong>마이그레이션 운영</strong>이 완성된다. 이 글에서 반복한 메시지는 하나다. <strong>도구는 SQL과 운영 지식을 대체하지 않는다.</strong> 그 대신, 팀이 같은 스키마를 코드로 논의하고 리뷰할 수 있게 해 준다. PostgreSQL을 오래 쓴 엔지니어일수록 Drizzle의 “투명함”이 오히려 편하게 느껴질 것이고, ORM에 익숙한 개발자는 초기에 SQL을 더 쓰도록 훈련할 필요가 있다.</p>\n<p>앞으로 서비스를 새로 시작한다면, <strong>스키마 네이밍</strong>, <strong>시간대</strong>, <strong>ID 전략</strong>(serial vs UUID vs snowflake), <strong>소프트 삭제 여부</strong>를 먼저 합의하고 Drizzle 스키마에 반영하라. 그 합의가 있을 때 마이그레이션은 반복 가능하고, 장애는 줄어든다.</p>\n<h2 id=\"부록-용어-정리\" style=\"position:relative;\"><a href=\"#%EB%B6%80%EB%A1%9D-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC\" aria-label=\"부록 용어 정리 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>부록: 용어 정리</h2>\n<ul>\n<li><strong>마이그레이션</strong>: 스키마 버전을 올리기 위한 SQL 묶음.</li>\n<li><strong>시드(seed)</strong>: 개발·테스트용 초기 데이터. 프로덕션과 혼동하지 말 것.</li>\n<li><strong>드리즐 커널(drizzle-kit config)</strong>: 생성 경로, 드라이버, 스키마 파일 위치를 가리키는 설정.</li>\n</ul>\n<p>이 용어들을 팀 온보딩 문서 첫 페이지에 넣어 두면, 신규 입사자가 ORM 논의에 빨리 합류할 수 있다.</p>\n<h2 id=\"drizzleconfigts-한-번만-제대로-잡기\" style=\"position:relative;\"><a href=\"#drizzleconfigts-%ED%95%9C-%EB%B2%88%EB%A7%8C-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9E%A1%EA%B8%B0\" aria-label=\"drizzleconfigts 한 번만 제대로 잡기 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>drizzle.config.ts: 한 번만 제대로 잡기</h2>\n<p><code class=\"language-text\">drizzle.config.ts</code>는 “마이그레이션 생성기가 어디를 바라보는지”를 결정한다. 흔한 설정 항목은 다음과 같다.</p>\n<ul>\n<li><strong>schema</strong>: <code class=\"language-text\">export</code>된 테이블 정의가 모인 파일 또는 glob</li>\n<li><strong>out</strong>: 생성된 SQL이 쌓일 디렉터리</li>\n<li><strong>dialect / driver</strong>: <code class=\"language-text\">postgresql</code> 등</li>\n<li><strong>dbCredentials</strong>: 로컬 개발용 URL (절대 커밋하지 말 것 — <code class=\"language-text\">.env</code>와 로더 사용)</li>\n</ul>\n<p>모노레포에서 여러 앱이 같은 DB를 공유하면, <strong>스키마 소스는 하나로 모으고</strong> 앱별로는 <strong>리포지토리 레이어만 분리</strong>하는 편이 마이그레이션 충돌을 줄인다. “앱 A만 쓰는 테이블”을 패키지 경계로 나누되, DB 스키마 파일은 공용 패키지에 두는 식이다.</p>\n<h2 id=\"관계형-로딩과-n1\" style=\"position:relative;\"><a href=\"#%EA%B4%80%EA%B3%84%ED%98%95-%EB%A1%9C%EB%94%A9%EA%B3%BC-n1\" aria-label=\"관계형 로딩과 n1 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>관계형 로딩과 N+1</h2>\n<p><code class=\"language-text\">relations()</code>를 정의하면 <code class=\"language-text\">query</code> API로 중첩 로딩을 편하게 쓸 수 있다. 다만 <strong>N+1 쿼리</strong>에 빠지기 쉽다. 리스트 API에서 사용자 100명을 가져온 뒤 각각 주문을 lazy로 읽으면, 쿼리가 폭발한다.</p>\n<p>완화 전략은 다음과 같다.</p>\n<ol>\n<li><strong>한 번에 조인 가능한 것은 조인</strong>으로 가져온다.</li>\n<li><strong>배치 로딩</strong>: ID 목록을 모아 <code class=\"language-text\">inArray</code>로 두 번째 쿼리를 한 번만 날린다.</li>\n<li><strong>데이터 로더 패턴</strong>: GraphQL이라면 필수에 가깝다.</li>\n</ol>\n<p>ORM이 편하다고 <strong>쿼리 개수를 잊지 말 것</strong>. 개발 환경에서 <code class=\"language-text\">DEBUG=drizzle:*</code> 류 로깅을 켜 두거나, APM에서 SQL 개수를 본다.</p>\n<h2 id=\"null-기본값-도메인-불변-조건\" style=\"position:relative;\"><a href=\"#null-%EA%B8%B0%EB%B3%B8%EA%B0%92-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B6%88%EB%B3%80-%EC%A1%B0%EA%B1%B4\" aria-label=\"null 기본값 도메인 불변 조건 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>NULL, 기본값, 도메인 불변 조건</h2>\n<p>스키마에서 <code class=\"language-text\">notNull()</code>을 남발하기 전에, 비즈니스적으로 <strong>“아직 모름”</strong>과 <strong>“없음”</strong>을 구분할지 결정한다. PostgreSQL의 <code class=\"language-text\">NULL</code> 의미를 팀이 통일하지 않으면, 통계·리포트·필터 쿼리에서 조용히 틀어진다.</p>\n<p>또한 애플리케이션에서만 검증하던 규칙(이메일 형식, 금애 범위)을 <strong>CHECK 제약</strong>으로 옮길지는 성능·운영 트레이드오프다. 마이그레이션 비용이 크므로, 초기 스키마보다는 <strong>도메인이 안정된 뒤</strong>에 단계적으로 추가하는 팀도 많다.</p>\n<h2 id=\"읽기-전용-복제본과-drizzle\" style=\"position:relative;\"><a href=\"#%EC%9D%BD%EA%B8%B0-%EC%A0%84%EC%9A%A9-%EB%B3%B5%EC%A0%9C%EB%B3%B8%EA%B3%BC-drizzle\" aria-label=\"읽기 전용 복제본과 drizzle permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>읽기 전용 복제본과 Drizzle</h2>\n<p>읽기 부하를 <strong>리드 레플리카</strong>로 분산할 때, Drizzle 인스턴스를 두 개(Writer/Reader) 두는 패턴이 흔하다. 라우팅 실수로 <strong>쓰기를 복제본에 보내면</strong> 조용히 실패하거나 지연만 생길 수 있으므로, 리포지토리 메서드 이름을 <code class=\"language-text\">findForReport</code>(읽기 전용)처럼 <strong>의도가 드러나게</strong> 짓는다.</p>\n<p>트랜잭션은 항상 <strong>writer 연결</strong>에서 연다. “읽기 후 같은 트랜잭션에서 쓰기”를 할 때 복제 지연으로 <strong>낙관적 락이 깨지는</strong> 경우도 있으니, 비즈니스 규칙과 함께 설계한다.</p>\n<h2 id=\"테스트-testcontainers와-마이그레이션\" style=\"position:relative;\"><a href=\"#%ED%85%8C%EC%8A%A4%ED%8A%B8-testcontainers%EC%99%80-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98\" aria-label=\"테스트 testcontainers와 마이그레이션 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>테스트: Testcontainers와 마이그레이션</h2>\n<p>로컬에 PostgreSQL을 직접 깔지 않고도, CI에서 <strong>도커로 DB를 띄운 뒤</strong> 마이그레이션을 적용하고 통합 테스트를 돌리는 패턴이 널리 쓰인다. Drizzle이라면:</p>\n<ol>\n<li>컨테이너 기동</li>\n<li><code class=\"language-text\">drizzle-kit migrate</code> 또는 마이그레이션 SQL 적용</li>\n<li>시드(필요 시)</li>\n<li>애플리케이션 테스트 실행</li>\n</ol>\n<p>이 순서가 스크립트로 고정되면, <strong>“내 컴퓨터에서는 되는데”</strong>가 줄어든다.</p>\n<h2 id=\"보안-연결-문자열과-비밀\" style=\"position:relative;\"><a href=\"#%EB%B3%B4%EC%95%88-%EC%97%B0%EA%B2%B0-%EB%AC%B8%EC%9E%90%EC%97%B4%EA%B3%BC-%EB%B9%84%EB%B0%80\" aria-label=\"보안 연결 문자열과 비밀 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>보안: 연결 문자열과 비밀</h2>\n<p><code class=\"language-text\">.env</code>에 <code class=\"language-text\">DATABASE_URL</code>을 두고, 프로덕션은 <strong>시크릿 매니저</strong>에서 주입한다. 로그에 쿼리 문자열이 찍히지 않게 <strong>마스킹</strong>하고, ORM이 에러를 출력할 때 <strong>바인딩된 값</strong>이 노출되지 않게 로그 레벨을 조정한다.</p>\n<p>팀에서 자주 하는 실수는 <strong>읽기 전용 계정</strong>을 따로 두지 않고 admin 계정을 애플리케이션에 넣는 것이다. 유출 시 피해 면적이 커진다.</p>\n<h2 id=\"버전-업그레이드-전략\" style=\"position:relative;\"><a href=\"#%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%A0%84%EB%9E%B5\" aria-label=\"버전 업그레이드 전략 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>버전 업그레이드 전략</h2>\n<p><code class=\"language-text\">drizzle-orm</code>, <code class=\"language-text\">drizzle-kit</code>, 드라이버(<code class=\"language-text\">pg</code>)는 함께 올리는 편이 안전하다. 릴리스 노트에서 <strong>브레이킹 체인지</strong>를 확인하고, 스테이징에서 <strong>마이그레이션 dry-run</strong>을 습관화한다.</p>\n<h2 id=\"실전-faq\" style=\"position:relative;\"><a href=\"#%EC%8B%A4%EC%A0%84-faq\" aria-label=\"실전 faq permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>실전 FAQ</h2>\n<p><strong>Q. 마이그레이션 SQL이 수동으로 너무 많이 고쳐진다.</strong><br>\nA. 정상에 가깝다. 특히 대용량 테이블은 ORM이 생성한 기본 <code class=\"language-text\">ALTER</code>가 위험할 수 있다. 팀에서 <strong>“위험 등급”</strong> 마이그레이션은 DBA 리뷰를 필수로 하라.</p>\n<p><strong>Q. 스키마 코드와 실제 DB가 어긋났다.</strong><br>\nA. drift다. <code class=\"language-text\">drizzle-kit introspect</code>로 실제 DB에서 스키마를 끌어와 비교하거나, <strong>한 방향으로만 진실을 둔다</strong>(코드 우선 vs DB 우선).</p>\n<p><strong>Q. Enum 타입은 어떻게 다루나?</strong><br>\nA. PostgreSQL <code class=\"language-text\">ENUM</code>은 값 추가·삭제가 까다롭다. 팀에 따라 <strong>체크 제약 + 문자열</strong> 또는 <strong>lookup 테이블</strong>을 선호하기도 한다.</p>\n<h2 id=\"배치데이터-마이그레이션과-drizzle\" style=\"position:relative;\"><a href=\"#%EB%B0%B0%EC%B9%98%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98%EA%B3%BC-drizzle\" aria-label=\"배치데이터 마이그레이션과 drizzle permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>배치·데이터 마이그레이션과 Drizzle</h2>\n<p>스키마 마이그레이션과 별개로, <strong>기존 행을 채우는 데이터 마이그레이션</strong>이 필요할 때가 있다. 예를 들어 새 컬럼을 nullable로 추가한 뒤, 배치 잡으로 값을 채우고, 마지막에 <code class=\"language-text\">NOT NULL</code>로 바꾸는 흐름이다. Drizzle은 이 과정을 자동으로 “예쁘게” 해 주지 않는다. 대신 <strong>운영 절차</strong>를 문서로 남기고, SQL 스크립트나 임시 Node 스크립트를 코드 리뷰하는 팀이 안전하다.</p>\n<p>데이터 마이그레이션 스크립트를 작성할 때는 <strong>배치 크기</strong>, <strong>슬립</strong>, <strong>재실행 가능성(idempotent)</strong> 을 명시한다. 한 번에 수백만 행을 업데이트하면 <strong>장시간 락</strong>과 <strong>WAL 폭증</strong>이 올 수 있다.</p>\n<h2 id=\"파티셔닝대용량-테이블을-앞둔-팀에게\" style=\"position:relative;\"><a href=\"#%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%84-%EC%95%9E%EB%91%94-%ED%8C%80%EC%97%90%EA%B2%8C\" aria-label=\"파티셔닝대용량 테이블을 앞둔 팀에게 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>파티셔닝·대용량 테이블을 앞둔 팀에게</h2>\n<p>주문·로그처럼 <strong>데이터가 끊임없이 쌓이는 테이블</strong>은 초기 스키마에서 파티셔닝 전략을 논의하는 것이 좋다. Drizzle이 파티션 정의를 완벽히 추상화하지 않을 수 있으므로, <strong>생성 DDL을 수동으로 관리</strong>하는 경우도 있다. 이때는 “스키마 코드 우선”과 “수동 DDL”의 경계를 팀 규칙으로 못 박아야 한다. 그렇지 않으면 신입 개발자가 <code class=\"language-text\">pgTable</code>만 보고 운영 DB의 실제 구조를 오해한다.</p>\n<h2 id=\"운영-플레이북에-넣을-질문-다섯-가지\" style=\"position:relative;\"><a href=\"#%EC%9A%B4%EC%98%81-%ED%94%8C%EB%A0%88%EC%9D%B4%EB%B6%81%EC%97%90-%EB%84%A3%EC%9D%84-%EC%A7%88%EB%AC%B8-%EB%8B%A4%EC%84%AF-%EA%B0%80%EC%A7%80\" aria-label=\"운영 플레이북에 넣을 질문 다섯 가지 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>운영 플레이북에 넣을 질문 다섯 가지</h2>\n<ol>\n<li>지금 장애가 <strong>ORM 버그</strong>인가 <strong>쿼리 플랜</strong> 문제인가 <strong>연결 풀</strong> 문제인가?</li>\n<li>최근 마이그레이션 중 <strong>롱 러닝 트랜잭션</strong>이 있었는가?</li>\n<li><strong>슬로우 쿼리 로그</strong>에 새로운 패턴이 보이는가?</li>\n<li>복제 지연이 임계치를 넘었는가?</li>\n<li>디스크 여유와 <strong>VACUUM</strong>, <strong>autovacuum</strong> 상태는?</li>\n</ol>\n<p>이 질문에 Drizzle은 답을 주지 않는다. 대신 <strong>같은 스키마를 기준으로 팀이 대화</strong>할 수 있게 해 준다.</p>\n<h2 id=\"마무리-한-줄-더\" style=\"position:relative;\"><a href=\"#%EB%A7%88%EB%AC%B4%EB%A6%AC-%ED%95%9C-%EC%A4%84-%EB%8D%94\" aria-label=\"마무리 한 줄 더 permalink\" class=\"anchor-header before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>마무리 한 줄 더</h2>\n<p>Drizzle은 <strong>SQL을 숨기는 ORM</strong>이라기보다, <strong>SQL을 팀이 함께 읽을 수 있게 정리해 주는 ORM</strong>에 가깝다. 그래서 성장하는 팀일수록 SQL 리뷰 문화와 함께할 때 시너지가 난다. 이 글의 글자 수를 채우는 것이 목적이 아니라, <strong>운영 중에 다시 펼쳐 보고 싶은 체크리스트</strong>가 목적이 되기를 바란다.</p>\n<p>추가로 한 가지 권한다. 스키마 변경 PR에는 <strong>“사용자에게 보이는 변화”</strong> 한 줄을 본문에 적어라. 내부 리팩터링만 보이는 DB 작업이 줄어들고, PM·디자이너와의 대화 비용이 줄어든다.</p>","tableOfContents":"<ul>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#drizzle%EC%9D%84-%EC%84%A0%ED%83%9D%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0%EC%99%80-%ED%95%9C%EA%B3%84\">Drizzle을 선택하는 이유와 한계</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0-%EA%B6%8C%EC%9E%A5%EC%95%88\">프로젝트 구조 권장안</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%A0%95%EC%9D%98-pg-%ED%85%8C%EC%9D%B4%EB%B8%94%EA%B3%BC-%ED%83%80%EC%9E%85-%EC%95%88%EC%A0%84%EC%84%B1\">스키마 정의: pg 테이블과 타입 안전성</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EA%B4%80%EA%B3%84%EC%99%80-%EC%B0%B8%EC%A1%B0-%EB%AC%B4%EA%B2%B0%EC%84%B1\">관계와 참조 무결성</a></li>\n<li>\n<p><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#drizzle-kit%EA%B3%BC-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C\">drizzle-kit과 마이그레이션 워크플로</a></p>\n<ul>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%99%9C-%EC%83%9D%EC%84%B1%EB%90%9C-sql-%EB%A6%AC%EB%B7%B0%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C%EA%B0%80\">왜 “생성된 SQL 리뷰”가 중요한가</a></li>\n</ul>\n</li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%BF%BC%EB%A6%AC-%EB%B9%8C%EB%8D%94%EC%99%80-sql-%EC%9B%90%EB%AC%B8%EC%9D%98-%ED%98%BC%ED%95%A9\">쿼리 빌더와 SQL 원문의 혼합</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EA%B3%BC-%EC%9E%AC%EC%8B%9C%EB%8F%84-%EC%A0%95%EC%B1%85\">트랜잭션과 재시도 정책</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%97%B0%EA%B2%B0-%ED%92%80%EA%B3%BC-%EC%84%9C%EB%B2%84%EB%A6%AC%EC%8A%A4\">연결 풀과 서버리스</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%A0%84%EB%9E%B5%EA%B3%BC-explain\">인덱스 전략과 EXPLAIN</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B0%B0%ED%8F%AC-%EC%A0%84%EB%9E%B5\">마이그레이션 배포 전략</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EA%B4%80%EC%B8%A1-%EA%B0%80%EB%8A%A5%EC%84%B1-%EB%AC%B4%EC%97%87%EC%9D%84-%EB%A1%9C%EA%B9%85%ED%95%A0-%EA%B2%83%EC%9D%B8%EA%B0%80\">관측 가능성: 무엇을 로깅할 것인가</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%ED%8C%80-%ED%98%91%EC%97%85-%EC%BD%94%EB%93%9C-%EB%A6%AC%EB%B7%B0-%EC%B2%B4%ED%81%AC%EB%A6%AC%EC%8A%A4%ED%8A%B8\">팀 협업: 코드 리뷰 체크리스트</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#prisma%EC%99%80%EC%9D%98-%EB%B9%84%EA%B5%90--%EC%A7%A7%EA%B2%8C\">Prisma와의 비교 — 짧게</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%8B%A4%ED%8C%A8-%EC%82%AC%EB%A1%80-%EC%9A%B4%EC%98%81%EC%97%90%EC%84%9C-%EC%9E%90%EC%A3%BC-%ED%84%B0%EC%A7%80%EB%8A%94-%EA%B2%83%EB%93%A4\">실패 사례: 운영에서 자주 터지는 것들</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%A0%95%EB%A6%AC\">정리</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%B6%80%EB%A1%9D-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC\">부록: 용어 정리</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#drizzleconfigts-%ED%95%9C-%EB%B2%88%EB%A7%8C-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9E%A1%EA%B8%B0\">drizzle.config.ts: 한 번만 제대로 잡기</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EA%B4%80%EA%B3%84%ED%98%95-%EB%A1%9C%EB%94%A9%EA%B3%BC-n1\">관계형 로딩과 N+1</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#null-%EA%B8%B0%EB%B3%B8%EA%B0%92-%EB%8F%84%EB%A9%94%EC%9D%B8-%EB%B6%88%EB%B3%80-%EC%A1%B0%EA%B1%B4\">NULL, 기본값, 도메인 불변 조건</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%9D%BD%EA%B8%B0-%EC%A0%84%EC%9A%A9-%EB%B3%B5%EC%A0%9C%EB%B3%B8%EA%B3%BC-drizzle\">읽기 전용 복제본과 Drizzle</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%ED%85%8C%EC%8A%A4%ED%8A%B8-testcontainers%EC%99%80-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98\">테스트: Testcontainers와 마이그레이션</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%B3%B4%EC%95%88-%EC%97%B0%EA%B2%B0-%EB%AC%B8%EC%9E%90%EC%97%B4%EA%B3%BC-%EB%B9%84%EB%B0%80\">보안: 연결 문자열과 비밀</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%A0%84%EB%9E%B5\">버전 업그레이드 전략</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%8B%A4%EC%A0%84-faq\">실전 FAQ</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%B0%B0%EC%B9%98%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98%EA%B3%BC-drizzle\">배치·데이터 마이그레이션과 Drizzle</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%ED%8C%8C%ED%8B%B0%EC%85%94%EB%8B%9D%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%85%8C%EC%9D%B4%EB%B8%94%EC%9D%84-%EC%95%9E%EB%91%94-%ED%8C%80%EC%97%90%EA%B2%8C\">파티셔닝·대용량 테이블을 앞둔 팀에게</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EC%9A%B4%EC%98%81-%ED%94%8C%EB%A0%88%EC%9D%B4%EB%B6%81%EC%97%90-%EB%84%A3%EC%9D%84-%EC%A7%88%EB%AC%B8-%EB%8B%A4%EC%84%AF-%EA%B0%80%EC%A7%80\">운영 플레이북에 넣을 질문 다섯 가지</a></li>\n<li><a href=\"/etc/Drizzle-ORM%EA%B3%BC-PostgreSQL-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81-%EC%99%84%EC%A0%84-%EA%B0%80%EC%9D%B4%EB%93%9C/#%EB%A7%88%EB%AC%B4%EB%A6%AC-%ED%95%9C-%EC%A4%84-%EB%8D%94\">마무리 한 줄 더</a></li>\n</ul>","frontmatter":{"title":"Drizzle ORM과 PostgreSQL — 스키마·마이그레이션·운영 완전 가이드","date":"April 15, 2026","description":"TypeScript 친화적 ORM인 Drizzle의 철학과 pg 스키마 정의, drizzle-kit으로 마이그레이션을 다루는 방법, 관계·쿼리 빌더·트랜잭션 패턴, 프로덕션 운영 시 인덱스·연결 풀·관측 포인트까지 한 번에 정리한다.","tags":["Drizzle ORM","PostgreSQL","TypeScript","마이그레이션","drizzle-kit","백엔드","데이터베이스"]}}},"pageContext":{"slug":"/etc/Drizzle-ORM과-PostgreSQL-스키마-마이그레이션-운영-완전-가이드/","previous":{"fields":{"slug":"/etc/MCP-Model-Context-Protocol-실전-가이드-AI-에디터에-도구를-붙이는-방법/"},"frontmatter":{"title":"MCP(Model Context Protocol) 실전 가이드 — AI 에디터에 도구를 붙이는 방법","tags":["MCP","Model Context Protocol","Cursor","AI","TypeScript","개발 생산성","도구 통합"]}},"next":null}},"staticQueryHashes":["3262363727","4027999303"]}