객체들끼리는 어떻게 협업을 할까?
아! 그 전에 객체지향이 모냐면...
데이터와 그 데이터를 다루는 행위를 하나로 묶고 메시지를 통해 협업하는 프로그래밍 패러다임 입니다
- 캡슐화: 관련된 프로퍼티와 메서드를 하나의 객체로 묶고, 내부 구현은 감추며 외부에는 필요한 인터페이스만 제공
- 책임 분리: 각 객체는 하나의 명확한 책임만을 가져, 역할이 겹치지 않도록 분리합니다.
- 메시지 전달: 객체들이 서로 소통하며 더 큰 기능을 하는거죠
객체지향을 처음 배울 때 이런 생각 해보셨나요? "그냥 하나의 클래스에 모든 기능을 다 넣으면 안 되나?"
하지만 생각을 해보면 한 사람이 주문받고, 요리, 서빙, 계산까지 다한다면 그 한사람이 아프면 가게가 마비가 되겠죠?
=> 바로 응집도가 낮고 단일 책임 원칙(SRP)을 위반한 결과입니다.
메시지를 통한 협업의 핵심
객체도 마찬가지예요.
하나의 책임(행위)을 위해 관련된 프로퍼티들을 하나의 객체 안에서 관리하고, 메시지를 통해 다른 객체와 협업하는 거죠.
그 후 메시지(뭐가 나에게 들어오고 어떤 걸 넘겨줘야 하는지)에 집중합니다.
// 좋은 예시 - 협업하는 객체들
class OrderTaker {
func takeOrder() -> Order { /* 주문만 받기 */ }
}
class Chef {
func cook(order: Order) -> Dish { /* 요리만 하기 */ }
}
class Server {
func serve(dish: Dish, to customer: Customer) { /* 서빙만 하기 */ }
}
각 객체는 자신의 책임에만 집중하고, 메시지를 통해 필요한 정보를 주고받아요.
핵심 질문은 이거예요: "뭐가 바뀌었을 때 어디에 영향이 가야 할까욥?" 🤔🤔🤔
- 주문 받는 방식이 바뀌면 (키오스크 → 앱 주문) → OrderTaker만 수정
- 요리 방법이 바뀌면 (새로운 조리법 추가) → Chef만 수정
- 서빙 방식이 바뀌면 (로봇 서빙 도입) → Server만 수정
각자의 책임 영역이 명확하게 나뉘어져 있어서, 한 부분의 변경이 다른 부분에 영향을 주지 않습니다
서로 객체간에 직접적으로 의존하지 않기 때문에 결합도가 낮아 한 클래스가 바뀌어도 다른 클래스에 영향을 주지 않고,
하나의 객체 내부에 관련된 기능들이 응집도가 높아 위에서 예시를 든것처럼 요리방식이 바뀌면 Chef에서만 수정하면 됩니다.
Swift에서 객체간 메시지 전달 방식들
병원에서 의사와 간호사의 협업을 통해 4가지 통신 방식을 아주아주 이해하기 쉽게 설명해볼게요
Delegate - 선호 간호사님, 이 환자 차트 정리해주세요🙏🏻
드라마 보면 의사선생님이 환자를 진료하고 나서 담당 간호사분께 "이 환자 차트 정리해주세요"라고 말씀하시는 걸 볼 수 있어요.
차트 정리는 누가 하죠? 의사가? 아니죠 간호사가 하죠!
의사는 차트가 어떤 구조, 어떤 양식으로 정리해야 하는지 몰라요!
그저 환자를 보고 "아, 이 환자 어제에 비해 1도 올라갔네요"라고 판단하면 간호사에게 전달하는 거죠.
그러면 간호사 객체에서는 "아, 네!"하고 구체적으로 그 +1도 올라갔다는 데이터를 받아서 차트를 업데이트합니다.
여기서 "+1도 온도 올라갔다"는 정보가 바로 프로토콜 메서드의 매개변수예요!
코드로 좀 더 이해해볼게요:)
1. 먼저 프로토콜 정의합니다
여기서 의사가 전달하는 데이터는 이름, 온도, 특이사항을 간호사에게 말하죠.
protocol ChartDelegate: AnyObject {
func patientChartDidUpdate(patientName: String, temperature: Double, notes: String)
}
여기서 중요한 컨벤션이 있는데 어떤걸할껀지 명사 + Did + 동사 형식으로 메서드 네이밍이 이루어져야 합니다!
2. 의사 객체에서 환자를 진료할때 프로토콜 메서드를 통해 데이터를 전달합니다.
class Doctor {
weak var chartDelegate: ChartDelegate? // 간호사에게 차트 관리 위임
func examinePatient(name: String) {
print("👨⚕️ 의사: \(name) 환자를 진료하겠습니다")
let currentTemp = measureTemperature()
if currentTemp > 37.5 {
print("👨⚕️ 의사: 이 환자 체온이 \(currentTemp)도네요. 간호사님!")
// 핵심! 의사는 차트 양식을 몰라도 됨
// 그냥 데이터만 전달하고 실제 처리는 간호사에게 위임
chartDelegate?.patientChartDidUpdate(
patientName: name,
temperature: currentTemp,
notes: "발열 증상 관찰됨"
)
}
}
private func measureTemperature() -> Double {
return Double.random(in: 36.0...39.0)
}
}
3. 간호사 객체에서는 그 프로토콜을 채택하고 구현해줍니다
여기서 코드에서는 생략되었지만 간호사가 의사의 Delegate로 설정되어 의사로부터 책임을 위임받는 것이에요. doctor.chartDelegate = nurse가 되죠!
// 3. 간호사 클래스 - 프로토콜을 채택하고 실제 구현
class Nurse: ChartDelegate {
func patientChartDidUpdate(patientName: String, temperature: Double, notes: String) {
print("👩⚕️ 간호사: 네, \(patientName) 환자 차트 업데이트하겠습니다!")
// 간호사만 알고 있는 차트 관리 방법
let chart = PatientChart(
name: patientName,
temperature: temperature,
timestamp: Date(),
notes: notes,
nurseSignature: "김간호사"
)
saveToHospitalSystem(chart)
print("📋 간호사: 차트 업데이트 완료! 병원 시스템에 저장했어요")
}
}
이렇게 설계함으로써:
1. 의사는 진료에만 집중을 할수 있습니다
2. 간호사는 차트 관리에만 집중을 합니다. 판단은 의사가
3. 역할이 명확하게 분리되는거죠
그런데 왜 Weak를 써야 할까요?🤔
만약 weak를 사용하지 않으면 어떻게 될까요?
class Doctor {
var chartDelegate: ChartDelegate? // strong 참조!
}
class Nurse: ChartDelegate {
var assignedDoctor: Doctor? // 만약 이것도 있다면?
}
// 사용할 때
let doctor = Doctor()
let nurse = Nurse()
doctor.chartDelegate = nurse // Doctor → Nurse (strong)
nurse.assignedDoctor = doctor // Nurse → Doctor (strong)
// 💥 순환 참조 발생!
// Doctor가 Nurse를 붙잡고 있고, Nurse가 Doctor를 붙잡고 있어서
// 둘 다 메모리에서 해제되지 않음!
weak로 다시 설정해줬을때 메모리 해제 순서는? doctor.chartDelegate = nurse로 설정했을 때
- Nurse 객체가 먼저 사라지면: weak 참조는 자동으로 nil이 됩니다. Doctor는 정상적으로 해제됩니다.
- Doctor 객체가 먼저 사라지면: Doctor와 함께 delegate 참조도 사라집니다. Nurse는 정상적으로 해제됩니다.
장점 👍
- 컴파일 타임 타입 체크(프로토콜이기에)
- 인터페이스 명확성: 어떤 메서드를 구현해야 하는지, 매개변수가 무엇인지 명확한 인터페이스를 받을수 있습니다
- 높은 응집도: 의사는 진료 관련 기능만하고 간호사는 차트 정리 기능만 하는거죠
- 느슨한 결합도: 데이터를 전달할때 의사는 간호사의 구체적인 구현을 모르고 추상화된 프로토콜만 알면 됩니다.
- 즉 다른 종류의 간호사들로 쉽게 교체 가능합니다. ChartDelegate를 위임받으면 되므로!!
- 이는 자연스럽게 변경 최소화로 이어집니다
- 간호사의 차트 저장 방식이 바뀌어도 -> 의사 객체 수정 불필요
- 의사의 진료방식이 바뀌어도 -> 간호사 객체 수정 불필요
- 각자의 책임 영역 내에서만 변경이 이루어집니다
단점 👎
- 1:1 관계만 가능: 여러 간호사가 동시에 알아야 한다면 곤란해요
- Delegate 체이닝의 복잡성
- A->B->C로 이벤트를 전달할때 각 객체마다 새로운 delegate 프로토콜을 선언해줘야해서 깊어질수록 관리가 어려웠습니다
- 이는 보일러플레이트 코드로 이어집니다
Closure - "검사 끝나면 이 번호로 연락주세요!"
병원에서 의사가 간호사에게 "혈액검사 좀 해주시고, 결과 나오면 바로 제게 연락주세요!"라고 하는 상황을 생각해보세요.
여기서 의사는 자신의 연락처(콜백)를 미리 알려주고, 간호사는 검사가 끝나면 그 번호로 바로 전화를 하죠.
이게 바로 Closure의 개념이에요!
class Nurse {
// 클로저를 받아서 보관할 프로퍼티
var onTestComplete: ((String) -> Void)?
func performBloodTest() {
print("👩⚕️ 간호사: 혈액검사 시작합니다")
// 2초 후 검사 완료 (시뮬레이션)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let result = "정상 수치"
print("👩⚕️ 간호사: 검사 완료! 의사선생님께 연락드려요")
// 의사가 준 클로저 실행!
self.onTestComplete?(result)
}
}
}
class Doctor {
func requestBloodTest() {
let nurse = Nurse()
// 간호사에게 "검사 끝나면 이거 실행해줘" 클로저 전달
nurse.onTestComplete = { result in
print("👨⚕️ 의사: 검사 결과 \(result) 확인했습니다!")
print("👨⚕️ 의사: 처방전 작성하겠습니다")
}
nurse.performBloodTest()
print("👨⚕️ 의사: 기다리는 동안 다른 환자 볼게요")
}
}
// 실행
let doctor = Doctor()
doctor.requestBloodTest()
Delegate보다 훨씬 간단하게 데이터를 전달할 수 있었네요
결합도 측면에서 보면 둘 다 비슷해요!
- Delegate: 추상화된 프로토콜에 의존
- Closure: 추상화된 함수 타입에 의존
Closure의 장단점
장점 👍
- 간결함: 프로토콜 선언 없이 바로 사용 가능해요
- 가독성: 호출하는 곳과 처리하는 곳이 가까워서 코드 흐름을 따라가기 쉬워요
- 유연함: 상황에 따라 다른 클로저를 전달할 수 있어요
단점 👎
- 메모리 누수 위험: [weak self]를 빼먹으면 순환참조가 발생할 수 있어요
- 디버깅 어려움: 클로저 체이닝이 길어지면 디버깅이 까다로워져요
- 재사용성 떨어짐: 같은 로직을 여러 곳에서 써야 한다면 중복 코드가 생길 수 있어요
그래서 저는 Delegate는 객체 간 명확하게 관계를 맺고 싶을 때 사용합니다. (역할이 분리된 복잡한 기능에 적합
Closure는 단일 이벤트 전달처럼 간단한 콜백이 필요할 때 사용합니다.
NotificationCenter - "👨🏼⚕️응급상황 발생! 모든 의료진 주목하세요!"
병원에서 응급상황이 발생하면 어떻게 될까요? 스피커로 "응급상황 발생! 모든 의료진은 응급실로 집합하세요!"라고 방송을 하죠.
이때 방송실에서는 누가 듣고 있는지 몰라도 되고, 듣는 사람들은 방송실이 어디 있는지 몰라도 돼요.
그냥 방송이 나오면 각자 알아서 반응하는 거죠.
이게 바로 NotificationCenter의 핵심이에요! 1:N 통신이 가능하고, 발송자와 수신자가 서로 모르는 느슨한 결합을 만들어줘요.
이거에 관해서는 이전 글 Observer Pattern에 자세히 적혀 있어서 참고하셔도 좋을거 같습니다
https://codeisfuture.tistory.com/143
포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter
작년인가? 포켓몬 빵 대란이 일어진 사건을 기억하시나요???다들 띠부씰을 얻기 위해서 포켓몬빵 오픈런을 하면서 오박사님이 "ㅅㄱ!"를 외치거나 다른 사람들과 가위바위보까지 하는 사건이 일
codeisfuture.tistory.com
KVO - 환자 상태 변화를 실시간으로 지켜보겠습니다"👀
병원에서 중환자실 모니터링을 생각해보세요. 환자의 심박수, 혈압, 체온 등이 실시간으로 변할 때마다 의료진이 즉시 알아야 하잖아요?
KVO는 마치 간호사가 환자 곁에서 24시간 지켜보다가, 상태가 조금이라도 변하면 "의사선생님! 환자 심박수가 70에서 85로 올라갔어요!"라고 즉시 보고하는 것과 같아요.
다른 패턴들은 모두 "누군가 의도적으로 호출"해야 했다면 KVO는 값이 변하는 순간 시스템이 알아서 알려주는게 특징인거죠
class Patient: NSObject {
@objc dynamic var heartRate: Int = 70
@objc dynamic var temperature: Double = 36.5
}
class Nurse: NSObject {
private var heartRateObservation: NSKeyValueObservation?
func startMonitoring(_ patient: Patient) {
heartRateObservation = patient.observe(\.heartRate, options: [.new, .old]) { patient, change in
let old = change.oldValue ?? 0
let new = change.newValue ?? 0
print("👩⚕️ 간호사: 심박수 \(old) → \(new)")
if new > 100 { print("🚨 응급상황!") }
}
}
}
// 사용
let patient = Patient()
let nurse = Nurse()
nurse.startMonitoring(patient)
patient.heartRate = 110 // 자동으로 감지되어 알림!
Patient는 자신의 상태만 관리하면 되고, 누가 관찰할지는 관찰자들이 알아서 결정해요. 완전히 느슨한 결합이죠!
하지만 당연히 단점도 있겟죠..?
KVO의 한계와 주의점
Objective-C의 유산
KVO는 Objective-C 시절부터 있던 기술이에요. 그래서 Swift에서 사용하려면 @objc dynamic이나 NSObject 상속같은 "옛날 흔적"들이 필요해요. 이게 좀 불편하죠.
성능 고려사항
모든 프로퍼티 변화를 감시하는 건 공짜가 아니에요.
특히 자주 변하는 값(애니메이션, 스크롤 위치 등)을 관찰할 때는 성능 영향을 고려해야 해요.
메모리 관리의 복잡성 관찰을 시작했으면 반드시 중단해야 해요. 안 그러면 메모리 누수나 크래시가 발생할 수 있거든요.
언제 쓸까요? 그러면 대체
적합한 상황:
- 사용자 입력값 실시간 검증 (텍스트 길이, 이메일 형식 등)
- 앱 설정값 변화 감지 (다크모드 전환, 언어 변경 등)
- 애니메이션 진행률 추적
- 네트워크 상태 변화 모니터링
부적합한 상황:
- 일회성 이벤트 처리 → Closure가 나음
- 복잡한 상호작용 → Delegate가 나음
- 전체 앱 알림 → NotificationCenter가 나음
정리해보자면 결국 객체들끼리는 응집도가 높고 결합도를 낮추는 것을 목표로 하여야 하는데 다른 큰 기능을 하기 위해서는 객체들끼리 협업을 해야합니다
그 협업에서 느슨한 결합을 유지하기 위한 방법으로 Swift에는 여러 방법이 있고 그중에 우선 4가지만 적었습니다. 이 외에도 반응형 프로그래밍 등 방법은 많답니다!
'iOS' 카테고리의 다른 글
스크롤뷰는 어떻게 스크롤될까? 🤔 UIView 렌더링부터 시작하는 완벽 분석 (0) | 2025.09.26 |
---|---|
데이터 보관은 어떻게이루어질까(직렬화: NSCoding부터 Codable까지) (0) | 2025.09.09 |
PHPickerController의 UTI 활용법 (4) | 2025.08.29 |
포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter (5) | 2025.08.27 |
내 아이폰에 터치를 해보았다(Reverse-preorder DFS) (3) | 2025.08.26 |