개요
목적
nnUNet 서비스의 내부 기능을 구현한다.
문제 상황
Cleancode 설계 원칙에 따라, 각 모듈을 분리하려 했으나, 분리에 기준을 명확하게 정하고, 이를 기록할 필요성이 느껴졌다.
- I/O 기능을 분리하여 단일책임 원칙 준수
- I/O 기능이 Feature 추출, TotalSegmentator 기능 등에 결합되어 경로 -> 실행 구조를 따르고 있다.
- 해당 구조를 수정하지 않으면 한번에 I/O 각 기능에 실행이 아닌, 각 기능을 실행할 때마다 I/O가 실행되어야 하는 불편함이 따른다.
- 각 모듈의 가독성과, 유지/보수성 증가를 위한 Clean Code를 어떻게 설계해야하는가.
- 단일책임 원칙 준수, 의존성 주입, 인터페이스 통일에 원칙을 준수하여 코드를 설계
- 그러나 이러한 원칙에 따라 코드를 분류할 때 어떤 코드를 같은 파일에 어떤 코드를 일반화시킨 I/O 등의 모듈에 넣어야할지 기준의 모호함이 발생
- 해당 모호함을 코드 변경 사유에 따라 정리하기 위해 각 코드들의 목적에 대한 정리가 필요.
-
결국 핵심은 현재 서비스를 만들 때 각 모듈 단위의 추상화를 통해서, 인터페이스를 이해하기 쉽게 정리하고, 교체를 용이하게 만드는 것
-
변경의 이유를 분리하라 (SRP)
같이 바뀌는 것끼리 묶고, 따로 바뀌는 건 나눈다.
변경 이유가 다른데 같이 묶으면 나중에 다 같이 바뀌는 지옥이 됨 -
인터페이스는 용도 기준으로 만들라
데이터의 타입이 아니라, 역할과 목적을 기준으로 묶어야 함
❌ 잘못된 추상화 ✅ 올바른 추상화 “이미지와 관련된 걸 ImageUtils에 다 넣자” “AI 모델의 입력 생성 → FeatureExtractor” -
구체적인 것이 아니라, 의도를 표현하라
함수/클래스명, 모듈명은 무엇을 한다가 아니라 왜 한다를 표현
❌ 잘못된 추상화 ✅ 올바른 추상화 load_image, save_image, sitk2nib DataAdapter (형식 변환의 책임) make_ap_image, extract_mask FeatureBuilder (모델 입력 생성의 책임) -
추상화는 실제 사용 사례를 최소 2개 생각하고 하라
“지금 하나만 있을 때는 절대 추상화하지 마라” → 재사용 포인트가 최소 2개 이상일 때 추상화가 필요
하나만 있을 때 수행하는 추상화는 다른 재사용 포인트가 생길 때 문제가 될 확률이 높다(기존의 것에 맞춰져 있을 확률이 높기 때문) -
인터페이스는 “최소한의 정보만 넘기도록”
함수에 다 때려 넣음 나중에 바꾸기 힘듦 → 필요한 최소한의 데이터만 넘기고, 의도를 명확히
-
모듈을 리팩토링할 떄 원칙을 정하자!
원칙
- 재사용되는 코드는 무조건 분리하기!
- 각 인터페이스가 최소의 정보만으로 통신이 가능하게! 조금 비효율적이더라도 일관된 구조로 통신이 가능하게!
- 코드가 확장이 편하도록, 전략 패턴, 어댑터 패턴 등을 적용하기!
- 각 모듈별로 실제 실행이 되는 usecase 부분을 각각 작성해두고, 전체 모듈의 usecase를 조립하는 pipeline 파일과, 이를 api와 연결하는 entrypoint 설계
“핵심은 코드가 어떤 식으로 수정될 수 있을까를 예상하고, 수정이 편하도록 관리하는 것이다.”
이를 위한 기능의 정리
현재 nnUNet 서비스에 존재하는 모든 기능을 정리
기능 | 설명 | 수정 가능 시나리오 | 일관성 제한 |
---|---|---|---|
gRPC 통신 | gRPC를 통해 API Gateway와 통신을 수행 | API Gateway와 추가적인 통신 명령이 생기거나, 제거될 수 있음 | case_dir만을 사용해 통신이 수행되야한다. |
nnUNet 추론 | 이미지를 입력받아 |
Clean Code를 위한 코드의 정리
각 코드가 담당하는 계층의 분리
코드를 추상화하는 목적에는
- 나중에 해당 코드를 재사용하고 읽기 편하게 만드려는 목적