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.
Files changed (51) hide show
  1. package/.claude/settings.json +42 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +32 -0
  5. package/.mcp.json +19 -0
  6. package/CLAUDE.md +126 -0
  7. package/README.md +142 -0
  8. package/agents/code-reviewer.md +84 -0
  9. package/agents/database-reviewer.md +91 -0
  10. package/agents/java-build-resolver.md +127 -0
  11. package/agents/java-performance-reviewer.md +262 -0
  12. package/agents/planner.md +99 -0
  13. package/agents/security-reviewer.md +119 -0
  14. package/agents/tdd-guide.md +189 -0
  15. package/bin/cli.js +144 -0
  16. package/commands/db-migrate.md +134 -0
  17. package/commands/dev-build.md +72 -0
  18. package/commands/dev-coverage.md +73 -0
  19. package/commands/dev-fix.md +75 -0
  20. package/commands/dev-plan.md +501 -0
  21. package/commands/dev-review.md +144 -0
  22. package/commands/dev-run.md +385 -0
  23. package/commands/dev-test.md +89 -0
  24. package/commands/dev-verify.md +95 -0
  25. package/commands/dev.md +45 -0
  26. package/commands/git-commit.md +112 -0
  27. package/commands/git-issue.md +74 -0
  28. package/commands/git-pr.md +184 -0
  29. package/commands/git-push.md +28 -0
  30. package/package.json +24 -0
  31. package/rules/architecture.md +33 -0
  32. package/rules/coding-style.md +113 -0
  33. package/rules/controller-patterns.md +63 -0
  34. package/rules/dto-patterns.md +76 -0
  35. package/rules/entity-patterns.md +70 -0
  36. package/rules/error-handling.md +56 -0
  37. package/rules/hooks.md +73 -0
  38. package/rules/repository-patterns.md +75 -0
  39. package/rules/security.md +101 -0
  40. package/rules/service-patterns.md +70 -0
  41. package/rules/testing.md +174 -0
  42. package/skills/api-design/SKILL.md +523 -0
  43. package/skills/architecture-decision-records/SKILL.md +179 -0
  44. package/skills/database-migrations/SKILL.md +429 -0
  45. package/skills/hexagonal-architecture/SKILL.md +276 -0
  46. package/skills/java-coding-standards/SKILL.md +236 -0
  47. package/skills/jpa-patterns/SKILL.md +218 -0
  48. package/skills/postgres-patterns/SKILL.md +147 -0
  49. package/skills/springboot-patterns/SKILL.md +255 -0
  50. package/skills/springboot-security/SKILL.md +241 -0
  51. package/skills/springboot-tdd/SKILL.md +236 -0
