- 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>
270 lines
6.4 KiB
Markdown
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
|
|
```
|