Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7de8693
build(gradle): add rest-assured test dependency
kakao-daniel-hoon Feb 20, 2026
b54759a
test(acceptance): add base test class and configuration
kakao-daniel-hoon Feb 20, 2026
3ba1ab1
test(category): add category acceptance test
kakao-daniel-hoon Feb 20, 2026
e38eb03
test(product): add product acceptance test
kakao-daniel-hoon Feb 20, 2026
0ef8b08
test(gift): add gift acceptance test
kakao-daniel-hoon Feb 20, 2026
48d8b5e
docs: add test strategy and AI usage documentation
kakao-daniel-hoon Feb 20, 2026
1d81b47
chore(skills): add write-acceptance-test skill
kakao-daniel-hoon Feb 20, 2026
5835b1c
docs(claude): add CLAUDE.md following best practices
kakao-daniel-hoon Feb 20, 2026
e7d84b9
docs(ai-usage): update with CLAUDE.md and skill usage details
kakao-daniel-hoon Feb 20, 2026
e10f543
docs(ai-usage): clarify CLAUDE.md analysis context
kakao-daniel-hoon Feb 20, 2026
0900e91
fix(controller): add @RequestBody to Product/Category controllers
kakao-daniel-hoon Feb 23, 2026
6f7fa99
test: separate ambiguous create/retrieve acceptance tests
kakao-daniel-hoon Feb 23, 2026
aced1c3
docs(skills): add test naming clarity guidelines
kakao-daniel-hoon Feb 23, 2026
1074dfc
test: add response body verification to creation tests
kakao-daniel-hoon Feb 23, 2026
163b30d
feat(error): add custom exception infrastructure
kakao-daniel-hoon Feb 23, 2026
e2a0f08
refactor: replace generic exceptions with BusinessException
kakao-daniel-hoon Feb 23, 2026
8bcef60
test(gift): add failure case tests for invalid Member-Id
kakao-daniel-hoon Feb 23, 2026
42c5ccf
refactor(error): split BusinessException into domain exceptions
kakao-daniel-hoon Feb 23, 2026
e8c3ba9
refactor(error): add invalid body test, remove unused fallback handler
kakao-daniel-hoon Feb 23, 2026
48315bb
test: add error code verification to failure tests
kakao-daniel-hoon Feb 23, 2026
ef8602a
docs(skills): add skills for step-2 test infrastructure
kakao-daniel-hoon Feb 23, 2026
3ebba44
test(cucumber): replace JUnit acceptance tests with Cucumber BDD
kakao-daniel-hoon Feb 24, 2026
7522784
infra(docker): replace H2 with PostgreSQL via Docker Compose
kakao-daniel-hoon Feb 24, 2026
9c2f08c
infra(docker): containerize Spring Boot app with Docker Compose
kakao-daniel-hoon Feb 25, 2026
10d4cd3
Merge branch 'kdaehun00' into step-2
kdaehun00 Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions .claude/skills/dockerize-app/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
name: dockerize-app
description: Spring Boot 애플리케이션을 Docker 컨테이너로 실행하여 프로덕션과 동일한 환경에서 End-to-End 테스트를 수행한다.
disable-model-invocation: true
argument-hint: (인자 없음)
---

Spring Boot 애플리케이션을 컨테이너화한다. 아래 단계를 순서대로 수행한다.

## 전제 조건

- 요구사항 2(PostgreSQL + Docker Compose)가 완료된 상태여야 한다
- docker-compose.yml에 PostgreSQL 서비스가 구성되어 있어야 한다

## 1단계: .dockerignore 작성
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker 컨테이너로 실행하여 프로덕션과 동일한 환경에서 End-to-End 테스트를 수행

해당 skill의 목적은 테스트 수행인가요? 아니면 Dockerfile 작성 및 수행일까요??

claude code skill을 잘 인식하게 하기위해 description을 수정해주면 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 스킬의 핵심 목적은 Dockerfile 작성 및 Docker Compose를 통한 애플리케이션 컨테이너화였습니다!
현재는 테스트를 수행한다는 의미가 내포되어있어 혼란이 있을 것 같습니다. 이 부분 수정하도록 하겠습니다!


