문서 읽는 데 51분 · day42

Day 42 — 구조와 행동 패턴: 감싸고, 골격을 나누고, 순회하기

목차 18
전체 48강 중 42강 · 자바 기초
난이도 · 입문

지난 시간 Day 41 에서는 객체의 행동을 유연하게 다루는 두 패턴을 배웠어요. 행동을 통째로 갈아끼우는 Strategy, 한 사건에 여러 곳이 반응하는 Observer 였죠. 둘 다 인터페이스로 "무엇을 할지" 를 떼어내서, 새 동작을 더해도 기존 코드를 건드리지 않았어요.

오늘은 그 흐름을 이어, 자주 쓰이는 패턴 셋을 더 만나봐요. 이미 만들어진 기능에 새 기능을 덧입히는 Decorator, 큰 흐름의 골격은 고정하고 일부 단계만 갈아끼우는 Template Method, 그리고 우리가 매일 쓰는 for-each 의 비밀을 푸는 Iterator 와 Iterable 이에요.

재미있는 건, 이 중 하나는 여러분이 이미 써봤다 는 거예요. 어디서 썼는지는 중간에 밝혀질 거예요. 오늘도 우리가 만들어둔 인스타그램 코드 위에서 패턴이 자라나요. 시작해봐요!

🎯 학습 목표

  • 기능을 상속으로만 덧붙이면 클래스가 어떻게 폭발하는지 — 그 불편을 먼저 느껴요.
  • Decorator — 같은 약속을 지키면서 기능을 한 겹씩 감싸 덧입히는 법. Day 41 의 알림 채널을 그대로 감싸요.
  • Template Method — 큰 흐름의 골격은 부모가 잠그고, 빈칸만 자식이 채우는 법. Day 12 에서 이미 써본 render() 가 사실 이 패턴이었어요.
  • Iterator 와 Iterablefor-each 가 속으로 무엇을 부르는지, 그래서 우리가 만든 컬렉션도 어떻게 for-each 로 돌릴 수 있는지.
  • 세 패턴이 각각 어떤 불편을 푸는지, 언제 무엇을 꺼낼지 가늠하는 눈.

오늘의 로드맵

  • Step 1 — 기능을 덧입히는 고통: 상속으로 풀면 클래스가 폭발한다.
  • Step 2 — 감싸서 덧입히는 Decorator.
  • Step 3 — "사실 여러분은 이미 써봤어요": Template Method 의 정체.
  • Step 4 — 골격은 고정, 빈칸만 교체: Template Method 직접 짜기.
  • Step 5for-each 의 비밀: Iterator 와 Iterable.
  • Step 6 — 내가 만든 컬렉션을 for-each 로 돌리기.
  • Step 7 — 세 패턴 한자리 비교, 그리고 JVM 속으로.

Step 1: 기능을 덧입히는 고통 — 상속으로 풀면 클래스가 폭발한다

Day 41 에서 알림 채널을 만들었던 걸 떠올려봐요. 이메일·푸시·문자 채널이 각자 NotificationChannel 약속을 지키며 메시지를 보냈죠. 그런데 운영을 하다 보면 욕심이 생겨요. "긴급한 알림 앞에는 🔔 이모지를 붙이고 싶다", "중요한 알림 뒤에는 (중요) 표시를 달고 싶다" 같은 거요.

이건 알림을 새 방식으로 보내는 게 아니에요. 보내던 결과에 한 겹을 덧입히는 일이죠. 가장 먼저 떠오르는 방법은 상속이에요. 이메일에 이모지를 붙이고 싶으니 EmailChannel 을 물려받아 EmojiEmailChannel 을 만드는 거죠.

문제는 조합이에요. 채널이 이메일·푸시·문자 셋이고, 덧입힐 기능이 이모지·중요표시 둘이라면, 그 조합을 상속으로 다 만들면 이렇게 돼요.

텍스트
 기본 채널:   Email · Push · SMS        (3가지)
 덧입힐 기능: 이모지 · 중요표시           (2가지)

 모든 조합을 상속으로 만들면:
   EmojiEmail    UrgentEmail    EmojiUrgentEmail
   EmojiPush     UrgentPush     EmojiUrgentPush
   EmojiSms      UrgentSms      EmojiUrgentSms
   ...
  덧입힐 기능이 하나만 더 생겨도, 만들어야 할 클래스가 또 배로 늘어나요

채널 하나, 기능 하나가 늘 때마다 곱셈으로 클래스가 불어나요. 게다가 "이모지 먼저, 그다음 중요표시" 와 "중요표시 먼저, 그다음 이모지" 처럼 순서까지 따지면 더 끔찍해지죠. 분명 더 나은 길이 있어야 해요.

우리가 가진 약속을 다시 봐요. 이게 출발점이에요.

Java
// com/instagram/javabasic/design/ocp/NotificationChannel.java
public interface NotificationChannel {

    // 메시지를 채널 형식으로 보내고, 보낸 결과 문구를 돌려줘요.
    String send(String message);
}

여기에 한 가지 아이디어를 더해봐요. 만약 "기능을 덧입히는 녀석" 자신도 NotificationChannel 인 척하면서, 안에 진짜 채널을 하나 품고 있다면 어떨까요? 그러면 그 녀석은 어디서든 채널처럼 쓰이면서, 품은 채널의 결과에 자기 한 겹을 덧붙일 수 있어요. 이게 바로 Decorator 패턴이에요.


Step 2: 감싸서 덧입히는 Decorator

Decorator(데코레이터) 패턴의 아이디어는 한 문장이에요. "같은 약속을 지키면서, 안에 같은 타입을 하나 품고, 그 결과에 한 겹을 덧입힌다." 여기서 "같은 약속" 은 NotificationChannel 이고, "품는 같은 타입" 도 NotificationChannel 이에요. 자기도 채널이면서, 또 다른 채널을 품는 거죠.

먼저 감싸기의 뼈대를 만들어요. 자기도 NotificationChannel 을 구현하면서, 안에 채널 하나를 품는 추상 클래스예요.

Java
// com/instagram/javabasic/design/structural/decorator/ChannelDecorator.java
public abstract class ChannelDecorator implements NotificationChannel {

    // 내가 감싸고 있는 안쪽 채널이에요. 한 번 정해지면 바뀌지 않아서 final 로 둬요.
    protected final NotificationChannel wrapped;

    // 만들 때 "무엇을 감쌀지" 를 외부에서 받아요.
    // 안쪽이 또 다른 데코레이터여도 상관없어서, 겹겹이 감쌀 수 있어요.
    protected ChannelDecorator(NotificationChannel wrapped) {
        this.wrapped = wrapped;
    }

    // send 본문은 일부러 비워 둬요 — 한 겹을 어떻게 덧입힐지는 자식이 정해요.
}

