Community코딩 & 개발github.com

Piyush-Nagar/coding-agents-skills

백엔드 서비스는 네 개의 형제 모듈로 구성된 **패키지-바이-피처(Package by Feature)** 구조를 따릅니다. 이 문서는 새 코드가 어디에 위치해야 하는지에 대한 진실의 원천입니다.

coding-agents-skills란 무엇인가요?

이 스킬은 백엔드 서비스의 패키지 구조를 정의하는 포괄적인 규칙을 제공합니다. **패키지-바이-피처(Package by Feature)** 원칙을 중심으로 capabilities, db, client, starter의 네 가지 모듈로 구성됩니다. 주로 Java/Spring Boot 예제로 설명되지만, 구조 자체는 언어에 구애받지 않으며 Kotlin, Go, Python, Node.js 등 모든 백엔드 언어에 적용됩니다. 각 기능은 독립적인 패키지를 가지며, 하나의 API 엔드포인트당 하나의 하위 기능으로 세분화됩니다. 이 구조는 **변경이 'orders' 기능에 영향을 미칠 때 관련 파일들이 서로 가까이 위치하도록** 보장하여, 기술 계층(controller, service, repository 등)별로 파일이 흩어지는 것을 방지합니다. 또한 db 모듈의 `*Repo` 인터페이스와 client 모듈의 `*Client` 인터페이스를 통한 명확한 의존성 규칙을 강제하여, 지속성 계층과 외부 통합을 교체 가능하게 유지하고 기능 테스트가 계약에 집중할 수 있도록 합니다. 이 스킬은 Claude Code, Cursor, Codex와 같은 AI 에이전트가 프로젝트의 패키징 규칙을 지키도록 가이드하는 데 유용합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Piyush-Nagar/coding-agents-skills/tree/HEAD/backend/core

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

패키지 구조

저희 백엔드 서비스는 네 개의 형제 모듈로 구성된 패키지-바이-피처 구조를 따릅니다. 이 문서는 새 코드가 어디에 위치해야 하는지에 대한 진실의 원천입니다.

언어 참고 사항. 아래 예제는 저희 주 백엔드 스택인 Java/Spring Boot로 작성되었지만, 구조 자체는 언어에 구애받지 않으며 저희가 사용하는 모든 백엔드 언어(Kotlin, Go, Python, Node 등)에 필수입니다. 파일 확장자와 프레임워크 애너테이션은 번역하되, 폴더 레이아웃, 명명 접미사(*Db, *Repo, *RepoImpl, *Dao, *Controller, *ControllerImpl) 및 인터페이스/구현 분할은 변경하지 마십시오.

핵심 원칙

기술 계층이 아닌 기능별로 그룹화하십시오. "orders" 기능에 대한 변경은 controllers/, services/, repositories/ 패키지 전체에 흩어진 파일이 아닌, 서로 옆에 있는 파일에 영향을 미쳐야 합니다. 기술 계층화는 모듈 경계(db, client, capabilities, starter)에서 발생하며, 패키지-바이-피처는 각 모듈 내부에서 이루어집니다.

네 가지 모듈

모든 서비스는 다음 네 개의 형제 모듈로 구성됩니다:

<service>/
├── capabilities/   # 기능/비즈니스 로직 — 두뇌
├── db/             # 지속성: 엔티티, 리포지토리, 마이그레이션
├── client/        # 외부 시스템과의 아웃바운드 통합
└── starter/        # Spring Boot 스타터 — 모든 것을 연결하고 실행

capabilities/ — 비즈니스 로직

각 기능은 고유한 패키지를 가지며, 각 기능 내부에는 API 엔드포인트(getById, create, updateById, getByQuery, …)당 하나의 하위 패키지가 있습니다. 각 하위 기능은 세 개의 형제 폴더를 소유합니다:

  • service/ — 해당 API에 대한 단일 서비스 클래스
  • dto/ — request/ 및 response/ (해당되는 경우)
  • mapper/ — 이 하위 기능 전용으로 *Db 엔티티 ↔ DTO(양방향) 변환

기능의 컨트롤러는 기능 패키지의 루트에 위치하며, 리포지토리와 마찬가지로 인터페이스와 구현으로 분할됩니다:

  • capabilities/<feature>/<Feature>Controller.java — 인터페이스; 라우트, HTTP 동사, 요청/응답 타입, API 문서 애너테이션 선언
  • capabilities/<feature>/<Feature>ControllerImpl.java — 구현; 프레임워크 애너테이션(@RestController)을 가지며 각 라우트를 일치하는 하위 기능 서비스에 위임

