Testing & Code Quality

[코드 냄새 #4] 갓 오브젝트(God Object) 완벽 정리

갓 오브젝트는 주변의 책임과 데이터를 계속 흡수하며 커지는 클래스로, 팀에서 가장 자주 바뀌고 가장 자주 충돌합니다. 커지는 것을 알아채는 신호와 해체 순서, 쪼개다 실패하는 두 가지 방식을 정리했습니다.

7분 읽기
[코드 냄새 #4] 갓 오브젝트(God Object) 완벽 정리 대표 이미지

프로젝트마다 그 파일이 하나씩 있습니다. 여는 순간 스크롤바가 실처럼 가늘어지는 파일.

이전 글 코드 냄새 #3에서 이어지는 내용입니다.

이름은 AppManager나 MainViewController, DataStore 같은 것이죠. 팀에서 가장 자주 바뀌면서 가장 자주 충돌하는 파일이기도 합니다.

갓 오브젝트(God Object)입니다.

1998년 나온 『AntiPatterns』에서는 이걸 블롭(The Blob)이라고 부릅니다(출판 정보). 주변의 책임과 데이터를 계속 흡수하며 커지는 덩어리를 가리키는 말이죠.

무엇이 갓 오브젝트인가

줄 수만으로 정하는 건 아닙니다. 2,000줄짜리 타입이라도 한 가지 일만 한다면 그건 그냥 큰 타입입니다(God Class 판별 연구).

갓 오브젝트를 가르는 건 아는 것의 범위입니다. 세 가지가 겹치면 거의 확실합니다.

  • 너무 많은 것을 압니다. 네트워크, 데이터베이스, 화면 상태, 로그인 정보, 결제까지 한 타입 안에 있습니다. import 목록만 봐도 드러납니다.
  • 너무 많은 것이 그것을 압니다. 프로젝트 곳곳에서 이 타입을 참조합니다. 싱글톤이면 참조 경로가 코드에 안 드러나 더 심합니다.
  • 상태가 흩어져 있습니다. 프로퍼티가 서른 개인데 어떤 조합이 유효한지는 아무도 모릅니다.

isLoading이 true인데 error도 nil이 아닌 상태가 가능한지, 같은 질문에 답할 수 없게 됩니다.

iOS에서 이 자리는 대체로 정해져 있습니다. 뷰 컨트롤러입니다.

화면 하나의 레이아웃, 네트워크 호출, 테이블 데이터 소스, 화면 전환, 상태 관리가 전부 한 파일에 모입니다.

이런 구조를 매시브 뷰 컨트롤러(Massive View Controller)라고 부릅니다. AppDelegate와 이름에 Manager가 붙은 타입도 단골입니다.

왜 계속 커지나

아무도 그렇게 만들려고 하지 않았습니다. 그게 핵심입니다.

갓 오브젝트는 설계 결정이 아니라 중력의 결과입니다.

새 기능을 넣어야 한다고 해 봅시다. 필요한 데이터가 이미 이 클래스 안에 다 있습니다.

선택 비용
기존 클래스에 메서드 하나 추가 20분
새 타입 생성 의존성 전달, 초기화 지점, 생명주기까지 반나절

합리적인 선택을 매번 하는데 그 선택이 매번 같은 방향입니다. 그렇게 200줄이던 파일이 3년 뒤 4,000줄이 됩니다.

이 성장에는 단계마다 정당한 이유가 하나씩 붙어 있습니다. 그래서 되돌리기가 어렵습니다.

여기에 깨진 유리창 효과가 얹힙니다. 이미 3,000줄인 파일에 50줄 더 넣는다고 아무도 반대하지 않습니다.

200줄짜리 파일에 50줄을 넣을 때와 심리적 저항이 다릅니다.

네트워크·DB·세션·결제를 모두 아는 4000줄 뷰 컨트롤러의 의존 관계 다이어그램
이 화살표 개수가 곧 테스트 난이도입니다

어디가 아픈가

피해는 네 가지로 나타납니다.

  • 테스트가 불가능해집니다. 이 타입 하나를 만들려면 네트워크, 데이터베이스, 사용자 세션이 전부 필요합니다. 순수한 계산 로직 하나를 검증하려는데 서버가 떠 있어야 하는 상황이 됩니다.
  • 병렬 작업이 막힙니다. 팀원 셋이 각자 다른 기능을 만드는데 전부 같은 파일을 고칩니다. 충돌이 상시로 나고 리뷰할 때 변경 사항이 뒤섞여 보입니다.
  • 변경 영향 범위를 알 수 없습니다. 프로퍼티 하나를 고쳤을 때 무엇이 깨질지 예측하려면 4,000줄을 다 알아야 합니다. 실제로는 아무도 모르니 고치지 않고 새 프로퍼티를 추가합니다. 그래서 또 커집니다.
  • 재사용이 안 됩니다. 그 안의 날짜 포맷 로직이 다른 화면에도 필요한데 꺼내오려면 클래스 전체가 딸려옵니다. 그래서 복사해서 붙입니다.

특히 첫 번째가 치명적입니다. 테스트가 없으니 쪼갤 수 없고 못 쪼개니 계속 커집니다.

이 순환이 갓 오브젝트를 오래 살아남게 하는 진짜 이유죠.

커지는 걸 어떻게 알아채나

느낌 말고 신호를 씁니다.

신호 1 · Git 변경 빈도

가장 실용적인 지표입니다. 최근 1년간 가장 많이 수정된 파일 목록을 뽑으면 갓 오브젝트가 상위에 있습니다.

git log --format=format: --name-only --since=1.year.ago \
  | grep '\.swift$' | sort | uniq -c | sort -rn | head -20

변경이 잦다는 건 그 파일이 서로 다른 이유로 바뀐다는 신호입니다. 단일 책임 원칙 위반의 실측값이죠.

신호 2 · 응집도

메서드들이 서로 다른 프로퍼티 집합만 건드린다면 그 타입은 사실 두 개일 가능성이 큽니다.

프로퍼티 A·B만 쓰는 메서드 다섯 개와 C·D만 쓰는 메서드 여섯 개가 있다면 경계선 후보가 이미 보입니다.

신호 3 · 타입 길이 규칙

SwiftLint를 쓴다면 type_body_length와 file_length가 기본으로 켜져 있습니다.

타입 본문 250줄, 파일 400줄이 기본 경고선이고 프로젝트에 맞게 조정하면 됩니다. 숫자 자체는 임의지만 선을 넘는 순간 대화가 시작된다는 게 값어치입니다.

해체 순서

한 번에 다시 쓰는 건 거의 실패합니다. 순서가 있습니다.

# 단계 무엇을 하나
1 그물 치기 테스트가 없다면 특성화 테스트부터 만듭니다
2 무상태 로직 분리 날짜 포맷·문자열 검증·금액 계산부터 뗍니다
3 상태 뭉치 분리 함께 바뀌는 프로퍼티를 한 타입으로 묶습니다
4 역할별 분리 데이터 소스·코디네이터·뷰 모델·자식 뷰 컨트롤러로 나눕니다
5 흡수 경로 차단 새 코드의 자리를 정하고 문턱을 둡니다

특성화 테스트는 코드가 옳은지가 아니라 지금 어떻게 동작하는지를 그대로 기록하는 테스트입니다. 마이클 페더스가 『레거시 코드 활용 전략』에서 정리한 기법이죠.

동작을 보존하며 옮기는 게 목표이니 현재 동작이 기준선이 됩니다.

2단계는 인스턴스 상태를 안 쓰는 로직이라 가장 쉽습니다. 이 단계에서 파일이 눈에 띄게 줄어드는 경우가 많습니다.

3단계에서 Swift는 enum이 유용합니다. 화면 상태 세 개가 늘 같이 바뀐다면 그건 상태 객체 하나입니다.

// 전: 유효하지 않은 조합이 표현 가능합니다
var isLoading = false
var items: [Item] = []
var error: Error?

// 후: 셋 중 하나만 존재합니다
enum ViewState {
    case loading
    case loaded([Item])
    case failed(Error)
}

enum으로 묶으면 불가능한 조합 자체를 없앨 수 있습니다.

4단계의 iOS 경로는 정해져 있습니다. 테이블 데이터 소스는 별도 타입으로, 화면 전환은 코디네이터로, 화면 상태와 표시 규칙은 뷰 모델로, 큰 화면은 자식 뷰 컨트롤러로 나눕니다.

5단계가 빠지면 6개월 뒤에 원상 복구됩니다. 다음 기능이 다시 그 파일로 가지 않도록 새 코드의 자리를 명확히 하고 파일 길이 규칙이나 리뷰 합의로 문턱을 만들어 둡니다.

거대한 단일 클래스를 단계별로 분리해 작은 타입들로 나누는 해체 과정 이미지
목표는 조각 수가 아니라 변경 이유를 하나로 만드는 것입니다

쪼개다 실패하는 두 가지 방식

실패 방식 무슨 일이 생기나
이름만 바꾸기 AppManager를 UserManager·DataManager·NetworkManager 셋으로 나눴는데 셋이 서로를 전부 참조합니다. 갓 오브젝트가 갓 클러스터가 됐을 뿐입니다
너무 잘게 나누기 4,000줄 클래스를 40개 타입으로 쪼개면 이번엔 흐름이 사라집니다. 앞 편에서 본 라비올리 코드입니다

나눌 때는 의존 방향이 한쪽으로만 흐르는지 함께 봐야 합니다.

해체의 목표는 조각을 만드는 게 아니라 각 조각이 자기 이유로만 바뀌게 하는 겁니다. 조각 수는 그 결과일 뿐입니다.

정리하면

  • 갓 오브젝트는 크기가 아니라 아는 범위로 판별합니다. 많이 알고 많이 알려져 있고 상태가 흩어져 있으면 해당합니다.
  • 나쁜 설계라서가 아니라 매번 가장 싼 선택을 한 결과로 자랍니다.
  • 가장 큰 피해는 테스트 불가입니다. 테스트가 없으니 못 쪼개고 못 쪼개니 계속 큽니다.
  • Git 변경 빈도 상위 목록이 실측 지표로 쓸 만합니다.
  • 해체는 특성화 테스트 → 무상태 로직 → 상태 뭉치 → 역할 분리 순서로 하고 마지막에 다시 커지는 경로를 막습니다.

다음 편은 갓 오브젝트를 쪼갠 다음에 만나는 문제입니다. 기능 하나 바꾸는데 파일 열두 개를 고쳐야 하는 상태, 산탄총 수술을 봅니다.

출처 및 확인 기준

  • AntiPatterns — Wiley · 공식 데이터 · 확인 2026-08-17 · 근거: 1998년 AntiPatterns 출간 정보와 The Blob 맥락
  • Human Perception on God Class Detection — Springer Nature · 논문 원문 · 확인 2026-08-17 · 근거: God Class 판별 기준의 사람·도구별 차이에 대한 통제 실험

이어서 읽기