send 를 일부러 비워 추상으로 남긴 게 핵심이에요. "어떻게 감쌀지" 는 자식마다 다르니까요. 이제 구체적인 데코레이터 둘을 만들어봐요. 먼저 결과 앞에 알림 이모지를 붙이는 녀석이에요.

Java
// com/instagram/javabasic/design/structural/decorator/EmojiChannel.java
public class EmojiChannel extends ChannelDecorator {

    public EmojiChannel(NotificationChannel wrapped) {
        super(wrapped);
    }

    @Override
    public String send(String message) {
        // 먼저 안쪽 채널에게 보내게 시키고, 그 결과 앞에 이모지 한 겹을 덧입혀요.
        return "🔔 " + wrapped.send(message);
    }
}

send 안을 잘 봐요. 자기가 직접 "[Email]" 같은 걸 만들지 않아요. 먼저 wrapped.send(message)안쪽 채널에게 일을 시키고, 그 결과 앞에 "🔔 " 만 덧붙이죠. 결과 뒤에 (중요) 를 붙이는 녀석도 모양은 똑같아요. send 한 줄만 다르죠.

Java
// com/instagram/javabasic/design/structural/decorator/UrgentChannel.java
@Override
public String send(String message) {
    // 먼저 안쪽 채널에게 보내게 시키고, 그 결과 뒤에 "(중요)" 한 겹을 덧입혀요.
    return wrapped.send(message) + " (중요)";
}

이제 써봐요. 채널 하나를 데코레이터로 감싸기만 하면 돼요.

Java
NotificationChannel email = new EmailChannel();

new EmojiChannel(email).send("새 댓글");        // "🔔 [Email] 새 댓글"
new UrgentChannel(email).send("좋아요");        // "[Email] 좋아요 (중요)"

여기까진 한 겹이었어요. Decorator 의 진짜 매력은 데코레이터가 데코레이터를 감쌀 수 있다 는 거예요. 둘 다 NotificationChannel 이니까, 이모지로 감싼 걸 다시 중요표시로 감쌀 수 있죠.

Java
new UrgentChannel(new EmojiChannel(email)).send("팔로우");   // "🔔 [Email] 팔로우 (중요)"

안쪽부터 바깥으로, 결과가 한 겹씩 입혀지는 모습이에요.

텍스트
   new UrgentChannel( new EmojiChannel( new EmailChannel() ) )
        가장 바깥           중간              가장 안쪽

   안쪽부터 바깥으로 한 겹씩 입혀져요:

   EmailChannel  이 send         "[Email] 팔로우"
   EmojiChannel  가 한 겹 덧입힘   "🔔 [Email] 팔로우"
   UrgentChannel 가 한 겹 덧입힘   "🔔 [Email] 팔로우 (중요)"

Step 1 의 상속 폭발과 비교해봐요. 거기선 조합마다 클래스를 새로 만들어야 했죠. 여기선 데코레이터를 몇 겹으로 쌓느냐 로 조합을 만들어요. 새 데코레이터 하나만 추가하면, 기존 모든 채널과 자유롭게 섞여요. 곱셈이 덧셈으로 바뀐 거예요.

💡 Decorator 도 Day 41 의 Strategy 처럼 상속이 아니라 조합 으로 풀어요. 차이는 목적이에요. Strategy 는 행동을 하나 골라 교체 하는 거고, Decorator 는 기능을 여러 겹 쌓아 덧입히는 거예요. "골라 끼우면 Strategy, 겹겹이 감싸면 Decorator" 로 기억해두면 돼요.

그리고 한 가지 더. 데코레이터도 결국 NotificationChannel 이라서, 채널을 받는 곳이라면 어디든 그대로 끼워져요. Day 41 에서 만든 NotificationSender 도 채널을 받아 품는 발송기였죠. 거기에 데코레이터를 넣어도 발송기는 아무것도 모른 채 잘 동작해요.

Java
NotificationSender sender = new NotificationSender(new EmojiChannel(new PushChannel()));
sender.notify("DM");                            // "🔔 [Push] DM"

NotificationSender 는 한 줄도 고치지 않았어요. 데코레이터가 "채널인 척" 을 완벽하게 해주니, 기존 코드는 데코레이터가 끼어든 줄도 몰라요. 같은 약속을 지키는 것의 힘이에요.

🙋 학생 질문 — "데코레이터가 안쪽 채널을 품는 게, Day 41 의 발송기가 전략을 품던 거랑 똑같아 보여요. 뭐가 다른가요?"

날카로운 질문이에요. 겉모습은 정말 닮았어요. 둘 다 "안에 같은 타입을 하나 품는다" 는 점이 같죠. 차이는 품은 것을 가지고 무엇을 하느냐 예요.

Day 41 의 NotificationSender 는 품은 전략에게 일을 완전히 떠넘겨요. 자기는 아무 결과도 덧붙이지 않고, 전략이 만든 걸 그대로 돌려주죠. 그리고 자기 자신은 NotificationChannel 이 아니에요 — 채널을 쓰는 쪽이지, 채널인 척하지 않아요.

Decorator 는 달라요. 품은 채널에게 일을 시킨 뒤, 그 결과에 자기 한 겹을 더 얹어요. 게다가 자기도 NotificationChannel 이라서, 또 다른 데코레이터에게 감싸일 수도 있죠. "떠넘기고 끝" 이면 Strategy 에 가깝고, "받아서 한 겹 더 얹고, 나도 같은 타입" 이면 Decorator 예요.


Step 3: "사실 여러분은 이미 써봤어요" — Template Method 의 정체

이제 두 번째 패턴이에요. 그런데 이건 새로 배우는 게 아니라, 이미 써본 걸 이름표만 붙이는 시간이에요. Day 12 에서 콘텐츠를 만들던 걸 기억하시나요? 이미지·영상·텍스트가 모두 Content 를 물려받았고, 피드 한 줄을 그리는 render() 가 있었죠. 그 코드를 다시 꺼내봐요.

Java
// com/instagram/javabasic/domain/content/Content.java
public abstract class Content {

    // 콘텐츠 종류를 글자로 — 자식마다 "이미지" / "영상" / "텍스트" 로 다르게 채워요.
    public abstract String getType();

    // 피드에 보일 짧은 미리보기 — 종류마다 보여줄 내용이 다르니 자식이 정해요.
    public abstract String preview();

    // 피드 한 줄을 조립하는 "틀" 은 부모가 정하고,
    // 그 안에서 종류와 미리보기 내용은 자식이 채운 getType()/preview() 에 맡겨요.
    public String render() {
        return "[" + getType() + "] " + preview() + " (♥ " + likeCount + ")";
    }
}

여기 render() 가 바로 Template Method(템플릿 메서드) 예요. 이름 그대로 "틀(template) 이 되는 메서드" 죠. 큰 흐름의 골격은 부모가 쥐고 있고, 그 안의 일부 단계만 빈칸으로 비워 자식에게 맡기는 패턴이에요.