프로젝트 루트에 `.dockerignore`를 생성하여 불필요한 파일을 제외한다.

```
.git
.gradle
.claude
.idea
build
*.md
```

## 2단계: Dockerfile 작성 (Multi-stage build)

프로젝트 루트에 `Dockerfile`을 생성한다.

```dockerfile
# Builder stage: Gradle로 빌드
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle settings.gradle ./
RUN ./gradlew dependencies --no-daemon || true
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# Runtime stage: 경량 JRE로 실행
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
```

Multi-stage build를 사용하는 이유:
- Builder stage에는 JDK, Gradle, 소스코드가 포함되지만 최종 이미지에는 JRE + JAR만 포함
- 이미지 크기를 최소화하고 보안 표면을 줄인다

## 3단계: docker-compose.yml에 앱 서비스 추가

기존 docker-compose.yml에 애플리케이션 서비스를 추가한다.

```yaml
services:
postgres:
# ... 기존 설정 유지

app:
build: .
ports:
- "28080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/gift_test
SPRING_DATASOURCE_USERNAME: test
SPRING_DATASOURCE_PASSWORD: test
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/actuator/health || exit 1"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
```

네트워크 구조:
- 컨테이너 내부: `app` → `postgres:5432` (Docker service name이 hostname)
- 호스트(테스트): `localhost:28080` → `app`, `localhost:5432` → `postgres`

## 4단계: Gradle 태스크 업데이트

```groovy
tasks.register('dockerBuild', Exec) {
commandLine 'docker', 'compose', 'build'
}

tasks.register('dockerUp', Exec) {
commandLine 'docker', 'compose', 'up', '-d', '--wait'
}

tasks.register('dockerDown', Exec) {
commandLine 'docker', 'compose', 'down'
}
```

## 5단계: 테스트 설정 변경

Docker 컨테이너의 앱에 HTTP 요청을 보내므로 embedded 서버가 필요 없다.

```java
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("cucumber")
public class CucumberSpringConfiguration {

@Before
public void setUp() {
RestAssured.baseURI = "http://localhost";
RestAssured.port = 28080;
}
}
```

- `webEnvironment = NONE`: 테스트 프로세스 내에서 Spring Boot 서버를 띄우지 않는다
- 테스트는 호스트에서 실행되고, Docker 컨테이너의 앱(28080)에 HTTP 요청을 보낸다

## 6단계: DB 직접 접근 (cleanup 용도)

테스트에서 DB 초기화를 위해 JdbcTemplate으로 PostgreSQL에 직접 접근한다.

application-cucumber.properties에서 DB 연결 설정을 유지한다:
```properties
spring.datasource.url=jdbc:postgresql://localhost:5432/gift_test
```

- 테스트(호스트) → `localhost:5432` → PostgreSQL 컨테이너
- 앱(컨테이너) → `postgres:5432` → PostgreSQL 컨테이너

같은 DB를 다른 hostname으로 접근하는 구조이다.

## 7단계: Spring Boot Actuator 추가 (healthcheck 용)

healthcheck에 actuator health endpoint를 사용한다면 의존성을 추가한다.

```groovy
implementation 'org.springframework.boot:spring-boot-starter-actuator'
```

또는 healthcheck를 curl/wget 대신 TCP 체크로 대체할 수도 있다.

## 8단계: 검증

```bash
./gradlew dockerBuild
./gradlew dockerUp
curl http://localhost:28080 # 앱 응답 확인
./gradlew cucumberTest # Docker 환경에서 테스트
./gradlew dockerDown
```

- `docker ps`로 컨테이너 상태를 확인한다
- `docker logs <container>`로 앱/DB 로그를 확인한다

## 트러블슈팅

