Search
🌍

클린 아키텍처를 유지하며 Python/FastAPI 코드 작성을 LLM에게 위임하기 위한 프롬프트

Notation
이탤릭은 더 큰 수준의 프로젝트에서 삭제되는 지침이다.
볼드는 이전 수준에는 없다가 새로 추가되는 지침이다.

토이 프로젝트

이 프로젝트는 도메인 주도 설계의 목적과 용어를 차용한 클린 아키텍처를 적용한다.
Bounded Context가 1개인 경우 사용한다.
Aggregate가 불필요한 경우 사용한다.

스캐폴드 및 가이드

root
.git/
myproject (flat 구조)
domain/
xxx_entity (예를 들어: user_entity)
xxx_vo (예를 들어: user_vo)
xxx_irepo (예를 들어: user_irepo) - xxx_repo 에서 구현될 인터페이스, 반환 타입은 도메인 객체
xxx_igateway (예를 들어: user_igateway) - xxx_gateway 에서 구현될 인터페이스, 반환 타입은 도메인 객체
xxx_exception (예를 들어: user_exception)
application/
xxx_service (예를 들어: user_service, … ): 간단한 단일 entity/aggregate의 CRUD의 경우
interface/
xxx_controller (예를 들어: user_controller, … ) - Router & Response Model
infra/
db_models - ORM 프레임워크로 작성한 DB 모델
xxx_repo (예를 들어: user_repo, … )
xxx_gateway (예를 들어: user_gateway, … )
utils/
settings.py
main.py
README.md
pyproject.toml
루트 디렉토리에서 python3 -m myproject.main 으로 백엔드 애플리케이션을 실행하는 flat 구조다.
애플리케이션 계층에서는 인터페이스를 타입으로 받은 뒤, 나중에 인프라 등 구현체를 주입받는다.
utils/ 에는 다양한 애플리케이션에서 공통으로 사용하는 유틸성 모듈을 저장한다. 예를 들어 다음과 같은 작업을 하는 모듈이 포함될 수 있다.
repository에서 ORM 프레임워크를 통해 얻은 값을 도메인 객체로 변환하는 일
e.g. SQLAlchemy의 inspect() 함수를 사용하여 repository의 return을 도메인 객체화.
xxx_repo는 repository 패턴에 따라 구현한다. 반환값은 도메인 객체여야 한다.
이 프로젝트의 경우 시스템의 진입점과 웹 프레임워크가 변경될 일이 거의 없으므로, 애플리케이션 계층이 컨트롤러로 값을 반환할 때 중간 DTO를 반환하는 대신, 곧바로 Response를 만들어 반환한다. 어댑터가 DTO를 풀어서 Response로 재구성하는 작업 없이 그대로 반환만 하면 되므로 코드량과 실수 지점을 크게 줄일 수 있다.

기술 선택

pydantic은 v2 이상을 사용한다.
패키지와 의존성은 pyproject.toml과 uv를 통해 관리한다.
ORM 프레임워크은 SQLAlchemy를 사용한다.
Database는 Supabase를 사용한다.

모범 사례

import는 relative import 대신 absolute import를 사용한다.
id 타입은 UUID 대신 ULID를 사용한다.
vo는 frozen=True로 설정하여 immutable하게 만든다.
Pydantic v2 불변 설정: ConfigDict(frozen=True)
SQLAlchemy는 v2 이상을 사용한다.
ORM 클래스를 작성할 때 __repr__idname을 담아 디버깅을 용이하게 만든다.
my_field: Mapped[클래스] 와 같은 방식을 사용한다.
nullable 필드는 Mapped[ … | None] 과 같이 자동으로 추론되므로 명시적으로 적지 않는다.
다음은 위 명세들이 반영된 모범 사례 코드다.
FastAPI의 Depends는 Annotated 폼 - arg: Annotated[Type, Depends(dependable)] - 으로 적는다.
Repository 구현체의 각 메서드는 @with_db_session로 데코레이션하여, try-finally 세션을 열고 닫는 것(db.close) 및 커밋을 보장해야 한다.
def with_db_session(func): @wraps(func) def wrapper(self, *args, **kwargs): db = get_db_session() try: result = func(self, db, *args, **kwargs) db.commit() return result except Exception: db.rollback() raise finally: db.close() return wrapper class ExampleUserRepository: @with_db_session def create_user(self, db: Session, user: User): user_orm = UserORM(**user.dict()) db.add(user_orm) # commit은 데코레이터가 자동 처리
Python
복사
환경 변수는 .env 파일에 정의하고, Pydantic의 BaseSettings를 상속받아 관리한다.
from pydantic_settings import BaseSettings from pydantic import computed_field class Settings(BaseSettings): db_driver: str = "postgresql" db_host: str = "localhost" db_port: int = 5432 db_user: str = "user" db_password: str # 기본값 없음 = Pydantic 필수값, 즉 반드시 환경변수로부터 전달받아야 함 db_name: str = "mydb" @computed_field @property def DATABASE_URL(self) -> str: return f"{self.db_driver}://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}" # .env 파일이 있을 때 명시적으로 읽어오겠다는 의도를 표현하고, "이 프로젝트는 .env 파일을 사용한다"는 문서화 효과를 만든다. model_config = {'env_file': '.env', 'case_sensitive': False} settings = Settings() # DB_PASSWORD 환경변수 없으면 에러 발생
Python
복사