render() 를 뜯어보면 골격과 빈칸이 또렷이 보여요.

텍스트
 render() 라는 "틀" 은 부모(Content)가 쥐고 있어요:

   "[" + getType() + "] " + preview() + " (♥ " + likeCount + ")"
          └── 빈칸 1 ──┘     └ 빈칸 2 ┘
           (자식이 채움)      (자식이 채움)

   ImageContent 가 채우면  getType()="이미지",  preview()="OO 님의 사진: ..."
   VideoContent 가 채우면  getType()="영상",    preview()="OO 님의 영상: ..."

"[ ] 안에 종류를 쓰고, 미리보기를 붙이고, 끝에 좋아요 수를 단다" 는 순서와 모양 은 부모가 정했어요. 자식은 그 틀을 다시 만들지 않아요. 그저 두 빈칸(getType, preview)만 자기답게 채우죠. Day 12 의 ImageContent 가 빈칸을 어떻게 채웠는지 보면 분명해요.

Java
// com/instagram/javabasic/domain/content/ImageContent.java
@Override
public String getType() {
    return "이미지";
}

@Override
public String preview() {
    return getAuthorName() + " 님의 사진: " + imageUrl;
}

ImageContentrender() 를 한 글자도 쓰지 않았어요. 그런데도 imageContent.render() 를 부르면 "[이미지] OO 님의 사진: ... (♥ 12)" 같은 완성된 한 줄이 나와요. 부모가 틀을 쥐고, 자식이 빈칸만 채운 결과죠. 이게 Template Method 예요. 여러분은 Day 12 에 이 패턴을 이미 써본 거예요. 다음 Step 에서는 이 감각으로 새 템플릿 메서드를 직접 짜봐요.


Step 4: 골격은 고정, 빈칸만 교체 — Template Method 직접 짜기

이번엔 인스타그램 프로필을 글로 내보내는(export) 기능을 만들어봐요. 프로필을 일반 텍스트로도 내보내고, 마크다운 형식으로도 내보내고 싶어요. 그런데 형식이 뭐가 됐든 순서는 똑같아야 해요. 머리말이 맨 위, 그다음 본문, 맨 아래 꼬리말. 이 "순서" 가 바로 부모가 쥘 골격이에요.

Java
// com/instagram/javabasic/design/behavioral/template/ProfileExporter.java
public abstract class ProfileExporter {

    // 템플릿 메서드 — 내보내는 "순서" 를 부모가 한 번 정해 잠가요(final).
    // 머리말·본문은 빈칸이라 자식이 채우고, 꼬리말은 모두 같아서 부모가 정해요.
    public final String export(String username) {
        return header(username) + "\n" + body(username) + "\n" + footer();
    }

    // 빈칸 — 머리말은 형식마다 다르니 자식이 채워요.
    protected abstract String header(String username);

    // 빈칸 — 본문도 형식마다 다르니 자식이 채워요.
    protected abstract String body(String username);

    // 공통 단계 — 꼬리말은 어느 형식이든 똑같아서 부모가 한 번만 정해 물려줘요.
    protected String footer() {
        return "— Instagram";
    }
}

Day 12 의 render() 와 비교하면 두 가지가 새로 보여요. 첫째, exportfinal 이 붙었어요. 골격(머리말→본문→꼬리말 순서)을 자식이 함부로 못 바꾸게 잠근 거예요. Template Method 에서 골격을 지키는 게 중요할 때 이렇게 final 로 못을 박아요. 둘째, footer() 는 빈칸이 아니라 본문이 있는 공통 단계 예요. 꼬리말은 어느 형식이든 똑같으니, 부모가 한 번만 정해 모두에게 물려주죠.

이제 빈칸을 채우는 자식 둘을 만들어요. 먼저 꾸밈없는 일반 텍스트 형식이에요.

Java
// com/instagram/javabasic/design/behavioral/template/PlainProfileExporter.java
public class PlainProfileExporter extends ProfileExporter {

    @Override
    protected String header(String username) {
        return "[" + username + "]";
    }

    @Override
    protected String body(String username) {
        return username + "님, 환영해요";
    }

    // footer 는 부모 것을 그대로 물려받아 써요 — "— Instagram"
}

마크다운 형식은 머리말·본문만 마크다운 모양으로 다르게 채워요. 순서는 손대지 않죠.

Java
// com/instagram/javabasic/design/behavioral/template/MarkdownProfileExporter.java
@Override
protected String header(String username) {
    return "## " + username;
}

@Override
protected String body(String username) {
    return "> " + username + "님, 환영해요";
}

두 자식에게 같은 일을 시켜봐요. 호출하는 코드는 완전히 똑같아요.

Java
new PlainProfileExporter().export("jaehoon");
// [jaehoon]
// jaehoon님, 환영해요
// — Instagram

new MarkdownProfileExporter().export("minji");
// ## minji
// > minji님, 환영해요
// — Instagram

머리말과 본문의 모양 은 다르지만, "머리말 → 본문 → 꼬리말" 이라는 순서 는 둘 다 똑같죠. 꼬리말 "— Instagram" 도 둘 다 같아요. 부모가 골격과 공통 단계를 쥐고, 자식은 빈칸만 채운 덕분이에요.

💡 Day 41 의 Strategy 와 오늘의 Template Method 는 자매처럼 닮았지만 반대예요. Strategy 는 조합 으로 풀어요 — 행동을 객체로 뽑아 바깥에서 갈아끼우죠. Template Method 는 상속 으로 풀어요 — 골격을 부모에 두고 빈칸만 자식이 채우죠. "통째로 갈아끼울 행동" 이면 Strategy, "큰 흐름은 같고 일부 단계만 다를 때" 면 Template Method 가 어울려요.


Step 5: for-each 의 비밀 — Iterator 와 Iterable

마지막 패턴은 우리가 매일 쓰면서도 정체를 몰랐던 것이에요. 바로 for-each(향상된 for문) 예요. Day 18 에서 리스트를 이렇게 돌렸던 걸 기억하시죠.

Java
for (Post post : posts) {
    System.out.println(post.getContent());
}

이 짧은 문법이 어떻게 리스트의 원소를 하나씩 꺼내는 걸까요? 사실 컴파일러가 속으로 다른 코드로 풀어서 실행해요. for-each 는 일종의 줄임말이고, 진짜 모습은 이래요.

Java
Iterator<Post> it = posts.iterator();   // ① "순회 담당자" 를 받아오고
while (it.hasNext()) {                   // ② 아직 남았는지 묻고
    Post post = it.next();               // ③ 하나 꺼내서
    System.out.println(post.getContent());
}

여기 두 단어가 비밀의 열쇠예요. IterableIterator 죠. 이름이 비슷해서 헷갈리기 쉬운데, 역할이 또렷이 달라요.

