backend-claude-code 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +42 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +32 -0
- package/.mcp.json +19 -0
- package/CLAUDE.md +126 -0
- package/README.md +142 -0
- package/agents/code-reviewer.md +84 -0
- package/agents/database-reviewer.md +91 -0
- package/agents/java-build-resolver.md +127 -0
- package/agents/java-performance-reviewer.md +262 -0
- package/agents/planner.md +99 -0
- package/agents/security-reviewer.md +119 -0
- package/agents/tdd-guide.md +189 -0
- package/bin/cli.js +144 -0
- package/commands/db-migrate.md +134 -0
- package/commands/dev-build.md +72 -0
- package/commands/dev-coverage.md +73 -0
- package/commands/dev-fix.md +75 -0
- package/commands/dev-plan.md +501 -0
- package/commands/dev-review.md +144 -0
- package/commands/dev-run.md +385 -0
- package/commands/dev-test.md +89 -0
- package/commands/dev-verify.md +95 -0
- package/commands/dev.md +45 -0
- package/commands/git-commit.md +112 -0
- package/commands/git-issue.md +74 -0
- package/commands/git-pr.md +184 -0
- package/commands/git-push.md +28 -0
- package/package.json +24 -0
- package/rules/architecture.md +33 -0
- package/rules/coding-style.md +113 -0
- package/rules/controller-patterns.md +63 -0
- package/rules/dto-patterns.md +76 -0
- package/rules/entity-patterns.md +70 -0
- package/rules/error-handling.md +56 -0
- package/rules/hooks.md +73 -0
- package/rules/repository-patterns.md +75 -0
- package/rules/security.md +101 -0
- package/rules/service-patterns.md +70 -0
- package/rules/testing.md +174 -0
- package/skills/api-design/SKILL.md +523 -0
- package/skills/architecture-decision-records/SKILL.md +179 -0
- package/skills/database-migrations/SKILL.md +429 -0
- package/skills/hexagonal-architecture/SKILL.md +276 -0
- package/skills/java-coding-standards/SKILL.md +236 -0
- package/skills/jpa-patterns/SKILL.md +218 -0
- package/skills/postgres-patterns/SKILL.md +147 -0
- package/skills/springboot-patterns/SKILL.md +255 -0
- package/skills/springboot-security/SKILL.md +241 -0
- package/skills/springboot-tdd/SKILL.md +236 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: java-coding-standards
|
|
3
|
+
description: "Java coding standards for Spring Boot services: naming, immutability, Optional usage, streams, exceptions, generics, and project layout."
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Java Coding Standards
|
|
8
|
+
|
|
9
|
+
Spring Boot 서비스에서 읽기 쉽고 유지보수 가능한 Java (17+) 코드 표준.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Spring Boot 프로젝트에서 Java 코드 작성 또는 리뷰
|
|
14
|
+
- 명명, 불변성, 예외 처리 관례 강제
|
|
15
|
+
- records, sealed classes, pattern matching 작업 (Java 17+)
|
|
16
|
+
- Optional, streams, generics 사용 리뷰
|
|
17
|
+
- 패키지 및 프로젝트 레이아웃 구성
|
|
18
|
+
|
|
19
|
+
## Core Principles
|
|
20
|
+
|
|
21
|
+
- 영리함보다 명확성 우선
|
|
22
|
+
- 기본적으로 불변; 공유 가변 상태 최소화
|
|
23
|
+
- 의미 있는 예외로 빠른 실패
|
|
24
|
+
- 일관된 명명 및 패키지 구조
|
|
25
|
+
|
|
26
|
+
## Naming
|
|
27
|
+
|
|
28
|
+
```java
|
|
29
|
+
// PASS: 클래스/레코드: PascalCase
|
|
30
|
+
public class OrderService {}
|
|
31
|
+
public record Money(BigDecimal amount, Currency currency) {}
|
|
32
|
+
|
|
33
|
+
// PASS: 메서드/필드: camelCase
|
|
34
|
+
private final OrderRepository orderRepository;
|
|
35
|
+
public OrderResponse findByOrderNumber(String orderNumber) {}
|
|
36
|
+
|
|
37
|
+
// PASS: 상수: UPPER_SNAKE_CASE
|
|
38
|
+
private static final int MAX_PAGE_SIZE = 100;
|
|
39
|
+
private static final Duration TOKEN_EXPIRY = Duration.ofHours(24);
|
|
40
|
+
|
|
41
|
+
// PASS: 패키지: 소문자 역방향 도메인
|
|
42
|
+
// com.example.app.service
|
|
43
|
+
// com.example.app.controller
|
|
44
|
+
// com.example.app.domain
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Immutability
|
|
48
|
+
|
|
49
|
+
```java
|
|
50
|
+
// PASS: record와 final 필드 선호
|
|
51
|
+
public record OrderDto(Long id, String customerName, BigDecimal amount, OrderStatus status) {}
|
|
52
|
+
|
|
53
|
+
public class Order {
|
|
54
|
+
private final Long id;
|
|
55
|
+
private final String customerName;
|
|
56
|
+
private final BigDecimal amount;
|
|
57
|
+
|
|
58
|
+
// getter만, setter 없음
|
|
59
|
+
public Long getId() { return id; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// PASS: 컬렉션 방어적 복사
|
|
63
|
+
public List<OrderItem> getItems() {
|
|
64
|
+
return List.copyOf(items); // 불변 복사 반환
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// FAIL: 가변 컬렉션 직접 반환
|
|
68
|
+
public List<OrderItem> getItems() {
|
|
69
|
+
return items; // 외부에서 수정 가능 — 위험
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Optional Usage
|
|
74
|
+
|
|
75
|
+
```java
|
|
76
|
+
// PASS: finder 메서드에서 Optional 반환
|
|
77
|
+
Optional<Order> findByOrderNumber(String orderNumber);
|
|
78
|
+
|
|
79
|
+
// PASS: map/flatMap 사용, get() 금지
|
|
80
|
+
return orderRepository.findByOrderNumber(orderNumber)
|
|
81
|
+
.map(OrderResponse::from)
|
|
82
|
+
.orElseThrow(() -> new OrderNotFoundException(orderNumber));
|
|
83
|
+
|
|
84
|
+
// PASS: 기본값
|
|
85
|
+
String name = Optional.ofNullable(dto.name()).orElse("Unknown");
|
|
86
|
+
|
|
87
|
+
// FAIL: Optional.get() 직접 사용
|
|
88
|
+
Order order = repo.findById(id).get(); // NoSuchElementException 위험
|
|
89
|
+
|
|
90
|
+
// FAIL: 매개변수로 Optional
|
|
91
|
+
public void process(Optional<String> name) {} // 그냥 null 허용으로
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Streams Best Practices
|
|
95
|
+
|
|
96
|
+
```java
|
|
97
|
+
// PASS: 변환에 스트림 사용, 파이프라인 짧게
|
|
98
|
+
List<String> activeNames = orders.stream()
|
|
99
|
+
.filter(o -> o.status() == OrderStatus.ACTIVE)
|
|
100
|
+
.map(Order::customerName)
|
|
101
|
+
.sorted()
|
|
102
|
+
.toList(); // Java 16+
|
|
103
|
+
|
|
104
|
+
// PASS: 메서드 참조
|
|
105
|
+
List<OrderResponse> responses = orders.stream()
|
|
106
|
+
.map(OrderResponse::from) // 람다보다 간결
|
|
107
|
+
.toList();
|
|
108
|
+
|
|
109
|
+
// FAIL: 복잡한 중첩 스트림 — 루프 사용
|
|
110
|
+
orders.stream()
|
|
111
|
+
.flatMap(o -> o.items().stream()
|
|
112
|
+
.filter(i -> i.price().compareTo(BigDecimal.TEN) > 0)
|
|
113
|
+
.map(i -> new ItemDto(o.id(), i.name(), i.price())))
|
|
114
|
+
.collect(Collectors.toList());
|
|
115
|
+
// → 이런 경우 명시적 루프가 더 명확
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Exceptions
|
|
119
|
+
|
|
120
|
+
```java
|
|
121
|
+
// PASS: 도메인 예외 — 컨텍스트 포함
|
|
122
|
+
public class OrderNotFoundException extends RuntimeException {
|
|
123
|
+
public OrderNotFoundException(Long id) {
|
|
124
|
+
super("Order not found: id=" + id);
|
|
125
|
+
}
|
|
126
|
+
public OrderNotFoundException(String orderNumber) {
|
|
127
|
+
super("Order not found: orderNumber=" + orderNumber);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// PASS: 기술적 예외 래핑
|
|
132
|
+
try {
|
|
133
|
+
return externalPaymentApi.charge(request);
|
|
134
|
+
} catch (HttpClientErrorException ex) {
|
|
135
|
+
throw new PaymentException("Payment failed: " + ex.getStatusCode(), ex);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// FAIL: 빈 catch 블록
|
|
139
|
+
try {
|
|
140
|
+
doSomething();
|
|
141
|
+
} catch (Exception e) {} // 절대 금지
|
|
142
|
+
|
|
143
|
+
// FAIL: 너무 광범위한 예외 포착
|
|
144
|
+
try {
|
|
145
|
+
service.process(request);
|
|
146
|
+
} catch (Exception e) { // RuntimeException 이상을 잡으면 안 됨
|
|
147
|
+
log.error("Error", e);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Modern Java Features
|
|
152
|
+
|
|
153
|
+
```java
|
|
154
|
+
// Records (Java 16+) — DTO에 이상적
|
|
155
|
+
public record CreateOrderRequest(
|
|
156
|
+
@NotBlank String customerName,
|
|
157
|
+
@NotNull @Positive BigDecimal amount) {}
|
|
158
|
+
|
|
159
|
+
// Sealed interfaces (Java 17+) — 닫힌 타입 계층
|
|
160
|
+
public sealed interface PaymentResult
|
|
161
|
+
permits PaymentResult.Success, PaymentResult.Failure {
|
|
162
|
+
record Success(String transactionId) implements PaymentResult {}
|
|
163
|
+
record Failure(String errorCode, String message) implements PaymentResult {}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Pattern matching instanceof (Java 16+)
|
|
167
|
+
if (event instanceof OrderCreatedEvent e) {
|
|
168
|
+
log.info("Order created: id={}", e.orderId()); // 캐스트 불필요
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Switch expressions (Java 14+)
|
|
172
|
+
String statusLabel = switch (order.status()) {
|
|
173
|
+
case PENDING -> "대기 중";
|
|
174
|
+
case ACTIVE -> "처리 중";
|
|
175
|
+
case COMPLETED -> "완료";
|
|
176
|
+
case CANCELLED -> "취소됨";
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Text blocks (Java 15+) — SQL 쿼리에 유용
|
|
180
|
+
String query = """
|
|
181
|
+
SELECT o.id, o.customer_name, o.amount
|
|
182
|
+
FROM orders o
|
|
183
|
+
WHERE o.status = :status
|
|
184
|
+
ORDER BY o.created_at DESC
|
|
185
|
+
""";
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Project Structure
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
src/main/java/com/example/
|
|
192
|
+
├── controller/ # REST 컨트롤러
|
|
193
|
+
│ └── OrderController.java
|
|
194
|
+
├── service/ # 비즈니스 로직
|
|
195
|
+
│ └── OrderService.java
|
|
196
|
+
├── repository/ # 데이터 접근
|
|
197
|
+
│ └── OrderRepository.java
|
|
198
|
+
├── domain/ # 도메인 모델/엔티티
|
|
199
|
+
│ ├── Order.java # 도메인 객체
|
|
200
|
+
│ └── OrderEntity.java # JPA 엔티티
|
|
201
|
+
├── dto/ # 요청/응답 DTO
|
|
202
|
+
│ ├── CreateOrderRequest.java
|
|
203
|
+
│ └── OrderResponse.java
|
|
204
|
+
├── exception/ # 도메인 예외
|
|
205
|
+
│ └── OrderNotFoundException.java
|
|
206
|
+
└── config/ # Spring 설정
|
|
207
|
+
├── SecurityConfig.java
|
|
208
|
+
└── JpaConfig.java
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Logging Standards
|
|
212
|
+
|
|
213
|
+
```java
|
|
214
|
+
// PASS: SLF4J, 구조적 매개변수
|
|
215
|
+
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
|
|
216
|
+
|
|
217
|
+
log.info("order_created id={} customer={}", order.getId(), order.getCustomerName());
|
|
218
|
+
log.warn("order_not_found id={}", id);
|
|
219
|
+
log.error("payment_failed orderId={} error={}", orderId, ex.getMessage(), ex);
|
|
220
|
+
|
|
221
|
+
// FAIL: 문자열 연결 (매개변수 사용)
|
|
222
|
+
log.info("Order created: " + order.getId()); // 성능 낭비
|
|
223
|
+
|
|
224
|
+
// FAIL: 민감 데이터 로깅
|
|
225
|
+
log.info("User login email={} password={}", email, password); // 절대 금지
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Testing Expectations
|
|
229
|
+
|
|
230
|
+
- `@ExtendWith(MockitoExtension.class)` 서비스 단위 테스트
|
|
231
|
+
- `@WebMvcTest` 컨트롤러 단위 테스트
|
|
232
|
+
- `@DataJpaTest` 레포지토리 테스트
|
|
233
|
+
- `@SpringBootTest` 통합 테스트에만
|
|
234
|
+
- JaCoCo 라인 커버리지 80% 최소
|
|
235
|
+
|
|
236
|
+
**Remember**: 명확하게 명명, 불변성 유지, 빠른 실패, 예외에 컨텍스트 포함, 모던 Java 기능으로 표현력 향상.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jpa-patterns
|
|
3
|
+
description: JPA/Hibernate patterns for entity design, relationships, query optimization, transactions, auditing, indexing, pagination, and pooling in Spring Boot.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# JPA/Hibernate Patterns
|
|
8
|
+
|
|
9
|
+
Spring Boot에서 데이터 모델링, 레포지토리, 성능 튜닝에 사용.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- JPA 엔티티 및 테이블 매핑 설계
|
|
14
|
+
- 관계 정의 (@OneToMany, @ManyToOne, @ManyToMany)
|
|
15
|
+
- 쿼리 최적화 (N+1 방지, fetch 전략, 프로젝션)
|
|
16
|
+
- 트랜잭션, 감사, 소프트 삭제 설정
|
|
17
|
+
- 페이지네이션, 정렬, 커스텀 레포지토리 메서드 설정
|
|
18
|
+
- HikariCP 연결 풀링 또는 2레벨 캐싱 튜닝
|
|
19
|
+
|
|
20
|
+
## Entity Design
|
|
21
|
+
|
|
22
|
+
```java
|
|
23
|
+
@Entity
|
|
24
|
+
@Table(name = "orders", indexes = {
|
|
25
|
+
@Index(name = "idx_orders_customer_id", columnList = "customer_id"),
|
|
26
|
+
@Index(name = "idx_orders_status_created", columnList = "status, created_at")
|
|
27
|
+
})
|
|
28
|
+
@EntityListeners(AuditingEntityListener.class)
|
|
29
|
+
public class OrderEntity {
|
|
30
|
+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
31
|
+
private Long id;
|
|
32
|
+
|
|
33
|
+
@Column(nullable = false, length = 100)
|
|
34
|
+
private String customerName;
|
|
35
|
+
|
|
36
|
+
@Column(nullable = false, precision = 19, scale = 4)
|
|
37
|
+
private BigDecimal amount;
|
|
38
|
+
|
|
39
|
+
@Enumerated(EnumType.STRING)
|
|
40
|
+
@Column(nullable = false, length = 20)
|
|
41
|
+
private OrderStatus status = OrderStatus.PENDING;
|
|
42
|
+
|
|
43
|
+
@CreatedDate
|
|
44
|
+
@Column(updatable = false)
|
|
45
|
+
private Instant createdAt;
|
|
46
|
+
|
|
47
|
+
@LastModifiedDate
|
|
48
|
+
private Instant updatedAt;
|
|
49
|
+
|
|
50
|
+
@Version
|
|
51
|
+
private Long version; // 낙관적 잠금
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
감사 활성화:
|
|
56
|
+
```java
|
|
57
|
+
@Configuration
|
|
58
|
+
@EnableJpaAuditing
|
|
59
|
+
class JpaConfig {}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Relationships and N+1 Prevention
|
|
63
|
+
|
|
64
|
+
```java
|
|
65
|
+
// 부모 엔티티
|
|
66
|
+
@Entity
|
|
67
|
+
public class OrderEntity {
|
|
68
|
+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
|
69
|
+
private List<OrderItemEntity> items = new ArrayList<>();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// N+1 방지 — JOIN FETCH 사용
|
|
73
|
+
@Query("select o from OrderEntity o left join fetch o.items where o.id = :id")
|
|
74
|
+
Optional<OrderEntity> findWithItems(@Param("id") Long id);
|
|
75
|
+
|
|
76
|
+
// 또는 @EntityGraph 사용
|
|
77
|
+
@EntityGraph(attributePaths = {"items"})
|
|
78
|
+
Optional<OrderEntity> findById(Long id);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Repository Patterns
|
|
82
|
+
|
|
83
|
+
```java
|
|
84
|
+
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
|
|
85
|
+
// 파생 쿼리
|
|
86
|
+
Page<OrderEntity> findByStatus(OrderStatus status, Pageable pageable);
|
|
87
|
+
Optional<OrderEntity> findByOrderNumber(String orderNumber);
|
|
88
|
+
|
|
89
|
+
// JPQL 쿼리
|
|
90
|
+
@Query("select o from OrderEntity o where o.amount >= :minAmount and o.status = :status")
|
|
91
|
+
Page<OrderEntity> findByMinAmountAndStatus(
|
|
92
|
+
@Param("minAmount") BigDecimal minAmount,
|
|
93
|
+
@Param("status") OrderStatus status,
|
|
94
|
+
Pageable pageable);
|
|
95
|
+
|
|
96
|
+
// 집계
|
|
97
|
+
@Query("select count(o) from OrderEntity o where o.status = :status")
|
|
98
|
+
long countByStatus(@Param("status") OrderStatus status);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## DTO Projections (성능 최적화)
|
|
103
|
+
|
|
104
|
+
엔티티 전체 로딩 없이 필요한 필드만 조회:
|
|
105
|
+
|
|
106
|
+
```java
|
|
107
|
+
// 인터페이스 프로젝션
|
|
108
|
+
public interface OrderSummary {
|
|
109
|
+
Long getId();
|
|
110
|
+
String getCustomerName();
|
|
111
|
+
BigDecimal getAmount();
|
|
112
|
+
OrderStatus getStatus();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Page<OrderSummary> findAllBy(Pageable pageable);
|
|
116
|
+
|
|
117
|
+
// 클래스 기반 프로젝션 (DTO 생성자)
|
|
118
|
+
public record OrderSummaryDto(Long id, String customerName, OrderStatus status) {}
|
|
119
|
+
|
|
120
|
+
@Query("select new com.example.dto.OrderSummaryDto(o.id, o.customerName, o.status) from OrderEntity o")
|
|
121
|
+
Page<OrderSummaryDto> findSummaries(Pageable pageable);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Transactions
|
|
125
|
+
|
|
126
|
+
```java
|
|
127
|
+
@Service
|
|
128
|
+
public class OrderService {
|
|
129
|
+
@Transactional
|
|
130
|
+
public OrderResponse updateStatus(Long id, OrderStatus newStatus) {
|
|
131
|
+
OrderEntity entity = repo.findById(id)
|
|
132
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
133
|
+
entity.setStatus(newStatus);
|
|
134
|
+
return OrderResponse.from(entity); // dirty checking으로 자동 저장
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Transactional(readOnly = true)
|
|
138
|
+
public Page<OrderResponse> findByStatus(OrderStatus status, Pageable pageable) {
|
|
139
|
+
return repo.findByStatus(status, pageable).map(OrderResponse::from);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
|
143
|
+
public void auditOrderChange(Long orderId, String action) {
|
|
144
|
+
// 별도 트랜잭션에서 감사 로그 저장
|
|
145
|
+
auditRepo.save(new AuditLog(orderId, action));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Pagination
|
|
151
|
+
|
|
152
|
+
```java
|
|
153
|
+
// 기본 페이지네이션
|
|
154
|
+
PageRequest page = PageRequest.of(0, 20, Sort.by("createdAt").descending());
|
|
155
|
+
Page<OrderEntity> orders = repo.findByStatus(OrderStatus.PENDING, page);
|
|
156
|
+
|
|
157
|
+
// 멀티 정렬
|
|
158
|
+
PageRequest page = PageRequest.of(0, 20,
|
|
159
|
+
Sort.by(Sort.Order.desc("status"), Sort.Order.asc("createdAt")));
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Indexing and Performance
|
|
163
|
+
|
|
164
|
+
- 공통 필터에 인덱스 추가 (`status`, `customerId`, FK)
|
|
165
|
+
- 쿼리 패턴에 맞는 복합 인덱스 (`status, created_at`)
|
|
166
|
+
- `select *` 지양 — 필요한 컬럼만 프로젝션
|
|
167
|
+
- `saveAll`과 `hibernate.jdbc.batch_size`로 배치 쓰기
|
|
168
|
+
|
|
169
|
+
## Connection Pooling (HikariCP)
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
spring:
|
|
173
|
+
datasource:
|
|
174
|
+
hikari:
|
|
175
|
+
maximum-pool-size: 20
|
|
176
|
+
minimum-idle: 5
|
|
177
|
+
connection-timeout: 30000
|
|
178
|
+
idle-timeout: 600000
|
|
179
|
+
max-lifetime: 1800000
|
|
180
|
+
validation-timeout: 5000
|
|
181
|
+
jpa:
|
|
182
|
+
properties:
|
|
183
|
+
hibernate:
|
|
184
|
+
jdbc:
|
|
185
|
+
batch_size: 50
|
|
186
|
+
order_inserts: true
|
|
187
|
+
order_updates: true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Migrations
|
|
191
|
+
|
|
192
|
+
- Flyway 또는 Liquibase 사용 — 프로덕션에서 Hibernate auto DDL 금지
|
|
193
|
+
- 마이그레이션은 멱등성과 추가적으로 유지; 계획 없이 컬럼 삭제 금지
|
|
194
|
+
|
|
195
|
+
```sql
|
|
196
|
+
-- V1__create_orders_table.sql
|
|
197
|
+
CREATE TABLE orders (
|
|
198
|
+
id BIGSERIAL PRIMARY KEY,
|
|
199
|
+
customer_name VARCHAR(100) NOT NULL,
|
|
200
|
+
amount NUMERIC(19, 4) NOT NULL,
|
|
201
|
+
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
|
202
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
203
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
204
|
+
version BIGINT NOT NULL DEFAULT 0
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE INDEX idx_orders_status_created ON orders (status, created_at DESC);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Testing Data Access
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# SQL 디버깅
|
|
214
|
+
logging.level.org.hibernate.SQL=DEBUG
|
|
215
|
+
logging.level.org.hibernate.orm.jdbc.bind=TRACE
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Remember**: 엔티티는 작게, 쿼리는 의도적으로, 트랜잭션은 짧게. fetch 전략과 프로젝션으로 N+1 방지, 읽기/쓰기 경로를 위한 인덱싱.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postgres-patterns
|
|
3
|
+
description: PostgreSQL database patterns for query optimization, schema design, indexing, and security. Based on Supabase best practices.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# PostgreSQL Patterns
|
|
8
|
+
|
|
9
|
+
Quick reference for PostgreSQL best practices. For detailed guidance, use the `database-reviewer` agent.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Writing SQL queries or migrations
|
|
14
|
+
- Designing database schemas
|
|
15
|
+
- Troubleshooting slow queries
|
|
16
|
+
- Implementing Row Level Security
|
|
17
|
+
- Setting up connection pooling
|
|
18
|
+
|
|
19
|
+
## Quick Reference
|
|
20
|
+
|
|
21
|
+
### Index Cheat Sheet
|
|
22
|
+
|
|
23
|
+
| Query Pattern | Index Type | Example |
|
|
24
|
+
|--------------|------------|---------|
|
|
25
|
+
| `WHERE col = value` | B-tree (default) | `CREATE INDEX idx ON t (col)` |
|
|
26
|
+
| `WHERE col > value` | B-tree | `CREATE INDEX idx ON t (col)` |
|
|
27
|
+
| `WHERE a = x AND b > y` | Composite | `CREATE INDEX idx ON t (a, b)` |
|
|
28
|
+
| `WHERE jsonb @> '{}'` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
|
29
|
+
| `WHERE tsv @@ query` | GIN | `CREATE INDEX idx ON t USING gin (col)` |
|
|
30
|
+
| Time-series ranges | BRIN | `CREATE INDEX idx ON t USING brin (col)` |
|
|
31
|
+
|
|
32
|
+
### Data Type Quick Reference
|
|
33
|
+
|
|
34
|
+
| Use Case | Correct Type | Avoid |
|
|
35
|
+
|----------|-------------|-------|
|
|
36
|
+
| IDs | `bigint` | `int`, random UUID |
|
|
37
|
+
| Strings | `text` | `varchar(255)` |
|
|
38
|
+
| Timestamps | `timestamptz` | `timestamp` |
|
|
39
|
+
| Money | `numeric(10,2)` | `float` |
|
|
40
|
+
| Flags | `boolean` | `varchar`, `int` |
|
|
41
|
+
|
|
42
|
+
### Common Patterns
|
|
43
|
+
|
|
44
|
+
**Composite Index Order:**
|
|
45
|
+
```sql
|
|
46
|
+
-- Equality columns first, then range columns
|
|
47
|
+
CREATE INDEX idx ON orders (status, created_at);
|
|
48
|
+
-- Works for: WHERE status = 'pending' AND created_at > '2024-01-01'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Covering Index:**
|
|
52
|
+
```sql
|
|
53
|
+
CREATE INDEX idx ON users (email) INCLUDE (name, created_at);
|
|
54
|
+
-- Avoids table lookup for SELECT email, name, created_at
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Partial Index:**
|
|
58
|
+
```sql
|
|
59
|
+
CREATE INDEX idx ON users (email) WHERE deleted_at IS NULL;
|
|
60
|
+
-- Smaller index, only includes active users
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**RLS Policy (Optimized):**
|
|
64
|
+
```sql
|
|
65
|
+
CREATE POLICY policy ON orders
|
|
66
|
+
USING ((SELECT auth.uid()) = user_id); -- Wrap in SELECT!
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**UPSERT:**
|
|
70
|
+
```sql
|
|
71
|
+
INSERT INTO settings (user_id, key, value)
|
|
72
|
+
VALUES (123, 'theme', 'dark')
|
|
73
|
+
ON CONFLICT (user_id, key)
|
|
74
|
+
DO UPDATE SET value = EXCLUDED.value;
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Cursor Pagination:**
|
|
78
|
+
```sql
|
|
79
|
+
SELECT * FROM products WHERE id > $last_id ORDER BY id LIMIT 20;
|
|
80
|
+
-- O(1) vs OFFSET which is O(n)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Queue Processing:**
|
|
84
|
+
```sql
|
|
85
|
+
UPDATE jobs SET status = 'processing'
|
|
86
|
+
WHERE id = (
|
|
87
|
+
SELECT id FROM jobs WHERE status = 'pending'
|
|
88
|
+
ORDER BY created_at LIMIT 1
|
|
89
|
+
FOR UPDATE SKIP LOCKED
|
|
90
|
+
) RETURNING *;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Anti-Pattern Detection
|
|
94
|
+
|
|
95
|
+
```sql
|
|
96
|
+
-- Find unindexed foreign keys
|
|
97
|
+
SELECT conrelid::regclass, a.attname
|
|
98
|
+
FROM pg_constraint c
|
|
99
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
|
|
100
|
+
WHERE c.contype = 'f'
|
|
101
|
+
AND NOT EXISTS (
|
|
102
|
+
SELECT 1 FROM pg_index i
|
|
103
|
+
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
-- Find slow queries
|
|
107
|
+
SELECT query, mean_exec_time, calls
|
|
108
|
+
FROM pg_stat_statements
|
|
109
|
+
WHERE mean_exec_time > 100
|
|
110
|
+
ORDER BY mean_exec_time DESC;
|
|
111
|
+
|
|
112
|
+
-- Check table bloat
|
|
113
|
+
SELECT relname, n_dead_tup, last_vacuum
|
|
114
|
+
FROM pg_stat_user_tables
|
|
115
|
+
WHERE n_dead_tup > 1000
|
|
116
|
+
ORDER BY n_dead_tup DESC;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Configuration Template
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
-- Connection limits (adjust for RAM)
|
|
123
|
+
ALTER SYSTEM SET max_connections = 100;
|
|
124
|
+
ALTER SYSTEM SET work_mem = '8MB';
|
|
125
|
+
|
|
126
|
+
-- Timeouts
|
|
127
|
+
ALTER SYSTEM SET idle_in_transaction_session_timeout = '30s';
|
|
128
|
+
ALTER SYSTEM SET statement_timeout = '30s';
|
|
129
|
+
|
|
130
|
+
-- Monitoring
|
|
131
|
+
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
|
132
|
+
|
|
133
|
+
-- Security defaults
|
|
134
|
+
REVOKE ALL ON SCHEMA public FROM public;
|
|
135
|
+
|
|
136
|
+
SELECT pg_reload_conf();
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Related
|
|
140
|
+
|
|
141
|
+
- Agent: `database-reviewer` - Full database review workflow
|
|
142
|
+
- Skill: `clickhouse-io` - ClickHouse analytics patterns
|
|
143
|
+
- Skill: `backend-patterns` - API and backend patterns
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
*Based on Supabase Agent Skills (credit: Supabase team) (MIT License)*
|