| 문제 | 해결 |
|------|------|
| 앱이 DB 연결 실패 | `depends_on: condition: service_healthy` 확인 |
| 포트 충돌 | `docker ps`로 기존 컨테이너 확인, `docker compose down` 후 재시작 |
| 이미지 캐시 문제 | `docker compose build --no-cache` |
| 앱 시작 지연 | healthcheck의 `start_period` 조정 |
118 changes: 118 additions & 0 deletions .claude/skills/setup-cucumber/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
name: setup-cucumber
description: Cucumber BDD 인프라를 구성한다. 의존성 추가, 테스트 러너, Spring 통합, 기존 테스트를 Gherkin 시나리오로 전환한다.
disable-model-invocation: true
argument-hint: (인자 없음)
---

Cucumber BDD 인프라를 구성한다. 아래 단계를 순서대로 수행한다.

## 1단계: 의존성 추가

build.gradle에 Cucumber 관련 의존성을 추가한다.

필요한 의존성:
- `io.cucumber:cucumber-java` — Step Definitions 작성
- `io.cucumber:cucumber-spring` — Spring Boot 통합
- `io.cucumber:cucumber-junit-platform-engine` — JUnit Platform에서 실행
- `org.junit.platform:junit-platform-suite` — Suite API로 Cucumber 실행

한글 Step Definitions(`조건`, `먼저`, `만약`, `그러면`)을 사용하려면 `io.cucumber.java.ko` 패키지를 import한다.

## 2단계: Cucumber 테스트 러너 생성

`src/test/java/gift/acceptance/` 에 CucumberTest 클래스를 생성한다.

```java
@Suite
@IncludeEngines("cucumber")
@SelectPackages("gift.acceptance")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "gift.acceptance")
@ConfigurationParameter(key = FEATURES_PROPERTY_NAME, value = "src/test/resources/features")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
class CucumberTest {
}
```

## 3단계: Spring 통합 설정

`src/test/java/gift/acceptance/` 에 CucumberSpringConfiguration 클래스를 생성한다.

```java
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration {

@LocalServerPort
private int port;

@Before
public void setUp() {
RestAssured.port = port;
}
}
```

- `@CucumberContextConfiguration`으로 Spring 컨텍스트를 Cucumber에 연결한다
- `@Before`는 `io.cucumber.java.Before`를 사용한다 (JUnit의 @BeforeEach가 아님)

## 4단계: Feature 파일 디렉토리 생성

`src/test/resources/features/` 디렉토리를 생성한다.

## 5단계: 시나리오 간 상태 공유 Bean

Step Definitions 간 HTTP 응답을 공유할 Bean을 생성한다.

```java
@Component
@ScenarioScope
public class SharedContext {
private Response response;

public Response getResponse() { return response; }
public void setResponse(Response response) { this.response = response; }
}
```

- `@ScenarioScope`로 시나리오마다 새 인스턴스를 생성하여 격리한다
- Step Definition 클래스들이 이 Bean을 주입받아 사용한다

## 6단계: 데이터 격리

Cucumber에서는 `@Sql` 어노테이션을 사용할 수 없다. 대신 `@Before` hook에서 데이터를 초기화한다.

```java
@Before
public void cleanup() {
// JdbcTemplate 또는 Repository를 사용하여 테이블 초기화
}
```

또는 각 시나리오의 `배경` (Background) 블록에서 데이터 상태를 명시한다.

## 7단계: 기존 테스트 전환

기존 RestAssured 기반 인수 테스트를 Feature 파일 + Step Definitions로 전환한다.

전환 순서:
1. 기존 테스트의 @DisplayName을 시나리오 제목으로 변환
2. 테스트 로직을 Given/When/Then으로 분리
3. Step Definition 메서드를 작성
4. 기존 테스트 클래스는 전환 완료 후 제거

## 8단계: 검증

```bash
./gradlew test
```

- 모든 Cucumber 시나리오가 실행되고 통과하는지 확인한다
- `pretty` 플러그인으로 한글 시나리오가 콘솔에 출력되는지 확인한다

## 주의사항

- Feature 파일은 `.feature` 확장자를 사용한다
- 한글 Gherkin 사용 시 파일 첫 줄에 `# language: ko`를 명시한다
- Step Definition 파라미터 추출은 Cucumber Expressions 또는 정규식을 사용한다
- 하나의 Step Definition 클래스가 너무 커지지 않도록 도메인별로 분리한다
Loading