Notation
•
이탤릭은 더 큰 수준의 프로젝트에서 삭제되는 지침이다.
•
볼드는 이전 수준에는 없다가 새로 추가되는 지침이다.
토이 프로젝트 가이드
•
이 프로젝트는 도메인 주도 설계의 목적과 용어를 차용한 클린 아키텍처를 적용한다.
•
Bounded Context가 1개인 경우 사용한다.
•
Aggregate라는 개념조차도 불필요할 정도로 작은 구상인 경우 사용한다.
스캐폴드
•
root
◦
.git/
◦
myproject (flat 구조)
▪
domain/
•
entities.py
•
value_objects.py
•
repositories.py - infra 레이어에서 구현될 인터페이스, 반환 타입은 도메인 객체
•
gateways.py - infra 레이어에서 구현될 인터페이스, 반환 타입은 파일 상단에 명세
•
exceptions.py
▪
application/
•
services.py
▪
interface/
•
controllers.py
•
schema.py
•
mappers.py - 도메인 타입
Request & Response Model. 시스템의 진입점과 웹 프레임워크가 변경될 일이 거의 없으므로, 애플리케이션 계층이 컨트롤러로 값을 반환할 때 중간 DTO를 반환하는 대신, 곧바로 Response를 만들어 반환한다.
▪
infra/
•
db_models.py - ORM 프레임워크로 작성한 DB 모델
•
mappers.py - ORM 프레임워크
도메인 객체 변환. 이때 SQLAlchemy의 inspect() 같은 함수를 사용하여 repository의 return을 도메인 객체화는 등 보일러플레이트를 최소화한다.
•
repositories.py
•
gateways.py
▪
utils.py
▪
settings.py
▪
db.py
▪
main.py - 루트에서 -m package_name.main 으로 실행
◦
README.md
◦
pyproject.toml
◦
…
기술 선택
•
pydantic은 v2 이상을 사용한다.
•
id 타입은 UUID 대신 ULID를 사용한다.
•
패키지와 의존성은 pyproject.toml과 uv를 통해 관리한다.
•
포매터는 ruff, 타입 체커는 ty를 사용한다.
◦
tests/ 디렉토리도 ruff, ty 체킹을 빠뜨리지 않도록 주의한다.
•
ORM 프레임워크은 SQLAlchemy를 사용한다.
◦
SQLAlchemy는 v2 이상을 사용한다.
•
Database는 Supabase를 사용한다.
모범 사례
•
SQLAlchemy
◦
ORM 클래스를 작성할 때 __repr__에 id와 name을 담아 디버깅을 용이하게 만든다.
◦
my_field: Mapped[클래스] 와 같은 방식을 사용한다.
◦
nullable 필드는 Mapped[ … | None] 과 같이 자동으로 추론되므로 명시적으로 적지 않는다.
다음은 위 명세들이 반영된 모범 사례 코드다.
•
FastAPI
◦
Depends: Annotated 폼 - arg: Annotated[Type, Depends(dependable)] - 으로 적는다.
•
환경 변수는 .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
복사
•
vo는 frozen=True로 설정하여 immutable하게 만든다.
◦
Pydantic v2 불변 설정: ConfigDict(frozen=True)
•
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
복사
코드 스타일
•
import는 relative import 대신 absolute import를 사용한다.
•
시간을 다룰 일이 있다면 timezone을 반드시 포함한다.
◦
timezone 관련 설정은 표준 라이브러리에 내장된 zoneinfo 모듈을 사용한다.
•
typing은 python 3.12 이상의 규칙을 따른다. (e.g. Union 대신 | 연산자 사용)
◦
만약 .python-version 에 그 3.12보다 상위의 버전이 있는 경우, 그 버전을 따른다.
•
코드를 모두 작성한 이후에는 ruff 포매팅을 수행한다.
•
주석은 high level 흐름이 잘 드러나도록 작성한다.
◦
args, kwargs, return에 대한 명세는 별도의 사용자 요구가 없는 경우 작성하지 않는다.
◦
주석으로 작성한 내용이 그냥 클래스명/함수(메서드)명/변수명에 곧바로 반영되는 편이 낫다.
•
위 스캐폴드 가이드, 모범 사례의 예시 코드와 별개로 별도로 첨부된 한글 코딩 컨벤션을 사용한다.
◦
한글 코딩 컨벤션이 제공되지 않은 경우, 사용자에게 한글 코딩 컨벤션 문서를 요구한다.
중형 이상 프로젝트 가이드
•
이 프로젝트는 도메인 주도 설계의 목적과 용어를 차용한 hexagonal/클린 아키텍처를 적용한다.
•
Bounded Context가 N개인 경우 사용한다.
◦
지금은 1개이지만 향후 분리가 예상되는 경우
◦
이미 2개 이상인 경우
•
이 문서에서 hexagon은 bounded context module의 기술적 경계를 가리키는 약식 표현으로 사용한다.
스캐폴드
•
root
◦
.git/
◦
package_name - flat 구조(하나의 pyproject.toml로 운영하는 경우)
bounded_context_name/
▪
shared_kernel/ - 다중 bounded-context간 공통으로 사용되는 최소한의 개념. settings, logging, db, utils 같은 코드를 넣는 것은 엄격히 금지된다.
•
…
▪
integration/ - 다중 bounded-context간 연결에 사용되는 어댑터, 매퍼로. 주로 hexagon A의 노출 타입(contract)
hexagon B의 노출 타입(contract)이 이루어진다. 가끔씩 kernel 요소 사용 가능여기서는 kernel을 제외한 hexagon 내부 타입의 교류가 있어서는 안 된다.
•
…
▪
platform/ - 설정, 로깅, DB, DI 같은 앱 공통 기술 인프라.
•
app_settings.py: 각 bounded-context 안에 넣을 수 있는 것들이 빠져 있지 않도록 주의!
•
logging.py
•
db.py
•
container.py - 각 bounded-context의 wiring.py을 조합
▪
main.py - 루트에서 -m package_name.main 으로 실행
◦
tests/ - 통합 테스트. 유닛 테스트는 테스트하고자 하는 파일과 최대한 나란히 작성한다.
◦
README.md
◦
pyproject.toml
◦
…
위 스캐폴드는 "디렉터리로 승격되었을 때"의 기준 예시다. 다만 entities/, exceptions/, controllers/, schemas/, repositories/, mappers/, types/ 같은 leaf 디렉터리가 사실상 __init__.py 하나만 담고 있고, 코드가 __init__.py에 숨어 있어서 가독성을 해친다면 entities.py, exceptions.py, controllers.py, schemas.py, repositories.py, mappers.py, types.py처럼 같은 레벨의 단일 파일로 다시 납작하게 만들어도 된다. 즉, __init__.py는 패키지 마커나 최소한의 re-export 용도로만 남기고, 실질적인 구현이 한 파일에 다 들어갈 정도의 규모라면 굳이 디렉터리 + __init__.py 조합을 유지하지 않아도 된다. 반대로 해당 영역이 다시 커져서 파일을 여러 개로 나눌 필요가 생기면, 그때 디렉터리 구조로 재승격하면 된다.
기술 선택
•
pydantic은 v2 이상을 사용한다.
•
id 타입은 UUID 대신 ULID를 사용한다.
•
패키지와 의존성은 pyproject.toml과 uv를 통해 관리한다.
•
포매터는 ruff, 타입 체커는 ty를 사용한다.
◦
tests/ 디렉토리도 ruff, ty 체킹을 빠뜨리지 않도록 주의한다.
•
ORM 프레임워크은 SQLAlchemy를 사용한다.
◦
SQLAlchemy는 v2 이상을 사용한다.
◦
DB 리비전 도구는 alembic을 사용한다.
•
Database는 Supabase를 사용한다.
•
테스트 프레임워크는 pytest를 사용한다.
•
의존성 주입
◦
dependency-injector의 IoC 컨테이너를 사용한다(Depends + Provide + @inject).
◦
IoC 컨테이너에서는 주입하고자 하는 클래스의 생명주기에 적절한 프로바이더를 선택하여 사용한다.
모범 사례
•
SQLAlchemy
◦
ORM 클래스를 작성할 때 __repr__에 id와 name을 담아 디버깅을 용이하게 만든다.
◦
my_field: Mapped[클래스] 와 같은 방식을 사용한다.
◦
nullable 필드는 Mapped[ … | None] 과 같이 자동으로 추론되므로 명시적으로 적지 않는다.
다음은 위 명세들이 반영된 모범 사례 코드다.
•
FastAPI
◦
Depends: Annotated 폼 - arg: Annotated[Type, Depends(dependable)] - 으로 적는다.
▪
FastAPI 의존성 주입 모듈(Depends)은 controllers/ 이외 레이어에서 사용하지 않도록 한다.
◦
표준 ApplicationError와 FastAPI 전역 핸들러를 사용한다.
▪
유즈케이스는 happy path만 반환하고, 컨트롤러는 성공 응답 조립만 한다.
▪
도메인 예외는 ApplicationError로 변환하고, FastAPI 전역 핸들러는 ApplicationError를 잡아서 프론트엔드가 처리할 수 있는 JSON 응답으로 return한다.
•
환경 변수는 .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
복사
•
도메인 레이어에서는 Pydantic을 사용하지 않는다.
◦
vo는 @dataclass(frozen=True)로 설정하여 immutable하게 만든다.
◦
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+; 만약 .python-version에 >=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
복사
•
세션의 생명주기와 리포지토리의 오케스트레이션을 담당하는 UoW(Unit of Work)를 도입한다.
◦
리포지토리는 스스로 세션을 열고 닫지 않고, 주입받은 세션을 사용하기만 한다.
◦
IoC 컨테이너는 다음과 같이 작성된다.
# TBD
Python
복사
◦
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 클래스는 다음과 같이 작성된다.
# TBD
Python
복사
코드 스타일
•
import는 relative import 대신 absolute import를 사용한다.
•
시간을 다룰 일이 있다면 timezone을 반드시 포함한다.
◦
timezone 관련 설정은 표준 라이브러리에 내장된 zoneinfo 모듈을 사용한다.
•
typing은 python 3.12 이상의 규칙을 따른다. (e.g. Union 대신 | 연산자 사용)
◦
만약 .python-version 에 그 3.12보다 상위의 버전이 있는 경우, 그 버전을 따른다.
•
코드를 모두 작성한 이후에는 ruff 포매팅을 수행한다.
•
주석은 high level 흐름이 잘 드러나도록 작성한다.
◦
args, kwargs, return에 대한 명세는 별도의 사용자 요구가 없는 경우 작성하지 않는다.
◦
주석으로 작성한 내용이 그냥 클래스명/함수(메서드)명/변수명에 곧바로 반영되는 편이 낫다.
•
위 스캐폴드 가이드, 모범 사례의 예시 코드와 별개로 별도로 첨부된 한글 코딩 컨벤션을 사용한다.
◦
한글 코딩 컨벤션이 제공되지 않은 경우, 사용자에게 한글 코딩 컨벤션 문서를 요구한다.
한글 컨벤션
우리는 한국인이기 때문에 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/use_case ...}
▪
엔티티나 VO의 경우 뒤의 suffix를 제거하고 작성하는 것을 권장, 만약 레거시 코드에서 클래스/타입이 {한글}{Entity/Vo/Enum/...}이 아니라 {한글}이라면 지어졌다면, 해당 스코프에서 객체/인스턴스의 이름을 {한글}_entity로 작성하여 명확히 분리
▪
참고: 곡_id_list(o), 곡_ids(x)
◦
함수 인자도 마찬가지의 네이밍 규칙 사용
◦
함수명은 띄어쓰기를 _로 하고 ~~하기와 같이 행위를 강조하도록 작성
▪
e.g. get_xxx -> xxx_가져오기, execute -> 실행하기
•
표현 계층, 인프라 계층
◦
한글 문자열을 주고받다가 인코딩-디코딩 문제를 일으킬 수 있는 부분은 반드시 영문을 사용할 것
▪
JSON 필드
▪
엔드포인트 주소, ...
◦
그 외에도 데이터베이스의 이름, 필드 이름 등과 관련된 부분은 반드시 영문을 사용할 것
▪
ORM의 이름
▪
ORM의 필드
◦
그 외에는 한글 컨벤션 사용
▪
e.g. 표현 계층에 작성된 dto 객체: {한글}_dto
▪
e.g. Response Schema의 클래스 이름: {한글}Response
예시
# 곡_난이도.py (컨트롤러)
from typing import Annotated
@router.post("/{song_id}/difficulty-ratings", response_model=선곡_난이도_평가정보Response)
@inject
def 곡_난이도평가_생성(
song_id: str,
request: 선곡_난이도_평가정보_생성Request,
auth_member: Annotated[멤버, Depends(get_auth_member)],
평가_usecase: Annotated[
곡난이도평가Service,
Depends(Provide[Container.곡_난이도_평가_usecase]),
],
):
곡_난이도_평가_entity = 평가_usecase.실행하기(
곡_id=song_id,
악기타입=request.instrument_type,
평가=request.rating,
댓글=request.comment,
등록_멤버_id=auth_member.id,
)
...
Python
복사
# 곡_난이도_평가.py (도메인 repository 인터페이스)
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
복사
주석/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 __call__(
self,
합주실_id: str,
시작시각: Optional[datetime] = None,
종료시각: Optional[datetime] = None,
) -> 가용성Vo:
...
Python
복사
위 함수의 경우 함수의 이름은 잘 작성했으나, "원본 가용성 - 점유 일정 = 점유정보를 고려한 실질가용성" 이라는 도메인 논리를 주석에서 설명하고 있습니다. 하지만 애플리케이션 계층에서 도메인 주석을 포함하는 순간, 도메인 논리가 변경되는 경우 애플리케이션 레이어의 주석을 함께 변경해야 한다는 문제가 발생합니다. 따라서 주석의 범위는 철저히 해당 계층 내부로 제한되어야 합니다.
차라리 주석을 작성하지 마세요.
# DO
class 합주실_점유정보를_고려한_실질가용성_가져오기Usecase:
def __call__(
self,
합주실_id: str,
시작시각: datetime | None = None,
종료시각: datetime | None = None,
) -> 가용성Vo:
...
# DO: '실질가용성'이라는 단어가 이미 '합주실 점유정보를 포함한다'라는 도메인 약속이 있는 경우
class 실질가용성_가져오기Usecase:
def __call__(
self,
합주실_id: str,
시작시각: datetime | None = None,
종료시각: datetime | None = None,
) -> 가용성Vo:
...
Python
복사
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
1.
•
앞의 글에서는 이 프롬프트가 프로그램 설계의 어느 단계에 사용될 수 있는지가 나타나 있다.
2.
•
이미 사람들이 널리 사용하며 잘 정의된 용어로 작성된 프롬프트는 강력하다.
3.
•
앞의 글은 이 글에서 다루는 프롬프트가 추구하는 개념적 배경을 다룬다.
4.
•
앞의 글에서는 Repository, Gateway의 개념을 다룬다.
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 : 생각에 참고한 자료입니다.