해당 기능의 모든 하위 기능에서 공유됩니다. 컨트롤러를 참조하십시오.

  • 의존 대상: db(*Repo 인터페이스만 — DB 사용 규칙 참조), client
  • 절대 의존하지 않음: starter
  • 포함하지 않음: main 클래스, @SpringBootApplication
  • 소유: HTTP 컨트롤러, API 요청/응답 DTO, 매퍼, 서비스

db/ — 지속성

테이블당 하나의 폴더로 구성됩니다. 각 테이블 폴더에는 정확히 세 개의 하위 폴더가 있습니다:

db/<tableName>/
├── model/
│   ├── <EntityName>Db.java          # 주요 JPA 엔티티
│   └── <Other>Db.java               # 하위 모델(임베디드 타입, 자식 엔티티, 프로젝션)
├── dao/
│   └── <EntityName>Dao.java         # 저수준 데이터 접근(Criteria/JPQL/네이티브 쿼리)
└── repository/
    ├── <EntityName>Repo.java        # PUBLIC 인터페이스 — capabilities가 볼 수 있는 유일한 타입
    └── impl/
        └── <EntityName>RepoImpl.java # Spring Data + Dao를 인터페이스 뒤에 구성

명명 규칙

계층접미사예시
JPA 엔티티DbTestcaseDb
리포지토리 인터페이스RepoTestcaseRepo
리포지토리 구현RepoImplTestcaseRepoImpl
데이터 접근 헬퍼DaoTestcaseDao

DB 사용 규칙

Capabilities는 *Repo 인터페이스에만 의존해야 합니다. *RepoImpl 및 *Dao는 내부 전용입니다 — db/<tableName>/ 패키지 외부에서 주입되거나 참조되지 않습니다. 이는 지속성 계층을 교체 가능하게 유지하고 기능 테스트가 계약에 집중할 수 있도록 합니다.

  • 의존 대상: 이 서비스 내 없음
  • 노출: *Repo 인터페이스 및 *Db 모델 타입
  • 절대 포함하지 않음: 비즈니스 로직, API DTO로의 변환

client/ — 아웃바운드 통합

이 서비스가 의존하는 다른 서비스를 호출하는 코드를 보관합니다. 주로 당사 플랫폼 내 의존 마이크로서비스(paymentService, userService, executionService, …)당 하나의 하위 폴더로 구성됩니다. 타사 SDK 래퍼나 메시지 프로듀서도 여기에 위치할 수 있지만, 지배적인 패턴(이 모듈이 존재하는 이유)은 의존 마이크로서비스 클라이언트입니다.

client/<microserviceName>/
├── <Microservice>Client.java       # 인터페이스 — capabilities가 의존하는 대상
├── impl/
│   └── <Microservice>ClientImpl.java   # 구현: HTTP/gRPC/SDK 호출, 재시도, 타임아웃
├── dto/                            # 이 마이크로서비스 API의 요청/응답 형식
│   ├── request/
│   └── response/
└── mapper/                         # 선택 사항: 상대방 형식과 당사 형식 간 변환

규칙

  • 의존 마이크로서비스당 하나의 하위 폴더, 서비스 이름을 따서 명명. 여러 마이크로서비스를 "관련 있다"고 해서 하나의 패키지에 합치지 마십시오.

  • Capabilities는 *Client 인터페이스에만 의존합니다. *ClientImpl은 내부 전용입니다 — *Repo / *RepoImpl과 동일한 규칙입니다.

  • 클라이언트 인터페이스는 마이크로서비스의 API를 자체 DTO로 노출합니다. Capabilities는 필요한 경우 이를 추가로 조정할 수 있습니다.

  • 횡단 관심사(재시도, 서킷 브레이킹, 요청/응답 로깅, 인증 헤더)는 impl/ 내부에 속합니다 — capabilities는 이를 절대 볼 수 없습니다.

  • 의존 대상: 이 서비스 내 없음

  • 노출: *Client 인터페이스 및 해당 요청/응답 DTO

  • 절대 포함하지 않음: 비즈니스 로직, 기능 오케스트레이션

starter/ — 구성 루트

실행되는 유일한 모듈입니다. @SpringBootApplication, 글로벌 REST 예외 핸들러, 보안 설정, 필터 및 application.yml을 포함합니다. 컨트롤러는 없음 — 컨트롤러는 capabilities/에 있습니다.

  • 의존 대상: capabilities(및 전이적으로 db, client)
  • 소유: 배포 가능한 아티팩트, 횡단 웹 설정(보안, CORS, 오류 처리)
  • 절대 소유하지 않음: 기능 컨트롤러 또는 기능 DTO

의존성 규칙

starter → capabilities → { db, client }

화살표는 단방향입니다. db와 client는 capabilities 또는 starter를 가져오지 않습니다. capabilities는 starter를 가져오지 않습니다.

컨트롤러

