# 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
```
