Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions .agents/skills/architecture-skill/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: architecture-skill
description: 변경분(PR diff)이 이 백엔드의 계층 구조(controller → usecase → service → repository → entity)와 트랜잭션 경계 규칙(진입점은 항상 usecase, @Transactional은 usecase 소유), 패키지/DTO/에러 컨벤션을 준수하는지 정적으로 검증한다. PR 리뷰, 머지 전 점검, 리팩토링 검증 시 사용. 위반을 리포트하고 수정 가능한 항목은 일괄 제안한다.
---

# Architecture Verification Skill

변경된 코드가 프로젝트 아키텍처를 준수하는지 **정적 분석(grep / import 파싱)** 으로 검증한다.
규칙을 새로 정의하지 않는다 — `docs/ai-reference/DESIGN.md`, `OOP.md`, `AGENTS.md`,
그리고 `.Codex/skills/architecture/`(작성-전 가이드)의 규칙을 **판정 가능한 형태로** 적용한다.

> 이 스킬은 **검증(verifying)** 전용이다. 코드를 작성하기 *전에* 규칙을 적용하려면
> `architecture` 스킬을 사용한다. 두 스킬은 같은 규칙을 공유하되 방향이 반대다.

## 언제 쓰나
- PR/브랜치를 머지하기 전 아키텍처 준수 점검
- 리팩토링(레이어 분리, usecase 도입 등)이 의도대로 됐는지 확인
- "이 PR이 우리 구조 잘 따르나?" 류 요청

## 검증 대상 (스코프)
기본은 **변경분만**: `git diff main...HEAD`.
- 인자 없음 → 현재 브랜치 vs `main` diff
- 인자로 브랜치/커밋 범위 지정 가능 (예: `feat/123`, `HEAD~3..HEAD`)
- `*.kt` 파일만 대상. 삭제된 파일은 제외.

## 검증 절차

### Step 1 — 변경 파일 수집 + 레이어 라벨링
```bash
git diff --name-only --diff-filter=d main...HEAD -- '*.kt'
```
각 파일을 **패키지 경로**로 `(도메인, 레이어)` 라벨링한다:
```
.../{domain}/controller/X.kt → (domain, controller)
.../{domain}/usecase/X.kt → (domain, usecase)
.../{domain}/service/X.kt → (domain, service)
.../{domain}/repository/X.kt → (domain, repository)
.../{domain}/entity/X.kt → (domain, entity)
.../{domain}/dto/... → (domain, dto)
.../common/... → (공통, 레이어 검증 제외 — 모든 도메인이 의존 허용)
```

### Step 2 — 정적 규칙 검증
각 변경 파일에 대해 `rules.md`의 규칙을 적용한다. 핵심은 **import 블록 파싱**이다
(이 프로젝트는 본문 FQCN을 금지하므로 import가 의존성의 단일 출처 — DESIGN.md Import 규칙).

검증 그룹:
- **A. 레이어 의존 방향** (import 파싱): controller→repository 직접 의존, service→타 도메인
service/repository 의존, 역방향 의존.
- **B. 패키지/네이밍**: 클래스 suffix와 패키지 레이어 불일치, DTO 위치/네이밍.
- **C. 컨벤션** (grep): raw 예외, BaseEntity 미상속, service의 JPA 인프라 주입,
controller의 Authorization 직접 접근, service의 엔티티 필드 직접 대입.
- **D. 진입점 & 트랜잭션 경계** (import + grep): usecase 있는 도메인에서 controller가
service 직접 호출, service의 `@Transactional` 잔존, usecase 진입 메서드의 `@Transactional` 누락.

규칙별 탐지 패턴과 ✅/❌ 예시는 **`rules.md`** 참조.

### Step 3 — 리포트
`report-template.md` 포맷으로 출력한다.
- 위반: `[규칙ID] 파일:라인 — 무엇이 / 왜 위반 / 수정 방향`
- 통과한 규칙 그룹 요약
- 위반 0건이면 "✅ 아키텍처 준수" 명시