텍스트
 Iterable  = "나는 순회될 수 있어요" 라는 약속
              메서드 하나: iterator() — 순회 담당자를 하나 만들어 줘요

 Iterator  = "순회를 실제로 진행하는 담당자"
              hasNext() — 아직 더 남았나요?
              next()    — 다음 걸 하나 꺼내 주세요

for-each 는 콜론(:) 오른쪽에 오는 대상이 Iterable 이기만 하면 동작해요. 먼저 그 대상의 iterator() 를 불러 순회 담당자(Iterator)를 하나 받고, 그 담당자에게 hasNext()next() 를 번갈아 물어가며 끝까지 도는 거예요. 우리가 그동안 쓴 List 가 바로 Iterable 이라서, for-each 가 통했던 거죠.

여기서 중요한 결론이 나와요. Iterable 약속만 지키면, 그게 무엇이든 for-each 로 돌릴 수 있다 는 거예요. 꼭 자바가 준 List 일 필요가 없어요. 우리가 직접 만든 클래스도, 약속만 지키면 for-each 의 콜론 오른쪽에 설 수 있죠. 다음 Step 에서 직접 만들어봐요.


Step 6: 내가 만든 컬렉션을 for-each 로 돌리기

게시물을 담아두는 우리만의 작은 피드 Feed 를 만들어봐요. 속은 평범한 배열(Post[]) 하나예요. List 같은 걸 쓰지 않았는데도, Iterable 약속만 지키면 for-each 로 돌릴 수 있다는 걸 직접 확인할 거예요.

Java
// com/instagram/javabasic/design/behavioral/iterator/Feed.java
public class Feed implements Iterable<Post> {

    private final Post[] posts;

    public Feed(Post[] posts) {
        this.posts = posts;
    }

    // Iterable 의 약속 — "나를 어떻게 순회할지" 담당하는 Iterator 를 만들어 돌려줘요.
    // for-each 가 속으로 이 메서드를 불러요.
    @Override
    public Iterator<Post> iterator() {
        return new FeedIterator();
    }

    // 순회 담당자 — "지금 어디까지 봤는지(cursor)" 를 기억하며 하나씩 꺼내줘요.
    private class FeedIterator implements Iterator<Post> {

        // 다음에 꺼낼 자리예요. 0부터 시작해 하나 꺼낼 때마다 한 칸 앞으로 가요.
        private int cursor = 0;

        // 아직 더 꺼낼 게 남았는지 — cursor 가 배열 끝에 닿기 전이면 true.
        @Override
        public boolean hasNext() {
            return cursor < posts.length;
        }

        // 지금 자리의 게시물을 꺼내고, cursor 를 한 칸 앞으로 옮겨요.
        @Override
        public Post next() {
            return posts[cursor++];
        }
    }
}

구조를 정리하면 이래요. FeedIterable 약속을 지키며 iterator() 하나만 제공해요. 진짜 순회는 안쪽의 FeedIterator 가 맡죠. FeedIteratorcursor(지금 위치)를 기억하면서, hasNext() 로 "남았나?" 에 답하고 next() 로 하나씩 꺼내요. cursor 가 배열을 한 칸씩 짚어가는 모습이에요.

텍스트
  posts 배열:   index 0     index 1     index 2
                 첫 게시물     둘째 게시물    셋째 게시물

  cursor 0  hasNext? 0<3 ✓  next() 로 '첫 게시물' 꺼내고 cursor=1
  cursor 1  hasNext? 1<3 ✓  next() 로 '둘째 게시물' 꺼내고 cursor=2
  cursor 2  hasNext? 2<3 ✓  next() 로 '셋째 게시물' 꺼내고 cursor=3
  cursor 3  hasNext? 3<3 ✗  멈춤

이제 진짜 마법을 볼 차례예요. Feed 는 우리가 방금 만든 평범한 클래스인데, for-each 가 그대로 통해요.

Java
Post[] posts = {
    new Post("첫 게시물", "jaehoon", 10),
    new Post("둘째 게시물", "minji", 20),
    new Post("셋째 게시물", "seungwoo", 30),
};
Feed feed = new Feed(posts);

for (Post post : feed) {        // ← 우리가 만든 Feed 인데도 for-each 가 통해요!
    System.out.println(post.getContent());
}
// 첫 게시물
// 둘째 게시물
// 셋째 게시물

feedList 가 아니에요. 그저 배열을 품은 우리 클래스죠. 그런데도 for-each 가 도는 이유는, Step 5 에서 본 그대로예요. feedIterable 이라서, for-each 가 속으로 feed.iterator() 를 부르고 hasNext()·next() 를 번갈아 물어가며 도는 거예요. 약속을 지킨 보답이죠.

💡 여기서 Iterator 가 주는 진짜 선물이 보여요. for-each 를 쓰는 쪽은 Feed 안이 배열인지 리스트인지 트리인지 전혀 몰라도 돼요. 그저 "하나씩 꺼내 줘" 만 부탁하면 되죠. 내부 구조가 어떻든 순회하는 코드는 똑같아요. "어떻게 저장됐는지 감추고, 순회하는 방법만 통일한다" — 이게 Iterator 패턴의 핵심이에요.

🙋 학생 질문 — "순회 담당자(FeedIterator)를 왜 굳이 따로 만들어요? Feed 자기가 cursor 를 가지면 안 되나요?"

좋은 의문이에요. 실제로 Feed 가 직접 cursor 를 들고 hasNext·next 를 가질 수도 있어요. 그런데 그러면 곤란한 상황이 생겨요.

Feed 하나를 두 군데서 동시에 돌린다고 해봐요. 이중 for 문으로 모든 게시물 쌍을 비교하는 경우처럼요. Feed 자신이 cursor 하나만 들고 있으면, 안쪽 반복과 바깥쪽 반복이 같은 cursor 를 건드려서 서로 엉켜요.

그래서 순회 상태(cursor)는 순회할 때마다 새로 만드는 담당자 에게 맡겨요. iterator() 를 부를 때마다 새 FeedIteratorcursor=0 으로 태어나죠. 그러면 반복마다 독립된 담당자가 생겨서 서로 간섭하지 않아요. "데이터(Feed) 와 순회 상태(Iterator) 를 분리한다" — 이게 둘을 나눈 이유예요.


Step 7: 세 패턴 한자리 비교, 그리고 JVM 속으로

오늘 만난 세 패턴은 푸는 불편이 서로 달라요. 한자리에 모아봐요.

구분 Decorator Template Method Iterator
한 줄 정체 같은 약속을 지키며 기능을 한 겹씩 덧입힘 골격은 부모가 잠그고 빈칸만 자식이 채움 내부 구조를 감추고 순회 방법만 통일
푸는 불편 기능 조합마다 클래스가 폭발 큰 흐름은 같은데 일부 단계만 다름 컬렉션마다 순회 코드가 제각각
상속 vs 조합 조합 (감싸기) 상속 (빈칸 채우기) 조합 (담당자 분리)
오늘 만든 예 ChannelDecorator 와 채널들 ProfileExporter 와 자식들 FeedFeedIterator