하위 기능이 아닌 기능당 하나의 컨트롤러가 있으며, capabilities/ 내부의 기능 패키지 루트에 위치합니다. *Repo / *RepoImpl 분할을 반영하여 두 개의 파일로 분할됩니다:

  • capabilities/<feature>/<Feature>Controller.java — 인터페이스. 라우트, HTTP 동사, 경로/쿼리 매개변수, 요청/응답 DTO 타입 및 API 문서 애너테이션(OpenAPI/Swagger)을 선언합니다. 인터페이스를 읽는 사람은 전체 HTTP 계약을 볼 수 있어야 합니다.
  • capabilities/<feature>/<Feature>ControllerImpl.java — 구현. 프레임워크 애너테이션(Spring의 @RestController)을 가지며, 하위 기능 서비스의 주입 대상이며, 라우트당 하나의 짧은 메서드를 포함하여 일치하는 service/를 호출하고 결과를 반환합니다. 위임 외에는 로직이 없습니다.

왜 분할합니까? 인터페이스는 API 계약입니다 — 안정적이고, 문서화되며, 독립적으로 테스트 가능합니다. 구현은 배선입니다 — 계약을 변경하지 않고 변경될 수 있습니다. *Repo vs *RepoImpl과 동일한 이유입니다.

컨트롤러는 각 HTTP 라우트를 capabilities/<feature>/<sub-feature>/service/의 일치하는 하위 기능 서비스에 위임합니다. 요청/응답 DTO는 하위 기능(capabilities/<feature>/<sub-feature>/dto/)에 속합니다. 컨트롤러는 얇은 HTTP 어댑터일 뿐입니다.

왜 starter/가 아닌 capabilities/ 내부에 있습니까? 컨트롤러, 해당 서비스 및 해당 DTO는 API가 변경될 때 모두 함께 변경됩니다 — 하나의 모듈에 유지하면 "하나의 기능, 하나의 장소"라는 패키지-바이-피처 약속이 유지됩니다. starter/는 횡단 배선(보안, 오류 핸들러, 앱 진입점)을 위해 예약되어 있으며, 기능별 웹 코드를 위한 것이 아닙니다.

실제 예제: testcase 기능

아래 트리는 설명을 위해 .java 확장자를 사용합니다. 언어의 파일 확장자와 프레임워크 관용구(코틀린 .kt, Go 패키지, Python 모듈 등)를 대체하되, 폴더 레이아웃과 명명 접미사는 동일하게 유지하십시오.

testcase-service/
├── capabilities/
│   └── com/example/testcase/capabilities/
│       └── testcase/                          # 주요 기능
│           ├── TestcaseController.java        # 인터페이스 — HTTP 계약, 라우트 선언, API 문서
│           ├── TestcaseControllerImpl.java    # 구현 — @RestController, 하위 기능 서비스에 위임
│           ├── getById/                       # 하위 기능 = 하나의 API
│           │   ├── service/
│           │   │   └── GetTestcaseByIdService.java
│           │   ├── dto/
│           │   │   └── response/
│           │   │       └── GetTestcaseByIdResponse.java
│           │   └── mapper/
│           │       └── GetTestcaseByIdMapper.java     # TestcaseDb → response
│           ├── create/
│           │   ├── service/
│           │   │   └── CreateTestcaseService.java
│           │   ├── dto/
│           │   │   ├── request/
│           │   │   │   └── CreateTestcaseRequest.java
│           │   │   └── response/
│           │   │       └── CreateTestcaseResponse.java
│           │   └── mapper/
│           │       └── CreateTestcaseMapper.java      # request → TestcaseDb, TestcaseDb → response
│           ├── updateById/
│           │   ├── service/
│           │   │   └── UpdateTestcaseByIdService.java
│           │   ├── dto/
│           │   │   ├── request/
│           │   │   │   └── UpdateTestcaseByIdRequest.java
│           │   │   └── response/
│           │   │       └── UpdateTestcaseByIdResponse.java
│           │   └── mapper/
│           │       └── UpdateTestcaseByIdMapper.java
│           └── getByQuery/
│               ├── service/
│               │   └── GetTestcaseByQueryService.java
│               ├── dto/
│               │   ├── request/
│               │   │   └── GetTestcaseByQueryRequest.java
│               │   └── response/
│               │       └── GetTestcaseByQueryResponse.java
│               └── mapper/
│                   └── GetTestcaseByQueryMapper.java
│
├── db/                                        # 테이블당 하나의 폴더
│   └── com/example/testcase/db/
│       └── testcase/                          # 테이블 이름
│           ├── model/
│           │   ├── TestcaseDb.java            # JPA 엔티티 — 주요
│           │   └── TestcaseStepDb.java        # 하위 모델 (자식/임베디드)
│           ├── dao/
│           │   └── TestcaseDao.java           # 쿼리/크라이테리아 헬퍼, 패키지-프라이빗 사용
│           └── repository/
│               ├── TestcaseRepo.java          # 인터페이스 — capabilities가 이에 의존
│               └── impl/
│                   └── TestcaseRepoImpl.java  # Spring Data + TestcaseDao 구성
│
├── client/                                   # 의존 마이크로서비스당 하나의 폴더
│   └── com/example/testcase/client/
│       ├── executionService/                 # 호출하는 의존 마이크로서비스
│       │   ├── ExecutionServiceClient.java        # 인터페이스
│       │   ├── impl/
│       │   │   └── ExecutionServiceClientImpl.java  # HTTP/gRPC 호출, 재시도, 타임아웃
│       │   ├── dto/
│       │   │   ├── request/
│       │   │   │   └── RunTestcaseRequest.java
│       │   │   └── response/
│       │   │       └── RunTestcaseResponse.java
│       │   └── mapper/
│       │       └── ExecutionServiceMapper.java
│       └── userService/                      # 다른 의존 마이크로서비스
│           ├── UserServiceClient.java
│           └── impl/
│               └── UserServiceClientImpl.java
│
└── starter/                                  # 횡단 배선만 — 컨트롤러 없음
    └── com/example/testcase/
        ├── TestcaseServiceApplication.java
        ├── config/
        │   ├── SecurityConfig.java
        │   └── WebConfig.java
        ├── error/
        │   └── GlobalExceptionHandler.java   # @ControllerAdvice — 횡단, 기능별 아님
        └── resources/application.yml