### Step 4 — 수정 일괄 제안
리포트 직후, **자동수정 가능한 위반**을 모아 한 번에 제안하고 사용자 승인 시 적용한다.

| 자동수정 O (기계적) | 자동수정 X (구조 변경 — 제안만) |
|---|---|
| BaseEntity 상속 추가 (C2) | controller→repository 의존 (A: usecase/service 경유 필요) |
| raw RuntimeException → CustomException (C1) | service→타 도메인 의존 (A: usecase로 끌어올려야 함) |
| DTO 파일을 dto/request·response로 이동 (B) | service의 JPA 인프라 주입 제거 (C3: saveAndFlush 등 설계 판단) |
| service의 `@Transactional` 제거 (D2) | controller→service 직접 호출 (D1: 패스스루 usecase 신설 필요) |
| usecase 진입 메서드에 `@Transactional` 추가 (D3) | |

승인 흐름: 리포트 → "수정 가능한 N건을 적용할까요?" → 승인 시 Edit 적용 → `./gradlew ktlintCheck` 권고.

## 한계 (정직하게 명시)
- "controller가 thin한가", "service가 단일 책임인가" 같은 **의미적 판정**은 정적 분석으로
단정할 수 없다. 분기/루프 수, 라인 수 휴리스틱으로 **플래그만** 하고 단정하지 않는다.
- 리플렉션·동적 빈 조회로 숨은 의존은 import에 안 잡힌다. 그래프(MCP)가 필요하면 별도로 안내한다.
50 changes: 50 additions & 0 deletions .agents/skills/architecture-skill/report-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 리포트 출력 포맷

검증 결과는 아래 형식으로 출력한다. 위반이 없으면 "통과" 섹션만 낸다.

```
## 🏛 아키텍처 검증 리포트
대상: <브랜치/범위> | 변경 .kt 파일: <N>개

### ❌ 위반 (<count>건)

[A2] session/service/SessionService.kt:7
무엇이: service가 curriculum 도메인 repository를 직접 import
왜: 크로스 도메인 의존은 usecase에서 조율해야 함 (service끼리 서로 모름)
방향: 해당 조회를 SessionLessonUsecase로 끌어올리고 SessionService는 자기 도메인만 다루도록
자동수정: ❌ 구조 변경 필요

[C2] curriculum/entity/HintNote.kt:12
무엇이: @Entity인데 BaseEntity 미상속
왜: 모든 엔티티는 createdAt/updatedAt 자동관리를 위해 BaseEntity 상속 필수
방향: `: BaseEntity()` 추가
자동수정: ✅

[D2] session/service/SessionQueryService.kt:22
무엇이: usecase 있는 도메인의 service에 @Transactional(readOnly) 잔존
왜: 트랜잭션 경계는 usecase가 소유해야 rollback-only 마킹 추적이 한 곳으로 고정됨
방향: service의 @Transactional 제거 (상위 usecase 진입점에 경계 있는지 D3로 교차 확인)
자동수정: ✅

### ⚠️ 검토 권장 (휴리스틱 — 단정 아님)

[thin?] user/controller/UserController.kt:30
controller 메서드에 분기/조합 로직 다수 — usecase 도입 검토

### ✅ 통과
- A. 레이어 의존 방향: controller→repository 직접 의존 없음, 역방향 의존 없음
- B. 패키지/네이밍: DTO request/response 분리 정상, suffix-패키지 일치
- C. 컨벤션: raw 예외 없음, JPA 인프라 주입 없음, @CurrentUser 사용
- D. 진입점·트랜잭션: controller→usecase 경유, service에 @Transactional 없음, usecase 진입점이 경계 소유

### 🔧 자동수정 제안
수정 가능한 위반 <M>건이 있습니다 (C2 ×1, C1 ×2).
적용할까요? (적용 후 ./gradlew ktlintCheck 권장)
```