Day 41 의 교훈이 오늘도 이어져요. Strategy·Observer·Decorator·Iterator 는 모두 조합 으로, Template Method 는 상속 으로 풀었죠. 그리고 패턴은 늘 불편이 실제로 있을 때 꺼내는 연장이에요. 채널이 하나뿐이고 덧입힐 기능도 없다면 Decorator 는 필요 없고, 순회할 일이 한 번뿐이면 그냥 배열을 직접 도는 게 더 읽기 쉬워요. "이 불편이 보이는가?" — 그때가 패턴을 꺼낼 때예요.

여기서 잠깐, 오늘 만든 코드를 다시 떠올려봐요. 데코레이터는 안쪽 채널을 품고, 그 채널이 또 다른 채널을 품으며 객체가 겹겹이 사슬처럼 이어졌어요. Feed 의 순회 담당자도 배열 속 게시물들을 계속 붙들고 있었죠. 그런데 이 객체들은 메모리 어디에 살고 있을까요? 더 이상 쓰지 않는 객체는 누가 치워줄까요? 만약 어떤 객체를 계속 붙들고 놓지 않으면 어떻게 될까요?

다음 시간 (Day 43) 엔 코드에서 한 걸음 물러나, 그 코드가 돌아가는 무대인 JVM 의 메모리 구조 를 들여다봐요. 객체가 사는 Heap, 메서드 호출이 쌓이는 Stack, 클래스 정보가 담기는 Metaspace 가 어떻게 나뉘는지, 안 쓰는 객체를 자동으로 치워주는 GC(가비지 컬렉터) 가 무슨 일을 하는지, 그리고 객체를 놓지 않아 생기는 메모리 누수 가 왜 위험한지를요. 우리가 만든 객체들이 실제로 어디서 살고 죽는지 알게 되는 시간이에요.


마무리

오늘은 객체를 감싸고, 골격을 나누고, 순회하는 세 패턴을 배웠어요.

  • Decorator — 같은 약속(NotificationChannel)을 지키며 안에 채널을 품고, 그 결과에 한 겹씩 기능을 덧입히는 패턴. 데코가 데코를 감싸 조합을 만들고, 기존 코드(NotificationSender)에 그대로 끼워졌죠.
  • Template Method — 큰 흐름의 골격은 부모가 final 로 잠그고, 빈칸만 자식이 채우는 패턴. Day 12 의 render() 가 사실 이 패턴이었고, 오늘 ProfileExporter 로 직접 짜봤어요.
  • Iterator 와 Iterablefor-each 의 비밀. Iterable 약속만 지키면 우리가 만든 Feedfor-each 로 돌릴 수 있고, 그 덕분에 내부 구조를 몰라도 순회 코드는 똑같아져요.

Day 40 부터 오늘까지, 우리는 생성·행동·구조를 아우르는 디자인 패턴을 두루 만났어요. 패턴은 외우는 게 아니라, 불편을 만났을 때 "아, 이건 그 패턴이 풀어주는 불편이구나" 하고 알아보는 눈을 기르는 거예요. 오늘 세 패턴도 그 눈에 한 칸씩 더해졌길 바라요.

다음 시간엔 코드 아래로 내려가, JVM 이 우리 객체를 어떻게 메모리에 올리고 치우는지 들여다봐요. 수고 많으셨어요!


과제

오늘 배운 세 패턴을 하나씩 직접 손에 익혀봐요. 각 과제는 본문에서 만든 코드를 참고하면 충분히 풀 수 있어요.

과제 1 (기본): 새 데코레이터를 만들어 감싸기

상황 배경: 특별한 알림은 양옆을 별(⭐)로 감싸 눈에 띄게 하고 싶어요. 본문의 EmojiChannel 을 참고해, 결과를 별로 감싸는 데코레이터를 직접 만들어볼 거예요. Step 2 의 ChannelDecoratorEmojiChannel 을 참고하세요.

🎯 해결 미션:

  1. ChannelDecorator 를 물려받는 StarChannel 을 만드세요. send 는 안쪽 결과를 별로 감싸야 해요 — 예를 들어 "⭐ " + 안쪽결과 + " ⭐" 처럼요.
  2. EmailChannelStarChannel 로 감싸 메시지를 보내, 양옆에 별이 붙는지 확인하세요.
  3. 이제 StarChannelEmojiChannel두 가지 순서로 겹쳐보세요. StarChannel(EmojiChannel(...))EmojiChannel(StarChannel(...)) 의 결과가 어떻게 달라지는지 비교하고, 왜 순서가 결과를 바꾸는지 한두 줄로 적어보세요.

과제 2 (응용): 새 export 형식을 Template Method 로 추가하기

상황 배경: 프로필을 카드 형태의 한 줄 텍스트로도 내보내고 싶어요. 본문의 ProfileExporter 골격은 그대로 두고, 빈칸만 새로 채워 형식 하나를 더 만들어볼 거예요. Step 4 의 PlainProfileExporter 를 참고하세요.

🎯 해결 미션:

  1. ProfileExporter 를 물려받는 CardProfileExporter 를 만들고, headerbody 를 카드 느낌으로 새로 채우세요 (예: 머리말 "=== " + username + " ===", 본문은 자유롭게).
  2. footer()건드리지 마세요. 부모의 공통 꼬리말 "— Instagram" 이 그대로 따라붙는지 확인하세요.
  3. 자식에서 export 의 순서(머리말→본문→꼬리말)를 바꾸려고 시도해보세요. 왜 바꿀 수 없는지(exportfinal 인 점과 연결해) 한두 줄로 설명해보세요.

과제 3 (심화): 골라서 순회하는 똑똑한 Iterator

상황 배경: 피드 전체가 아니라, 좋아요가 일정 수 이상인 인기 게시물만 골라 순회하고 싶어요. for-each 코드는 그대로 두고, 순회 담당자만 똑똑하게 만들어볼 거예요. Step 6 의 FeedFeedIterator 를 참고하세요.

🎯 해결 미션:

  1. Feed 를 참고해 PopularFeed 를 만드세요. 생성할 때 게시물 배열과 함께 기준 좋아요 수(예: 15)를 받아요.
  2. 안쪽 Iterator 가 hasNext·next 로 게시물을 꺼낼 때, 기준 미만인 게시물은 건너뛰고 기준 이상만 돌려주게 만드세요. (힌트: hasNext 가 다음 "유효한" 게시물이 있는지까지 확인하도록 cursor 를 미리 앞으로 옮겨두는 방법을 고민해보세요.)
  3. for-eachPopularFeed 를 돌려, 인기 게시물만 나오는지 확인하세요. for-each 쪽 코드는 Feed 때와 한 글자도 다르지 않다는 점을 음미해보세요 — 순회 규칙만 바뀌었을 뿐이에요.

