harness/skills/zio-harness/references/database.md
ythong c25e55b64b feat: harness + zio-harness Claude Code 플러그인 초기 배포
- harness: 도메인 한 줄 → 에이전트 팀 자동 생성 메타 스킬 (v1.2.0)
- zio-harness: React + Spring Boot + Mobile 풀스택 개발 에이전트 팀 (v1.0.0)
- 에이전트 4종: orchestrator / analyst / bot / agent
- PROJECT_MAP.md 폴더 구조 메모리 시스템
- references/: react, spring-boot, mobile, playwright, database, folder-map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:48:56 +09:00

270 lines
6.4 KiB
Markdown

# Database + MCP 가이드
## 개요
데이터베이스 작업은 두 경로로 처리한다:
- **MCP (Model Context Protocol)**: Claude Code가 DB를 직접 조회/탐색할 때
- **Spring Boot JPA**: 애플리케이션 코드에서 DB를 다룰 때
---
## MCP 데이터베이스 설정
### PostgreSQL MCP 설정
```json
// .claude/settings.json (프로젝트)
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/myapp"],
"env": {
"PGPASSWORD": "${DB_PASSWORD}"
}
}
}
}
```
### MySQL MCP 설정
```json
{
"mcpServers": {
"mysql": {
"command": "npx",
"args": ["-y", "@benborla29/mcp-server-mysql"],
"env": {
"MYSQL_HOST": "localhost",
"MYSQL_PORT": "3306",
"MYSQL_USER": "root",
"MYSQL_PASS": "${DB_PASSWORD}",
"MYSQL_DB": "myapp"
}
}
}
}
```
### MCP로 DB 탐색하는 방법
MCP가 활성화되면 Claude Code에서 직접 DB를 조회한다:
- 스키마 확인: "현재 DB 테이블 목록 보여줘"
- 데이터 조회: "users 테이블에서 최근 가입한 10명 조회해줘"
- 스키마 설계: "이 요구사항에 맞는 테이블 구조 제안해줘"
---
## 데이터베이스 설계 패턴
### 공통 컬럼 (BaseEntity)
```sql
-- 모든 테이블에 포함
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
```
### 사용자 테이블 표준
```sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- bcrypt 해시
name VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'USER',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
```
### 관계 설계
```sql
-- 1:N 관계 (posts → users)
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500) NOT NULL,
content TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
-- N:M 관계 (users ↔ tags)
CREATE TABLE user_tags (
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
tag_id BIGINT REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, tag_id)
);
```
---
## Flyway 마이그레이션 패턴
```
src/main/resources/db/migration/
├── V1__init_schema.sql # 초기 스키마
├── V2__add_posts_table.sql # 새 테이블
├── V3__add_user_avatar.sql # 컬럼 추가
└── V4__create_tags.sql # N:M 관계
```
### 마이그레이션 파일 작성 규칙
```sql
-- V2__add_posts_table.sql
-- 항상 트랜잭션 안에서 실행됨 (PostgreSQL)
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
title VARCHAR(500) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts(user_id);
```
- 파일명: `V{숫자}__{설명}.sql` (밑줄 2개)
- 한 번 적용된 파일은 수정 금지 (새 버전 파일 추가)
- 롤백 스크립트: `R{버전}__{설명}.sql` (선택)
---
## JPA Repository 고급 패턴
### QueryDSL / JPQL
```java
// 커스텀 조회 (인터페이스 + Impl 패턴)
public interface UserRepositoryCustom {
List<User> findActiveUsersWithPosts(Pageable pageable);
}
@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
@PersistenceContext EntityManager em;
@Override
public List<User> findActiveUsersWithPosts(Pageable pageable) {
return em.createQuery(
"SELECT DISTINCT u FROM User u JOIN FETCH u.posts WHERE u.isActive = true",
User.class)
.setFirstResult((int) pageable.getOffset())
.setMaxResults(pageable.getPageSize())
.getResultList();
}
}
// Spring Data JPA 인터페이스
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
Optional<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.role = :role AND u.isActive = true")
List<User> findActiveByRole(@Param("role") String role);
Page<User> findByNameContaining(String name, Pageable pageable);
}
```
---
## DB 연결 설정
### application.yml
```yaml
spring:
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/myapp}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # Flyway 사용 시 validate (create/update 금지)
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
open-in-view: false # 성능: 트랜잭션 외부 지연 로딩 비활성화
flyway:
enabled: true
locations: classpath:db/migration
```
---
## 성능 최적화
### N+1 문제 해결
```java
// 잘못된 예 (N+1 발생)
List<User> users = userRepository.findAll();
users.forEach(u -> u.getPosts().size()); // 각 사용자마다 쿼리 발생
// 올바른 예 (fetch join)
@Query("SELECT u FROM User u LEFT JOIN FETCH u.posts")
List<User> findAllWithPosts();
```
### 페이지네이션
```java
// 컨트롤러
@GetMapping
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "createdAt") String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort).descending());
return ResponseEntity.ok(userService.findAll(pageable).map(UserResponse::from));
}
```
---
## 로컬 개발 환경 (Docker)
```yaml
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
```bash
# DB 시작
docker-compose up -d postgres
# 접속
psql -h localhost -U postgres -d myapp
```