## 규칙
- 위반은 **규칙ID 오름차순**(A→B→C)으로 정렬. 같은 규칙은 파일 경로순.
- "방향"은 추상적 훈계가 아니라 **구체적 다음 행동**으로 적는다.
- 위반 0건이면: `✅ 아키텍처 준수 — 위반 없음` + 통과 그룹 요약.
- 자동수정 제안 섹션은 **자동수정 O 항목이 1건 이상일 때만** 출력한다.
- 사용자가 승인하면 Edit으로 적용하고, 적용한 항목 목록 + `./gradlew ktlintCheck` 실행을 권한다.
180 changes: 180 additions & 0 deletions .agents/skills/architecture-skill/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# 검증 규칙 카탈로그

판정 가능한 규칙만 모았다. 각 규칙은 **탐지 패턴**(정적 분석 방법)과 **✅/❌ 예시**, **자동수정 여부**를 가진다.
권위 출처: `docs/ai-reference/DESIGN.md`, `OOP.md`, `AGENTS.md`.

레이어 순서: `controller → usecase → service → repository → entity`. `common/*`은 전 도메인 공통(의존 허용).
도메인: `conversation, curriculum, gamification, notification, session, user`.

---

## A. 레이어 의존 방향 (import 블록 파싱)

각 파일 상단 `import org.prography.samsung.backend.{domain}.{layer}.*`를 추출해 판정한다.

### A1 — controller가 repository 직접 의존 금지
controller는 usecase 또는 service만 호출한다. repository 직접 접근 금지.
- **탐지**: `*/controller/*.kt`의 import에 `.repository.` 포함
- **자동수정**: ❌ (구조 변경 — service/usecase 경유로 흐름 재설계 필요)
```kotlin
// ❌ controller에서
import org.prography.samsung.backend.session.repository.TutoringSessionRepository
// ✅ usecase/service 경유
import org.prography.samsung.backend.session.usecase.SessionLessonUsecase
```

### A2 — service가 다른 도메인의 service/repository 의존 금지 ★이 PR의 핵심
service는 자기 도메인 안에서만 동작한다. **크로스 도메인 조율은 usecase에서** 한다.
- **탐지**: `{domainX}/service/*.kt`의 import에 `{domainY}.service.` 또는 `{domainY}.repository.`
(domainY ≠ domainX, domainY ≠ common)
- **자동수정**: ❌ (해당 호출을 usecase로 끌어올려야 함)
```kotlin
// ❌ session/service/SessionService 에서
import org.prography.samsung.backend.curriculum.repository.CurriculumRepository
// ✅ 크로스 도메인은 usecase에서 조율 (UserHomeUsecase가 sessionService + userProfileService 조합하듯)
```
> 참조 구현: `user/usecase/UserHomeUsecase` — usecase가 `userProfileService` + `sessionService`를
> 조합한다. service끼리는 서로 모른다.

### A3 — 역방향/건너뛰기 의존 금지
하위 레이어가 상위를 import 하면 위반.
- **탐지**:
- service의 import에 `.usecase.` 또는 `.controller.`
- repository의 import에 `.service.`/`.usecase.`/`.controller.`
- usecase의 import에 `.controller.`
- **자동수정**: ❌

### A4 — usecase의 정당한 크로스 도메인 (위반 아님 — 오탐 방지)
usecase는 여러 도메인의 service를 의존해도 **정상**이다. A2와 혼동하지 말 것.
- usecase가 `{otherDomain}.service.*`를 import → ✅ 허용
- usecase가 `{anyDomain}.repository.*`를 직접 import → ⚠️ 플래그 (usecase는 service 경유 권장,
단 자기 도메인 repository 직접 접근은 기존 패턴 확인 후 판단)

---

## B. 패키지 구조 / 네이밍

### B1 — 클래스 suffix와 패키지 레이어 일치
- **탐지**: 파일 경로 레이어 ↔ 클래스명 suffix 대조
- `*Service` 클래스가 `service/` 밖에 있음 → 위반
- `*Usecase`/`*UseCase`가 `usecase/` 밖 → 위반
- `*Controller`가 `controller/` 밖 → 위반
- `*Repository`가 `repository/` 밖 → 위반
- **자동수정**: ❌ (파일 이동은 import 영향 큼 — 제안만)