생각해볼 주제

혼자 고민해도 좋고, 동료와 토론해도 좋아요. 정답을 외우기보다 "나라면 어떻게 설명할까" 를 떠올리며 읽어보세요.

1. Decorator 의 감싸기는 왜 상속보다 유연할까?

오늘 Step 1 에서, 채널과 기능을 상속으로 조합하면 클래스가 곱셈으로 폭발하는 걸 봤어요. 그런데 Decorator 로 풀었더니 새 기능 하나에 데코레이터 하나만 추가하면 됐죠.

"기능을 더할 때 곱셈인가 덧셈인가" 를 떠올려보세요. 상속으로 모든 조합을 미리 만들면, 채널 수 × 기능 수 만큼 클래스가 필요해요. Decorator 는 데코레이터를 실행 중에 겹쳐 쌓아 조합을 그때그때 만들죠. 그래서 미리 만들어둘 필요가 없어요.

또 순서도 생각해보세요. 상속이라면 "이모지 먼저" 와 "중요표시 먼저" 를 각각 다른 클래스로 만들어야 하지만, Decorator 는 감싸는 순서만 바꾸면 돼요. 왜 "곱셈을 덧셈으로 바꾼다" 는 말이 Decorator 를 잘 설명하는지, 오늘 코드와 연결해 정리해보세요.

2. Template Method 는 상속, Strategy 는 조합 — 언제 무엇을 골라야 할까?

오늘 Template Method 는 골격을 부모에 두고 빈칸만 자식이 채웠어요. 반면 Day 41 의 Strategy 는 행동을 객체로 뽑아 바깥에서 갈아끼웠죠. 둘 다 "일부를 다르게 한다" 는 목적은 같은데, 하나는 상속을, 하나는 조합을 썼어요.

"큰 흐름이 정해져 있는가, 아니면 행동을 통째로 바꿔야 하는가" 를 떠올려보세요. 머리말→본문→꼬리말처럼 순서가 고정 이고 일부 단계만 다를 때는 Template Method 가 어울려요. 반면 행동 자체를 실행 중에 통째로 교체 해야 한다면 Strategy 가 낫죠.

또 "한 객체가 살아 있는 동안 바꿀 수 있는가" 도 갈림길이에요. 상속으로 정한 골격은 객체가 태어날 때 굳지만, 조합한 전략은 도중에 갈아끼울 수 있어요. 두 패턴을 언제 어떻게 골라야 할지, 오늘과 지난 시간 코드를 나란히 두고 정리해보세요.

3. Iterator 가 "내부 구조를 감춘다" 는 게 왜 좋은 설계일까?

오늘 Feed 는 속이 배열이었지만, for-each 를 쓰는 쪽은 그 사실을 전혀 몰라도 됐어요. 만약 나중에 Feed 속을 리스트나 트리로 바꿔도, 순회하는 코드는 한 글자도 안 바뀌죠.

"쓰는 쪽이 내부를 얼마나 알아야 하는가" 를 떠올려보세요. 만약 Feed 를 쓸 때마다 "이건 배열이니까 인덱스로 돌고, 저건 리스트니까 다르게 돌고" 를 일일이 알아야 한다면, 내부를 바꾸는 순간 쓰는 코드가 전부 깨져요.

Iterator 는 "하나씩 꺼내 줘" 라는 통일된 방법만 노출하고 내부는 감춰요. 그래서 내부가 바뀌어도 바깥은 흔들리지 않죠. 이건 Day 41 에서 본 "서로 덜 알수록 덜 흔들린다" 는 이야기와도 이어져요. 내부를 감추는 게 왜 좋은 설계인지, 그 말과 연결해 정리해보세요.

✅ 예시 답안정답 보기

아래는 과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니니, 내 코드와 흐름을 비교하며 "왜 이렇게 했는지" 를 짚어보세요.

과제 예시답안

과제 1 예시답안 — 새 데코레이터를 만들어 감싸기

핵심 접근

별로 감싸는 것도 "안쪽 결과에 한 겹을 덧입히는" 일이에요. 그러니 새 채널 클래스를 처음부터 만들 필요가 없어요. Step 2 의 EmojiChannel 처럼 감싸기 뼈대(ChannelDecorator)를 물려받아, send 한 곳만 채우면 돼요. 안쪽 채널에게 먼저 일을 시키고, 그 결과를 별 사이에 끼우는 거죠.

예시 구현

EmojiChannel 과 모양이 똑같아요. 다른 건 send 한 줄뿐이에요.

Java
// com/instagram/javabasic/design/structural/solution/day42/StarChannel.java
public class StarChannel extends ChannelDecorator {

    public StarChannel(NotificationChannel wrapped) {
        super(wrapped);
    }

    @Override
    public String send(String message) {
        // 안쪽 결과를 별 두 개 사이에 끼워 넣어요.
        return "⭐ " + wrapped.send(message) + " ⭐";
    }
}

이제 순서를 바꿔 감싸보면, 결과가 달라져요. 데코레이터는 안쪽부터 바깥으로 한 겹씩 입혀지니까요.

Java
NotificationChannel email = new EmailChannel();

new EmojiChannel(new StarChannel(email)).send("hi");   // "🔔 ⭐ [Email] hi ⭐"
new StarChannel(new EmojiChannel(email)).send("hi");   // "⭐ 🔔 [Email] hi ⭐"

첫 번째는 별로 먼저 감싼 뒤 이모지를 둘렀고, 두 번째는 이모지로 먼저 감싼 뒤 별을 둘렀어요. 안쪽이 먼저 결과를 만들고 바깥이 그 위에 덧입히니, 누가 안에 있고 누가 밖에 있느냐에 따라 최종 모양이 달라지는 거예요.

채점 포인트

  • ChannelDecorator 를 물려받아 send 만 채웠는가 (새 채널을 처음부터 만들지 않았는가)
  • send 안에서 wrapped.send(message) 로 안쪽에게 먼저 일을 시켰는가
  • 두 가지 순서로 겹쳐보고, 결과가 달라지는 이유(안쪽부터 바깥으로 입혀짐)를 설명했는가

흔한 실수

  • wrapped.send(message) 를 부르지 않고 message 를 직접 별로 감싸는 경우. 그러면 안쪽 채널([Email])이 사라져요. 데코레이터의 핵심은 "안쪽에게 시키고, 그 결과에 덧입히기" 예요.
  • 순서를 바꿔도 결과가 같을 거라 예상하는 경우. 데코레이터는 겹친 순서가 결과에 그대로 드러나요. 직접 출력해서 확인해보면 분명해져요.
과제 2 예시답안 — 새 export 형식을 Template Method 로 추가하기

