개요
본 글은 Push Ifs Up and Fors Down이라는 코드 최적화 휴리스틱을 다룬다. 조건문(if)은 가능한 한 **호출부(caller)**로 올리고, 반복문(for/foreach)은 하위 레벨로 내리거나 배치(batch) 단위로 처리하는 접근이다. 이 원칙은 matklad의 2023년 노트에서 정리된 바 있으며, C#·Rust·일반적인 명령형 코드에 모두 적용할 수 있다.
이 포스트가 도움이 되는 대상
- 조건문·반복문이 난잡하게 얽힌 레거시를 정리하려는 개발자
- 성능과 가독성을 동시에 잡고 싶은 백엔드·라이브러리 작성자
- 클린 코드·리팩토링 실전 패턴을 찾는 팀
원칙 요약
두 가지 규칙은 서로 보완적이다.
| 원칙 | 의미 | 효과 |
|---|---|---|
| Push Ifs Up | 조건 검사는 호출하는 쪽에서 하고, 피호출 함수는 전제가 만족된 경우만 다룬다. | 제어 흐름이 한곳에 모여 복잡도·중복·버그 감소. |
| Push Fors Down | 반복은 가능하면 배치 API나 하위 레이어에서 한 번에 처리한다. | 조건 평가 횟수 감소, 벡터화·캐시 활용 등 성능 이득. |
아래 Mermaid 다이어그램은 두 원칙의 제어 흐름 차이를 요약한다.
flowchart LR
subgraph pushIfsUp["Push Ifs Up"]
CallerA["호출부조건 검사"]
CalleeA["피호출 함수단일 경로"]
CallerA -->|"전제 만족 시"| CalleeA
end
subgraph pushForsDown["Push Fors Down"]
CallerB["호출부한 번 호출"]
BatchB["배치 처리내부에서 반복"]
CallerB --> BatchB
end
조건문을 호출부로 올리기 (Push Ifs Up)
함수 안에서 if로 전제조건을 검사하는 대신, 호출하는 쪽에서 검사하고 타입·계약으로 “이 함수는 전제가 만족될 때만 호출된다”를 표현하는 방식이다.
동기
- 제어 흐름 집중:
if가 여러 함수에 흩어져 있으면 같은 조건이 반복되고, 데드 브랜치를 찾기 어렵다. 한 함수에 모으면 중복·불필요 분기가 눈에 잘 들어온다. - 단일 책임: 피호출 함수는 “유효한 입력만 처리”하는 직선형 로직에 집중하고, “유효한지 판단”은 호출부 책임으로 둔다.
- 전제 검사 최소화: 전제를 위로 올리면 호출 체인 위쪽 한 곳에서만 검사해도 되어, 전체 검사 횟수가 줄어드는 경우가 많다.
예시: null·전제 검사
비권장 (조건문이 함수 내부):
| |
권장 (조건문을 호출부로):
| |
C#에서는 nullable 참조 타입(Walrus?)과 null 검사 후 non-null 보장을 활용하면, “호출부에서 검사 후 넣어준다”는 계약을 타입으로도 표현할 수 있다.
“Enum 해체” 리팩터링
여러 단계에서 같은 조건이 enum/match로 반복되는 경우, 조건을 위로 올려 한 번만 분기하게 만드는 패턴이다.
Before: 조건이 데이터 구조로 재등장
| |
After: 조건을 호출부로 올려 한 번만 분기
| |
조건이 한 곳에만 나타나므로 동작이 읽기 쉽고, 불필요한 enum·중간 타입을 줄일 수 있다.
반복문을 하위 레벨로 내리기 (Push Fors Down)
“많은 것은 많게” 다루는 데이터 지향 사고다. 호출부에서 for/foreach로 하나씩 호출하기보다, 배치 단위 API를 두고 반복·벡터화는 하위 레이어에서 처리하도록 한다.
동기
- 성능: 배치로 처리하면 초기화·분기 비용을 분산시키고, 벡터화·캐시 친화적 접근이 가능해진다. 벡터화 인터프리터 등 극단적 사례에서도 동일한 아이디어가 쓰인다.
- 표현력: “컬렉션 전체에 대한 연산”을 한 단위로 생각하면, jQuery 스타일의 컬렉션 API나 선형대수 식처럼 사고하기 쉬워진다.
예시: 단일 호출 vs 배치 호출
비권장 (호출부에서 반복):
| |
권장 (반복을 하위로):
| |
배치 API가 있으면 나중에 정렬 순서 변경, 필드별 SoA 처리, 병렬화 등을 한 곳에서 적용하기 수월하다.
조건문과 반복문 조합
두 원칙을 같이 쓰면, 반복문 안에서 매번 도는 조건을 반복문 밖으로 빼서 한 번만 평가하게 할 수 있다.
flowchart LR
subgraph badApproach["비권장"]
LoopBad["반복문내부에 if"]
end
subgraph goodApproach["권장"]
CondOnce["조건 한 번 평가"]
LoopA["분기 A반복문"]
LoopB["분기 B반복문"]
CondOnce --> LoopA
CondOnce --> LoopB
end
비권장 (루프 안에서 조건 반복 평가):
| |
권장 (조건을 위로, 반복을 아래로):
| |
이렇게 하면 조건이 한 번만 평가되고, 분기 예측·벡터화에도 유리해질 수 있다. TigerBeetle처럼 제어 평면에서 결정을 한 번 내리고, 데이터 평면에서는 배치 단위로 처리하는 구조와도 맞닿아 있다.
실전 적용 팁
- 전제조건 문서화: “이 메서드는 null이 아닌 인자만 받는다”처럼 호출부 책임을 주석·XML 문서로 명시하면, 나중에 리팩터링할 때 안전하다.
- 배치 API 설계: 기존에
ProcessOne만 있다면ProcessBatch(IEnumerable<T>)를 추가하고,ProcessOne은ProcessBatch(Enumerable.Repeat(item, 1))등으로 위임하는 식으로 정리할 수 있다. - 점진적 적용: 한 번에 모든 호출부를 바꾸지 말고, 새 코드부터 “조건은 위로, 반복은 배치로”를 적용하고, 핫 경로나 복잡한 분기부터 단계적으로 리팩터링한다.
결론
- Push Ifs Up: 조건문을 호출부로 올려 제어 흐름을 한곳에 모으고, 피호출 함수는 전제가 만족된 경우만 다루게 하면 가독성과 유지보수성이 좋아진다.
- Push Fors Down: 반복을 배치·하위 레이어로 내리면 조건 평가 횟수 감소와 벡터화·캐시 활용으로 성능 이득을 얻을 수 있다.
- 두 원칙을 함께 쓰면 “조건은 한 번, 반복은 배치 단위”로 코드를 정리할 수 있어, C#을 비롯한 객체 지향·명령형 코드에서도 실용적으로 쓸 수 있다.
참고 문헌
- Push Ifs Up And Fors Down — matklad, 2023. 원칙의 출처와 Rust 예시, enum 해체·배치 처리 설명.
- How to break out of an IF statement — Stack Overflow. 중첩
if제어 흐름을 정리하는 방법들(메서드 추출, early return 등). - Vectorized Interpreters (Graydon Hoare, 2023) — 배치·벡터화 관점에서 “Push Fors Down”과 연결되는 발표 자료.
![Featured image of post [CleanCode] 코드 최적화: 조건문 올리고 반복문 내리기](/post/2025-05-20-the-secrets-of-code-optimization-pushing-conditionals-up-and-loops-down/index_hu_3636f953f4ca63d8.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Tutorial] Learn Prompting - 프롬프트 엔지니어링 무료 가이드 정리](/post/2022-12-30-learn-prompting/wordcloud_hu_6a9d105de4834753.webp)
![[.NET] 런타임별 Finalizer 호출 차이와 IDisposable 권장](/post/2021-04-27-distructor-called-by-runtime/wordcloud_hu_7b7dd809a5d4bdc3.webp)
![[Data Structure] C# Lock-Free 우선순위 큐 구현과 동시성 기법](/post/2025-02-28-csharp-lock-free-priority-queue/index_hu_fbb7019f46127494.webp)