### B2 — DTO 패키지 분리 & 네이밍
이 PR이 `SharedDtos`/`UserDtos`/`SessionDtos`를 도메인별 `dto/request`·`dto/response`로 분리했다.
- **탐지**:
- `*Request` 클래스가 `dto/request/` 밖 → 위반
- `*Response` 클래스가 `dto/response/` 밖 → 위반
- `dto/` 안에 `*Request`/`*Response`/`*Command` 외 네이밍의 전송 객체 → ⚠️ 플래그
- **자동수정**: ✅ (request/response 하위 패키지로 파일 이동 + package 선언 수정 제안)
> 단, `*Command`(service 레이어 전송)는 위치 규칙이 느슨하다 — 플래그만.

---

## C. 컨벤션 (grep 패턴)

### C1 — raw 예외 금지
- **탐지**: 변경 `.kt`에서 `throw RuntimeException` / `throw IllegalStateException` /
`throw IllegalArgumentException` / service·usecase에서 `throw .*(ErrorBaseCode\.`
- **올바름**: `throw CustomException(DomainErrorCode.SOME_CODE)`
- **자동수정**: ✅ (CustomException 치환 — 단 적절한 DomainErrorCode 선택은 사용자 확인)
> `ErrorBaseCode`는 인프라/프레임워크 레벨(GlobalExceptionHandler·auth filter) 전용. service throw 금지.

### C2 — @Entity는 BaseEntity 상속 필수
- **탐지**: `@Entity` 선언된 클래스의 헤더에 `: BaseEntity()` 없음
- **자동수정**: ✅ (`: BaseEntity()` 추가 + import 추가)
```kotlin
// ✅
@Entity
class SomeEntity(...) : BaseEntity()
```

### C3 — service에 JPA 인프라 직접 주입 금지
service는 "무엇을(비즈니스)"만 안다. "어떻게 DB에 반영하는가"는 repository.
- **탐지**: `*/service/*.kt` 또는 `*/usecase/*.kt` 생성자 파라미터에
`EntityManager` / `JdbcTemplate` / `DataSource`
- **자동수정**: ❌ (saveAndFlush 등으로 대체 — 설계 판단 필요, OOP.md §1 참조)

### C4 — service에서 엔티티 필드 직접 대입 금지
상태 변경은 엔티티 메서드로 캡슐화.
- **탐지** (휴리스틱): service/usecase에서 `<entityVar>.<field> =` 대입 패턴
(`val`/`var` 선언 제외, 단순 프로퍼티 set 대입). 컬렉션 `.clear()`/`.add()` 직접 조작도 플래그.
- **자동수정**: ❌ (엔티티 메서드명 설계 필요 — 플래그 + 제안만)
```kotlin
// ❌ profile.onboardingCompleted = true
// ✅ profile.completeOnboarding()
```

### C5 — controller에서 Authorization 헤더 직접 접근 금지
인증 사용자는 `@CurrentUser userId: Long`로 받는다.
- **탐지**: `*/controller/*.kt`에서 `@RequestHeader.*Authorization` 또는
`request.getHeader("Authorization")` 류
- **자동수정**: ❌ (`@CurrentUser`로 시그니처 변경 — 제안만)

### C6 — 외부 의존성 포트/어댑터 추상화 (LLM 등)
새 외부 벤더 연동 시 `.claude/skills/architecture/ports-and-adapters.md` 기준 적용.
- **탐지** (휴리스틱): service가 벤더 SDK 타입/예외를 직접 import하거나, 한 클래스 안
`when (provider)` 분기로 벤더 선택 → 추상화 누수 의심
- **자동수정**: ❌ (포트/어댑터 분리는 설계 — 플래그 + 해당 서브문서로 안내)

---

## D. 진입점 & 트랜잭션 경계 (import + grep) ★PR #30 합의 규칙

핵심 규칙: **Controller → Usecase → Service, Controller는 usecase만 호출하고, `@Transactional`은
usecase가 소유한다.** service는 트랜잭션 경계를 선언하지 않고 usecase의 경계에 참여만 한다.
이렇게 하면 rollback-only 마킹 지점이 한 곳(usecase)으로 고정되어 "왜 커밋이 안 되지?" 디버깅이 쉬워진다.