핵심 접근

새 형식을 더한다고 해서 "머리말→본문→꼬리말" 순서까지 새로 정할 필요는 없어요. 그 골격은 부모(ProfileExporter)가 이미 쥐고 있고, final 로 잠가뒀으니까요. 우리가 할 일은 빈칸(header·body)만 카드 모양으로 채우는 것뿐이에요. 꼬리말은 손도 대지 않아요.

예시 구현

빈칸 둘만 새로 채우면 끝이에요.

Java
// com/instagram/javabasic/design/behavioral/solution/day42/CardProfileExporter.java
public class CardProfileExporter extends ProfileExporter {

    @Override
    protected String header(String username) {
        return "=== " + username + " ===";
    }

    @Override
    protected String body(String username) {
        return username + " 님 · 프로필 카드";
    }

    // footer() 는 따로 만들지 않아요 — 부모의 공통 꼬리말 "— Instagram" 이 그대로 따라붙어요.
}

내보내면 이렇게 나와요. 머리말·본문은 카드 모양으로 새로 채웠지만, 순서와 꼬리말은 부모가 정한 그대로예요.

Java
new CardProfileExporter().export("minji");
// === minji ===
// minji 님 · 프로필 카드
// — Instagram

3번 미션, "순서를 바꿔보려는 시도" 의 답은 이거예요. export 메서드가 부모에서 final 로 잠겨 있어서, 자식은 export 를 다시 정의(오버라이드)할 수 없어요. 순서를 바꾸려면 export 를 새로 써야 하는데, final 이 그걸 막죠. 골격을 지켜야 하는 Template Method 에서, 이 final 이 "순서는 절대 못 바꾼다" 는 약속을 코드로 못 박아주는 거예요.

채점 포인트

  • ProfileExporter 를 물려받아 header·body 빈칸만 채웠는가
  • footer() 를 건드리지 않고 부모 공통 단계를 그대로 물려받았는가
  • exportfinal 이라 순서를 못 바꾼다는 점을, 골격 보호와 연결해 설명했는가

흔한 실수

  • export 자체를 다시 만들려다 컴파일 에러를 만나는 경우. 그게 바로 final 의 의도예요. 에러가 나는 건 실패가 아니라, 골격이 잘 잠겨 있다는 신호예요.
  • footer() 까지 새로 오버라이드해서 꼬리말을 바꾸는 경우. 과제는 "공통 단계는 그대로 물려받기" 가 핵심이라, 꼬리말은 두는 게 맞아요. (물론 형식마다 꼬리말이 달라야 한다면 그땐 오버라이드가 정답이고요.)
과제 3 예시답안 — 골라서 순회하는 똑똑한 Iterator

핵심 접근

for-each 코드는 그대로 두고 "인기 게시물만" 나오게 하려면, 바꿀 곳은 단 하나예요. 순회 담당자(Iterator)요. FeedFeedIterator 는 모든 게시물을 차례로 꺼냈지만, 이번엔 기준 미만인 게시물을 건너뛰며 꺼내게 만들면 돼요. 핵심은 hasNext() 가 "다음 인기 게시물이 있는 자리" 까지 cursor 를 미리 옮겨두는 거예요.

예시 구현

PopularFeed 의 뼈대는 Feed 와 같아요. 좋아요 기준(minLikes)을 하나 더 받는 것과, 순회 담당자가 똑똑해진 것만 달라요.

Java
// com/instagram/javabasic/design/behavioral/solution/day42/PopularFeed.java
private class PopularIterator implements Iterator<Post> {

    private int cursor = 0;

    // 다음 "인기 게시물" 이 있는 자리까지 cursor 를 미리 옮겨두고, 남았는지 답해요.
    @Override
    public boolean hasNext() {
        while (cursor < posts.length && posts[cursor].getLikeCount() < minLikes) {
            cursor++;
        }
        return cursor < posts.length;
    }

    @Override
    public Post next() {
        return posts[cursor++];
    }
}

hasNext() 안의 while 이 똑똑함의 비결이에요. 기준에 못 미치는 게시물을 만나면 cursor 를 계속 앞으로 밀어, 다음 인기 게시물에서 멈춰요. 그 뒤 next() 는 지금 가리키는 게시물만 꺼내면 되죠. 그래서 건너뛴 게시물은 for-each 에 아예 나타나지 않아요.

쓰는 쪽 코드는 Step 6 의 Feed 때와 한 글자도 다르지 않아요.

Java
Post[] posts = {
    new Post("A", "u1", 10), new Post("B", "u2", 20), new Post("C", "u3", 30),
    new Post("D", "u4", 5),  new Post("E", "u5", 25),
};

for (Post post : new PopularFeed(posts, 15)) {   // Feed 때와 똑같은 for-each
    System.out.println(post.getContent());
}
// B
// C
// E

좋아요 10·5 인 A·D 는 빠지고, 15 이상인 B·C·E 만 나왔어요. for-each 는 그대로인데 순회 규칙만 바뀐 거예요.

채점 포인트

  • for-each 쪽 코드를 바꾸지 않고, 순회 담당자(Iterator)만 손봤는가
  • hasNext() 가 기준 미만 게시물을 건너뛰며 다음 유효한 자리를 찾는가
  • Feed 때와 for-each 코드가 똑같다는 점(순회 규칙만 바뀜)을 짚었는가

흔한 실수

  • next() 안에서만 건너뛰려다 꼬이는 경우. hasNext() 가 "남았나?" 에 답할 때 이미 다음 유효한 자리를 알아야, for-each 가 정확히 멈춰요. 그래서 건너뛰는 일은 hasNext() 에 두는 게 깔끔해요.
  • 아예 생성자에서 인기 게시물만 골라 새 배열로 거른 뒤 그냥 Feed 를 쓰는 경우. 결과는 맞지만, 과제의 의도(순회 담당자를 똑똑하게)와는 달라요. "거르기" 를 미리 하지 않고 순회하는 도중에 처리하는 게 Iterator 의 힘을 보여주는 길이에요.

생각해볼 주제 예시답안

생각해볼 주제 1 예시답안 — Decorator 의 감싸기는 왜 상속보다 유연할까?

[문제 상황 요약]

채널과 덧입힐 기능을 상속으로 조합하면 클래스가 곱셈으로 폭발하는데, Decorator 로 풀면 새 기능 하나에 데코레이터 하나만 추가하면 됐어요. 왜 감싸기가 상속보다 유연한지에 대한 질문이에요.

[튜터의 가이드 및 해설]

두 각도로 풀어볼게요.

