Day 41 — 행동 패턴: 행동을 갈아끼우고, 사건을 구독하기
목차 16
지난 시간 Day 40 에서는 객체를 만들어내는 네 가지 방법을 배웠어요. Singleton 으로 딱 하나만, Builder 로 이름표 붙여 쌓고, Factory 로 무엇을 만들지 한 곳에서 결정했죠. 전부 "객체가 태어나는 순간" 을 다루는 생성 패턴이었어요.
오늘은 한 걸음 더 나아가요. 객체를 다 만들었으면, 이제 그 객체가 어떻게 동작하느냐 가 남아요. 그런데 동작이라는 건 자주 바뀌어요. 알림을 이메일로 보내다가 푸시로 바꾸고 싶고, 좋아요가 눌리면 알림도 보내고 숫자도 올리고 기록도 남겨야 하죠. 이렇게 행동을 유연하게 다루는 패턴들을 행동 패턴(behavioral pattern) 이라고 불러요.
오늘은 그중 가장 많이 쓰이는 두 가지를 만나봐요. 행동을 통째로 갈아끼우는 Strategy, 그리고 한 사건에 여러 곳이 알아서 반응하게 하는 Observer. 둘 다 우리가 이미 만들어둔 인스타그램 코드 위에서 자연스럽게 자라나요. 시작해봐요!
🎯 학습 목표
if-else로 동작을 분기할 때 코드가 어떻게 부풀어 오르는지 — 그 불편을 몸으로 느껴요.- Strategy — 행동(알고리즘)을 통째로 갈아끼우는 법. 지난 Day 14 에서 만든 알림 채널을 그대로 전략으로 써요.
- 람다로 전략을 더 가볍게 — 단일 메서드 인터페이스의 힘.
- Observer — 한 사건에 여러 곳이 각자 반응하게 하는 법 (발행과 구독의 분리).
- Strategy 와 Observer 가 어떻게 다른지, 언제 무엇을 꺼낼지 가늠하는 눈.
오늘의 로드맵
- Step 1 — 같은 일, 다른 방식:
if-else가 늘어나는 고통. - Step 2 — 행동을 통째로 갈아끼우는 Strategy.
- Step 3 — 사건이 나면 여러 곳이 반응: 일일이 부르는 고통.
- Step 4 — 구독하고 통지받는 Observer.
- Step 5 — 두 패턴 한자리 비교, 그리고 다음 패턴으로 잇기.
Step 1: 같은 일, 다른 방식 — if-else 가 늘어나는 고통
인스타그램에서 알림을 보낸다고 해봐요. 그런데 사용자마다 받고 싶은 방식이 달라요. 누구는 이메일로, 누구는 휴대폰 푸시로, 누구는 문자로 받고 싶어 하죠. 가장 먼저 떠오르는 방법은 "방식을 보고 갈라서 보내기" 예요.
String sendNotification(String type, String message) {
if (type.equals("EMAIL")) {
return "[Email] " + message;
} else if (type.equals("PUSH")) {
return "[Push] " + message;
} else if (type.equals("SMS")) {
return "[SMS] " + message;
}
return message;
}
처음엔 깔끔해 보여요. 그런데 문제는 알림 방식이 앞으로도 계속 늘어난다 는 거예요. 슬랙으로도 보내고 싶고, 카카오톡으로도 보내고 싶어지죠. 그럴 때마다 이 메서드를 열어서 else if 한 줄씩 끼워 넣어야 해요.
sendNotification(type, message) 안에서…
if (type == EMAIL) → 이메일로 보내기
else if (type == PUSH) → 푸시로 보내기
else if (type == SMS) → 문자로 보내기
else if (type == SLACK) → ??? (새 방식이 생기면 또 여기에 한 줄)
else if (type == ...) → ??? (그리고 또, 또…)
이게 왜 문제일까요? Day 14 에서 배운 OCP, 기억하시죠. "기능을 확장할 때는 새 코드를 더하는 쪽이 좋지, 멀쩡히 돌던 기존 코드를 건드리는 건 위험하다" 는 원칙이요. 그런데 지금 이 sendNotification 은 방식 하나 늘 때마다 매번 열어서 고쳐야 해요. 잘 돌던 이메일·푸시 코드 사이에 손을 집어넣는 셈이라, 실수로 옆줄을 건드릴 위험도 같이 커지죠.
그때 Day 14 에서 우리는 이미 답을 한 번 봤어요. 바로 NotificationChannel 이라는 인터페이스였죠. "알림을 보낼 수 있다" 는 역할만 약속으로 정해두고, 이메일·푸시·문자는 각자 그 약속을 지키는 클래스로 따로 떼어냈어요.
// com/instagram/javabasic/design/ocp/NotificationChannel.java
public interface NotificationChannel {
// 메시지를 채널 형식으로 보내고, 보낸 결과 문구를 돌려줘요.
String send(String message);
}
이 인터페이스를 지키는 채널들도 이미 만들어뒀고요. 각자 자기 방식대로 send 만 다르게 구현했죠.
// com/instagram/javabasic/design/ocp/EmailChannel.java
public class EmailChannel implements NotificationChannel {
@Override
public String send(String message) {
return "[Email] " + message;
}
}
PushChannel, SmsChannel 도 똑같은 모양이에요. 각자 "[Push] ", "[SMS] " 만 다르게요. 그때는 이 채널들을 한꺼번에 여러 개에 뿌리는 걸 봤어요. 오늘은 여기서 한 발 더 나아가요. 한 발송기가 상황에 따라 채널 하나를 골라 쓰고, 보내던 도중에도 다른 채널로 갈아끼우는 모습을 볼 거예요. 그게 바로 Strategy 패턴이에요.
Step 2: 행동을 통째로 갈아끼우는 Strategy
Strategy(전략) 패턴의 아이디어는 한 문장이에요. "바뀌는 행동을 객체로 뽑아내서, 통째로 갈아끼울 수 있게 하자." 여기서 "바뀌는 행동" 이 바로 알림을 보내는 방식이에요. 이메일로 보낼지 푸시로 보낼지 — 이 행동 하나하나가 곧 하나의 "전략" 이죠.
우리는 이미 전략의 약속(NotificationChannel)도, 전략들(EmailChannel·PushChannel…)도 가지고 있어요. 이제 필요한 건 그 전략을 품고 있다가, 보낼 때 그 전략에게 맡기는 발송기 하나예요. 이걸 NotificationSender 라고 부를게요.
// com/instagram/javabasic/design/behavioral/strategy/NotificationSender.java
public class NotificationSender {
// 지금 사용 중인 전략(알림 채널)이에요. 런타임에 다른 채널로 바꿀 수 있어요.
private NotificationChannel channel;
// 처음 만들 때 어떤 전략으로 시작할지 외부에서 받아요.
public NotificationSender(NotificationChannel channel) {
this.channel = channel;
}
// 보내던 도중에도 전략(채널)을 새것으로 갈아끼워요.
public void changeChannel(NotificationChannel channel) {
this.channel = channel;
}
// 실제로 보낼 때는 직접 하지 않고, 지금 품고 있는 전략에게 맡겨요.
public String notify(String message) {
return channel.send(message);
}
}
핵심은 notify 메서드예요. 자기가 직접 이메일을 보내거나 푸시를 보내지 않아요. 그냥 지금 품고 있는 전략의 send 를 부를 뿐이에요. 그래서 어떤 전략을 품느냐에 따라 똑같은 notify 호출이 완전히 다르게 동작해요.
NotificationSender sender = new NotificationSender(new EmailChannel());
sender.notify("새 댓글"); // "[Email] 새 댓글"
sender.changeChannel(new PushChannel());
sender.notify("새 댓글"); // "[Push] 새 댓글"
같은 sender 에게 같은 notify("새 댓글") 을 시켰는데, 중간에 전략을 갈아끼웠더니 결과가 달라졌어요. 발송기 자체는 한 줄도 안 바꿨는데 말이죠.
new NotificationSender( EmailChannel )
│ notify("새 댓글")
▼
품은 전략에게 맡김 → "[Email] 새 댓글"
changeChannel( PushChannel ) ← 전략만 통째로 교체
│ notify("새 댓글")
▼
품은 전략에게 맡김 → "[Push] 새 댓글"
Step 1 의 if-else 와 비교해봐요. 거기선 새 방식이 생기면 sendNotification 을 열어 고쳐야 했어요. 여기선 새 채널 클래스를 하나 만들어서 changeChannel 으로 끼워주기만 하면 돼요. NotificationSender 는 영원히 그대로예요. 이게 바로 Day 14 에서 봤던 "확장에는 열리고, 수정에는 닫힌" 그 모습이에요.
💡 Strategy 의 핵심은 상속이 아니라 조합 이에요. "이메일 발송기", "푸시 발송기" 처럼 발송기를 종류별로 상속받아 만드는 게 아니라, 발송기는 하나만 두고 그 안에 전략을 넣었다 뺐다 하는 거죠. 그래서 프로그램이 돌아가는 도중에도 행동을 바꿀 수 있어요.
여기서 작은 보너스가 하나 있어요. NotificationChannel 을 다시 보면 메서드가 send 딱 하나 뿐이에요. 이렇게 추상 메서드가 하나뿐인 인터페이스는, Day 25 에서 배운 람다 로도 바로 만들 수 있어요. 굳이 클래스 파일을 새로 만들지 않고도 전략을 그 자리에서 적어줄 수 있는 거죠.
NotificationSender sender = new NotificationSender(msg -> "[Slack] " + msg);
sender.notify("hi"); // "[Slack] hi"
msg -> "[Slack] " + msg 한 줄이 곧 하나의 전략이에요. 슬랙 채널 클래스를 따로 만들지 않았는데도 발송기는 아무 불평 없이 받아줘요. 전략이 "약속(인터페이스)만 지키면 된다" 는 사실이, 람다와 만나면 이렇게 가벼워져요.
🙋 학생 질문 — "전략을 갈아끼운다는 게, 그냥 자식 클래스에서 메서드를 오버라이드하는 거랑 뭐가 다른가요?"
아주 좋은 질문이에요. 둘 다 "동작을 다르게 한다" 는 점은 같아요. 차이는 언제, 얼마나 자유롭게 바꿀 수 있느냐 예요.
상속으로 오버라이드하면, 그 동작은 클래스를 만들 때 이미 정해져요. EmailSender 객체는 영원히 이메일만 보내요. 푸시로 바꾸려면 아예 다른 객체(PushSender)를 새로 만들어야 하죠.
Strategy 는 달라요. 발송기는 하나뿐인데, 그 안의 전략을 프로그램이 돌아가는 도중에 바꿀 수 있어요. 방금 본 changeChannel 처럼요. 사용자가 설정에서 "이제 푸시로 받을래" 를 누르면, 그 순간 전략만 갈아끼우면 끝이에요. "상속은 한 번 정하면 굳고, 조합은 언제든 바꾼다" — 이렇게 기억해두면 편해요.
Step 3: 사건이 나면 여러 곳이 반응 — 일일이 부르는 고통
이번엔 다른 상황이에요. 누군가 내 게시물에 좋아요 를 눌렀어요. 그러면 한 가지 일만 일어나는 게 아니에요. 여러 일이 동시에 벌어져야 하죠.
- 게시물 주인에게 "OO님이 좋아합니다" 알림 을 보내야 하고,
- 게시물의 좋아요 수 를 1 올려야 하고,
- "누가 언제 좋아요를 눌렀다" 는 활동 기록 도 남겨야 해요.
가장 먼저 떠오르는 방법은, 좋아요를 처리하는 like 메서드 안에서 이 셋을 직접 다 부르는 거예요.
좋아요가 눌리면 like() 안에서 직접…
like(user) {
notificationService.send(...) ← 알림 보내기
likeCounter.increment() ← 좋아요 수 올리기
activityLog.add(...) ← 활동 기록 남기기
}
→ 좋아요 났을 때 할 일이 늘 때마다 like() 를 또 열어 고쳐야 해요.
문제가 보이시나요? like 메서드가 좋아요 이후에 일어날 모든 일 을 다 알고 있어야 해요. 알림 서비스도 알아야 하고, 카운터도 알아야 하고, 로그도 알아야 하죠. 나중에 "좋아요를 누르면 추천 점수도 올리자" 가 추가되면? 또 like 를 열어서 한 줄 더 끼워야 해요. Step 1 의 if-else 와 똑같은 고통이 다른 옷을 입고 돌아온 거예요.
게다가 이건 방향이 좀 이상해요. 게시물은 그냥 "나 좋아요 받았어!" 하고 사건을 알리기만 하면 충분한데, 그 사건에 누가 어떻게 반응할지까지 게시물이 일일이 챙기고 있으니까요. 알리는 쪽과 반응하는 쪽이 너무 딱 붙어 있는 거죠.
여기서 발상을 뒤집어봐요. 게시물은 그냥 "좋아요 났다!" 고 방송만 하고, 거기에 관심 있는 쪽들이 알아서 듣고 반응 하게 하면 어떨까요? 그게 Observer 패턴이에요.
Step 4: 구독하고 통지받는 Observer
Observer(관찰자) 패턴은 유튜브 구독을 떠올리면 쉬워요. 채널이 새 영상을 올리면, 채널이 구독자 한 명 한 명에게 전화를 거는 게 아니에요. 그냥 영상을 올리면, 구독해둔 사람들에게 알림이 자동으로 가죠. 채널은 누가 구독했는지 정도만 알지, 그 사람들이 알림을 받고 무엇을 하는지(보든 말든)는 신경 쓰지 않아요.
여기서 등장인물은 둘이에요. 사건을 알리는 쪽(게시물)을 주체(Subject), 그 사건을 듣고 반응하는 쪽을 관찰자(Observer) 라고 불러요. 먼저 관찰자가 지켜야 할 약속부터 정해요.
// com/instagram/javabasic/design/behavioral/observer/PostObserver.java
public interface PostObserver {
// 게시물에 좋아요가 눌리면 호출돼요.
// postId 는 어떤 게시물인지, likedBy 는 누가 눌렀는지예요.
void onLiked(String postId, String likedBy);
}
"좋아요 사건을 구독하고 싶으면, onLiked 를 할 줄 알아야 한다" 는 약속이에요. 무엇을 할지는 각 관찰자가 알아서 정해요. 이제 사건을 알리는 주체, 게시물을 만들어요.
// com/instagram/javabasic/design/behavioral/observer/LikeablePost.java
public class LikeablePost {
private final String postId;
// 이 게시물의 좋아요 사건을 구독 중인 관찰자들이에요.
private final List<PostObserver> observers = new ArrayList<>();
public LikeablePost(String postId) {
this.postId = postId;
}
// 관찰자를 구독 목록에 등록해요.
public void subscribe(PostObserver o) {
observers.add(o);
}
// 관찰자를 구독 목록에서 빼요. 그 뒤로는 더 이상 통지받지 않아요.
public void unsubscribe(PostObserver o) {
observers.remove(o);
}
// 좋아요가 눌렸어요. 등록된 모든 관찰자에게 한 명씩 알려줘요.
public void like(String likedBy) {
for (PostObserver o : observers) {
o.onLiked(postId, likedBy);
}
}
}
여기서 가장 중요한 건 like 메서드예요. Step 3 에서는 like 안에서 알림·카운터·로그를 직접 불렀죠. 여기선 그런 게 하나도 없어요. 그냥 구독자 목록을 돌면서 "좋아요 났어요" 라고 알려줄 뿐이에요. 구독자가 그걸 받고 무슨 일을 하는지는 LikeablePost 가 전혀 몰라요. 그래서 관찰자가 몇 개로 늘어나든 이 클래스는 그대로예요.
이제 그 사건에 반응할 관찰자들을 만들어요. 셋 다 같은 onLiked 를 받지만, 각자 하는 일은 완전히 달라요. 먼저 좋아요 수를 세는 관찰자예요.
// com/instagram/javabasic/design/behavioral/observer/LikeCountObserver.java
public class LikeCountObserver implements PostObserver {
private int count;
@Override
public void onLiked(String postId, String likedBy) {
count++;
}
public int getCount() {
return count;
}
}
다음은 알림 문구를 만드는 관찰자고요.
// com/instagram/javabasic/design/behavioral/observer/NotificationObserver.java
public class NotificationObserver implements PostObserver {
private String lastMessage;
@Override
public void onLiked(String postId, String likedBy) {
lastMessage = likedBy + "님이 회원님의 게시물을 좋아합니다";
}
public String getLastMessage() {
return lastMessage;
}
}
활동 기록을 남기는 ActivityLogObserver 도 같은 모양이에요. onLiked 안에서 기록을 한 줄 쌓죠. 이제 이들을 게시물에 구독시키고, 좋아요를 한 번 눌러봐요.
LikeablePost post = new LikeablePost("post-1");
post.subscribe(new LikeCountObserver());
post.subscribe(new NotificationObserver());
post.subscribe(new ActivityLogObserver());
post.like("minji"); // 사건 한 번 → 구독자 셋이 각자 반응
post.like("minji") 라는 한 번의 사건 으로, 구독자 셋이 동시에 깨어나 각자 할 일을 해요. 카운터는 숫자를 올리고, 알림 관찰자는 "minji님이 회원님의 게시물을 좋아합니다" 문구를 만들고, 로그 관찰자는 기록을 남기죠.
LikeablePost ("post-1")
│ like("minji") ← 사건 한 번
▼
구독자 전원에게 onLiked 통지
├─→ LikeCountObserver → 좋아요 수 +1
├─→ NotificationObserver → "minji님이…" 문구 생성
└─→ ActivityLogObserver → 기록 한 줄 추가
만약 "추천 점수도 올리자" 가 추가되면? like 메서드는 손도 안 대요. RecommendObserver 를 하나 만들어서 subscribe 해주면 끝이에요. 반대로 어떤 반응이 더 이상 필요 없어지면 unsubscribe 로 빼면 되고요. 빼고 나면 그 관찰자는 더 이상 통지받지 않아요.
💡 Observer 의 핵심은 알리는 쪽과 반응하는 쪽을 떼어놓는 것 이에요. 게시물은 "사건이 났다" 만 방송하고, 무엇을 할지는 구독자들이 각자 정해요. 그래서 한쪽을 고쳐도 다른 쪽이 흔들리지 않죠. "방송하는 사람은 듣는 사람이 뭘 하는지 모른다" — 이게 느슨한 연결의 핵심이에요.
🙋 학생 질문 — "이거 어디서 많이 보던 구조 같아요. 버튼 누르면 뭔가 실행되는 거랑 비슷한가요?"
정확해요! 화면에서 버튼을 만들 때 "이 버튼이 눌리면 이걸 해줘" 하고 등록해두는 걸 본 적 있을 거예요. 그게 바로 Observer 예요. 버튼은 "나 눌렸어!" 사건만 방송하고, 거기에 등록해둔 동작이 실행되죠. 버튼은 자기가 눌렸을 때 무슨 일이 일어날지 미리 알지 못해요.
사실 Observer 는 우리 주변 소프트웨어 곳곳에 숨어 있어요. 알림 시스템, 채팅의 메시지 도착, 주식 가격이 바뀌면 화면이 갱신되는 것까지 — 전부 "사건이 나면 구독자들이 반응한다" 는 같은 뼈대예요. 오늘 이 작은 LikeablePost 를 이해했다면, 그 큰 시스템들의 속도 같은 원리로 돌아간다는 걸 알게 된 거예요.
Step 5: 두 패턴 한자리 비교, 그리고 다음 패턴으로 잇기
오늘 만난 두 패턴은 둘 다 "행동을 유연하게" 다뤘지만, 푸는 불편이 서로 달라요. 한자리에 모아봐요.
| 구분 | Strategy | Observer |
|---|---|---|
| 한 줄 정체 | 행동 하나를 골라 통째로 교체 | 한 사건에 여러 구독자가 각자 반응 |
| 관계 | 발송기 1 : 전략 1 (그때그때 하나) | 게시물 1 : 구독자 N (동시에 여럿) |
| 핵심 동작 | changeChannel 으로 갈아끼움 |
subscribe 해두면 통지받음 |
| 푸는 불편 | if-else 로 동작 고르는 분기 |
사건 후속 처리를 한 곳이 다 떠안음 |
| 오늘 만든 예 | NotificationSender |
LikeablePost + 관찰자들 |
재미있는 건, 둘이 닮은 구석도 있다는 거예요. 둘 다 "무엇을 할지" 를 인터페이스로 떼어냈어요. Strategy 는 NotificationChannel 로, Observer 는 PostObserver 로요. 그래서 둘 다 새 동작이나 새 구독자를 추가해도 기존 코드를 건드리지 않아요. Day 14 에서 배운 OCP 가 두 패턴 모두의 바탕에 깔려 있는 거죠.
인터페이스로 약속만 정해두면, 그 뒤는 얼마든지 유연해진다는 걸 오늘 두 번 확인한 셈이에요.
차이는 방향과 개수 예요. Strategy 는 여러 전략 중 하나 를 골라 쓰다가 갈아끼우는 거고(교체), Observer 는 한 사건에 여럿 이 동시에 반응하는 거예요(통지). "골라 끼우면 Strategy, 모여서 반응하면 Observer" 로 기억해두면 헷갈리지 않아요.
⚠️ 여기서도 Day 40 의 교훈은 그대로예요. 패턴은 불편이 실제로 있을 때 꺼내는 연장이에요. 동작이 한 가지뿐이라면 굳이 Strategy 로 뽑을 필요가 없고, 사건에 반응하는 곳이 하나뿐이라면 그냥 직접 부르는 게 더 읽기 쉬워요. "분기가 자꾸 늘어나나?", "한 사건에 반응할 곳이 여럿인가?" — 그 불편이 보일 때, 그때가 패턴을 꺼낼 때예요.
마무리
오늘은 객체의 행동을 유연하게 다루는 두 가지 패턴을 배웠어요.
- Strategy — 바뀌는 행동을 객체로 뽑아, 통째로 갈아끼우는 패턴. Day 14 의
NotificationChannel을 전략으로 재활용했고, 단일 메서드 인터페이스라 람다로도 가볍게 만들 수 있었죠. - Observer — 한 사건을 여러 구독자가 각자 듣고 반응하는 패턴. 알리는 쪽(
LikeablePost)과 반응하는 쪽(관찰자들)을 떼어놓아, 반응이 늘어도 사건을 알리는 코드는 그대로였어요.
그리고 두 패턴 모두 인터페이스로 "무엇을 할지" 를 분리해, 확장에는 열리고 수정에는 닫힌 구조를 만든다는 것도요.
다음 시간(Day 42)엔 행동 패턴 가족을 마저 만나봐요. 이미 만들어진 기능에 새 기능을 덧입히는 Decorator, 큰 흐름의 골격은 고정해두고 일부 단계만 갈아끼우는 Template Method — 이건 사실 Day 12 에서 Content 의 render() 로 한 번 맛본 패턴이에요 — 그리고 우리가 매일 쓰는 for-each 의 비밀을 푸는 Iterator 와 Iterable 까지요.
오늘 "행동을 분리한다" 는 감각을 잡았다면, 다음 패턴들도 한결 수월할 거예요. 수고 많으셨어요!
과제
오늘 배운 두 패턴을 직접 손으로 만들어볼 시간이에요. 셋 다 인스타그램에서 실제로 일어날 법한 상황이에요. 막히면 본문 코드를 참고하되, 먼저 "이 불편을 어떤 패턴이 풀어주지?" 를 떠올려보세요.
과제 1 (기초): 피드 정렬 전략을 Strategy 로 갈아끼우기
상황 배경: 인스타그램 피드를 보여줄 때, 정렬 방식이 여러 가지예요. 최신순으로 볼 수도 있고, 좋아요가 많은 인기순으로 볼 수도 있죠. 사용자가 정렬 버튼을 누르면 그때그때 방식을 바꿔야 해요. Step 2 의 NotificationSender 를 참고하세요.
🎯 해결 미션:
FeedSortStrategy라는 인터페이스를 만들고, 게시물 목록을 받아 정렬된 목록을 돌려주는 메서드 하나(예:List<Post> sort(List<Post> posts))를 약속으로 두세요.- 이 약속을 지키는 전략 둘을 만드세요.
LatestSortStrategy(최신순)와PopularSortStrategy(좋아요 많은 순)요. FeedSorter라는 클래스를 만들어, 전략을 품고 있다가sort(...)가 불리면 품은 전략에게 맡기게 하세요. 그리고 전략을 갈아끼우는 메서드도 두세요.- 최신순으로 한 번 정렬해보고, 전략을 인기순으로 갈아끼운 뒤 다시 정렬해, 결과 순서가 달라지는지 확인하세요.
과제 2 (응용): 팔로우 사건을 Observer 로 퍼뜨리기
상황 배경: 누군가 나를 팔로우하면, 좋아요 때처럼 여러 일이 한꺼번에 일어나야 해요. 나에게 "OO님이 팔로우하기 시작했습니다" 알림이 가야 하고, 내 팔로워 수가 1 올라가야 하죠. Step 4 의 LikeablePost 와 관찰자들을 참고하세요.
🎯 해결 미션:
- 팔로우 사건을 구독하는 관찰자 약속
FollowObserver인터페이스를 만들고,onFollowed(String followedBy)메서드를 두세요. - 사건을 알리는 주체
FollowablePost대신FollowableUser클래스를 만들어,subscribe·unsubscribe·follow(String followedBy)를 두세요.follow는 구독자 전원에게 통지해야 해요. - 관찰자 둘을 만드세요. 팔로워 수를 세는
FollowerCountObserver와, 알림 문구를 만드는FollowNotificationObserver요. - 한 명을 팔로우시켜보고, 두 관찰자가 각각 제대로 반응했는지(수가 올랐는지, 문구가 생겼는지) 확인하세요. 그다음 관찰자 하나를
unsubscribe하고 다시 팔로우시켜, 뺀 관찰자는 더 이상 반응하지 않는지도 확인해보세요.
과제 3 (심화): 람다 전략으로 새 채널 추가 + if-else 와 비교
상황 배경: 본문에서 NotificationSender 에 슬랙 채널을 람다로 추가해봤죠. 이번엔 직접 새 채널 두 개를 추가해보고, 만약 Step 1 의 if-else 방식이었다면 같은 일을 하는 데 코드를 몇 군데 고쳐야 했을지 비교해볼 거예요. Step 2 의 람다 전략 부분을 참고하세요.
🎯 해결 미션:
- 본문의
NotificationSender를 그대로 써서, 카카오톡 채널("[KaKao] " + msg)과 디스코드 채널("[Discord] " + msg)을 람다로 각각 만들어 끼워보세요.NotificationSender클래스는 한 줄도 고치지 않아야 해요. - 두 채널로 각각 같은 메시지를 보내, 앞에 붙는 이름표가 다르게 나오는지 확인하세요.
- 이제 상상해보세요. 만약 Step 1 의
if-else방식으로 같은 두 채널을 추가한다면, 어떤 메서드를 몇 번 열어 고쳐야 할까요? Strategy 방식에서는 기존 코드를 몇 줄 고쳤나요? 둘을 비교해 "왜 Strategy 가 확장에 강한지" 를 한두 줄로 적어보세요.
생각해볼 주제
혼자 고민해도 좋고, 동료와 토론해도 좋아요. 정답을 외우기보다 "나라면 어떻게 설명할까" 를 떠올리며 읽어보세요.
1. Strategy 는 "조합", 상속은 "고정" 이라는데, 왜 조합이 더 유연할까?
오늘 Strategy 를 보면, 발송기는 하나뿐인데 그 안의 전략을 도중에 갈아끼웠어요. 만약 상속으로 EmailSender·PushSender 를 따로 만들었다면 이렇게 도중에 바꾸진 못했을 거예요.
"한 객체가 살아 있는 동안 행동을 바꿀 수 있는가" 를 떠올려보세요. 상속으로 행동을 정하면 그 객체는 태어날 때 이미 행동이 굳어버려요. 반면 조합(전략을 품기)은 품은 것을 바꾸면 행동도 바뀌죠.
또 행동의 조합이 많아질 때도 생각해보세요. 채널이 3가지, 말투가 2가지라면 상속으로는 6가지 클래스가 필요하지만, 조합이라면 3 + 2 만 만들어 섞으면 돼요. "상속보다 조합을 우선하라" 는 오래된 설계 조언이 왜 나왔는지, 오늘 코드와 연결해 정리해보세요.
2. Observer 에서 구독자가 아주 많아지면, 어떤 점을 조심해야 할까?
오늘은 구독자가 셋뿐이라 편하게 통지했어요. 그런데 인기 게시물이라 구독자가 수천 개라면? 혹은 한 구독자가 통지를 받고 아주 느린 작업을 한다면 어떻게 될까요?
"통지가 한 명씩 차례로 돌아간다" 는 점을 떠올려보세요. like 안의 반복문은 구독자를 하나씩 부르고 그게 끝나야 다음으로 넘어가요. 그러면 느린 구독자 하나가 전체를 붙잡을 수 있죠.
또 하나, 구독만 하고 unsubscribe 를 깜빡하면 어떻게 될까요? 주체가 그 관찰자를 계속 목록에 쥐고 있어서, 더 이상 필요 없는 객체가 메모리에 남는 일도 생겨요. "편리하게 여럿에게 알린다" 는 장점의 뒷면에 어떤 비용이 숨어 있는지 같이 저울에 올려보세요.
3. Strategy 도 Observer 도 결국 "인터페이스로 분리" 였는데, 왜 인터페이스가 이렇게 자주 나올까?
오늘 두 패턴 모두 인터페이스(NotificationChannel, PostObserver)로 "무엇을 할지" 를 떼어냈어요. 사실 Day 13 부터 인터페이스는 계속 등장했죠. 왜 좋은 설계 이야기에는 늘 인터페이스가 따라올까요?
"두 코드가 서로 얼마나 알아야 하는가" 를 떠올려보세요. NotificationSender 는 EmailChannel 이라는 구체적인 이름을 몰라요. 그저 NotificationChannel 이라는 약속만 알죠. LikeablePost 도 LikeCountObserver 를 몰라요. PostObserver 만 알아요.
이렇게 구체적인 상대 대신 약속 만 알고 지내면, 상대가 바뀌거나 새로 생겨도 내가 흔들리지 않아요. 인터페이스가 좋은 설계의 단골인 이유를, "서로 덜 알수록 덜 흔들린다" 는 말과 연결해 정리해보세요.
✅ 예시 답안정답 보기
아래는 과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니니, 내 코드와 흐름을 비교하며 "왜 이렇게 했는지" 를 짚어보세요.
과제 예시답안
과제 1 예시답안 — 피드 정렬 전략을 Strategy 로 갈아끼우기
핵심 접근
피드를 최신순으로 볼지 인기순으로 볼지는 "바뀌는 행동" 이에요. 이걸 if (mode.equals("LATEST")) ... else ... 로 가르면, 정렬 방식이 늘 때마다 그 분기를 또 열어 고쳐야 해요. 그래서 Step 2 의 NotificationSender 처럼, 정렬 방식 하나하나를 전략 객체로 뽑아내고, 정렬기는 그 전략을 품고 있다가 통째로 갈아끼우게 만들어요.
예시 구현
먼저 "정렬할 줄 안다" 는 약속을 인터페이스로 정해요. 받은 목록은 건드리지 않고 새 목록을 돌려주는 게 약속이에요.
// com/instagram/javabasic/design/behavioral/solution/day41/FeedSortStrategy.java
public interface FeedSortStrategy {
// 게시물 목록을 받아, 정렬된 새 목록을 돌려줘요. (원본은 보존)
List<Post> sort(List<Post> posts);
}
이 약속을 지키는 전략 둘을 만들어요. 최신순은 복사본을 뒤집고, 인기순은 좋아요 수로 내림차순 정렬해요.
// LatestSortStrategy — 나중에 올라온 게시물이 맨 앞으로
public List<Post> sort(List<Post> posts) {
List<Post> result = new ArrayList<>(posts); // 원본 보존용 복사본
Collections.reverse(result);
return result;
}
// PopularSortStrategy — 좋아요 많은 게시물이 맨 앞으로
public List<Post> sort(List<Post> posts) {
return posts.stream()
.sorted(Comparator.comparingInt(Post::getLikeCount).reversed())
.toList();
}
정렬기는 전략을 품고, 정작 정렬은 품은 전략에게 맡겨요. changeStrategy 한 줄로 방식을 갈아끼우죠.
// com/instagram/javabasic/design/behavioral/solution/day41/FeedSorter.java
public class FeedSorter {
private FeedSortStrategy strategy;
public FeedSorter(FeedSortStrategy strategy) {
this.strategy = strategy;
}
public void changeStrategy(FeedSortStrategy strategy) {
this.strategy = strategy;
}
public List<Post> sort(List<Post> posts) {
return strategy.sort(posts); // 직접 안 하고 전략에 맡김
}
}
최신순으로 정렬해보고, changeStrategy(new PopularSortStrategy()) 로 갈아끼운 뒤 다시 정렬하면 순서가 달라져요. FeedSorter 자체는 한 줄도 안 바꿨는데도요.
채점 포인트
- 정렬 방식을
FeedSortStrategy인터페이스로 약속했는가 - 최신순·인기순을 각각 별도 전략 클래스로 분리했는가
FeedSorter가 구체 전략 이름을 모르고, 품은 전략에 위임하는가- 전략을 갈아끼우는 메서드로 런타임에 정렬 방식을 바꿔봤는가
흔한 실수
- 정렬할 때
Collections.sort(posts)로 원본 리스트를 직접 뒤섞는 경우. 원본을 보던 다른 코드가 영향을 받아요. 복사본을 만들거나stream().sorted().toList()로 새 목록을 돌려주는 게 안전해요. FeedSorter안에서if (전략이 인기순이면)식으로 또 분기하는 경우. 그러면 전략을 뽑아낸 의미가 사라져요. 정렬기는 전략의 종류를 몰라야 하고, 그냥strategy.sort(...)만 불러야 해요.
과제 2 예시답안 — 팔로우 사건을 Observer 로 퍼뜨리기
핵심 접근
팔로우가 일어나면 알림도 보내고 팔로워 수도 올려야 해요. 이걸 follow 메서드 안에서 직접 다 부르면, 후속 작업이 늘 때마다 follow 를 고쳐야 하죠. Step 4 의 LikeablePost 처럼, 사용자는 "팔로우 났다!" 만 방송하고, 거기에 반응할 관찰자들이 알아서 듣게 만들어요.
예시 구현
먼저 팔로우 사건을 구독하는 약속과, 사건을 알리는 주체를 만들어요.
// FollowObserver.java
public interface FollowObserver {
// 팔로우가 일어나면 호출돼요. followedBy 는 팔로우를 누른 사람 이름이에요.
void onFollowed(String followedBy);
}
// com/instagram/javabasic/design/behavioral/solution/day41/FollowableUser.java
public class FollowableUser {
private final List<FollowObserver> observers = new ArrayList<>();
public void subscribe(FollowObserver observer) {
observers.add(observer);
}
public void unsubscribe(FollowObserver observer) {
observers.remove(observer);
}
// 누군가 팔로우했어요. 등록된 모든 관찰자에게 한 명씩 알려줘요.
public void follow(String followedBy) {
for (FollowObserver observer : observers) {
observer.onFollowed(followedBy);
}
}
}
관찰자 둘은 같은 사건을 받지만 하는 일이 달라요. 하나는 수를 세고, 하나는 문구를 만들어요.
// FollowerCountObserver — 팔로우 횟수만 셈
public void onFollowed(String followedBy) {
count++;
}
// FollowNotificationObserver — 알림 문구를 만들어 둠
public void onFollowed(String followedBy) {
lastMessage = followedBy + "님이 회원님을 팔로우하기 시작했습니다";
}
둘을 subscribe 한 뒤 follow("jaehoon") 을 부르면, 수가 1 오르고 문구도 만들어져요. 그다음 한 관찰자를 unsubscribe 하고 다시 팔로우시키면, 뺀 관찰자는 더 이상 반응하지 않아요.
채점 포인트
- 팔로우 사건을
FollowObserver인터페이스로 약속했는가 FollowableUser가subscribe·unsubscribe·follow를 갖추고,follow가 구독자 전원에 통지하는가- 두 관찰자가 같은
onFollowed를 받아 각자 다르게 반응하는가 unsubscribe후 그 관찰자가 통지받지 않는 걸 확인했는가
흔한 실수
follow안에서FollowerCountObserver를 직접 이름으로 부르는 경우. 그러면 주체가 구체 관찰자에 묶여서, Observer 로 분리한 의미가 없어져요. 반드시FollowObserver약속을 통해서만 통지해야 새 관찰자를 자유롭게 더할 수 있어요.- 구독만 해두고 필요 없어진 관찰자를
unsubscribe하지 않는 경우. 주체가 그 관찰자를 계속 목록에 쥐고 있어서, 더 이상 필요 없는 객체가 메모리에 남게 돼요.
과제 3 예시답안 — 람다 전략으로 새 채널 추가 + if-else 와 비교
핵심 접근
NotificationChannel 은 메서드가 send 하나뿐이라, 굳이 클래스를 새로 만들지 않고 람다로 그 자리에서 전략을 적어줄 수 있어요. NotificationSender 는 약속(인터페이스)만 알고 있으니, 람다든 클래스든 가리지 않고 받아줘요.
예시 구현
// 카카오톡 채널 — 람다 한 줄이 곧 전략
NotificationSender sender = new NotificationSender(msg -> "[KaKao] " + msg);
sender.notify("새 좋아요"); // "[KaKao] 새 좋아요"
// 디스코드 채널로 갈아끼우기 — 역시 람다 한 줄
sender.changeChannel(msg -> "[Discord] " + msg);
sender.notify("새 좋아요"); // "[Discord] 새 좋아요"
두 채널을 추가하는 동안 NotificationSender 클래스는 한 글자도 안 바꿨어요. 새 동작을 더하기만 했지, 기존 코드는 건드리지 않은 거죠.
채점 포인트
- 카카오·디스코드 채널을 람다로 만들어 끼웠는가
NotificationSender를 한 줄도 고치지 않았는가if-else방식과 비교해 "왜 Strategy 가 확장에 강한지" 를 자기 말로 정리했는가
흔한 실수
- 새 채널마다 클래스 파일을 꼭 만들어야 한다고 생각하는 경우. 메서드가 하나뿐인 인터페이스는 람다로 충분해요. 물론 채널 로직이 복잡하거나 재사용이 많으면 그땐 정식 클래스가 더 나아요 — 한 줄로 끝나는 가벼운 전략일 때 람다가 빛나요.
- 비교 정리의 핵심:
if-else방식이라면 채널 두 개를 더할 때sendNotification메서드를 열어서else if두 줄을 끼워야 해요(기존 코드 수정). Strategy 방식은 발송기 바깥에서 람다를 끼우기만 하면 돼서, 기존 코드를 건드리지 않아요(확장만). 이 차이가 곧 "수정에 닫히고 확장에 열린" 구조예요.
생각해볼 주제 예시답안
생각해볼 주제 1 예시답안 — Strategy 는 "조합", 상속은 "고정" 이라는데, 왜 조합이 더 유연할까?
[문제 상황 요약]
Strategy 는 발송기 하나에 전략을 넣었다 뺐다 했는데, 상속으로 EmailSender·PushSender 를 따로 만들었다면 그렇게 못 했어요. 왜 조합(전략을 품기)이 상속보다 유연한지에 대한 질문이에요.
[튜터의 가이드 및 해설]
두 가지 각도로 풀어볼게요.
첫째, 바꿀 수 있는 시점이 달라요. 상속으로 행동을 정하면, 그 객체는 태어날 때 행동이 굳어버려요. EmailSender 객체는 영원히 이메일만 보내요. 푸시로 바꾸려면 아예 다른 객체를 새로 만들어야 하죠. 반면 조합은 품은 전략을 바꾸면 행동도 바뀌어요. 사용자가 설정에서 "이제 푸시로 받을래" 를 누른 그 순간, 살아 있는 발송기의 전략만 갈아끼우면 끝이에요.
둘째, 조합이 늘어날 때 수가 폭발하지 않아요. 채널이 3가지(이메일·푸시·문자), 말투가 2가지(정중·친근)라고 해봐요. 상속으로 모든 조합을 만들면 3 × 2 = 6가지 클래스가 필요해요. 조합이라면 채널 3개와 말투 2개, 합쳐서 5개만 만들어 섞으면 되죠. 종류가 늘수록 이 차이는 더 벌어져요.
그래서 "상속보다 조합을 우선하라" 는 오래된 조언이 나온 거예요. 상속은 "이건 저것의 한 종류다" 가 분명할 때 좋고, "행동을 갈아끼우고 싶다" 가 핵심이면 조합이 답이에요.
🎯 면접관을 홀리는 핵심 멘트
"상속은 행동을 컴파일 시점에 고정하고, 조합은 런타임에 갈아끼울 수 있게 해요. Strategy 가 조합을 쓰는 이유가 그거예요 — 객체가 살아 있는 동안 행동을 바꿔야 하니까요. 게다가 행동의 축이 여럿이면 상속은 조합 수만큼 클래스가 폭발하지만, 조합은 축별로 더해서 섞으면 돼요. 그래서 저는 '~의 한 종류다' 가 명확할 때만 상속을 쓰고, 행동을 교체해야 하면 조합을 택합니다."
생각해볼 주제 2 예시답안 — Observer 에서 구독자가 아주 많아지면, 어떤 점을 조심해야 할까?
[문제 상황 요약]
오늘은 구독자가 셋뿐이라 편하게 통지했어요. 그런데 구독자가 수천 개거나, 한 구독자가 느린 작업을 한다면 어떤 문제가 생기는지에 대한 질문이에요.
[튜터의 가이드 및 해설]
세 가지를 짚어볼게요.
첫째, 통지가 한 명씩 차례로 돌아가요. like 안의 반복문은 구독자를 하나 부르고, 그게 끝나야 다음으로 넘어가요. 그래서 느린 구독자 하나가 있으면 그 뒤 구독자들이 전부 기다리게 돼요. 좋아요 한 번 누르는 게 느려지는 거죠. 반응이 오래 걸리는 작업이라면, 통지를 받자마자 바로 처리하지 말고 "나중에 처리할 일" 로 미뤄두는 식으로 떼어내는 걸 고민하게 돼요.
둘째, 구독 해지를 깜빡하면 메모리에 남아요. 주체는 구독자 목록을 쥐고 있어요. 더 이상 필요 없는 관찰자를 unsubscribe 하지 않으면, 주체가 그걸 계속 붙잡고 있어서 사라져야 할 객체가 메모리에 남아요. 구독했으면 필요 없어질 때 해지하는 습관이 중요해요.
셋째, 한 구독자가 터지면 어떻게 될까요. 반복문 도중 한 관찰자가 예외를 던지면, 그 뒤 관찰자들은 통지를 못 받을 수 있어요. 구독자가 많아질수록 "한 명의 실패가 전체를 막지 않게" 하는 안전장치도 생각하게 되죠.
정리하면, Observer 의 "여럿에게 한 번에 알린다" 는 편리함 뒤에는 순서·성능·생명주기라는 비용이 숨어 있어요. 구독자가 적을 땐 안 보이지만, 규모가 커지면 챙겨야 할 것들이에요.
🎯 면접관을 홀리는 핵심 멘트
"Observer 는 통지가 순차적이라, 느린 구독자 하나가 전체를 붙잡을 수 있어요. 그래서 무거운 반응은 비동기로 떼어내는 걸 고려해요. 또 구독자를
unsubscribe하지 않으면 주체가 계속 참조를 쥐고 있어 메모리 누수가 나고요. 한 구독자의 예외가 나머지 통지를 막지 않도록 격리하는 것도 규모가 커지면 중요해집니다. 편리함의 뒷면에 순서·생명주기·실패 격리 비용이 있다는 걸 같이 봐야 해요."
생각해볼 주제 3 예시답안 — Strategy 도 Observer 도 결국 "인터페이스로 분리" 였는데, 왜 인터페이스가 이렇게 자주 나올까?
[문제 상황 요약]
오늘 두 패턴 모두 인터페이스(NotificationChannel, PostObserver)로 "무엇을 할지" 를 떼어냈어요. 왜 좋은 설계 이야기에는 늘 인터페이스가 따라오는지에 대한 질문이에요.
[튜터의 가이드 및 해설]
핵심은 "두 코드가 서로 얼마나 알아야 하는가" 예요.
NotificationSender 를 다시 보면, EmailChannel 이라는 구체적인 이름을 몰라요. 그저 NotificationChannel 이라는 약속만 알죠. LikeablePost 도 LikeCountObserver 를 몰라요. PostObserver 만 알아요. 이렇게 구체적인 상대 대신 약속만 알고 지내면, 상대가 바뀌거나 새로 생겨도 내가 흔들리지 않아요. 새 채널 클래스가 생겨도 발송기는 그대로, 새 관찰자가 생겨도 게시물은 그대로인 게 그래서예요.
이걸 조금 더 멋진 말로 "구체적인 것이 아니라 약속(추상)에 기대라" 고 해요. 구체 클래스는 자주 바뀌고 새로 생기지만, 잘 정한 약속은 좀처럼 안 바뀌어요. 그래서 변하지 않는 약속에 기대면, 그 뒤에서 구체적인 것들이 아무리 바뀌어도 내 코드는 안전한 거죠.
Day 13 에서 인터페이스를 배운 뒤로 좋은 설계 이야기마다 인터페이스가 나오는 게 우연이 아니에요. "서로 덜 알수록 덜 흔들린다" — 이 한 문장이 인터페이스가 단골인 이유예요.
🎯 면접관을 홀리는 핵심 멘트
"Strategy 든 Observer 든, 한쪽이 다른 쪽의 구체 클래스가 아니라 인터페이스에만 기대게 만들어요. 구체 클래스는 자주 바뀌고 늘어나지만 잘 정한 약속은 안정적이니까요. 그래서 약속에 의존하면 그 뒤의 구현이 바뀌거나 추가돼도 내 코드는 안 흔들려요. 결국 좋은 설계의 공통분모는 '구체가 아니라 추상에 의존하라', 한 문장으로는 '서로 덜 알수록 덜 흔들린다' 입니다."