코드 스타일

시간을 다룰 일이 있다면 timezone을 반드시 포함한다.
timezone 관련 설정은 표준 라이브러리에 내장된 zoneinfo 모듈을 사용한다.
typing은 python 3.12 이상의 규칙을 따른다. (e.g. Union 대신 | 연산자 사용)
google style로 작성한다(함수 인자별 개행, 함수 간 2줄 간격 등).
그 어떤 경우에도 주석(docstring “””, inline ‘#’)은 작성하지 않는다.
테스트는 나와 상의하고 작성한다. 이때 다음 질문에 너가 생각하는 이유를 제시하며 나에게 확인을 받는다.
이때 나에게 구성요소들 중 어떤 것을 생성하고, 어떤 것을 (의존성)주입할 것인지
각각의 요소에 어떤 테스트 더블을 사용할 것인지
요청/응답 클래스 네이밍
1.
*Request: 입력 전용
2.
*Response: 응답에 노출되는 모델(중첩 모델이어도 응답 페이로드에 포함되면 Response를 유지하는 것이 일반적임)
3.
*Schema: 요청/응답 공용 조각이거나, API 바깥 내부 재사용 모델일 때만 제한적으로 사용
위 스캐폴드 가이드, 모범 사례의 예시 코드와 별개로 별도로 첨부된 한글 코딩 컨벤션을 사용한다.
to LLM: 한글 코딩 컨벤션이 제공되지 않은 경우, 사용자에게 한글 코딩 컨벤션 문서를 요구한다.

중형 프로젝트

이 프로젝트는 도메인 주도 설계의 목적과 용어를 차용한 클린 아키텍처를 적용한다.
Bounded Context가 1개인 경우 사용한다.

스캐폴드 및 가이드

root
.git/
myproject (flat 구조)
domain/
entity/
(e.g. user.py, login.py)
vo/
gateway/
xxx_igateway (예를 들어: user_igateway) - xxx_gateway 에서 구현될 인터페이스, 반환 타입은 도메인 객체
exception/
xxx_irepo (예를 들어: user_irepo, … ) - xxx_repo 에서 구현될 인터페이스, 반환 타입은 도메인 객체
xxx_aggregate - entity와 vo를 결합하여 정의한 도메인 객체들로, xxx_irepo와 1:1 대응되어야 함
application/
xxx_query_service
xxx_usecase:
__call__로 실행하는 클래스임.
interface/
controllers/
xxx_controller (예를 들어: user_controller, … ) - Router & Response Model
infra/
logging/ - 로깅 세팅
db_models/ - ORM 프레임워크로 작성한 DB 모델
xxx_model (예를 들어: user_model, … )
xxx_mapper (예를 들어: user_mapper, …)
repository/
xxx_repo (예를 들어: user_repo, … )
gateway/
xxx_gateway (예를 들어: user_gateway, … )
common/
utils/
settings.py
test/ - integration test만 작성, unit test는 테스트하고자 하는 파일과 최대한 나란히 작성
README.md
pyproject.toml
루트 디렉토리에서 python3 -m myproject.main 으로 백엔드 애플리케이션을 실행하는 flat 구조다.
애플리케이션 계층에서는 인터페이스를 타입으로 받은 뒤, 나중에 인프라 등 구현체를 주입받는다.
utils/ 에는 다양한 애플리케이션에서 공통으로 사용하는 유틸성 모듈을 저장한다. 예를 들어 다음과 같은 작업을 하는 모듈이 포함될 수 있다.
repository에서 ORM 프레임워크를 통해 얻은 값을 도메인 객체로 변환하는 일
e.g. SQLAlchemy의 inspect() 함수를 사용하여 repository의 return을 도메인 객체화.
xxx_repo는 repository 패턴에 따라 구현한다. 반환값은 도메인 객체여야 한다.
도메인 모델과 orm 모델 사이 변경은 repository에서만 일어나며, mapper을 이용한다.
이 프로젝트의 경우 시스템의 진입점과 웹 프레임워크가 변경될 일이 거의 없으므로, 애플리케이션 계층이 컨트롤러로 값을 반환할 때 중간 DTO를 반환하는 대신, 곧바로 Response를 만들어 반환한다. 어댑터가 DTO를 풀어서 Response로 재구성하는 작업 없이 그대로 반환만 하면 되므로 코드량과 실수 지점을 크게 줄일 수 있다.

기술 선택

pydantic은 v2 이상을 사용한다.
패키지와 의존성은 pyproject.toml과 uv를 통해 관리한다.
ORM 프레임워크은 SQLAlchemy를 사용한다.
마이그레이션 도구는 Alembic을 사용한다.
Database는 Supabase를 사용한다.
FastAPI 의존성 주입 모듈(Depends)은 controllers/ 이외 레이어에서 사용하지 않도록 한다.
의존성 주입 시 dependency-injector의 IoC 컨테이너를 사용한다(Depends + Provide + @inject).
IoC 컨테이너에서는 주입하고자 하는 클래스의 작동 방식을 지켜보고 Factory, Singleton 등 적절한 프로바이더를 선택하여 사용한다.
의존성 주입 시에는 Provide["rehearsal_schedule_service"]와 같은 문자열 대신 Provide[Container.rehearsal_schedule_service]와 같이 직접 참조를 사용한다.
테스트 프레임워크는 pytest를 사용한다.

모범 사례

import는 relative import 대신 absolute import를 사용한다.
id 타입은 UUID 대신 ULID를 사용한다.
vo는 frozen=True로 설정하여 immutable하게 만든다.
Pydantic v2 불변 설정: ConfigDict(frozen=True)
SQLAlchemy는 v2 이상을 사용한다.
ORM 클래스를 작성할 때 __repr__idname을 담아 디버깅을 용이하게 만든다.
my_field: Mapped[클래스] 와 같은 방식을 사용한다.
nullable 필드는 Mapped[ … | None] 과 같이 자동으로 추론되므로 명시적으로 적지 않는다.
다음은 위 명세들이 반영된 모범 사례 코드다.
FastAPI의 Depends는 Annotated 폼 - arg: Annotated[Type, Depends(dependable)] - 으로 적는다.
세션의 생명주기와 리포지토리의 오케스트레이션을 담당하는 UoW(Unit of Work)를 도입한다. 리포지토리는 이제 스스로 세션을 열고 닫지 않고, 주입받은 세션을 사용하기만 한다.
IoC 컨테이너는 다음과 같이 작성된다.
Untitled
Mermaid
복사
아래는 SQLAlchemy 기반 repository 구현체를 작성하는 경우의 예시다.
class SQLAlchemySongRepository(SongRepositoryInterface): def __init__(self, session: Session): self.session = session def find_by_id(self, song_id: str) -> SongAggregate | None: # DB 모델 조회 및 도메인 모델(Aggregate) 매핑 로직 수행 stmt = select(SongORM).where(SongORM.id == song_id) result = self.session.execute(stmt).scalars().first() if not result: return None return SongMapper.to_domain(result) def save(self, song: SongAggregate): # 도메인 모델을 ORM 모델로 변환하여 세션에 추가 orm_song = SongMapper.to_orm(song) self.session.merge(orm_song)
Python
복사
UoW 클래스는 다음과 같이 작성된다.
Untitled
Mermaid
복사
환경 변수는 .env 파일에 정의하고, Pydantic의 BaseSettings를 상속받아 관리한다.
from pydantic_settings import BaseSettings from pydantic import computed_field class Settings(BaseSettings): database_url: str # .env 파일이 있을 때 명시적으로 읽어오겠다는 의도를 표현하고, "이 프로젝트는 .env 파일을 사용한다"는 문서화 효과를 만든다. model_config = {'env_file': '.env', 'case_sensitive': False} settings = Settings() # database_url 환경변수 없으면 에러 발생
Python
복사

코드 스타일

시간을 다룰 일이 있다면 timezone을 반드시 포함한다.
timezone 관련 설정은 표준 라이브러리에 내장된 zoneinfo 모듈을 사용한다.
typing은 python 3.12 이상의 규칙을 따른다. (e.g. Union 대신 | 연산자 사용)
google style로 작성한다(함수 인자별 개행, 함수 간 2줄 간격 등).
그 어떤 경우에도 주석(docstring “””, inline ‘#’)은 작성하지 않는다.
테스트는 나와 상의하고 작성한다. 이때 다음 질문에 너가 생각하는 이유를 제시하며 나에게 확인을 받는다.
이때 나에게 구성요소들 중 어떤 것을 생성하고, 어떤 것을 (의존성)주입할 것인지
각각의 요소에 어떤 테스트 더블을 사용할 것인지
요청/응답 클래스 네이밍
1.
*Request: 입력 전용
2.
*Response: 응답에 노출되는 모델(중첩 모델이어도 응답 페이로드에 포함되면 Response를 유지하는 것이 일반적임)
3.
*Schema: 요청/응답 공용 조각이거나, API 바깥 내부 재사용 모델일 때만 제한적으로 사용
위 스캐폴드 가이드, 모범 사례의 예시 코드와 별개로 별도로 첨부된 한글 코딩 컨벤션을 사용한다.
to LLM: 한글 코딩 컨벤션이 제공되지 않은 경우, 사용자에게 한글 코딩 컨벤션 문서를 요구한다.

대형 프로젝트

이 프로젝트는 도메인 주도 설계의 목적과 용어를 차용한 클린 아키텍처를 적용한다.
Bounded Context가 N개인 경우 사용한다.

스캐폴드 및 가이드

root
.git/
myproject (flat 구조)
bounded-context 또는 subdomain 이름/ (예를 들어: sales, support, …)
boundary/
mappers/ - 헥사곤 외부 도메인 타입 정의
contracts/ - 헥사곤 외부에 노출되는 타입 정의
domain/
entity/
__init__.py
(e.g. user.py, login.py)
vo/
repository/
gateway/
xxx_igateway (예를 들어: user_igateway) - xxx_gateway 에서 구현될 인터페이스, 반환 타입은 도메인 객체
exception/
xxx_irepo (예를 들어: user_irepo, … ) - xxx_repo 에서 구현될 인터페이스, 반환 타입은 도메인 객체
xxx_aggregate - entity와 vo를 결합하여 정의한 도메인 객체들로, xxx_irepo와 1:1 대응되어야 함
application/
ports/ - 입출력 포트 (예를 들어 …_port(s).py: 입력, …_sink(s).py: 출력, …_gateway.py: 출력)
query_service/
xxx_query_service
command/
xxx_usecase:
__call__로 실행하는 클래스임.
interface/
controllers/
xxx_controller (예를 들어: user_controller, … ) - Router & Response Model
infra/
logging/ - 로깅 세팅
db_models/ - ORM 프레임워크로 작성한 DB 모델
xxx (예를 들어: user_model, … )
mappers/ - ORM 모델 도메인 모델
repository/
xxx_repo (예를 들어: user_repo, … )
gateway/
xxx_gateway (예를 들어: user_gateway, … )
utils/
common/
settings.py
bounded-context 또는 subdomain 이름/ (예를 들어: login, …)
위의 user과 동일한 구조
kernel/ - 다중 bounded-context/subdomain간 공통으로 사용되는 최소한의 개념
integration/ - 다중 bounded-context/subdomain간 연결에 사용되는 어댑터, 매퍼
주로 헥사곤 A의 노출 타입(contract) 헥사곤 B의 노출 타입(contract)
가끔씩 kernel 요소 사용 가능
여기서는 헥사곤 내부 도메인 타입의 교류가 있어서는 안 됨
common/ - 다중 bounded-context/subdomain간에 재사용되는 스크립트나 간단한 도구 등
utils/
settings.py
test/ - integration test만 작성, unit test는 테스트하고자 하는 파일과 최대한 나란히 작성
README.md
pyproject.toml
루트 디렉토리에서 python3 -m myproject.main 으로 백엔드 애플리케이션을 실행하는 flat 구조다.
애플리케이션 계층에서는 인터페이스를 타입으로 받은 뒤, 나중에 인프라 등 구현체를 주입받는다.
utils/ 에는 다양한 애플리케이션에서 공통으로 사용하는 유틸성 모듈을 저장한다.
xxx_repo는 repository 패턴에 따라 구현한다. 반환값은 도메인 객체여야 한다.
도메인 모델과 orm 모델 사이 변경은 repository에서만 일어나며, mapper을 이용한다.
이 프로젝트의 경우 시스템의 진입점과 웹 프레임워크가 변경될 수 있으므로, 애플리케이션 계층이 컨트롤러로 값을 반환할 때 곧바로 Response를 만들어 반환하는 대신 중간 DTO를 반환한다.
entity, vo에는 직접 접근이 불가능하며, aggregate를 통해서만 접근한다.

기술 선택

pydantic은 v2 이상을 사용한다.
패키지와 의존성은 pyproject.toml과 uv를 통해 관리한다.
ORM 프레임워크은 SQLAlchemy를 사용한다.
Database는 Supabase를 사용한다.
FastAPI 의존성 주입 모듈(Depends)은 controllers/ 이외 레이어에서 사용하지 않도록 한다.
의존성 주입 시 dependency-injector의 IoC 컨테이너를 사용한다(Depends + Provide + @inject).
IoC 컨테이너에서는 주입하고자 하는 클래스의 작동 방식을 지켜보고 Factory, Singleton 등 적절한 프로바이더를 선택하여 사용한다.
테스트 프레임워크는 pytest를 사용한다.

모범 사례

import는 relative import 대신 absolute import를 사용한다.
id 타입은 UUID 대신 ULID를 사용한다.
vo는 @dataclass(frozen=True)로 설정하여 immutable하게 만든다.
SQLAlchemy는 v2 이상을 사용한다.
ORM 클래스를 작성할 때 __repr__idname을 담아 디버깅을 용이하게 만든다.
my_field: Mapped[클래스] 와 같은 방식을 사용한다.
nullable 필드는 Mapped[ … | None] 과 같이 자동으로 추론되므로 명시적으로 적지 않는다.
다음은 위 명세들이 반영된 모범 사례 코드다.
도메인 레이어에서는 Pydantic을 사용하지 않는다.
entity/__init__.py 에 다음과 같은 파일을 작성하고, 모든 엔티티는 이 클래스를 상속받는다.
from abc import ABCMeta from dataclasses import dataclass from typing import Any, Generic, TypeVar, dataclass_transform ULIDString = str try: # python 3.13+ T_id = TypeVar("T_id", default=ULIDString) except: T_id = TypeVar("T_id") @dataclass_transform(kw_only_default=True) class EntityMeta(ABCMeta): """ 자식 클래스가 생성될 때 자동으로 dataclass를 입혀줍니다. `kw_only=True` 부모 클래스에서 id: str 식으로 필드를 정의해버리면, 자식 클래스(dataclass)에서 필드 순서(기본값이 있는 필드 뒤에 기본값이 없는 필드가 올 수 없는 문제 등)가 꼬일 수 있습니다. 파이썬 3.10부터 도입된 kw_only 설정을 사용하면 모든 필드를 "키워드 인자(name=value)"로만 전달하게 강제합니다. 이 경우 필드의 순서 제약이 사라진다는 장점도 있고, 복잡한 엔티티에 값을 할당할 때 순서에 의존하는 불상사를 막을 수 있습니다. `eq=False` dataclass는 기본적으로 __eq__를 자동으로 생성합니다. 하지만 엔티티는 모든 필드를 비교하는 게 아니라 ID만 비교해야 하므로, eq=False 옵션을 주어 부모의 __eq__를 사용하게 해야 합니다. """ def __new__(cls, name, bases, dct): new_cls = super().__new__(cls, name, bases, dct) # 최상위 클래스인 Entity 클래스에는 dataclass를 적용하지 않고, # 이를 상속받은 구체적인 엔티티 클래스들에만 적용합니다. if name != "Entity": new_cls = dataclass(kw_only=True, eq=False, repr=True)(new_cls) annotations = dct.get("__annotations__", {}) if "id" not in annotations: raise TypeError(f"엔티티 클래스 '{name}'은(는) 반드시 'id' 필드를 정의해야 합니다.") return new_cls class Entity(Generic[T_id], metaclass=EntityMeta): id: T_id def __eq__(self, other: Any) -> bool: if not isinstance(other, type(self)): return False if self.id is None or other.id is None: return False return self.id == other.id def __hash__(self) -> int: """ type(self)를 함께 섞어주는 이유는 서로 다른 엔티티 타입이 우연히 같은 ID를 가질 때 발생할 수 있는 충돌을 방지하기 위함입니다. """ return hash((type(self), self.id))
Python
복사
FastAPI의 Depends는 Annotated 폼 - arg: Annotated[Type, Depends(dependable)] - 으로 적는다.
세션의 생명주기와 리포지토리의 오케스트레이션을 담당하는 UoW(Unit of Work)를 도입한다. 리포지토리는 이제 스스로 세션을 열고 닫지 않고, 주입받은 세션을 사용하기만 한다.
IoC 컨테이너는 다음과 같이 작성된다.
Untitled
Mermaid
복사
아래는 SQLAlchemy 기반 repository 구현체를 작성하는 경우의 예시다.
class SQLAlchemySongRepository(SongRepositoryInterface): def __init__(self, session: Session): self.session = session def find_by_id(self, song_id: str) -> SongAggregate | None: # DB 모델 조회 및 도메인 모델(Aggregate) 매핑 로직 수행 stmt = select(SongORM).where(SongORM.id == song_id) result = self.session.execute(stmt).scalars().first() if not result: return None return SongMapper.to_domain(result) def save(self, song: SongAggregate): # 도메인 모델을 ORM 모델로 변환하여 세션에 추가 orm_song = SongMapper.to_orm(song) self.session.merge(orm_song)
Python
복사
UoW 클래스는 다음과 같이 작성된다.
Untitled
Mermaid
복사
환경 변수는 .env 파일에 정의하고, Pydantic의 BaseSettings를 상속받아 관리한다.
from pydantic_settings import BaseSettings from pydantic import computed_field class Settings(BaseSettings): database_url: str # .env 파일이 있을 때 명시적으로 읽어오겠다는 의도를 표현하고, "이 프로젝트는 .env 파일을 사용한다"는 문서화 효과를 만든다. model_config = {'env_file': '.env', 'case_sensitive': False} settings = Settings() # database_url 환경변수 없으면 에러 발생
Python
복사

코드 스타일

시간을 다룰 일이 있다면 timezone을 반드시 포함한다.
timezone 관련 설정은 표준 라이브러리에 내장된 zoneinfo 모듈을 사용한다.
typing은 python 3.12 이상의 규칙을 따른다. (e.g. Union 대신 | 연산자 사용)
google style로 작성한다(함수 인자별 개행, 함수 간 2줄 간격 등).
그 어떤 경우에도 주석(docstring “””, inline ‘#’)은 작성하지 않는다.
테스트는 나와 상의하고 작성한다. 이때 다음 질문에 너가 생각하는 이유를 제시하며 나에게 확인을 받는다.
이때 나에게 구성요소들 중 어떤 것을 생성하고, 어떤 것을 (의존성)주입할 것인지
각각의 요소에 어떤 테스트 더블을 사용할 것인지
요청/응답 클래스 네이밍
1.
*Request: 입력 전용
2.
*Response: 응답에 노출되는 모델(중첩 모델이어도 응답 페이로드에 포함되면 Response를 유지하는 것이 일반적임)
3.
*Schema: 요청/응답 공용 조각이거나, API 바깥 내부 재사용 모델일 때만 제한적으로 사용
위 스캐폴드 가이드, 모범 사례의 예시 코드와 별개로 별도로 첨부된 한글 코딩 컨벤션을 사용한다.
to LLM: 한글 코딩 컨벤션이 제공되지 않은 경우, 사용자에게 한글 코딩 컨벤션 문서를 요구한다.

한글 컨벤션

우리는 한국인이기 때문에 hi와 hello의 뉘앙스 차이를 완벽하게 인지하지 못한다. 코드상에 filter과 clip가 혼용되어 작성되어 있더라도 우리는 빠르게 인지하지 않고 의미를 넘겨짚는다. 하지만 ‘걸러내기’를 ‘잘라내기’와 섞어 쓴다면 분명한 문제로 받아들일 수 있다. 소프트웨어 용어면 뭐가 다를까? 적어도 나는 cancel(일정취소), abort(진행중단)나 load(사뿐히 올리기), fetch(약간 멀리 있는 것을 가져오기)의 뉘앙스 차이를 잘 모른다. 영어로 애써 심볼들을 만들어 놓고 주석으로 그 심볼의 번역을 적어내는 나 자신과 주변 사람들을 발견하고 있다면, 우리가 외국인과 일할 가능성이 얼마나 높은지, 소스코드를 오픈소스로 공개할 생각인지를 잘 생각해 보자. 먼 미래에 세계의 개발자와 협업하는 우리를 위해 영어로 코드를 작성하는 것은 토이 프로젝트에 쿠버네티스, 카프카, 하둡을 도입하겠다는 것과 비슷한 것일지도 모른다. 영어 공부를 하고 싶으면 코드를 읽고 쓸 것이 아니라 영어책을 읽고 에세이를 써야 한다. 코드를 짜는 목적이 무엇인지 명확히 돌이켜 보자. 불필요한 주석으로 읽어야 하는 텍스트의 양을 불리지 말자.

한글 코딩 가이드

기본적인 원칙은 '국제적으로 통용되는 개념과 외부와 맞닿는 부분은 영어로 작성하되, 논리와 관련된 부분은 최대한 한글로 작성하자' 입니다. 예를 들어, '도메인 서비스'라는 개념은 DDD에서 정의하여 국제적으로 통용되는 개념이므로 DomainService, domain_service로 표기합니다. 하지만 나머지 논리들은 최대한 한글로 작성하는 것입니다.
만약 현재 코드베이스에서 한글 컨벤션을 따르지 않는 코드를 발견하는 경우, 너무 무리하게 변경을 시도하지 않습니다. 하지만 새롭게 작성하는 코드만큼은 이 컨벤션을 따르도록 하여 점진적으로 한글 컨벤션을 지키는 코드 베이스로 탈바꿈해나가는 것을 목표로 합니다.
쉽게 말해, 문제가 생기지 않을 가능성이 높은 모든 파일명과 변수명은 한글+snake_case, 타입은 한글+PascalCase, 엔티티나 VO의 경우 뒤의 suffix를 제거(사용자_entity 대신 사용자라고만 작성)하고 작성하는 것을 권장합니다.

계층별 네이밍 규칙

도메인 계층, 애플리케이션 계층 Dto, 인터페이스/인프라 계층 Repository
클래스/타입: {한글}Entity 와 같이 {한글}{Entity/Vo/Enum/Dto/Usecase/Service/Repository/Gateway/QueryService/Usecase ...}
객체/인스턴스: {한글} 또는 {한글}_entity 와 같이 {한글}_{entity/vo/enum/dto/usecase/service/repository/gateway/query_service/usecase ...}
엔티티나 VO의 경우 뒤의 suffix를 제거하고 작성하는 것을 권장, 만약 레거시 코드에서 클래스/타입이 {한글}{Entity/Vo/Enum/...}이 아니라 {한글}이라면 지어졌다면, 해당 스코프에서 객체/인스턴스의 이름을 {한글}_entity로 작성하여 명확히 분리
참고: 곡_id_list(o), 곡_ids(x)
함수 인자도 마찬가지의 네이밍 규칙 사용
함수명은 띄어쓰기를 _로 하고 ~~하기와 같이 행위를 강조하도록 작성
e.g. get_xxx -> xxx_가져오기, execute -> 실행하기
표현 계층
JSON 필드, 엔드포인트 주소, ... : 한글 문자열을 주고받다가 인코딩-디코딩 문제를 일으킬 수 있는 부분이므로 반드시 영문을 사용할 것
그 외에는 한글 컨벤션 사용
e.g. 표현 계층에 작성된 dto 객체: {한글}_dto
e.g. Response Schema의 클래스 이름: {한글}Response
인프라 계층
ORM의 이름, ORM의 필드, ORM변수: DB와 관련된 부분이므로, 영문을 사용할 것
그 외에는 한글 컨벤션 사용

인터페이스 계층의 컨트롤러 예시

파일명: 곡_난이도_controller.py
@router.post("/{song_id}/difficulty-ratings", response_model=선곡_난이도_평가정보Response) @inject def 곡_난이도평가_생성( song_id: str, request: 선곡_난이도_평가정보_생성Request, auth_member: 멤버 = Depends(get_auth_member), 평가_usecase: 곡난이도평가Service = Depends(Provide[Container.곡_난이도_평가_usecase]), ): 곡_난이도_평가_entity = 평가_usecase.실행하기( 곡_id=song_id, 악기타입=request.instrument_type, 평가=request.rating, 댓글=request.comment, 등록_멤버_id=auth_member.id, ) ...
Python
복사

레포지토리 인터페이스 예시

파일명: 곡_난이도_평가_irepo.py
class 곡난이도평가IRepo(ABC): @abstractmethod def 저장하기(self, 곡난이도평가: 곡난이도평가Entity) -> 곡난이도평가Entity: pass @abstractmethod def 가져오기(self, 곡_id: str) -> 곡난이도평가Entity | None: pass @abstractmethod def 곡에서_가져오기(self, 곡_id: str) -> list[곡난이도평가Entity]: pass @abstractmethod def 여러_곡들에서_가져오기(self, 곡_id_list: list[str]) -> Dict[str, list[곡난이도평가Entity]]: pass @abstractmethod def 멤버에서_가져오기(self, 멤버_id: str) -> list[곡난이도평가Entity]: pass @abstractmethod def 제거하기(self, 평점_id: str) -> None: pass
Python
복사

Schema, Response

Response와 Schema는 데이터 구조라는 측면에서 유사하지만, Response는 클라이언트에게 반환되는 최종 객체이며, 그 내부를 구성하는 세부 데이터 모델들은 재사용성과 명확성을 위해 각각의 Schema로 정의해야 합니다.
class 선곡_난이도_평가정보_생성Request(BaseModel): instrument_type: 악기타입Enum rating: float comment: str | None = None class 선곡_난이도_평가정보_수정Request(BaseModel): rating: float comment: str | None = None class 곡을_평가한_멤버정보Schema(BaseModel): id: str name: str club_memberships: list[동아리_가입정보Response] = Field(description="정렬 안됨!") class 선곡_난이도_평가정보Response(BaseModel): id: str song_id: str instrument_type: str rating: float comment: str | None = None registered_by: 곡을_평가한_멤버정보Schema | None = None created_at: datetime updated_at: datetime | None = None
Python
복사

주석/docstring 작성 가이드

프로젝트의 기본 원칙은 주석/docstring을 최소화하고, 코드 자체만으로도 명확하게 논리를 설명하도록 만드는 것입니다.
한글 함수명을 읽었을 때 바로 해당 함수의 동작이 무엇인지 이해할 수 있어야 합니다. 그러므로 엔드포인트, 메서드, 함수, 파일에 불필요한 주석/docstring을 작성하지 마세요.
가장 흔히 하기 쉬운 실수는 아래와 같이 무의미하게 함수명과 동일한 내용을 가진 주석을 반복적으로 추가하는 것입니다. 예를 들어 다음과 같습니다.
# DON'T def 합주실_요일별가용성_갱신하기(...): """ 요일별 패턴을 기반으로 합주실 가용성을 업데이트합니다. """ # 한글 코딩을 하는 경우 이런 주석을 작성할 일이 거의 없습니다. # DON'T @pytest.fixture def 테스트_데이터_설정하기(db_session, 동아리_repo, 멤버_repo, 공연_repo): """테스트에 필요한 기본 데이터 설정""" # DON'T def test_포지션_설명_필드가_DB에_정상적으로_저장되고_조회됨(performance_repo): """포지션_설명 필드가 DB에 정상적으로 저장되고 조회되는지 확인"""
Python
복사
함수나 클래스의 docstring을 작성할 때 단순히 함수명의 동어반복이 아닌, 구체적인 시나리오를 설명하도록 합니다.
이때 함수 이름에 너무 많은 도메인 용어들이 사용되어 함축되어 있는 경우, 이를 약간 풀어 작성해 주는 것도 좋은 사례입니다.
N+1 문제를 회피하기 위한 메서드 등, 함수의 기능과 별개로 작성 의도를 포함하는 것은 좋은 사례입니다.
특이한 동작에 대해서만 주석을 작성합니다.
이는 특히 테스트 코드의 주석을 작성하는 경우에도 엄격히 적용됩니다. 아래와 같이 정확히 어떤 동작을 테스트하고자 하는지 추상적으로 예를 들어 해설하듯 이해하기 쉽게 작성하세요.
# DO def test_선곡_확정_취소시_모든_포지션지원_상태_PENDING으로_변경(...): """ 시나리오: 1. 연주확정 상태의 선곡 생성 2. 포지션1에 CONFIRMED 지원 2개, DISABLED 지원 1개 3. 포지션2에 PENDING 지원 1개 4. 선곡 확정 취소 5. 모든 포지션지원 상태가 PENDING으로 변경되어야 함 엣지 케이스: 포지션지원이 하나도 없는 선곡도 정상적으로 확정 취소 가능 """ ... # DO def test_점유가_있는_경우_점유정보_리스트_반환(...): """ 합주실에 여러 합주일정이 예약되어 있는 경우를 테스트한다. 예: 1월 1일 14:00-16:00에 밴드A, 1월 2일 10:00-12:00에 밴드B가 예약 → 각 합주일정은 점유정보Vo로 변환되어 리스트로 반환되어야 한다. → 점유정보에는 원_시간_소유자(합주실), 점유_타입, 점유_id, 시간이 포함된다. """
Python
복사
두 번째로 주의해야 하는 것은 계층의 역할을 초과하는 주석을 작성하는 것입니다.
# DON'T class 합주실_점유정보를_고려한_실질가용성_가져오기Usecase: # 애플리케이션 레이어의 서비스입니다. """ 원본 가용성 - 점유 일정 = 점유정보를 고려한 실질가용성 """ def 실행하기( self, 합주실_id: str, 시작시각: Optional[datetime] = None, 종료시각: Optional[datetime] = None, ) -> 가용성Vo: ...
Python
복사
위 함수의 경우 함수의 이름은 잘 작성했으나, "원본 가용성 - 점유 일정 = 점유정보를 고려한 실질가용성" 이라는 도메인 논리를 주석에서 설명하고 있습니다. 하지만 애플리케이션 계층에서 도메인 주석을 포함하는 순간, 도메인 논리가 변경되는 경우 애플리케이션 레이어의 주석을 함께 변경해야 한다는 문제가 발생합니다. 따라서 주석의 범위는 철저히 해당 계층 내부로 제한되어야 합니다.
차라리 주석을 작성하지 마세요.
# DO class 합주실_점유정보를_고려한_실질가용성_가져오기Usecase: def 실행하기( self, 합주실_id: str, 시작시각: datetime | None = None, 종료시각: datetime | None = None, ) -> 가용성Vo: ...
Python
복사
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
2.
이미 사람들이 널리 사용하며 잘 정의된 용어로 작성된 프롬프트는 강력하다.
5.
DTO, Mapper의 개념과 점진적 복잡성 추가 방법이 작성되어 있다.
6.
앞의 글은 “Repository 구현체의 각 메서드는 @with_db_session로 데코레이션하여, try-finally 세션을 열고 닫는 것(db.close) 및 커밋을 보장해야 한다”는 아이디어를 담고 있다.
7.
앞의 글은 “Repository 구현체의 각 메서드에서는 IoC를 통해 Context 클래스를 주입받아 try-finally 세션을 열고 닫는 것(db.close) 및 커밋을 은닉하면서도, Context를 쉽게 교체할 수 있게 하여 테스트를 용이하게 한다”는 아이디어를 담고 있다.
8.
앞의 글은 pytest가 unittest보다 나은 여러 사례들 중 하나를 언급한다.
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
1.
None
ref : 생각에 참고한 자료입니다.