@@ -0,0 +1,70 @@
1
+ ---
2
+ paths:
3
+ - "**/entity/**/*.java"
4
+ - "**/domain/**/*.java"
5
+ ---
6
+ # Entity 패턴
7
+
8
+ ## 클래스 구조
9
+
10
+ ```java
11
+ @Entity
12
+ @Table(name = "foos")
13
+ @Getter
14
+ @Setter
15
+ public class Foo extends BaseEntity {
16
+ @Id
17
+ @Column(name = "foo_id", length = 255)
18
+ private String id;
19
+ }
20
+ ```
21
+
22
+ ## ID
23
+
24
+ - 타입: `String`
25
+ - 생성: `UUID.randomUUID().toString().replace("-", "")` 또는 프로젝트 공통 ID 생성기 사용
26
+ - DB auto-increment 사용 금지 — 서비스에서 생성 후 주입
27
+
28
+ ## Enum 컬럼
29
+
30
+ ```java
31
+ @Enumerated(EnumType.STRING)
32
+ @Column(length = 30, nullable = false)
33
+ private FooStatus status = FooStatus.ACTIVE;
34
+ ```
35
+
36
+ - `EnumType.STRING` 필수 — `ORDINAL` 사용 금지
37
+ - `length = 30` 설정 (값 추가 시 마이그레이션 불필요)
38
+
39
+ ## 기본값 처리
40
+
41
+ nullable 컬럼 기본값은 `@PrePersist`에서 처리한다.
42
+
43
+ ```java
44
+ @PrePersist
45
+ void applyDefaults() {
46
+ if (content == null) content = "";
47
+ if (count == null) count = 0;
48
+ }
49
+ ```
50
+
51
+ ## 연관관계
52
+
53
+ `@ManyToOne`, `@OneToMany` 매핑 대신 **ID 참조**를 선호한다.
54
+ 조인이 필요하면 Repository(QueryDSL)에서 명시적으로 처리한다.
55
+
56
+ ```java
57
+ // 권장
58
+ private String ownerId;
59
+
60
+ // 지양 (필요한 경우에만 사용)
61
+ @ManyToOne(fetch = FetchType.LAZY)
62
+ @JoinColumn(name = "owner_id")
63
+ private Owner owner;
64
+ ```
65
+
66
+ ## 금지 사항
67
+
68
+ - `FetchType.EAGER` 사용 금지
69
+ - 엔티티에서 DTO import 금지
70
+ - 비즈니스 로직을 엔티티에 과도하게 집중시키지 않음 (상태 전이 메서드는 허용)
@@ -0,0 +1,56 @@
1
+ ---
2
+ paths:
3
+ - "**/*.java"
4
+ ---
5
+ # 에러 처리 패턴
6
+
7
+ ## 예외 클래스 구조
8
+
9
+ 도메인 예외는 HTTP 상태코드 기반으로 분류하고 `@RestControllerAdvice`에서 중앙 처리한다.
10
+
11
+ ```java
12
+ // 사용할 예외 클래스 (프로젝트에 맞게 정의)
13
+ throw new BadRequestException("잘못된 요청입니다.");
14
+ throw new NotFoundException("리소스를 찾을 수 없습니다.");
15
+ throw new ForbiddenException("접근 권한이 없습니다.");
16
+ throw new ConflictException("이미 존재합니다.");
17
+ throw new UnauthorizedException("인증이 필요합니다.");
18
+ ```
19
+
20
+ ## GlobalExceptionHandler
21
+
22
+ ```java
23
+ @RestControllerAdvice
24
+ public class GlobalExceptionHandler {
25
+
26
+ @ExceptionHandler(NotFoundException.class)
27
+ ResponseEntity<ErrorResponse> handleNotFound(NotFoundException ex) {
28
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
29
+ .body(ErrorResponse.of(404, ex.getMessage()));
30
+ }
31
+
32
+ @ExceptionHandler(MethodArgumentNotValidException.class)
33
+ ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
34
+ String message = ex.getBindingResult().getFieldErrors().stream()
35
+ .map(e -> e.getField() + ": " + e.getDefaultMessage())
36
+ .collect(Collectors.joining(", "));
37
+ return ResponseEntity.badRequest().body(ErrorResponse.of(400, message));
38
+ }
39
+ }
40
+ ```
41
+
42
+ ## 에러 응답 포맷
43
+
44
+ ```json
45
+ {
46
+ "status": 404,
47
+ "code": "NOT_FOUND",
48
+ "message": "리소스를 찾을 수 없습니다."
49
+ }
50
+ ```
51
+
52
+ ## 규칙
53
+
54
+ - 비즈니스 예외는 반드시 커스텀 예외 클래스 사용 — `RuntimeException` 직접 throw 금지
55
+ - catch 블록에서 예외 무시(swallow) 금지 — 반드시 로깅하거나 재throw
56
+ - 예외 메시지에 민감한 정보(비밀번호, 토큰 등) 포함 금지
package/rules/hooks.md ADDED
@@ -0,0 +1,73 @@
1
+ ---
2
+ paths:
3
+ - "**/*.java"
4
+ - "**/pom.xml"
5
+ - "**/build.gradle"
6
+ - "**/build.gradle.kts"
7
+ ---
8
+ # Java Hooks
9
+
10
+ > This file extends [common/hooks.md](../common/hooks.md) with Java-specific content.
11
+
12
+ ## PostToolUse 훅
13
+
14
+ `.claude/settings.json`에 설정:
15
+
16
+ ### 컴파일 검증
17
+
18
+ Java 파일 편집 후 자동 컴파일:
19
+
20
+ ```json
21
+ {
22
+ "hooks": {
23
+ "PostToolUse": [
24
+ {
25
+ "matcher": "Edit|Write",
26
+ "command": "if echo \"$FILE_PATH\" | grep -q '\\.java$'; then (./mvnw compile -q --no-transfer-progress 2>&1 | tail -5 || ./gradlew compileJava -q 2>&1 | tail -5) && echo '[Hook] Compile OK' || echo '[Hook] Compile failed'; fi",
27
+ "description": "Java 파일 편집 후 컴파일 검증"
28
+ }
29
+ ]
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Checkstyle 검사
35
+
36
+ ```json
37
+ {
38
+ "hooks": {
39
+ "PostToolUse": [
40
+ {
41
+ "matcher": "Edit|Write",
42
+ "command": "if echo \"$FILE_PATH\" | grep -q '\\.java$'; then ./mvnw checkstyle:check -q 2>&1 | tail -3 || echo '[Hook] Checkstyle failed'; fi",
43
+ "description": "Java 파일 편집 후 스타일 검사"
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## Stop 훅
51
+
52
+ ### 최종 빌드 검증
53
+
54
+ 세션 종료 시:
55
+
56
+ ```json
57
+ {
58
+ "hooks": {
59
+ "Stop": [
60
+ {
61
+ "command": "(./mvnw verify -q --no-transfer-progress 2>&1 | tail -10 || ./gradlew check -q 2>&1 | tail -10) && echo '[Hook] Final verify OK' || echo '[Hook] Verify failed'",
62
+ "description": "세션 종료 시 전체 빌드/테스트 검증"
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ ```
68
+
69
+ ## 권장 순서
70
+
71
+ 1. 컴파일 검증 (빠른 피드백)
72
+ 2. Checkstyle (스타일 강제)
73
+ 3. 최종 `verify` (커버리지 포함 전체 검사)
@@ -0,0 +1,75 @@
1
+ ---
2
+ paths:
3
+ - "**/repository/**/*.java"
4
+ ---
5
+ # Repository 패턴
6
+
7
+ ## 3단계 구조
8
+
9
+ 복잡한 쿼리가 있는 엔티티는 3개 파일로 구성한다.
10
+
11
+ ```java
12
+ // 1. JPA 인터페이스
13
+ public interface FooRepository extends JpaRepository<Foo, String>, FooRepositoryCustom {
14
+ Optional<Foo> findByOwnerId(String ownerId);
15
+ List<Foo> findAllByAccountId(String accountId);
16
+ }
17
+
18
+ // 2. Custom 인터페이스
19
+ public interface FooRepositoryCustom {
20
+ List<Foo> findByCondition(String accountId, FooStatus status);
21
+ }
22
+
23
+ // 3. QueryDSL 구현체
24
+ @Repository
25
+ @RequiredArgsConstructor
26
+ public class FooRepositoryImpl implements FooRepositoryCustom {
27
+ private final JPAQueryFactory queryFactory;
28
+
29
+ @Override
30
+ public List<Foo> findByCondition(String accountId, FooStatus status) {
31
+ QFoo foo = QFoo.foo;
32
+ return queryFactory
33
+ .selectFrom(foo)
34
+ .where(foo.accountId.eq(accountId), foo.status.eq(status))
35
+ .fetch();
36
+ }
37
+ }
38
+ ```
39
+
40
+ ## QueryDSL 규칙
41
+
42
+ - Q-type은 메서드 내 지역 변수로 선언: `QFoo foo = QFoo.foo`
43
+ - `.fetchOne()` — 정확히 하나 기대 (없으면 null, 둘 이상이면 예외)
44
+ - `.fetchFirst()` — 첫 번째 또는 null (유니크 보장 쿼리에 사용 금지)
45
+ - `.fetch()` — 리스트
46
+ - null-safe 집계: `.sum()`, `.count()` 뒤에 `.coalesce(0)` 체이닝
47
+
48
+ ```java
49
+ QFoo foo = QFoo.foo;
50
+ QBar bar = QBar.bar;
51
+
52
+ Integer total = queryFactory
53
+ .select(foo.amount.sum().coalesce(0))
54
+ .from(foo)
55
+ .join(bar).on(bar.id.eq(foo.barId))
56
+ .where(foo.accountId.eq(accountId))
57
+ .fetchOne();
58
+ ```
59
+
60
+ ## JPA vs QueryDSL 분리 기준
61
+
62
+ ```java
63
+ // JPA 네이밍으로 충분
64
+ Optional<Foo> findByBarId(String barId);
65
+ List<Foo> findAllByAccountIdOrderByCreatedAtDesc(String accountId);
66
+
67
+ // QueryDSL 사용 (조인, 집계, 동적 조건)
68
+ List<Foo> findByMultipleConditions(String accountId, List<FooStatus> statuses);
69
+ ```
70
+
71
+ ## 금지 사항
72
+
73
+ - `FetchType.EAGER` 사용 금지
74
+ - 루프 내 지연 로딩 호출 금지 (N+1 유발)
75
+ - Repository에서 비즈니스 로직 작성 금지
@@ -0,0 +1,101 @@
1
+ ---
2
+ paths:
3
+ - "**/*.java"
4
+ - "**/application*.yml"
5
+ - "**/application*.properties"
6
+ ---
7
+ # Java Security
8
+
9
+ > This file extends [common/security.md](../common/security.md) with Java-specific content.
10
+
11
+ ## 시크릿 관리
12
+
13
+ - 절대 소스 코드에 API 키, 토큰, 자격 증명을 하드코딩하지 않기
14
+ - 환경 변수 사용: `System.getenv("API_KEY")`
15
+ - 프로덕션 시크릿에는 시크릿 관리자 사용 (Vault, AWS Secrets Manager)
16
+ - 시크릿이 있는 로컬 설정 파일은 `.gitignore`에 포함
17
+
18
+ ```java
19
+ // BAD
20
+ private static final String API_KEY = "sk-abc123...";
21
+
22
+ // GOOD — 환경 변수
23
+ String apiKey = System.getenv("PAYMENT_API_KEY");
24
+ Objects.requireNonNull(apiKey, "PAYMENT_API_KEY must be set");
25
+ ```
26
+
27
+ ## SQL 인젝션 방지
28
+
29
+ - 항상 파라미터화된 쿼리 사용 — 사용자 입력을 SQL에 절대 연결하지 않기
30
+ - `PreparedStatement` 또는 프레임워크의 파라미터화 쿼리 API 사용
31
+
32
+ ```java
33
+ // BAD — 문자열 연결로 SQL 인젝션 위험
34
+ String sql = "SELECT * FROM orders WHERE name = '" + name + "'";
35
+
36
+ // GOOD — PreparedStatement
37
+ PreparedStatement ps = conn.prepareStatement("SELECT * FROM orders WHERE name = ?");
38
+ ps.setString(1, name);
39
+
40
+ // GOOD — JDBC 템플릿
41
+ jdbcTemplate.query("SELECT * FROM orders WHERE name = ?", mapper, name);
42
+
43
+ // GOOD — Spring Data JPA 파라미터
44
+ @Query("select o from Order o where o.name = :name")
45
+ List<Order> findByName(@Param("name") String name);
46
+ ```
47
+
48
+ ## 입력 검증
49
+
50
+ - 처리 전 시스템 경계에서 모든 사용자 입력 검증
51
+ - Bean Validation 사용 (`@NotNull`, `@NotBlank`, `@Size`) — DTO에 적용
52
+ - 파일 경로와 사용자 제공 문자열 새니타이즈
53
+ - 검증 실패 시 명확한 오류 메시지와 함께 거부
54
+
55
+ ```java
56
+ // Spring Boot DTO 검증
57
+ public record CreateOrderRequest(
58
+ @NotBlank @Size(max = 100) String customerName,
59
+ @NotNull @Positive BigDecimal amount,
60
+ @NotNull @FutureOrPresent LocalDate deliveryDate
61
+ ) {}
62
+
63
+ // 컨트롤러에서 @Valid 적용
64
+ @PostMapping("/orders")
65
+ ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
66
+ return ResponseEntity.status(HttpStatus.CREATED)
67
+ .body(orderService.create(request));
68
+ }
69
+ ```
70
+
71
+ ## 인증 및 인가
72
+
73
+ - 절대 커스텀 암호화 구현 금지 — 검증된 라이브러리 사용
74
+ - 비밀번호는 bcrypt 또는 Argon2로 저장, MD5/SHA1 금지
75
+ - 서비스 경계에서 인가 검사 강제
76
+ - 로그에서 민감 데이터 제거 — 비밀번호, 토큰, PII 절대 로깅 금지
77
+
78
+ ## 의존성 보안
79
+
80
+ - `mvn dependency:tree` 또는 `./gradlew dependencies`로 전이 의존성 감사
81
+ - OWASP Dependency-Check 또는 Snyk으로 알려진 CVE 스캔
82
+ - 의존성 최신 상태 유지 — Dependabot 또는 Renovate 설정
83
+
84
+ ## 오류 메시지
85
+
86
+ - API 응답에 스택 트레이스, 내부 경로, SQL 오류 절대 노출 금지
87
+ - 핸들러 경계에서 예외를 안전하고 일반적인 클라이언트 메시지로 매핑
88
+ - 서버 측에 상세 오류 로깅; 클라이언트에는 일반 메시지 반환
89
+
90
+ ```java
91
+ @ExceptionHandler(Exception.class)
92
+ ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
93
+ log.error("Unexpected error", ex); // 서버 로그에만 상세 정보
94
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
95
+ .body(ApiResponse.error("Internal server error")); // 클라이언트에 일반 메시지
96
+ }
97
+ ```
98
+
99
+ ## 참고
100
+
101
+ skill: `springboot-security` — Spring Security 인증/인가 패턴
@@ -0,0 +1,70 @@
1
+ ---
2
+ paths:
3
+ - "**/service/**/*.java"
4
+ ---
5
+ # Service 레이어 패턴
6
+
7
+ ## 클래스 구조
8
+
9
+ ```java
10
+ @Service
11
+ @RequiredArgsConstructor
12
+ @Slf4j
13
+ public class FooService {
14
+ private final FooRepository fooRepository;
15
+ }
16
+ ```
17
+
18
+ 모든 의존성은 `@RequiredArgsConstructor` + `private final`. `@Autowired` 사용 금지.
19
+
20
+ ## @Transactional
21
+
22
+ - `org.springframework.transaction.annotation.Transactional` 만 사용 — `jakarta.transaction.Transactional` 금지
23
+ - SELECT만 수행하는 메서드: `@Transactional(readOnly = true)` 필수
24
+ - INSERT/UPDATE/DELETE 또는 다른 `@Transactional` 메서드 호출: `@Transactional`
25
+
26
+ ```java
27
+ @Transactional(readOnly = true)
28
+ public FooDtos.FooResponse getFoo(String id) { ... }
29
+
30
+ @Transactional
31
+ public FooDtos.FooResponse createFoo(String ownerId, FooDtos.CreateRequest request) { ... }
32
+ ```
33
+
34
+ ## 조회 + 소유권 체크 패턴
35
+
36
+ 엔티티 조회 후 소유권 검증이 반복되면 private 메서드로 분리한다.
37
+
38
+ ```java
39
+ private Foo findOwnedFoo(String ownerId, String fooId) {
40
+ Foo foo = fooRepository.findById(fooId)
41
+ .orElseThrow(() -> new NotFoundException("리소스를 찾을 수 없습니다."));
42
+ if (!foo.getOwnerId().equals(ownerId)) {
43
+ throw new ForbiddenException("접근 권한이 없습니다.");
44
+ }
45
+ return foo;
46
+ }
47
+ ```
48
+
49
+ ## Service 분리 기준
50
+
51
+ 단일 Service가 커질 때:
52
+ - **파이프라인 단계별**: `FooService` → `FooProcessingService`, `FooQueryService`
53
+ - **외부 서비스별**: `AuthService` → `AppleAuthService`, `KakaoAuthService`
54
+ - **내부 호출 전용**: `FooInternalService` — Kafka 컨슈머 등에서만 호출
55
+
56
+ ## 입력 정규화
57
+
58
+ null 또는 유효하지 않은 값은 서비스 진입점에서 정규화한다.
59
+
60
+ ```java
61
+ private int normalizeDuration(Integer duration) {
62
+ return (duration == null || duration < 0) ? 0 : duration;
63
+ }
64
+ ```
65
+
66
+ ## 금지 사항
67
+
68
+ - `@Transactional(readOnly = true)` 메서드에서 엔티티 변경 금지
69
+ - Service 간 순환 의존 금지
70
+ - HTTP 관련 객체(HttpServletRequest 등) Service에서 직접 사용 금지
@@ -0,0 +1,174 @@
1
+ ---
2
+ paths:
3
+ - "**/*.java"
4
+ - "**/pom.xml"
5
+ - "**/build.gradle"
6
+ - "**/build.gradle.kts"
7
+ ---
8
+ # Java Testing
9
+
10
+ > This file extends [common/testing.md](../common/testing.md) with Java-specific content.
11
+
12
+ ## 테스트 프레임워크
13
+
14
+ - **JUnit 5** (`@Test`, `@ParameterizedTest`, `@Nested`, `@DisplayName`)
15
+ - **AssertJ** — 유창한 단언문 (`assertThat(result).isEqualTo(expected)`)
16
+ - **Mockito** — 의존성 모킹
17
+ - **Testcontainers** — 데이터베이스나 서비스가 필요한 통합 테스트
18
+
19
+ ## 테스트 구성
20
+
21
+ ```
22
+ src/test/java/com/example/app/
23
+ service/ # 서비스 레이어 단위 테스트
24
+ controller/ # 웹 레이어 / API 테스트
25
+ repository/ # 데이터 접근 테스트
26
+ integration/ # 교차 레이어 통합 테스트
27
+ ```
28
+
29
+ `src/main/java` 패키지 구조를 `src/test/java`에 미러링.
30
+
31
+ ## 단위 테스트 패턴
32
+
33
+ ```java
34
+ @ExtendWith(MockitoExtension.class)
35
+ class OrderServiceTest {
36
+
37
+ @Mock
38
+ private OrderRepository orderRepository;
39
+
40
+ private OrderService orderService;
41
+
42
+ @BeforeEach
43
+ void setUp() {
44
+ orderService = new OrderService(orderRepository);
45
+ }
46
+
47
+ @Test
48
+ @DisplayName("findById가 존재하는 주문을 반환")
49
+ void findById_existingOrder_returnsOrder() {
50
+ var order = new Order(1L, "Alice", BigDecimal.TEN);
51
+ when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
52
+
53
+ var result = orderService.findById(1L);
54
+
55
+ assertThat(result.customerName()).isEqualTo("Alice");
56
+ verify(orderRepository).findById(1L);
57
+ }
58
+
59
+ @Test
60
+ @DisplayName("findById가 주문 없을 때 예외 발생")
61
+ void findById_missingOrder_throws() {
62
+ when(orderRepository.findById(99L)).thenReturn(Optional.empty());
63
+
64
+ assertThatThrownBy(() -> orderService.findById(99L))
65
+ .isInstanceOf(OrderNotFoundException.class)
66
+ .hasMessageContaining("99");
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## 웹 레이어 테스트 (MockMvc)
72
+
73
+ ```java
74
+ @WebMvcTest(OrderController.class)
75
+ class OrderControllerTest {
76
+ @Autowired MockMvc mockMvc;
77
+ @MockBean OrderService orderService;
78
+
79
+ @Test
80
+ @DisplayName("POST /orders — 201 Created 반환")
81
+ void createOrder_returns201() throws Exception {
82
+ when(orderService.create(any())).thenReturn(orderResponse());
83
+
84
+ mockMvc.perform(post("/api/orders")
85
+ .contentType(MediaType.APPLICATION_JSON)
86
+ .content("""{"customerName":"Alice","amount":100}"""))
87
+ .andExpect(status().isCreated())
88
+ .andExpect(jsonPath("$.data.customerName").value("Alice"));
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## 통합 테스트 (Testcontainers)
94
+
95
+ ```java
96
+ @Testcontainers
97
+ class OrderRepositoryIT {
98
+
99
+ @Container
100
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
101
+
102
+ private OrderRepository repository;
103
+
104
+ @BeforeEach
105
+ void setUp() {
106
+ var dataSource = new PGSimpleDataSource();
107
+ dataSource.setUrl(postgres.getJdbcUrl());
108
+ dataSource.setUser(postgres.getUsername());
109
+ dataSource.setPassword(postgres.getPassword());
110
+ repository = new JdbcOrderRepository(dataSource);
111
+ }
112
+
113
+ @Test
114
+ void save_and_findById() {
115
+ var saved = repository.save(new Order(null, "Bob", BigDecimal.ONE));
116
+ var found = repository.findById(saved.getId());
117
+ assertThat(found).isPresent();
118
+ }
119
+ }
120
+ ```
121
+
122
+ ## 테스트 슬라이스 선택
123
+
124
+ | 테스트 대상 | 사용할 어노테이션 |
125
+ |------------|----------------|
126
+ | 서비스 단위 테스트 | `@ExtendWith(MockitoExtension.class)` |
127
+ | 컨트롤러 테스트 | `@WebMvcTest` |
128
+ | JPA 레포지토리 테스트 | `@DataJpaTest` |
129
+ | 통합 테스트 | `@SpringBootTest` + `@AutoConfigureMockMvc` |
130
+
131
+ ## 테스트 이름 규칙
132
+
133
+ - 메서드명: `methodName_scenario_expectedBehavior()`
134
+ - `@DisplayName`: 사람이 읽기 좋은 설명
135
+
136
+ ## 커버리지 (JaCoCo)
137
+
138
+ Maven:
139
+ ```xml
140
+ <plugin>
141
+ <groupId>org.jacoco</groupId>
142
+ <artifactId>jacoco-maven-plugin</artifactId>
143
+ <version>0.8.14</version>
144
+ <executions>
145
+ <execution><goals><goal>prepare-agent</goal></goals></execution>
146
+ <execution>
147
+ <id>report</id>
148
+ <phase>verify</phase>
149
+ <goals><goal>report</goal></goals>
150
+ </execution>
151
+ <execution>
152
+ <id>check</id>
153
+ <goals><goal>check</goal></goals>
154
+ <configuration>
155
+ <rules>
156
+ <rule>
157
+ <limits>
158
+ <limit>
159
+ <counter>LINE</counter>
160
+ <value>COVEREDRATIO</value>
161
+ <minimum>0.80</minimum>
162
+ </limit>
163
+ </limits>
164
+ </rule>
165
+ </rules>
166
+ </configuration>
167
+ </execution>
168
+ </executions>
169
+ </plugin>
170
+ ```
171
+
172
+ ## 참고
173
+
174
+ skill: `springboot-tdd` — MockMvc와 Testcontainers를 사용한 Spring Boot TDD 패턴