객체지향 프로그래밍(OOP)을 배우다 보면 수많은 원칙과 패턴들을 접하게 됩니다. SOLID, 디자인 패턴, 클린 코드 등등… 때로는 그 복잡함에 압도되기도 하죠. 하지만 그중에서도 이해하기 쉽고, 적용했을 때 코드 품질에 변화를 가져다주는 황금률이 하나 있습니다. 바로 "Tell, Don't Ask (묻지 말고 시켜라)"입니다.
이 원칙은 프로그래밍의 가장 기본적인 활동 중 하나인 "객체 간의 상호작용"에 대한 강력한 지침을 제공합니다. 이름만 들어도 벌써 뭔가 직관적이지 않나요? 굳이 물어보지 말고, 그냥 하던 대로 시키라는 겁니다.
왜 "묻지 말고 시키라"는 걸까?
자, 예를 들어봅시다. 저는 한 팀의 리더이고, 팀원에게 특정 작업을 지시해야 합니다.
나쁜 예 (Ask):
"팀원 A 씨, 지금 이 파일의 상태가 '편집 중'인가요? 만약 '편집 중'이라면, 해당 파일을 '검토 대기' 상태로 바꿔주세요."
리더는 팀원 A의 파일 상태를 "물어보고" 그 결과에 따라 "직접 판단"하여 팀원 A에게 "무엇을 할지" 지시합니다. 이 과정에서 리더는 팀원 A의 내부 상태(파일 상태)를 너무 많이 알고 있습니다.
좋은 예 (Tell):
"팀원 A 씨, 이 파일을 '검토 대기' 상태로 변경해주세요."
이 경우, 리더는 팀원 A의 내부 상태에 대해 전혀 묻지 않습니다. 그냥 "요청"만 할 뿐이죠.파일 상태가 '편집 중'이든 아니든, 팀원 A는 자신의 내부 로직에 따라 알아서 처리할 겁니다. 만약 '편집 중'이 아니라면 '검토 대기'로 바꾸는 것이 불가능할 수도 있겠죠. 그럴 경우 팀원 A는 "죄송합니다, 현재 상태에서는 그렇게 할 수 없습니다"라고 알려줄 것입니다. 리더는 그 이유를 알 필요 없이, 불가능하다는 결과만 알면 됩니다.
코드로 보는 "Tell, Don't Ask"
이 개념을 실제 코딩에 적용해보면 훨씬 더 명확해집니다.
Ask (나쁜 예):
// 외부에서 Account 객체의 상태를 직접 확인하고 제어
class Account {
private double balance; // 잔액
public double getBalance() { // 외부에서 잔액을 '물어볼 수 있게' 함
return balance;
}
public void setBalance(double balance) { // 외부에서 잔액을 '직접 설정할 수 있게' 함
this.balance = balance;
}
// ... 기타 메서드
}
class TransactionProcessor {
public void processWithdrawal(Account account, double amount) {
if (account.getBalance() >= amount) { // 잔액을 '물어보고'
account.setBalance(account.getBalance() - amount); // 직접 '설정'
System.out.println("출금 완료!");
} else {
System.out.println("잔액 부족!");
}
}
}
이 코드는 TransactionProcessor가 Account 객체의 내부 상태인 balance를 직접 알고(getBalance()), 그 값에 따라 조건문으로 판단하고(if (account.getBalance() >= amount)), 심지어 직접 값을 변경(setBalance())하고 있습니다. Account는 단지 데이터를 담는 DTO (Data Transfer Object)나 VO (Value Object)처럼 행동하고 있죠. 만약 출금 정책이 변경되면 (수수료 추가, 마이너스 통장 허용 등), TransactionProcessor뿐만 아니라 Account의 balance를 직접 다루는 모든 곳을 수정해야 할 수도 있습니다.
Tell (좋은 예):
class Account {
private double balance;
public Account(double balance) {
this.balance = balance;
}
// 외부에서는 잔액을 직접 묻거나 설정하는 대신, '행동'을 요청
public boolean withdraw(double amount) { // 출금이라는 '행동'을 '요청'
if (balance >= amount) {
balance -= amount;
System.out.println("출금 완료!");
return true;
} else {
System.out.println("잔액 부족!");
return false;
}
}
// ... 기타 메서드
}
class TransactionProcessor {
public void processWithdrawal(Account account, double amount) {
account.withdraw(amount); // 계좌에게 '출금해달라'고 시킴
// 더 이상 계좌의 잔액을 묻지도, 직접 설정하지도 않음
}
}
이제 TransactionProcessor는 Account 객체의 내부 구현에 대해 아무것도 알지 못합니다. 그저 account.withdraw(amount)라고 "요청"할 뿐이죠. 출금 로직(잔액 확인, 수수료, 마이너스 통장 여부 등)이 변경되더라도, TransactionProcessor는 전혀 수정할 필요가 없습니다. 오직 Account 클래스 내부만 변경하면 됩니다. 이것이 바로 캡슐화(Encapsulation)의 힘입니다.
Tell, Don't Ask의 함정
이 원칙은 모든 getter 메서드를 없애라는 의미가 아닙니다. 객체의 상태를 기반으로 외부에서 어떤 결정을 내리고 그에 따라 객체의 상태를 변경하려는 경우에 특히 적용해야 합니다. 객체의 내부 상태가 외부에 노출되어, 외부 로직이 그 상태를 해석하고 조작하려 할 때 문제가 발생하기 쉽습니다.
객체가 자신의 데이터를 가장 잘 알고 있으므로, 데이터에 대한 모든 판단과 변경은 그 데이터를 소유한 객체 스스로에게 위임해야 합니다.
하지만, 모든 상황에서 "Tell, Don't Ask"가 최적의 솔루션은 아닙니다. 때로는 데이터 자체의 구조가 더 중요하거나, 순수한 데이터 처리 로직이 더 적합한 경우가 있습니다.
이제 문제를 알아봅니다.
1. God object 가능성
"Tell, Don't Ask"를 너무 철저하게 적용하려다 보면, 때로는 하나의 객체가 너무 많은 책임을 떠안게 될 수 있습니다. 모든 결정을 자신이 내려야 하니, 관련된 모든 로직을 자기 안에 담으려 할 수 있기 때문입니다.
객체의 메서드가 너무 많아지고, 내부 로직이 복잡해져서 단일 책임 원칙(SRP)을 위반할 위험이 있습니다. 이는 결국 God Object( https://en.wikipedia.org/wiki/God_object) 안티패턴으로 이어질 수 있으며, 오히려 코드를 이해하고 변경하기 더 어렵게 만듭니다.
2. DTO, VO
순수한 데이터 컨테이너 역할을 하는 DTO(Data Transfer Object)나 Value Object는 본질적으로 getter 메서드를 많이 가집니다. 이들은 데이터를 "물어보기" 위해 설계된 객체들입니다. "Tell, Don't Ask"를 너무 엄격하게 적용하면, 이러한 객체들의 존재 자체가 원칙 위배로 간주될 수 있습니다.
3. 객체에게 "시키기" 위해 또 다른 객체에게 "시키고", 그 객체가 또 다른 객체에게 "시키는" 연쇄적인 위임이 발생할 수 있습니다.
메서드 호출 체인이 길어지거나, 중간에 있는 객체들이 단순히 다른 객체의 메서드를 호출해주는 래퍼(Wrapper) 역할만 하게 될 수 있습니다. 이는 디버깅을 어렵게 만들고, 특정 기능이 어디서 시작되는지 추적하기 어렵게 만듭니다.
결론
- 객체의 본질적인 책임에 집중: 모든 로직을 한 객체에 몰아넣기보다, 객체의 핵심 책임을 정의하고 그 범위 내에서 "Tell"을 적용합니다. 다른 책임은 다른 객체에게 위임하거나 협업을 통해 해결합니다.
- 도메인 모델의 복잡성 반영: 도메인 모델이 복잡할수록 "Tell, Don't Ask"가 빛을 발하지만, 단순히 데이터를 전달하는 계층에서는 과도한 적용이 오히려 독이 될 수 있습니다.
- 가독성과 유지보수성 사이의 균형: 원칙을 지키면서도 코드를 읽는 사람이 비즈니스 로직의 흐름을 쉽게 이해할 수 있도록 노력해야 합니다.
마무리하며
"Tell, Don't Ask"는 단순한 문구처럼 보이지만, 객체지향 설계의 핵심을 꿰뚫는 강력한 원칙입니다.
이 원칙을 마음에 새기고 코드를 작성한다면, 작성하는 코드는 더 견고하고, 유연하며, 유지보수가 쉬운 방향으로 발전할 것입니다.
이제부터 코드를 짤 때마다 스스로에게 질문해보세요. "나는 지금 객체에게 묻고 있는가, 아니면 시키고 있는가?" 이 작은 질문이 유연한 시스템을 구축하는데 도움이 될 것입니다.
'1. 개발' 카테고리의 다른 글
| [OpenSource] 아파치 Gravitino 릴리즈(v0.9.1, v1.0.0) 기여 (0) | 2025.10.01 |
|---|---|
| [Spring Security]: 시큐리티 필터 예외 바꿔치기 문제 (0) | 2025.09.07 |
| 회고[C/C++]: C 개발 이야기(메모리 누수) (1) | 2025.06.05 |
| CI/CD: 비효율의 굴레를 끊어내는 여정 (1) | 2024.12.27 |
| Spring Cloud Gateway, 비동기 응답 문제 ( Global Filter가 아닌, Gateway Filter) (0) | 2024.11.01 |