> **적용 범위**: `usecase/` 레이어가 있는 도메인만. 현재 `session`, `user`.
> usecase가 아직 없는 도메인(`curriculum`, `notification` 등)은 이 그룹 검증에서 제외한다
> — controller가 service를 직접 호출하고 service가 `@Transactional`을 갖는 게 정상이다.
> 판정 전에 해당 도메인에 `.../{domain}/usecase/` 디렉토리가 존재하는지 먼저 확인한다.

### D1 — controller가 service 직접 호출 금지
usecase 레이어가 있는 도메인에서 controller는 **usecase만** 주입/호출한다. 패스스루(단순 위임)라도
service를 직접 부르지 않고 패스스루 usecase를 거친다 — 진입 경로를 하나로 고정하기 위함.
- **탐지**: `{domain}/controller/*.kt`의 import·생성자에 `{domain}.service.` (단, `{domain}/usecase/` 존재 시)
- **자동수정**: ❌ (패스스루 usecase 신설 + controller 배선 변경 — 구조 변경)
```kotlin
// ❌ usecase 있는 user 도메인인데 controller가 service 직접 주입
class UserController(private val userProfileService: UserProfileService)
// ✅ usecase 경유 (패스스루라도)
class UserController(private val userProfileUsecase: UserProfileUsecase)
```
> 참조: `UserProfileUsecase`(getProfile/getSettings/updateSettings),
> `OnboardingUsecase`(getStatus/saveCurriculum/...) — 단순 위임도 usecase가 감싼다.

### D2 — usecase 있는 도메인의 service에 @Transactional 잔존 금지
service는 트랜잭션 경계를 선언하지 않는다. `@Transactional(readOnly = true)`도 마찬가지.
- **탐지**: `{domain}/service/*.kt`에 `@Transactional` (단, `{domain}/usecase/` 존재 시)
- **자동수정**: ✅ (해당 어노테이션 줄 + 미사용 import 제거. 단 **같은 경계를 담당하는 usecase
진입 메서드에 `@Transactional`이 있는지 D3로 함께 확인** 후 제거 — 무경계 상태 방지)
```kotlin
// ❌ session/service/SessionQueryService
@Transactional(readOnly = true)
fun getStatus(userId: Long): SessionStatusResponse
// ✅ 경계는 usecase가 소유, service는 무경계
fun getStatus(userId: Long): SessionStatusResponse
```

### D3 — usecase 진입(public) 메서드에 @Transactional 필수
controller가 호출하는 usecase의 public 메서드는 트랜잭션 경계를 소유해야 한다.
읽기 전용 흐름은 `@Transactional(readOnly = true)`, 쓰기는 `@Transactional`.
- **탐지**: `{domain}/usecase/*.kt`의 public `fun` 중 `@Transactional`(또는 readOnly) 미부착
(private helper 제외)
- **자동수정**: ✅ (쓰기/읽기 판단은 흐름 확인 필요 — readOnly 여부는 사용자 확인 후 부착)
> D2·D3는 짝이다: service에서 걷어낸 경계가 usecase에 반드시 존재해야 무경계 구멍이 안 생긴다.
> 검증 시 "service에서 제거된 트랜잭션이 상위 usecase 진입점에 있는가"를 교차 확인한다.

---

## 휴리스틱 (단정하지 않고 플래그만)

| 항목 | 신호 | 행동 |
|------|------|------|
| controller가 thin하지 않음 | controller 메서드에 `if`/`when`/`for` 다수, 라인 수 큼 | "비즈니스 로직 의심" 플래그 |
| usecase 누락 의심 | controller가 동일 흐름에서 service 2개+ 직접 조합 | "usecase 도입 검토" 플래그 |

> 휴리스틱은 위반으로 단정하지 않는다. 리포트에 "⚠️ 검토 권장"으로만 표기한다.
Loading
Loading