신 객체
고친 사람 github-actions[bot]
신 객체는 여러 클래스가 나눠 맡아야 할 일을 혼자 끌어안은 객체입니다. 무엇이든 알고 무엇이든 해서 신이라는 이름이 붙었습니다. 처음에는 한곳에 다 있어 편해 보입니다. 커질수록 작은 수정 하나가 코드 전체를 흔들게 됩니다.
쉽고 빠른 이해
신 객체는 클래스 하나가 프로그램의 일을 거의 다 떠맡은 모습입니다. 쇼핑몰 코드에서 ShopManager 하나가 회원 가입, 주문, 결제, 재고, 메일 발송을 전부 처리하는 식입니다.
사람들이 이 길로 드는 까닭은 새 기능을 붙이는 가장 짧은 길이라서입니다. 필요한 데이터가 이미 그 클래스에 다 있습니다.
어떻게 이렇게 되나:
- 처음에는 한 가지 일만 하는 작은 클래스로 시작합니다
- 새 기능이 올 때마다 데이터가 있는 이 클래스에 메서드를 하나 더 붙입니다
- 결국 다른 코드는 전부 이 클래스를 부르고, 이 클래스는 모든 데이터를 만집니다
대가는 모든 수정이 이 클래스를 거친다는 점입니다. 메일 문구 하나를 고쳐도 주문 코드가 섞인 파일을 열어야 합니다. 결제 하나를 테스트하려 해도 회원·재고·메일을 다 준비해야 합니다.
다시 고칠 일 없는 짧은 스크립트라면 한곳에 몰아도 치르는 값이 적습니다.
상세
이 절은 쇼핑몰의 ShopManager 클래스 하나가 신 객체로 자라는 과정을 따라갑니다. 왜 이 길로 들어서는지, 어디서 값을 치르는지, 어떻게 알아보고 나누는지를 봅니다. 끝으로 한곳에 모아도 되는 때와 이름이 비슷한 이웃을 가릅니다.
사장 혼자 모든 일을 하는 가게
작은 가게에서는 사장 혼자 계산도 하고 물건도 채우고 장부도 씁니다. 손님이 적을 때는 이 방식이 제일 수월합니다. 누구에게 물어볼 필요 없이 사장이 다 알기 때문입니다.
가게가 커진 뒤에도 일이 계속 사장에게만 몰리면 사정이 달라집니다. 장부 적는 법 하나를 바꾸려 해도 사장의 하루 일정을 전부 다시 짜야 합니다.
모든 일을 떠맡은 클래스
객체 지향 프로그래밍은 일을 여러 객체에 나눠 맡깁니다. 객체 하나는 제 데이터를 들고, 그 데이터로 할 수 있는 일을 합니다. 이렇게 나누면 한 가지를 고칠 때 객체 하나만 열면 됩니다.
신 객체는 이 나눔이 무너진 모습입니다. 객체 하나가 다른 객체들의 데이터까지 쥐고, 그 데이터로 하는 일도 대신합니다. 영어로는 god object 나 god class 라고 부릅니다. 블롭(Blob)이라는 별명도 있습니다.
코드에서는 이 객체가 클래스 하나로 드러납니다. 그래서 신 클래스라고도 합니다. 이 문서도 아래에서는 클래스로 부릅니다.
아래는 그렇게 자란 쇼핑몰 클래스 ShopManager 입니다. 앞 가게의 사장이 바로 이 클래스입니다. 필드 넷과 메서드 여섯만 추렸습니다.
class ShopManager {
Map<Long, Member> members; // 회원
Map<Long, Order> orders; // 주문
Map<Long, Integer> stock; // 상품별 재고
MailSender mail; // 메일 발송
void signUp(Member m) { … }
void order(long member, long item) { … }
void pay(long orderId) { … }
void restock(long item, int n) { … }
void sendNewsletter() { … }
String salesReport() { … }
}
필드 넷은 서로 다른 일의 데이터입니다. 회원, 주문, 재고, 메일 발송은 한 클래스에 있을 까닭이 없습니다. 메서드도 갈립니다. restock 은 회원 데이터를 안 쓰고, signUp 은 재고를 안 씁니다.
끌리는 이유
처음부터 이런 클래스를 설계하는 사람은 드뭅니다. ShopManager 도 처음에는 회원 가입 하나만 맡았습니다. 신 객체는 한 번에 만들어지지 않고 조금씩 자랍니다.
주문 기능을 붙이던 때를 떠올려 봅니다. 주문을 받으려면 회원 데이터가 필요합니다. 그 데이터는 이미 ShopManager 에 있습니다. 새 클래스를 만들어 회원 데이터를 넘겨받게 하는 것보다, 여기에 메서드 하나를 더 쓰는 쪽이 훨씬 짧습니다.
결제, 재고, 메일도 같은 이유로 여기 붙습니다. 매번의 선택은 그 순간에는 가장 짧은 길이었습니다. 한 파일만 열면 필요한 것이 다 보인다는 점도 편합니다.
한곳에 모인 뒤 치르는 값
이 소절은 ShopManager 가 다 자란 뒤 코드를 고칠 때 생기는 일을 봅니다. 먼저 이 클래스를 둘러싼 호출 관계를 봅니다. 그다음 그 관계가 낳는 문제 다섯 가지를 차례로 짚습니다.
flowchart TD
subgraph callers["부르는 쪽"]
A["회원 화면"]
B["주문 화면"]
C["관리자 화면"]
end
G["ShopManager"]
subgraph targets["손대는 쪽"]
D["회원 데이터"]
E["주문 데이터"]
F["메일 서버"]
end
A --> G
B --> G
C --> G
G --> D
G --> E
G --> F
그림에서 화면 셋이 부르는 화살표도, 데이터로 가는 화살표도 모두 ShopManager 한 상자를 지납니다. 이 상자를 고치면 화면 셋이 모두 영향을 받을 수 있습니다.
첫째, 작은 변경이 멀리 번집니다. 거의 모든 코드가 이 클래스 하나에 기대고 있기 때문입니다. 그래서 메일 발송 코드만 고쳐도 주문 화면이 깨질 수 있습니다. 클래스 같은 코드 덩어리가 다른 코드에 얼마나 기대는지를 결합도라고 합니다.
둘째, 고칠 곳을 찾기 어려워집니다. ShopManager 안의 메서드들은 저마다 다른 목적을 위해 있습니다. 메일 문구 하나를 고치려 해도 주문과 결제 코드 사이를 헤집으며 찾아야 합니다. 한 클래스 안의 코드가 한 목적으로 얼마나 모여 있는지가 응집도입니다.
셋째, 데이터를 지키는 담장이 무너집니다. 원래 객체는 제 데이터를 숨기고 정해 둔 메서드로만 다루게 합니다. 이 담장이 캡슐화입니다. 고치는 길이 좁으니 잘못 고친 곳을 찾기 쉽습니다.
신 객체에서는 담장 안이 쇼핑몰 전체만큼 넓어집니다. 메일 코드도 같은 클래스 안에 있어서 재고 필드를 마음대로 고칠 수 있습니다. 담장은 있어도 지켜 주는 것이 없습니다. 결국 어디서나 고칠 수 있는 전역 상태와 거의 같아집니다.
넷째, 기능 하나만 떼어 시험하기 어렵습니다. pay 하나를 확인하려 해도 ShopManager 를 만들어야 합니다. 그러려면 회원, 재고, 메일 발송까지 준비해야 합니다. 코드 한 조각을 떼어 따로 확인하는 단위 테스트가 이렇게 무거워집니다.
다섯째, 여러 사람이 함께 일할 때 부딪힙니다. 서로 다른 기능을 맡아도 결국 같은 파일을 고칩니다. 두 사람이 한 파일의 가까운 줄을 따로 고치면 합칠 때 병합 충돌이 납니다. 신 객체가 있는 코드에서는 이 충돌이 그 파일에 몰립니다.
신 객체를 알아보는 신호
신 객체는 조금씩 자라서 언제 선을 넘었는지 알기 어렵습니다. 대신 겉으로 드러나는 조짐이 있습니다. 설계 문제를 짐작하게 하는 이런 조짐을 코드 스멜이라고 부릅니다.
| 신호 | 무엇을 보나 |
|---|---|
| 이름 | Manager · System · Handler 처럼 무엇이든 담을 수 있는 이름인가 |
| 필드 쓰임 | 메서드마다 쓰는 필드가 갈리나 |
| 가져다 쓰는 클래스 | 이 클래스가 기대는 다른 클래스가 유난히 많나 |
| 부르는 곳 | 이 클래스를 부르는 코드가 곳곳에 퍼져 있나 |
| 변경 이력 | 무슨 기능을 고치든 이 파일이 커밋에 끼나 |
| 한 문장 설명 | 이 클래스가 하는 일을 「그리고」 없이 한 문장으로 말할 수 있나 |
표에서 가장 믿을 만한 신호는 필드 쓰임입니다. 어떤 필드는 몇몇 메서드만 쓰고, 다른 필드는 또 다른 메서드들만 쓴다면 한 클래스 안에 클래스 여럿이 숨어 있다는 뜻입니다. 줄 수나 메서드 수로 여기부터 신 객체라고 긋는 선은 없습니다.
책임별로 나누는 방법
이 소절은 ShopManager 를 여러 클래스로 나누는 순서를 봅니다. 나누는 기준부터 세웁니다. 그다음 그 기준에 따라 코드를 옮깁니다.
기준은 단일 책임 원칙입니다. 클래스 하나는 바뀌는 이유가 하나뿐이도록 짜라는 설계 원칙입니다. 회원 가입 규칙이 바뀌는 이유와 재고 계산이 바뀌는 이유는 다릅니다. 둘이 한 클래스에 있으면 어느 쪽이 바뀌어도 이 클래스를 고치게 됩니다.
나누는 일은 동작을 바꾸지 않고 구조만 바꾸는 리팩터링입니다. 한 번에 다 쪼개지 않고, 테스트로 동작을 지켜 가며 조금씩 옮깁니다.
- 메서드마다 쓰는 필드를 적어 봅니다. 같은 필드를 쓰는 메서드끼리 묶입니다
- 묶음 하나를 새 클래스로 옮깁니다. 이 작업을 클래스 추출이라고 부릅니다
- 원래 클래스의 메서드는 한동안 새 클래스에 일을 넘기기만 합니다. 부르는 쪽을 한꺼번에 고치지 않아도 됩니다
- 부르는 쪽을 하나씩 새 클래스로 돌리고, 빈 껍데기가 된 원래 메서드를 지웁니다
나눈 뒤에는 MemberService, OrderService, PaymentService, StockService, MailService 가 각자 제 데이터와 제 일만 맡습니다. 메일 문구를 고칠 때는 메일을 맡은 클래스 하나만 엽니다.
주문에 회원 데이터가 필요하다는 사정은 그대로 남습니다. 이제는 OrderService 가 회원 데이터를 직접 쥐지 않고, 회원을 맡은 객체를 바깥에서 넘겨받아 씁니다. 필요한 객체를 이렇게 바깥에서 넘겨주는 방식이 의존성 주입입니다. 끌리는 이유였던 「데이터가 이미 거기 있다」를 이 방식이 대신 풀어 줍니다.
flowchart TD
subgraph callers["부르는 쪽"]
A["회원 화면"]
B["주문 화면"]
C["관리자 화면"]
end
subgraph services["나눈 클래스"]
M["MemberService"]
O["OrderService"]
S["MailService"]
end
subgraph targets["제 데이터"]
D["회원 데이터"]
E["주문 데이터"]
F["메일 서버"]
end
A --> M
B --> O
C --> S
O --> M
M --> D
O --> E
S --> F
앞의 호출 관계를 나눈 뒤 모양으로 다시 그렸습니다. 다섯 클래스 가운데 셋만 골랐습니다. 이제 화살표가 한 상자에 모이지 않습니다. 클래스마다 제 데이터로만 화살표가 갑니다. OrderService 에서 MemberService 로 가는 화살표가 바깥에서 넘겨받은 회원 객체입니다.
한곳에 모아도 되는 때
모든 큰 클래스가 신 객체인 것은 아닙니다. 이 소절은 한곳에 모여 있어도 값을 덜 치르는 경우 셋을 가릅니다.
첫째는 한 번 쓰고 버릴 작은 스크립트나 시험 삼아 만든 시제품입니다. 다시 고칠 일이 없고 고칠 사람도 한 명이면 나눠서 얻을 것이 적습니다. 오래 쓸 코드가 되는 순간부터는 앞의 값을 치르기 시작합니다.
둘째는 퍼사드 패턴입니다. 퍼사드는 여러 객체 앞에 창구 하나를 두는 설계입니다. 창구는 요청을 받아 안쪽 객체에 넘기기만 하고, 스스로 데이터를 쥐거나 계산하지 않습니다. 겉모습은 신 객체와 닮았지만 일을 넘기느냐 직접 하느냐가 다릅니다.
셋째는 프로그램이 시작할 때 객체를 전부 만들고 서로 이어 주는 코드입니다. 이 코드는 거의 모든 클래스를 압니다. 그래도 만들고 이어 줄 뿐 주문이나 결제 같은 업무 일은 하지 않아서 신 객체와 다릅니다.
이름이 비슷한 이웃
「너무 많은 것이 한곳에 몰렸다」를 가리키는 이름은 여럿입니다. 무엇이 몰렸는지와 그 규모가 저마다 다릅니다.
| 이름 | 무엇이 몰렸나 | 신 객체와 다른 점 |
|---|---|---|
| 유틸리티 클래스 | 서로 관계없는 정적 메서드 | 데이터를 쥐지 않고 함수만 모인다 |
| 기능 편애 | 제 데이터보다 남의 데이터를 더 쓰는 메서드 | 클래스가 아니라 메서드 하나의 문제다 |
| 모놀리스 | 한 배포 단위에 모든 기능 | 배포 단위의 이야기다. 안의 클래스는 잘 나뉘어 있을 수 있다 |
| 큰 진흙 공 | 뚜렷한 구조가 없는 시스템 전체 | 클래스 하나가 아니라 시스템 전체가 엉킨 것이다 |
신 객체는 이 가운데 클래스 하나의 규모에 있습니다. 모놀리스는 잘 나뉜 클래스로도 지을 수 있어서, 둘은 따로 따져야 합니다.
관련 항목
신 객체가 어기는 설계 원칙
단일 책임 원칙 · 관심사의 분리 · 캡슐화 · 정보 은닉 · SOLID · 디미터 법칙
신 객체가 무너뜨리는 성질
응집도 · 결합도 · 모듈성 · 테스트 용이성 · 유지보수성
신 객체가 불러오는 변경 문제
파급 효과 · 병합 충돌 · 순환 의존 · 회귀 버그 · 기술 부채
신 객체를 알아보는 코드 스멜
코드 스멜 · 거대한 클래스 · 긴 메서드 · 기능 편애 · 산탄총 수술 · 발산적 변경 · 데이터 뭉치
신 객체를 나누는 리팩터링 기법
리팩터링 · 클래스 추출 · 메서드 옮기기 · 위임 · 의존성 주입 · 단위 테스트 · 모의 객체
한곳에 몰린 구조를 가리키는 다른 안티패턴
유틸리티 클래스 · 큰 진흙 공 · 모놀리스 · 분산 모놀리스 · 빈약한 도메인 모델 · 전역 상태 · 싱글턴 패턴
신 객체와 겉모습이 닮은 설계 패턴
퍼사드 패턴 · 중재자 패턴 · 서비스 레이어 · 컨트롤러
신 객체가 속하는 상위 분류
안티패턴 · 객체 지향 설계 · 객체 지향 프로그래밍 · 소프트웨어 아키텍처 · 객체
다른 이름: god object · god class · 신 클래스 · 갓 오브젝트 · 갓 클래스 · 블롭 · Blob