상속을 줄이고 델리게이트와 시그널로 푸는 이유

객체지향 프로그래밍에서 상속은 여전히 중요한 도구다. 공통 인터페이스를 만들고, 안정적인 기반 동작을 재사용하는 데는 실제로 도움이 된다. 하지만 이벤트 처리나 UI 연결처럼 누가 누구를 호출할지 자주 바뀌는 문제에서는 상속이 금방 무거워질 수 있다.
그래서 많은 프레임워크는 상속 하나로 모든 연결을 해결하려 하지 않는다. 대신 델리게이트(delegate)나 시그널과 슬롯(signals and slots)처럼, 호출 관계를 더 느슨하게 연결하는 메커니즘을 함께 제공한다.
델리게이트는 “나중에 호출할 함수를 값처럼 들고 다니는 방식”이다
델리게이트는 호출 대상을 값처럼 전달하는 메커니즘이다. 마이크로소프트의 C# delegates 가이드는 델리게이트를 메서드에 대한 참조를 담는 형식으로 설명한다.
이 방식이 유용한 이유는 간단하다.
- 버튼이 클릭됐을 때 어떤 동작을 할지
- 리스트 항목이 선택됐을 때 무엇을 실행할지
- 특정 조건이 충족됐을 때 어떤 로직을 연결할지
이런 문제를 굳이 버튼 클래스를 상속해서 해결하지 않아도 되기 때문이다. 필요한 동작만 연결해 두고, 나중에 교체할 수도 있다.
즉 델리게이트는 동작을 붙이는 일과 타입 계층을 설계하는 일을 분리해 준다.
Qt의 시그널과 슬롯은 느슨한 연결이 왜 중요한지 잘 보여 준다
Qt 문서의 Signals & Slots는 한 객체가 상태 변화를 알리고, 다른 객체가 그 알림을 받아 반응하는 구조를 설명한다. 여기서 핵심은 두 객체가 서로를 강하게 알 필요가 없다는 점이다.
Qt는 이 구조의 장점을 아주 명확하게 적는다.
- 타입 안전성을 유지할 수 있고
- 송신자와 수신자가 느슨하게 결합되며
- 객체는 누가 신호를 받는지 몰라도 되고
- 슬롯은 일반 함수처럼 구현할 수 있다
이건 상속과 다른 종류의 힘이다. 상속은 무엇의 하위 타입인가를 표현하는 데 강하고, 시그널과 슬롯은 누가 누구의 변화에 반응하는가를 표현하는 데 강하다.
상속이 문제라기보다, 상속으로 이벤트를 전부 해결하려 할 때 문제가 커진다
예를 들어 버튼을 눌렀을 때 메시지를 띄우고 싶다고 해 보자. 이때 매번 새 버튼 클래스를 상속받아 MyButton, SaveButton, CancelButton을 만드는 방식도 가능하다. 하지만 UI가 커질수록 이런 구조는 금방 퍼진다.
- 이벤트 하나마다 서브클래스가 늘고
- 클래스 이름이 동작 이름으로 바뀌며
- 재사용보다 연결용 코드가 더 많아지고
- 변경할 때마다 타입 계층까지 건드리게 된다
로버트 나이스트롬의 Subclass Sandbox는 상속이 유용할 때도 있지만, 기반 클래스가 지나치게 많은 책임을 끌어안으면 결합이 커지고 바꾸기 어려워진다고 설명한다. 즉 상속은 통제된 범위에서는 좋지만, 모든 변화를 받아내는 그릇이 되면 부담이 커진다.
그래서 실무에서는 “상속할 것”과 “연결할 것”을 나눠 본다
상속이 잘 맞는 경우는 여전히 분명하다.
- 공통 수명 주기와 규약이 안정적일 때
- 동일한 역할군을 타입으로 묶어야 할 때
- 기반 클래스가 보호막 역할을 해 줄 때
반대로 델리게이트나 시그널이 잘 맞는 경우도 뚜렷하다.
- 이벤트 연결이 자주 바뀔 때
- 여러 객체가 느슨하게 통신해야 할 때
- UI와 애플리케이션 로직을 분리하고 싶을 때
결국 중요한 것은 상속을 버리는 신념이 아니라, 타입 관계와 실행 시 연결 관계를 구분하는 습관이다.
핵심 정리
상속은 여전히 유용한 도구지만, 이벤트 처리와 느슨한 협력이 중요한 영역에서는 델리게이트와 시그널 같은 방식이 더 잘 맞는 경우가 많다. 델리게이트는 호출 대상을 값처럼 다루게 해 주고, 시그널과 슬롯은 송신자와 수신자를 느슨하게 연결해 준다.
그래서 좋은 설계는 상속을 무조건 줄이는 데 있지 않다. 무엇을 타입 계층으로 표현할지, 무엇을 연결 메커니즘으로 풀지를 분리해 보는 데 더 가깝다.