실용 규칙

  • 기존 기능에 새 API를 추가하는 경우? capabilities/<feature>/ 내부에 자체 service/, dto/{request,response}/ 및 mapper/와 함께 새 하위 기능 패키지를 만든 다음 <Feature>Controller.java(인터페이스)에 라우트 선언을 추가하고 <Feature>ControllerImpl.java에 위임 메서드를 추가하십시오. 두 번째 컨트롤러를 만들지 마십시오.
  • 완전히 새로운 기능을 추가하는 경우? capabilities/에 기능 패키지(루트에 컨트롤러, API당 하위 기능)를 만들고 db/<tableName>/에 일치하는 테이블 폴더(model/, dao/, repository/{impl/} 포함)를 만드십시오. 외부 시스템을 호출하는 경우에만 클라이언트를 추가하십시오. starter/는 건드리지 마십시오.
  • 항상 *Repo를 주입하고, 절대 *RepoImpl 또는 *Dao를 주입하지 마십시오. Capabilities는 인터페이스에만 의존합니다. *RepoImpl 및 *Dao는 db/<tableName>/ 내부 전용입니다 — 쿼리 전략이나 백엔드 저장소를 기능 코드 변경 없이 교체할 수 있도록 존재합니다.
  • 항상 *Client를 주입하고, 절대 *ClientImpl을 주입하지 마십시오. 의존 마이크로서비스 호출에도 동일한 규칙입니다. Capabilities는 인터페이스에 의존합니다. 전송(HTTP, gRPC, SDK)은 impl/ 뒤에 있습니다.
  • client/ 내부에 의존 마이크로서비스당 하나의 폴더. 폴더 이름은 마이크로서비스 이름을 따르십시오. "모든 내부 서비스"를 하나의 패키지로 그룹화하지 마십시오.
  • 하위 기능 세분성은 capabilities/에서 멈춥니다. db/는 테이블별로 구성되고, client/는 외부 시스템별로 구성됩니다. 이를 API별로 분할하면 응집력 있는 코드가 분산됩니다.
  • 요청/응답 DTO와 해당 매퍼는 하위 기능별로 있으며, 절대 공유되지 않습니다. 오늘 create와 updateById가 비슷해 보이더라도, 두 개의 DTO와 두 개의 매퍼를 유지하십시오. 공유는 독립적으로 변할 두 API를 결합시킵니다.
  • 매퍼는 엔티티 ↔ DTO를 직접 변환합니다. 중간 도메인 모델 계층은 없습니다. <SubFeature>Mapper는 *Db ↔ 해당 하위 기능의 요청/응답 DTO를 변환합니다.
  • 세 개의 하위 기능이 유사한 코드를 가지고 있습니까? 동일한 패턴을 네 개 이상의 위치에서 볼 때까지 공유 서비스나 매퍼를 추출하려는 충동을 참으십시오.
  • 기능 간 재사용? 공유 유틸리티는 capabilities/의 common/ 패키지에 넣되, 진정한 공유(예: 날짜 유틸리티, 공통 예외)인 경우에만 해당합니다. 여러 하위 기능에서 동일한 DTO나 매퍼 변환을 사용하고 있다면 잘못된 것입니다.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

관련 스킬