첫째, 곱셈이 덧셈으로 바뀌어요. 채널이 3가지(이메일·푸시·문자), 덧입힐 기능이 2가지(이모지·중요표시)라고 해봐요. 상속으로 모든 조합을 미리 만들면 3 × 2 만큼, 거기에 두 기능을 함께 쓴 것까지 더하면 클래스가 더 늘어요. Decorator 는 데코레이터를 실행 중에 겹쳐 쌓아 조합을 그때그때 만들어요. 그래서 채널 3개와 데코레이터 2개, 합쳐서 몇 개만 있으면 어떤 조합이든 만들 수 있죠. 종류가 늘수록 이 차이는 더 벌어져요.

둘째, 순서를 자유롭게 바꿀 수 있어요. "이모지 먼저, 그다음 중요표시" 와 "중요표시 먼저, 그다음 이모지" 를 상속으로 다루려면 각각 다른 클래스가 필요해요. Decorator 는 감싸는 순서만 바꾸면 끝이에요. 같은 데코레이터들을 어떤 순서로 두르느냐로 다른 결과를 만들죠.

그래서 "곱셈을 덧셈으로 바꾼다" 는 말이 Decorator 를 잘 설명해요. 미리 모든 조합을 만들어두는 대신, 작은 조각을 실행 중에 겹쳐 쌓는 거예요.

🎯 면접관을 홀리는 핵심 멘트

"상속으로 기능 조합을 다루면 채널 수 × 기능 수만큼 클래스가 폭발하지만, Decorator 는 같은 인터페이스를 지키는 작은 데코레이터를 런타임에 겹쳐 쌓아 조합을 만들어요. 그래서 곱셈이 덧셈으로 바뀌고, 감싸는 순서만 바꿔 다른 결과도 낼 수 있죠. 새 기능은 데코레이터 하나만 추가하면 기존 채널 전부와 자유롭게 섞입니다. 미리 다 만들어두지 않고 필요할 때 조립한다는 게 핵심이에요."

생각해볼 주제 2 예시답안 — Template Method 는 상속, Strategy 는 조합 — 언제 무엇을 골라야 할까?

[문제 상황 요약]

Template Method 는 골격을 부모에 두고 빈칸만 자식이 채웠고, Strategy 는 행동을 객체로 뽑아 바깥에서 갈아끼웠어요. 둘 다 "일부를 다르게 한다" 는 목적은 같은데 하나는 상속, 하나는 조합을 써요. 언제 무엇을 골라야 하는지에 대한 질문이에요.

[튜터의 가이드 및 해설]

두 가지 기준으로 갈라볼게요.

첫째, 큰 흐름이 고정인가, 행동을 통째로 바꿔야 하는가. 프로필 내보내기처럼 "머리말→본문→꼬리말" 순서가 정해져 있고 일부 단계만 형식별로 다르다면, 골격을 부모가 쥐는 Template Method 가 어울려요. 반면 알림 채널처럼 행동 전체(이메일이냐 푸시냐)를 통째로 바꿔야 한다면 Strategy 가 낫죠. "틀은 같고 빈칸만 다르다" 면 Template Method, "행동 자체가 다르다" 면 Strategy 예요.

둘째, 언제 바꿔야 하는가. 상속으로 정한 골격은 객체가 태어날 때 굳어요. CardProfileExporter 객체는 영원히 카드 형식이죠. 반면 조합한 전략은 살아 있는 도중에도 갈아끼울 수 있어요. 사용자가 설정에서 알림 방식을 바꾸는 순간 전략만 교체하면 되는 것처럼요. "실행 중에 행동을 바꿔야 한다" 면 그것만으로도 Strategy 가 답이에요.

정리하면, 변하지 않는 골격 속의 빈칸이면 Template Method(상속), 통째로 교체하고 특히 실행 중에 바꿔야 하면 Strategy(조합) 예요. 둘은 경쟁자가 아니라 상황이 다른 도구예요.

🎯 면접관을 홀리는 핵심 멘트

"저는 두 가지를 봐요. 첫째, 큰 흐름이 고정이고 일부 단계만 다르면 골격을 부모가 쥐는 Template Method 를, 행동을 통째로 교체해야 하면 Strategy 를 씁니다. 둘째, 실행 중에 바꿔야 하는가를 보죠. 상속으로 정한 골격은 객체가 태어날 때 굳지만, 조합한 전략은 런타임에 갈아끼울 수 있으니까요. 그래서 '틀은 같고 빈칸만 다르다' 면 Template Method, '행동을 살아 있는 동안 바꿔야 한다' 면 Strategy 를 택합니다."

생각해볼 주제 3 예시답안 — Iterator 가 "내부 구조를 감춘다" 는 게 왜 좋은 설계일까?

[문제 상황 요약]

Feed 는 속이 배열이었지만, for-each 를 쓰는 쪽은 그 사실을 몰라도 됐어요. 나중에 속을 리스트나 트리로 바꿔도 순회 코드는 그대로죠. 내부를 감추는 게 왜 좋은 설계인지에 대한 질문이에요.

[튜터의 가이드 및 해설]

핵심은 "쓰는 쪽이 내부를 얼마나 알아야 하는가" 예요.

만약 Feed 를 쓸 때마다 그 속이 배열인지 리스트인지 알아야 한다면 어떻게 될까요. "이건 배열이니까 인덱스로 돌고, 저건 리스트니까 다르게 돌고" 를 일일이 따져야 해요. 그러다 Feed 속을 바꾸는 순간, 그걸 쓰던 코드가 전부 깨지죠. 내부와 바깥이 너무 많이 알고 지내서, 한쪽이 바뀌면 다른 쪽도 흔들리는 거예요.

Iterator 는 그 사이에 "하나씩 꺼내 줘" 라는 통일된 창구 하나만 둬요. 쓰는 쪽은 hasNext()·next() 만 알면 되고, 그 뒤가 배열이든 트리든 신경 쓸 필요가 없어요. 그래서 내부 구조를 나중에 바꿔도, 순회하는 코드는 한 글자도 안 바뀌어요. PopularFeed 가 좋은 예예요. 순회 규칙을 "인기 게시물만" 으로 바꿨는데도, for-each 쪽은 그대로였죠.

이건 Day 41 에서 본 "서로 덜 알수록 덜 흔들린다" 와 똑같은 이야기예요. 내부를 감추고 통일된 방법만 노출하면, 그 뒤가 아무리 바뀌어도 바깥은 안전해요.

🎯 면접관을 홀리는 핵심 멘트

"Iterator 는 컬렉션의 내부 구조(배열·리스트·트리)를 감추고, '하나씩 꺼내 줘' 라는 통일된 순회 방법만 노출해요. 그래서 쓰는 쪽은 내부를 몰라도 되고, 내부를 나중에 바꿔도 순회 코드는 그대로죠. 실제로 같은 for-each 코드로 전체 순회도, 인기 게시물만 거르는 순회도 할 수 있어요. 결국 '내부를 감추고 통일된 창구만 노출하라', 한 문장으로는 '서로 덜 알수록 덜 흔들린다' 입니다."

전체 목록 자바 기초