문서 읽는 데 85분 · day13

Day13: 인터페이스 — 여러 역할(약속)을 동시에 입히기

전체 21강 중 13강 · 자바 기초
난이도 · 입문

안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.

Day 13에 오신 걸 환영합니다! 지난 시간 마지막에 제가 떡밥 하나를 흘려뒀어요. 기억나시나요? "여러 역할을 동시에 맡고 싶다면?" 이라는 이야기였죠.

Day 12에서 우리는 추상 클래스 Content 를 만들었어요. 직접 만들 수 없는 "뼈대 부모" 를 두고, getType()·preview() 라는 빈칸을 걸어두면 자식(이미지·영상·텍스트)이 그 빈칸을 채우는 구조였죠. 부모가 짠 틀(render())은 그대로 물려받고요. 추상 클래스를 제대로 익히고 왔어요.

그런데 그때 제가 한계도 하나 짚었어요. 자바에서 자식은 부모를 딱 하나만 물려받을 수 있다는 거였죠. ImageContentContent 를 물려받았으면, 다른 부모를 동시에 또 물려받진 못해요. 이걸 "단일 상속" 이라고 불러요.

여기서 현실의 요구가 부딪혀요. 인스타 피드의 사진을 떠올려보세요. 이 사진은 "콘텐츠" 이면서, 동시에 "공유할 수 있는 것" 이고, 또 "댓글을 달 수 있는 것" 이기도 하잖아요. 한 콘텐츠가 여러 역할을 동시에 갖고 싶은 거죠. 그런데 부모는 Content 하나로 이미 꽉 차버렸어요. "공유 가능" 이나 "댓글 가능" 같은 역할을 더 물려받을 자리가 없는 거예요.

오늘은 이 답답함을 시원하게 풀어드릴게요. "부모는 하나뿐" 이라는 벽을 넘어, 한 클래스가 여러 역할을 동시에 받게 해주는 문법이 바로 오늘의 주제, 인터페이스(interface) 입니다.

조금 더 와닿게 일상으로 가볼게요. 리모컨 버튼을 생각해봐요. 리모컨에는 "전원", "볼륨 올리기", "채널 바꾸기" 같은 버튼이 있죠. 이 버튼들은 "이런 기능이 있어야 한다" 는 약속일 뿐, 그 버튼을 누르면 실제로 무슨 일이 일어나는지는 TV마다 달라요. 삼성 TV의 전원 버튼과 LG TV의 전원 버튼은 누르면 둘 다 켜지지만, 안에서 도는 동작은 제각각이죠. 인터페이스가 딱 이래요. "이런 버튼(기능)이 있어야 한다" 는 약속만 정해두고, 실제 동작은 그걸 구현하는 쪽이 채우는 거예요.

또 다른 비유는 자격증이에요. 한 사람이 운전면허도 있고, 동시에 요리사 자격증도 가질 수 있잖아요. 자격증은 "이 사람은 운전을 할 수 있다", "이 사람은 요리를 할 수 있다" 는 역할 증명이에요. 부모는 한 명뿐이지만, 자격증은 여러 개를 동시에 딸 수 있죠. 인터페이스도 똑같아요. 한 클래스가 "공유 가능" 자격증과 "댓글 가능" 자격증을 동시에 가질 수 있어요.

오늘 우리가 갈 길을 미리 그려볼게요.

  • 먼저 왜 추상 클래스만으로는 부족한지, 그래서 인터페이스가 왜 필요한지 짚어요.
  • 그다음 interface 키워드로 첫 인터페이스 Shareable(공유 가능) 을 만들어, "약속만 적는다" 가 뭔지 봐요.
  • 이어서 implementsImageContentContent 를 물려받으면서 동시에 여러 역할을 받는 모습을 봐요. 부모는 하나(extends), 역할은 여럿(implements) 이죠.
  • 콘텐츠 종류별로 역할을 다르게 끼우고(영상은 공유+댓글, 텍스트는 댓글만), instanceof 로 역할을 구분해요.
  • 인터페이스가 기본 동작을 미리 주는 default 메서드, 객체 없이 부르는 static 메서드와 상수도 차례로 익혀요.
  • 마지막으로 추상 클래스와 인터페이스, 둘을 나란히 놓고 언제 무엇을 쓸지 정리하며 마무리해요.

오늘의 실습 도메인은 지난 시간 그대로, 우리에게 익숙해진 콘텐츠(Content) 예요. Day 12에서 만든 Content·ImageContent·VideoContent·TextContent 위에 "역할" 을 얹는 거라서, 새 도메인을 또 익힐 필요가 없어요.

자, 그럼 추상 클래스의 벽이 어디서 막히는지부터 눈으로 확인하러 가볼까요?

🎯 학습 목표

  • 추상 클래스의 단일 상속 한계를 이해하고, 한 클래스가 여러 역할을 갖고 싶을 때 왜 인터페이스가 필요한지 설명할 수 있다
  • interface 키워드로 "약속만 담는" 인터페이스를 정의하고, 추상 메서드(빈칸)가 무엇인지 안다
  • implements 로 한 클래스가 extends(부모 하나) + implements(역할 여럿)를 동시에 받는 구조를 직접 만든다
  • 콘텐츠 종류마다 역할을 다르게 끼우고, instanceof 로 "이 콘텐츠가 어떤 역할을 가졌는지" 구분할 수 있다
  • 인터페이스의 default 메서드(기본 동작 물려주기), static 메서드와 상수(객체 없이 부르기)를 이해한다
  • 추상 클래스 vs 인터페이스 의 차이를 알고, 상황에 맞게 무엇을 쓸지 판단 기준을 세운다

Step 1. 인터페이스는 왜 필요할까? — 추상 클래스의 한계

지난 시간에 추상 클래스를 멋지게 배웠어요. Content 라는 뼈대 부모를 두고, 사진·영상·텍스트 자식이 그 빈칸을 채웠죠. 그러면 추상 클래스 하나로 모든 게 해결될 것 같은데, 왜 오늘 또 새로운 문법을 배우는 걸까요? 거기엔 추상 클래스만으로는 풀기 답답한 한 가지 문제가 있어요.

부모는 딱 하나만 — "단일 상속" 의 벽

자바에는 분명한 규칙이 하나 있어요. 자식은 부모를 딱 하나만 물려받을 수 있다. 이걸 단일 상속이라고 불러요. ImageContentextends Content 로 콘텐츠를 물려받았으면, 거기에 또 다른 부모를 extends 로 동시에 붙일 수가 없어요.

// 이런 건 안 돼요! 부모를 둘 동시에 extends 할 수 없어요.
// class ImageContent extends Content extends Shareable { ... }   ← 자바가 막아요

왜 자바가 이걸 막을까요? 부모가 둘이면 충돌이 생기거든요. 예를 들어 두 부모가 똑같은 이름의 메서드를 서로 다르게 만들어뒀다고 해볼게요. 그러면 자식은 둘 중 누구 것을 물려받아야 할지 알 수가 없어요. 이런 혼란을 막으려고 자바는 "부모는 한 명뿐" 이라고 못 박아둔 거예요.

그런데 한 콘텐츠는 여러 "역할" 을 동시에 갖고 싶어요

문제는 현실이 그렇게 단순하지 않다는 거예요. 인스타 사진 한 장을 다시 떠올려보세요. 이 사진은,

  • "콘텐츠" 예요 (작성자·좋아요가 있죠)
  • 동시에 "공유할 수 있는 것" 이에요 (공유 링크를 만들 수 있죠)
  • 동시에 "댓글을 달 수 있는 것" 이기도 해요

세 가지 성격을 한 몸에 갖고 있어요. 그런데 추상 클래스로는 "콘텐츠" 하나밖에 못 물려받아요. "공유 가능" 과 "댓글 가능" 이라는 나머지 두 역할은 더 입힐 방법이 없는 거죠.

여기서 발상을 살짝 바꿔볼게요. "콘텐츠다" 는 그 사진의 정체(무엇인가) 예요. 반면 "공유할 수 있다", "댓글을 달 수 있다" 는 그 사진이 할 수 있는 일(역할) 이죠. 정체는 하나지만, 할 수 있는 일은 여러 개일 수 있잖아요.

   추상 클래스(extends)         인터페이스(implements)
   "무엇인가" (정체)            "무엇을 할 수 있나" (역할)
   ┌─────────────────┐         ┌──────────────────────────┐
   │  부모는 딱 하나   │         │  역할은 여러 개 동시에 OK   │
   │                  │         │                          │
   │  Content 하나만  │         │  Shareable + Commentable │
   │  extends 가능    │         │  + ... 얼마든지 implements│
   └─────────────────┘         └──────────────────────────┘
        TV 한 대                   리모컨 버튼 여러 개

그래서 자바는 "정체" 와 "역할" 을 다른 문법으로 나눠뒀어요. 정체(부모)는 extends 로 하나만 물려받고, 역할은 인터페이스 라는 따로 마련된 문법으로 여러 개를 동시에 받게요. 인터페이스는 추상 클래스보다 더 순수하게 "이런 일을 할 수 있어야 한다" 는 약속만 담아요.

인터페이스는 "약속표" 예요

인터페이스를 한마디로 정리하면 약속표예요. "이 역할을 가지려면 이런 메서드를 반드시 만들어야 한다" 는 약속들의 목록이죠. Day 12의 추상 메서드(빈칸) 기억나시죠? 인터페이스는 그 빈칸들만 모아둔, 더 순수한 약속표라고 보면 돼요.

추상 클래스와 비교하면 이런 느낌이에요. 추상 클래스는 "공통 부분(작성자·좋아요)은 내가 채워줄게, 대신 종류마다 다른 빈칸은 너희가 채워" 라며 살을 일부 갖고 있어요. 반면 인터페이스는 처음엔 "나는 약속만 정할게, 실제 동작은 전부 너희가 채워" 라며 살을 거의 안 가져요. (오늘 뒤에서 보겠지만 default·static 같은 예외도 생겼어요. 그래도 출발점은 "순수한 약속표" 예요.)

💡 자바에서 부모는 extends 로 딱 하나만 물려받을 수 있어요(단일 상속). 그래서 한 클래스가 여러 "역할" 을 동시에 갖고 싶을 때는 인터페이스 를 써요. 추상 클래스가 "무엇인가(정체)" 를 표현한다면, 인터페이스는 "무엇을 할 수 있나(역할)" 를 표현하는 약속표예요.

자, 인터페이스가 왜 필요한지 감을 잡았어요. 말로만 들으면 아직 흐릿하죠. 다음 Step에서는 진짜 인터페이스를 하나 직접 만들어보면서 "약속만 적는다" 가 어떤 모양인지 눈으로 확인해볼게요.


Step 2. 첫 인터페이스 정의 — Shareable

이제 진짜 인터페이스를 하나 만들어볼 차례예요. 우리가 만들 첫 역할은 "공유할 수 있다" 예요. 인스타 콘텐츠는 친구에게 공유 링크를 보낼 수 있잖아요. 그 "공유 가능" 이라는 역할을 약속표로 만들어볼게요. 이름은 Shareable 이에요. 영어로 "공유할 수 있는" 이라는 뜻이죠.

class 대신 interface 라고 적어요

클래스를 만들 때는 class 라고 썼죠. 인터페이스를 만들 땐 그 자리에 interface 라고 적어요. Shareable.java 를 함께 볼게요.

// com/instagram/javabasic/domain/content/Shareable.java
public interface Shareable {

    // 구현 클래스가 반드시 채워야 할 약속 — 공유 링크 주소.
    // 본문이 없는 빈칸이에요(세미콜론으로 끝나요). 채우는 건 구현 클래스의 몫이에요.
    String getShareUrl();

    // default 메서드 — 인터페이스가 "기본 동작" 을 미리 만들어 물려줘요.
    // 구현 클래스가 따로 만들지 않아도 이 동작을 그대로 쓸 수 있어요.
    // 위에서 약속한 getShareUrl() 을 불러 공유 문구를 조립해요.
    default String share() {
        return "공유 링크가 생성됐어요: " + getShareUrl();
    }
}

맨 윗줄 public interface Shareable 을 보세요. class 가 있어야 할 자리에 interface 라고 적혀 있죠. 이 한 단어가 "이건 클래스가 아니라 약속표예요" 라고 알려주는 거예요.

getShareUrl() — 본문 없는 "약속"

안쪽의 String getShareUrl(); 을 보세요. 어디서 본 모양이죠? 맞아요, Day 12의 추상 메서드와 똑같아요. 중괄호 { } 가 없고 세미콜론 ; 하나로 끝나요. "공유 링크 주소를 돌려주는 메서드가 반드시 있어야 한다" 는 약속만 적고, 그 안에 뭘 채울지는 비워둔 빈칸이에요.

여기서 재미있는 점 하나. 추상 클래스의 빈칸에는 abstract 라는 단어를 붙였잖아요(public abstract String getType();). 그런데 인터페이스 안에서는 abstract 를 안 붙여도 돼요. 왜냐하면 인터페이스 안의 메서드는 원래부터 다 빈칸(약속) 이라는 게 기본이거든요. "여긴 어차피 약속표니까, 굳이 abstract 라고 표시 안 해도 다 약속인 줄 안다" 는 거예요. 그래서 String getShareUrl(); 처럼 깔끔하게 적기만 하면 자동으로 추상 메서드가 돼요.

 추상 클래스의 빈칸                 인터페이스의 빈칸
   public abstract String getType();   String getShareUrl();
          ↑                                  ↑
   abstract 를 붙여야 빈칸           안 붙여도 자동으로 빈칸
                                    (인터페이스는 원래 약속표라서)

share()default 는 잠깐 미뤄둘게요

그 아래에 default String share() { ... } 라는 게 보이죠? 얘는 getShareUrl() 과 다르게 중괄호 안에 내용이 들어 있어요. "약속만 담는다" 던 인터페이스인데 동작이 들어 있다니 좀 의아하실 거예요.

이건 default 메서드라고 하는, 인터페이스의 특별한 기능이에요. 인터페이스가 기본 동작을 미리 만들어서 물려주는 거죠. 그런데 이걸 지금 다 설명하면 한 번에 너무 많은 게 쏟아져요. 그래서 default 의 자세한 이야기는 Step 5에서 따로 풀게요. 지금은 "약속(getShareUrl)도 있고, 기본 동작을 미리 주는 특별한 메서드(share)도 있구나" 정도만 눈에 담아두면 충분해요.

🙋 학생 질문 — "튜터님, 인터페이스 이름은 왜 끝이 '-able' 로 끝나요?"

좋은 관찰이에요! Shareable(공유 가능), Commentable(댓글 가능)처럼 인터페이스 이름이 -able 로 끝나는 걸 자주 보게 될 거예요.

영어에서 -able 은 "~할 수 있는" 이라는 뜻이에요. share(공유하다) + able = "공유할 수 있는", comment(댓글 달다) + able = "댓글을 달 수 있는" 처럼요. 인터페이스는 "이 클래스가 무엇을 할 수 있는가(역할)" 를 표현하니까, 이름도 자연스럽게 "~할 수 있는" 형태가 되는 거죠.

물론 모든 인터페이스가 꼭 -able 로 끝나야 하는 건 아니에요. 하지만 "역할" 을 나타내는 인터페이스에는 이 이름 짓기가 잘 어울려서 자바 곳곳에서 관습처럼 쓰여요. 이름만 봐도 "아, 이건 어떤 역할을 약속하는 인터페이스구나" 하고 알아챌 수 있으니 편하죠.

💡 인터페이스는 class 대신 interface 키워드로 만들어요. 안에 적는 메서드(getShareUrl();)는 본문 없는 "약속(빈칸)" 이고, 추상 클래스와 달리 abstract 를 안 붙여도 자동으로 빈칸이 돼요. 인터페이스는 "이런 일을 할 수 있어야 한다" 는 약속표라고 기억하면 돼요.

자, 첫 약속표 Shareable 을 만들었어요. 그런데 약속표는 혼자서는 아무 일도 안 해요. 누군가 이 약속을 "내가 지킬게" 하고 받아가서 빈칸을 채워야 진짜 동작이 되죠. 다음 Step에서는 ImageContent 가 이 약속을 어떻게 받아가는지, 그것도 여러 약속을 동시에 받는 모습을 직접 볼게요.


Step 3. implements — 여러 약속을 동시에 받기

약속표(Shareable)를 만들었으니, 이제 그 약속을 누군가 받아가야 할 차례예요. 약속을 받아가는 쪽을 "구현 클래스" 라고 불러요. 약속의 빈칸을 실제로 구현(채우기)하니까요. 오늘의 주인공은 지난 시간에 만든 ImageContent 예요. 사진은 공유도 되고 댓글도 달리니까, 두 역할을 동시에 받기 딱 좋은 친구죠.

implements — "이 약속을 내가 지킬게"

부모를 물려받을 때는 extends 를 썼죠. 약속(인터페이스)을 받아갈 때는 implements 라는 키워드를 써요. 영어로 "구현하다, 실행하다" 라는 뜻이에요. "이 약속을 내가 실제로 만들어 지킬게" 라는 선언이죠.

ImageContent.java 의 맨 윗줄부터 볼게요.

// com/instagram/javabasic/domain/content/ImageContent.java
public class ImageContent extends Content implements Shareable, Commentable {

이 한 줄에 오늘의 핵심이 다 들어 있어요. 천천히 끊어 읽어볼게요.

  • public class ImageContent — 사진 콘텐츠 클래스를 만들어요 (여기까진 지난 시간 그대로)
  • extends Content — 부모 Content 를 물려받아요. 부모는 하나뿐이에요 (단일 상속)
  • implements Shareable, Commentable — 여기가 새로운 부분! "공유 가능" 과 "댓글 가능", 두 역할을 동시에 받아요

implements 뒤에 Shareable, Commentable 처럼 쉼표로 여러 개를 나열할 수 있다는 게 핵심이에요. extends 는 하나밖에 못 붙이지만, implements 는 얼마든지 여러 개를 한꺼번에 받을 수 있어요. 자격증을 여러 개 따는 것처럼요.

   public class ImageContent extends Content implements Shareable, Commentable
                              └──────┬──────┘ └──────────────┬───────────────┘
                              부모 하나 (정체)          역할 여러 개 (할 수 있는 일)
                              "사진은 콘텐츠다"      "공유도 되고 댓글도 달린다"

참고로 Commentable(댓글 가능) 은 여기서 처음 등장했는데, "댓글을 달 수 있다" 는 또 하나의 역할 약속표예요. 자세한 내부 모습은 Step 6에서 따로 펼쳐볼 거라, 지금은 "사진은 공유 역할이랑 댓글 역할을 둘 다 받았구나" 정도만 알아두면 돼요.

약속을 받았으면 빈칸을 채워야 해요

약속을 받기만 하고 빈칸을 안 채우면 어떻게 될까요? Day 12에서 배운 그 강제력이 여기서도 똑같이 작동해요. 약속(인터페이스)의 빈칸을 안 채우면 컴파일 에러가 나요. 그래서 ImageContent 는 받은 약속들을 모두 @Override 로 채워뒀어요. 채운 부분만 발췌해서 볼게요.

// Shareable 의 약속을 채워요 — 이미지의 공유 링크 주소.
// share() 는 default 라 따로 만들 필요 없이 자동으로 물려받아요.
@Override
public String getShareUrl() {
    return "instagram.com/p/img-" + getAuthorName();
}

// Commentable 의 약속을 채워요 — 댓글 달기.
// 한도에 찼으면(Commentable.isFull) 더 받지 않고 그냥 돌아가요.
@Override
public void addComment(String text) {
    if (Commentable.isFull(commentCount)) return;
    this.lastComment = text;
    this.commentCount++;
}

// Commentable 의 약속을 채워요 — 현재 댓글 수
@Override
public int getCommentCount() {
    return commentCount;
}

getShareUrl()Shareable 에서 받은 약속이에요. 사진의 공유 링크 주소를 만들어 돌려주죠. addComment()getCommentCount()Commentable 에서 받은 약속이고요. 세 메서드 모두 위에 @Override 가 붙어 있죠? Day 12에서 "부모의 빈칸을 채울 때 @Override 를 붙인다" 고 했는데, 인터페이스의 약속을 채울 때도 똑같이 @Override 를 붙여요. "내가 받은 약속을 여기서 구현했어요" 라는 표시예요.

(addComment 안의 Commentable.isFull(...) 이 뭔지는 Step 6에서 다룰 거예요. 지금은 "댓글 한도를 검사하는 부분이구나" 정도로만 넘어가요.)

여기서 한 번 정리해볼게요. ImageContent 는 지금 세 군데에서 빈칸을 받아 채웠어요.

   ImageContent 가 채운 빈칸들
   ┌─────────────────────────────────────────────┐
   │  Content(부모) 에서:  getType(), preview()   │  ← extends 로 받은 빈칸
   │  Shareable 에서:      getShareUrl()          │  ← implements 로 받은 약속
   │  Commentable 에서:    addComment(),          │  ← implements 로 받은 약속
   │                       getCommentCount()      │
   └─────────────────────────────────────────────┘

부모에서 받은 빈칸이든, 인터페이스에서 받은 약속이든, 자식은 전부 채워야 일반 클래스가 돼요. ImageContent 는 다섯 개를 다 채웠으니 new ImageContent(...) 로 멀쩡히 만들 수 있어요.

한 객체를 "콘텐츠" 로도, "공유 가능" 으로도, "댓글 가능" 으로도 볼 수 있어요

인터페이스의 진짜 매력은 지금부터예요. ImageContent 객체 하나를 만들었을 때, 이걸 여러 타입으로 바라볼 수 있어요.

ImageContent image = new ImageContent("minji", 120, "beach.jpg");

Content content = image;        // "이건 콘텐츠다" 로 보기
Shareable shareable = image;    // "이건 공유 가능한 것이다" 로 보기
Commentable commentable = image; // "이건 댓글 가능한 것이다" 로 보기

같은 사진 객체 하나인데, Content 타입으로도, Shareable 타입으로도, Commentable 타입으로도 받을 수 있어요. Day 11에서 배운 업캐스팅 기억나시죠? "관리자도 회원이다" 가 참이라 Member 타입에 담을 수 있었잖아요. 마찬가지로 "사진도 공유 가능한 것이다" 가 참이니 Shareable 타입에 담을 수 있는 거예요.

이게 왜 좋을까요? 예를 들어 "공유 가능한 것들만 모아서 공유 링크를 뽑는 기능" 을 만든다고 해볼게요. 그럼 그 기능은 Shareable 타입만 알면 돼요. 그게 사진인지 영상인지는 신경 쓸 필요 없이, "공유할 수 있다" 는 역할만 보고 일을 처리할 수 있죠. 역할 단위로 코드를 다룰 수 있다는 게 인터페이스의 큰 힘이에요.

이 다중 구현 동작은 코드베이스의 ContentRoleTest.java 에서 검증돼 있어요. ImageContent 객체를 Shareable 로도 Commentable 로도 받아서 각각의 역할이 제대로 동작하는지 확인하죠.

💡 implements 는 인터페이스(약속)를 받아가는 키워드예요. extends 는 부모 하나만 붙이지만, implementsShareable, Commentable 처럼 쉼표로 여러 역할을 동시에 받을 수 있어요. 받은 약속의 빈칸은 @Override 로 모두 채워야 하고, 채운 뒤엔 같은 객체를 Content·Shareable·Commentable 등 여러 타입으로 바라볼 수 있어요.

자, 한 콘텐츠가 여러 역할을 동시에 받는 걸 봤어요. 그런데 모든 콘텐츠가 똑같은 역할을 가질 필요는 없죠. 다음 Step에서는 콘텐츠 종류마다 역할을 다르게 끼우는 모습을 보고, "이 콘텐츠가 어떤 역할을 가졌는지" 구분하는 방법도 배워볼게요.


Step 4. 콘텐츠 종류별 역할 다르게 — 역할 분화

지난 Step에서 ImageContent 가 공유·댓글 두 역할을 받았죠. 그런데 모든 콘텐츠가 같은 역할을 가질까요? 현실은 그렇지 않아요. 영상은 사진처럼 공유도 되고 댓글도 달리지만, 글만 있는 텍스트 콘텐츠는 댓글은 받아도 공유는 안 한다고 정해볼 수 있어요. 인터페이스의 진짜 매력이 여기서 드러나요. 필요한 역할만 골라 끼울 수 있다는 거죠.

영상은 둘 다, 텍스트는 댓글만

먼저 VideoContent 의 맨 윗줄을 볼게요.

// com/instagram/javabasic/domain/content/VideoContent.java
public class VideoContent extends Content implements Shareable, Commentable {

영상은 사진과 똑같아요. extends Content 로 콘텐츠를 물려받고, implements Shareable, Commentable 로 공유·댓글 두 역할을 다 받았죠.

이번엔 TextContent 의 맨 윗줄을 볼게요.

// com/instagram/javabasic/domain/content/TextContent.java
public class TextContent extends Content implements Commentable {

차이가 보이시나요? 텍스트는 implements Commentable 만 적었어요. Shareable 이 빠졌죠. "텍스트는 댓글은 받지만 공유는 안 한다" 는 결정을 이 한 줄로 표현한 거예요. 그래서 TextContent 안에는 getShareUrl() 이 없어요. 받지 않은 약속은 채울 필요가 없으니까요.

코드의 주석에도 이 의도가 또렷이 적혀 있어요.

// 여기가 역할 분화의 핵심이에요 — 텍스트는 댓글은 받지만(Commentable) 공유는 안 해요(Shareable 없음).
// 그래서 implements 에 Commentable 만 적어요.

세 콘텐츠가 받은 역할을 표로 한눈에 정리하면 이래요.

                 공유 가능        댓글 가능
                 (Shareable)     (Commentable)
   ImageContent     O               O
   VideoContent     O               O
   TextContent      X               O      ← 텍스트만 공유 역할이 없어요

이게 인터페이스가 추상 클래스와 다른 점이에요. 추상 클래스로 상속하면 부모가 가진 걸 자식이 통째로 다 물려받죠. 하지만 인터페이스는 클래스마다 필요한 역할만 골라 끼울 수 있어요. 리모컨에 비유하면, 어떤 리모컨엔 "녹화" 버튼이 있고 어떤 리모컨엔 없는 것처럼요. 기기마다 필요한 버튼(역할)만 달아두는 거죠.

instanceof 로 "이 콘텐츠가 공유 가능한지" 구분해요

이제 이런 상황을 생각해볼게요. 콘텐츠들이 배열에 섞여 있는데, 그중 "공유 가능한 것" 만 골라 공유 링크를 뽑고 싶어요. 그런데 텍스트는 공유 역할이 없으니 getShareUrl() 을 부르면 안 되겠죠. 어떻게 "이 콘텐츠가 공유 가능한 역할인지" 를 알 수 있을까요?

여기서 Day 11에서 배운 instanceof 가 다시 등장해요. instanceof 는 "이 객체가 어떤 타입인지" 를 확인하는 도구였죠. 그때는 자식 타입인지 확인했는데, 인터페이스도 똑같이 확인할 수 있어요. "이 객체가 Shareable 역할을 가졌니?" 하고 물어보는 거예요.

Content content = new TextContent("seungwoo", 12, "오늘 점심 맛있었다");

if (content instanceof Shareable shareable) {
    // 공유 가능한 콘텐츠일 때만 이 안으로 들어와요
    System.out.println(shareable.getShareUrl());
} else {
    // 텍스트는 Shareable 이 아니라서 여기로 빠져요
    System.out.println("이 콘텐츠는 공유할 수 없어요");
}

content instanceof Shareable shareable 한 줄을 읽어볼게요. "content 가 Shareable 역할을 가졌으면, 그걸 shareable 이라는 이름으로 받아라" 는 뜻이에요. Day 11에서 배운 그 패턴 그대로예요. instanceof 로 역할을 확인하면서 동시에 변수까지 만들어주는 거죠.

만약 content 가 사진이나 영상이면 Shareable 역할을 가졌으니 if 안으로 들어가서 공유 링크를 뽑아요. 하지만 위 코드처럼 텍스트라면 Shareable 이 아니니까 else 로 빠져서 "공유할 수 없어요" 가 나오죠. 이렇게 instanceof 로 역할을 미리 확인하면, 공유 역할이 없는 콘텐츠에 getShareUrl() 을 잘못 부르는 사고를 막을 수 있어요.

"텍스트는 Commentable 이지만 Shareable 은 아니다" 라는 이 역할 분화도 코드베이스의 ContentRoleTest.java 에서 검증돼 있어요. 텍스트 객체에 instanceof Commentable 은 참, instanceof Shareable 은 거짓이 나오는 걸 확인하죠.

🙋 학생 질문 — "튜터님, 텍스트도 그냥 공유 역할 받아두면 편하지 않아요? 왜 일부러 빼요?"

좋은 질문이에요! "이왕이면 다 받아두지" 싶은 마음, 충분히 이해해요. 하지만 역할을 정직하게 나누는 데는 분명한 이유가 있어요.

인터페이스는 "이 클래스는 이런 일을 할 수 있다" 는 약속이자 보증이에요. 만약 텍스트에 Shareable 을 붙여두면, 다른 코드에서 "아, 이 텍스트는 공유할 수 있구나" 하고 믿고 getShareUrl() 을 부르겠죠. 그런데 사실 텍스트엔 공유할 마땅한 게 없어서 빈 문자열이나 엉뚱한 값을 돌려준다면? 약속을 어긴 셈이 돼요. 거짓 보증을 한 거죠.

그래서 "정말 그 역할을 제대로 해낼 수 있는 클래스만 그 인터페이스를 받는다" 가 좋은 습관이에요. 텍스트가 Shareable 을 안 받으면, instanceof Shareable 로 걸러질 때 자연스럽게 빠지니까 오히려 안전해요. 역할을 정직하게 나눠두면, 코드만 봐도 "이 콘텐츠가 뭘 할 수 있고 뭘 못 하는지" 가 한눈에 보이는 거예요.

💡 인터페이스는 클래스마다 필요한 역할만 골라 끼울 수 있어요. 영상은 Shareable, Commentable 둘 다, 텍스트는 Commentable 만 받았죠. 그리고 instanceof Shareable 로 "이 콘텐츠가 공유 가능한 역할인지" 를 확인한 뒤에만 그 역할의 메서드를 부르면, 역할이 없는 콘텐츠에 잘못 부르는 사고를 막을 수 있어요.

자, 역할을 골라 끼우고 구분하는 것까지 익혔어요. 그런데 Step 2에서 잠깐 미뤄둔 게 있었죠. Shareable 안에 있던 default 메서드 share() 말이에요. 다음 Step에서는 "약속만 담는다" 던 인터페이스가 어떻게 기본 동작까지 미리 줄 수 있는지 풀어볼게요.


Step 5. default 메서드 — 인터페이스도 동작을 미리 줄 수 있어요

Step 2에서 Shareable 안에 share() 라는 좀 특이한 메서드가 있었죠. getShareUrl() 은 본문 없는 빈칸이었는데, share() 는 중괄호 안에 동작이 들어 있었어요. "인터페이스는 약속만 담는다" 고 했는데 동작이 들어 있으니 의아했을 거예요. 이제 그 정체를 밝힐 시간이에요.

default — 인터페이스가 기본 동작을 미리 만들어 물려줘요

Shareable.javashare() 부분을 다시 볼게요.

// com/instagram/javabasic/domain/content/Shareable.java

// default 메서드 — 인터페이스가 "기본 동작" 을 미리 만들어 물려줘요.
// 구현 클래스가 따로 만들지 않아도 이 동작을 그대로 쓸 수 있어요.
// 위에서 약속한 getShareUrl() 을 불러 공유 문구를 조립해요.
default String share() {
    return "공유 링크가 생성됐어요: " + getShareUrl();
}

메서드 이름 앞에 default 라는 단어가 붙어 있죠. 이게 default 메서드예요. "기본으로 제공하는" 이라는 뜻이에요. 인터페이스가 "이 동작은 내가 기본으로 만들어줄 테니, 구현 클래스는 따로 안 만들어도 돼" 라며 미리 살을 채워주는 메서드예요.

getShareUrl() 과 비교하면 역할이 또렷이 갈려요.

   getShareUrl()  ← 빈칸 (약속만). 구현 클래스가 반드시 채워야 함
                     사진/영상마다 공유 주소가 다르니 부모가 정할 수 없어요

   share()        ← default (기본 동작 완성). 구현 클래스가 안 채워도 됨
                     "공유 링크가 생성됐어요: " + 주소 조립은 어디서나 똑같으니
                     인터페이스가 미리 만들어줘요

getShareUrl() 은 클래스마다 달라요. 사진은 instagram.com/p/img-minji, 영상은 instagram.com/p/vid-jaehoon 처럼요. 그래서 인터페이스가 미리 정할 수 없고, 빈칸으로 둬서 각 클래스가 채워요.

반면 share() 가 하는 일 — "공유 링크가 생성됐어요: " 뒤에 주소를 붙여 문구를 만드는 것 — 은 사진이든 영상이든 똑같아요. 주소만 다를 뿐 문구 조립 방식은 공통이죠. 그러니 인터페이스가 한 번만 만들어두고 모두에게 물려주는 거예요. 그 안에서 클래스마다 다른 주소는 빈칸인 getShareUrl() 을 불러서 채워요. (Day 12 템플릿 메서드의 "틀은 공통, 빈칸은 각자" 와 똑같은 아이디어죠!)

구현 클래스는 share() 를 안 만들어도 그냥 써요

그래서 ImageContentVideoContent 를 다시 보면, 둘 다 getShareUrl() 은 채웠지만 share() 는 만들지 않았어요. 만들 필요가 없거든요. 인터페이스가 준 기본 동작을 그대로 물려받아 쓰면 되니까요.

ImageContent image = new ImageContent("minji", 120, "beach.jpg");

System.out.println(image.share());
// 출력: 공유 링크가 생성됐어요: instagram.com/p/img-minji

ImageContent 어디에도 share() 본문이 없는데, image.share() 가 멀쩡히 동작해요. Shareable 인터페이스가 준 default 동작이 불려서, 그 안에서 image 가 채운 getShareUrl()("instagram.com/p/img-minji")이 합쳐진 거죠.

이 default 동작도 코드베이스의 ContentRoleTest.java 에서 검증돼 있어요. 이미지의 share()"공유 링크가 생성됐어요: instagram.com/p/img-minji" 를, 영상의 share()"공유 링크가 생성됐어요: instagram.com/p/vid-jaehoon" 를 돌려주는 걸 확인하죠.

default 라는 게 생겼을까요?

여기서 한 가지 궁금증. 인터페이스는 원래 "약속만 담는 순수한 약속표" 였다면서, 왜 default 같은 기본 동작 기능이 생겼을까요? 여기엔 현실적인 이유가 있어요.

이런 상황을 상상해볼게요. 어떤 인터페이스를 이미 100개의 클래스가 implements 하고 있다고 해봐요. 그런데 그 인터페이스에 새 약속(빈칸)을 하나 추가하고 싶어졌어요. 만약 그냥 빈칸으로 추가하면? Day 12에서 배운 그 강제력 때문에, 100개 클래스가 전부 그 빈칸을 채워야 해요. 안 채우면 전부 컴파일 에러가 나죠. 새 기능 하나 추가하려다 멀쩡하던 클래스 100개가 다 망가지는 거예요.

default 는 바로 이 문제를 풀어줘요. 새 메서드를 default로 추가하면, 기본 동작이 이미 들어 있으니 기존 100개 클래스는 아무것도 안 고쳐도 그대로 잘 돌아가요. 필요한 클래스만 골라서 자기 식대로 바꾸면 되고요. "기존 코드를 안 깨면서 인터페이스에 새 기능을 더할 수 있게" 만들어진 장치인 거죠.

💡 default 메서드는 인터페이스가 기본 동작을 미리 만들어 물려주는 메서드예요. 클래스마다 다른 부분(getShareUrl())은 빈칸으로 두고, 어디서나 똑같은 부분(share() 문구 조립)은 default로 미리 채워주죠. 구현 클래스는 default 메서드를 따로 안 만들어도 그대로 가져다 써요. 덕분에 기존 코드를 깨지 않고 인터페이스에 새 기능을 더할 수 있어요.

자, 인터페이스가 기본 동작도 줄 수 있다는 걸 봤어요. 그런데 인터페이스에는 동작을 담는 또 다른 방법, 그리고 값을 담는 방법도 있어요. 다음 Step에서 Commentable 을 펼쳐보면서 static 메서드와 상수를 마저 익혀볼게요.


Step 6. static 메서드 & 상수 — Commentable

이번엔 Step 3·4에서 이름만 나왔던 Commentable(댓글 가능) 을 제대로 펼쳐볼 차례예요. 여기엔 인터페이스가 가진 또 다른 두 가지 기능이 들어 있어요. 객체 없이 부르는 static 메서드, 그리고 변하지 않는 값을 담는 상수예요.

Commentable 전체를 펼쳐봐요

Commentable.java 를 통째로 볼게요. 짧으니 한눈에 들어와요.

// com/instagram/javabasic/domain/content/Commentable.java
public interface Commentable {

    // 인터페이스 안의 변수는 자동으로 public static final 이에요(상수).
    // 한 콘텐츠에 달 수 있는 댓글의 최대 개수예요.
    int MAX_COMMENTS = 100;

    // 약속 — 댓글을 단다 / 현재 댓글 수를 돌려준다. 본문은 구현 클래스가 채워요.
    void addComment(String text);
    int getCommentCount();

    // static 메서드 — 객체 없이 인터페이스 이름으로 바로 부를 수 있어요(Commentable.isFull(...)).
    // 지금 댓글 수가 한도에 찼는지 검사하는 공통 도우미예요.
    static boolean isFull(int currentCount) {
        return currentCount >= MAX_COMMENTS;
    }
}

세 가지 부분으로 나눠 읽어볼게요. 맨 위는 상수(MAX_COMMENTS), 가운데는 약속(addComment·getCommentCount), 맨 아래는 static 메서드(isFull)예요. 가운데 약속 두 개는 Step 3에서 본 빈칸과 똑같으니, 새로운 두 가지(상수·static)에 집중해볼게요.

MAX_COMMENTS — 인터페이스 안의 변수는 자동으로 "상수"

맨 위의 int MAX_COMMENTS = 100; 을 보세요. 클래스 안에서 봤던 필드 선언처럼 생겼죠. 그런데 인터페이스 안에 변수를 적으면 특별한 일이 일어나요. 자동으로 "상수" 가 돼요.

상수가 뭐냐면, 한번 정해지면 절대 바뀌지 않는 값이에요. MAX_COMMENTS = 100 이라고 정해두면, 프로그램 어디서도 이 값을 99나 101로 바꿀 수 없어요. "댓글은 최대 100개" 라는 규칙을 코드 한 군데에 못 박아두는 거죠.

   클래스 안의 변수            인터페이스 안의 변수
   int count = 0;             int MAX_COMMENTS = 100;
        ↑                            ↑
   바꿀 수 있는 값            자동으로 상수 (못 바꿈)
   (객체마다 따로 가짐)        (모두가 공유하는 고정값)

주석에 "자동으로 public static final 이에요" 라고 적혀 있죠. final 이 "한번 정하면 못 바꾼다" 는 뜻이고, static 이 "객체마다 따로 갖지 않고 모두가 공유한다" 는 뜻이에요. 인터페이스 안의 변수는 이 표시들을 안 붙여도 자동으로 다 붙어요. 그래서 그냥 int MAX_COMMENTS = 100; 이라고만 적어도 "모두가 공유하는, 절대 안 바뀌는 값" 이 되는 거예요.

이렇게 정한 상수는 어디서든 Commentable.MAX_COMMENTS 로 꺼내 쓸 수 있어요. 객체를 안 만들어도 인터페이스 이름으로 바로 부르죠. "댓글 한도는 몇이지?" 가 궁금하면 Commentable.MAX_COMMENTS 한 줄이면 돼요.

isFull() — 객체 없이 부르는 static 메서드

맨 아래 static boolean isFull(int currentCount) 를 보세요. 메서드 이름 앞에 static 이 붙어 있죠. 이게 인터페이스의 static 메서드예요.

보통 메서드는 객체를 만들어야 부를 수 있었죠. image.addComment(...) 처럼 image 라는 객체가 있어야 했잖아요. 그런데 static 메서드는 객체 없이 인터페이스 이름으로 바로 불러요.

boolean full = Commentable.isFull(100);   // true  (100은 한도에 찼어요)
boolean ok   = Commentable.isFull(99);    // false (99는 아직 여유 있어요)

Commentable.isFull(100) 처럼 인터페이스 이름에 점을 찍고 바로 부르는 거예요. 객체를 만들 필요가 없죠. isFull 이 하는 일은 단순해요. 받은 숫자가 MAX_COMMENTS(100) 이상이면 true(꽉 찼다), 아니면 false(아직 여유 있다)를 돌려줘요.

이런 걸 왜 static으로 둘까요? isFull 같은 검사는 특정 콘텐츠 하나에 묶이는 게 아니라, "댓글 수가 한도에 찼나?" 라는 공통 도우미 역할이거든요. 사진이든 영상이든 텍스트든 똑같은 기준으로 검사하니까, 굳이 객체마다 따로 둘 필요 없이 인터페이스에 공용 도구로 하나만 두는 거예요. 이 상수와 static 메서드도 코드베이스의 ContentRoleTest.java 에서 검증돼 있어요. MAX_COMMENTS 가 100이고, isFull(99) 는 거짓·isFull(100) 은 참이 나오는 걸 확인하죠.

한도 가드 — addComment 가 static 도우미를 활용해요

이제 Step 3에서 잠깐 넘어갔던 addComment 의 첫 줄이 이해될 거예요. ImageContentaddComment 를 다시 볼게요.

@Override
public void addComment(String text) {
    if (Commentable.isFull(commentCount)) return;
    this.lastComment = text;
    this.commentCount++;
}

첫 줄 if (Commentable.isFull(commentCount)) return; 을 읽어볼게요. "지금 댓글 수(commentCount)가 한도에 찼는지 Commentable.isFull 로 검사하고, 찼으면 return 으로 그냥 돌아가라(댓글을 더 받지 마라)" 는 뜻이에요. 한도를 넘기지 않게 막아주는 안전장치, 즉 가드예요.

한도에 안 찼으면 다음 두 줄로 내려가서 마지막 댓글을 기록하고 댓글 수를 하나 늘려요. 이렇게 인터페이스의 static 도우미(isFull)와 상수(MAX_COMMENTS)가, 구현 클래스의 실제 동작(addComment) 안에서 한도를 지키는 역할을 하는 거죠. 그래서 댓글을 105번 달려고 해도 100에서 딱 멈춰요. 이 한도 동작 역시 ContentRoleTest.java 에서 검증돼 있어요.

그런데 여기서 눈치채셨을 수도 있어요. 지금은 마지막 댓글 하나(lastComment)와 개수(commentCount)만 기억하고 있죠. 진짜라면 댓글 목록 전체를 차곡차곡 보관해야 할 텐데요. 여러 개의 값을 줄줄이 담아두는 도구는 다음 단계에서 따로 배울 거라, 오늘은 "개수를 세고 한도를 지킨다" 까지만 다뤘어요. 댓글을 통째로 모아 보관하는 방법은 그때 만나요.

💡 인터페이스 안의 변수(MAX_COMMENTS = 100)는 자동으로 상수가 돼서 절대 안 바뀌고, Commentable.MAX_COMMENTS 로 어디서나 꺼내 써요. static 메서드(isFull)는 객체 없이 Commentable.isFull(...) 로 바로 부르는 공통 도우미예요. 구현 클래스의 addComment 는 이 둘을 활용해 댓글이 한도를 넘지 않게 지켜요.

자, 이제 인터페이스의 거의 모든 조각을 봤어요. 약속(빈칸), default 메서드, static 메서드, 상수까지요. 그러면 마지막 질문이 남아요. "오늘 배운 인터페이스랑 지난 시간 배운 추상 클래스, 비슷해 보이는데 대체 뭐가 다르고 언제 뭘 써야 하지?" 다음 Step에서 둘을 나란히 놓고 깔끔하게 정리할게요.


Step 7. 추상 클래스 vs 인터페이스 — 언제 무엇을 쓸까

드디어 마지막 Step이에요. 지난 시간 추상 클래스, 오늘 인터페이스. 둘 다 "빈칸을 걸어 자식에게 채우게 한다" 는 점이 닮아서 헷갈리기 쉬워요. 이제 둘을 다 직접 만들어봤으니, 나란히 놓고 차이를 깔끔하게 가를 수 있어요. (Day 12에서 약속드린 대로, 양쪽을 다 익힌 다음에 비교하는 거예요.)

"이다(is-a)" 와 "할 수 있다(can-do)"

가장 본질적인 차이는 이 한 쌍의 말로 정리돼요.

  • 추상 클래스는 "무엇이다(is-a)" 를 표현해요. "이미지는 콘텐츠", "관리자는 회원이다" 처럼 정체를 나타내죠.
  • 인터페이스는 "무엇을 할 수 있다(can-do)" 를 표현해요. "이미지는 공유할 수 있다", "이미지는 댓글을 달 수 있다" 처럼 역할을 나타내죠.
   추상 클래스 (is-a, 정체)        인터페이스 (can-do, 역할)
   "이미지는 콘텐츠다"             "이미지는 공유할 수 있다"
   ┌────────────────┐            ┌──────────────────────┐
   │  딱 하나만      │            │  여러 개 동시에 OK     │
   │  (단일 상속)    │            │  (다중 구현)          │
   │  공통 필드+동작  │            │  순수한 역할 약속      │
   │  을 함께 물려줌  │            │  (default·static 예외)│
   └────────────────┘            └──────────────────────┘

우리 콘텐츠 예제가 딱 이 그림이에요. ImageContentContent "이고"(extends, 하나), 동시에 Shareable·Commentable 을 "할 수 있어요"(implements, 여럿). 정체는 하나로 고정되지만 할 수 있는 일은 여러 개를 겹쳐 가질 수 있는 거죠.

핵심 차이 비교표

둘 다 직접 만들어봤으니, 차이를 표로 정리해볼게요.

따져볼 점 추상 클래스 인터페이스
키워드 abstract class / extends interface / implements
몇 개까지? 부모 하나만 (단일 상속) 여러 개 동시에 (다중 구현)
무엇을 표현? 정체 ("~이다", is-a) 역할 ("~할 수 있다", can-do)
공통 필드(상태) 가질 수 있음 (작성자·좋아요) 못 가짐 (상수만 가능)
빈칸(약속) abstract 메서드 자동으로 추상 메서드
완성된 동작 일반 메서드 (render) default 메서드 (share)
우리 예제 Content Shareable, Commentable

표에서 특히 두 줄을 눈여겨보세요. "공통 필드" 줄과 "몇 개까지" 줄이에요. 이 둘이 판단의 핵심이거든요.

추상 클래스는 authorName·likeCount 같은 공통 필드(상태)를 가질 수 있어요. 자식들이 공통으로 가질 데이터를 부모에 모아둘 수 있는 거죠. 반면 인터페이스는 상태를 못 가져요. 변하는 값을 담을 수 없고, 안 바뀌는 상수(MAX_COMMENTS)만 둘 수 있어요. 그래서 "여러 자식이 공통 데이터를 나눠 가져야 한다" 면 추상 클래스가 어울려요.

그리고 추상 클래스는 하나만 물려받지만 인터페이스는 여러 개를 받을 수 있죠. "한 클래스에 여러 역할을 겹쳐 입혀야 한다" 면 인터페이스밖에 답이 없어요.

무엇을 쓸지 판단하는 흐름

새로 뭔가를 설계할 때, 이 순서로 자문해보면 답이 나와요.

  새로 무언가를 정의할 때
        │
        ▼
  "~이다(정체)" 인가, "~할 수 있다(역할)" 인가?
        │
   ┌────┴──────────────────────────┐
  "~이다" (정체)               "~할 수 있다" (역할)
   │                              │
   ▼                              ▼
  공통 필드(상태)를            여러 개를 동시에
  자식들이 나눠 갖나?          겹쳐 입혀야 하나?
   │                              │
   ▼                              ▼
  그렇다 → 추상 클래스         그렇다 → 인터페이스
  (예: Content)               (예: Shareable, Commentable)

한 줄로 외우면 이래요. 상태(필드)와 공통 동작을 물려줘야 하면 추상 클래스, 순수한 역할 약속이고 여러 개를 겹쳐야 하면 인터페이스. 그리고 둘은 배타적이지 않아요. 우리 ImageContent 처럼 추상 클래스 하나(extends Content) + 인터페이스 여럿(implements Shareable, Commentable)을 함께 쓰는 게 아주 흔한 그림이에요.

🙋 학생 질문 — "튜터님, default 메서드가 생겼으면 인터페이스도 동작을 가지잖아요. 그럼 추상 클래스랑 거의 같은 거 아니에요?"

정말 예리한 질문이에요! default 메서드 덕분에 인터페이스도 동작을 가질 수 있게 됐으니, 경계가 흐려진 것처럼 보이죠.

하지만 결정적인 차이가 두 개 남아 있어요. 첫째, 인터페이스는 상태(변하는 필드)를 못 가져요. 추상 클래스는 authorName 처럼 객체마다 다른 값을 담는 필드를 가질 수 있지만, 인터페이스는 안 바뀌는 상수만 가능해요. "데이터를 품고 있느냐" 가 큰 갈림길이에요.

둘째, 인터페이스는 여러 개를 동시에 구현할 수 있어요. 추상 클래스는 아무리 default 비슷한 걸 흉내 내도 하나만 물려받을 수 있죠. "여러 역할을 겹쳐 입힌다" 는 건 여전히 인터페이스만 할 수 있는 일이에요.

그래서 default가 생겼어도 둘은 분명히 다른 도구예요. "상태를 품고 정체를 표현하면 추상 클래스, 상태 없이 역할만 여러 겹 입히면 인터페이스" 라는 기준은 그대로 유효해요.

💡 추상 클래스는 "~이다(정체)" 를 표현하고 공통 필드(상태)를 가지며 하나만 물려받아요. 인터페이스는 "~할 수 있다(역할)" 를 표현하고 상태 없이 약속만 담으며 여러 개를 동시에 받아요. 상태와 공통 동작을 물려줘야 하면 추상 클래스, 역할을 여러 겹 입혀야 하면 인터페이스예요. 둘은 함께 써도 좋고요(ImageContent 가 그 예).

작은 역할로 쪼개면 유연해져요

마지막으로 한 가지만 더 짚고 갈게요. 오늘 우리는 "공유 가능", "댓글 가능" 처럼 역할을 작게 쪼개서 따로따로 만들었어요. 하나의 거대한 약속표에 모든 걸 욱여넣지 않고요.

이게 왜 좋을까요? 텍스트 콘텐츠를 떠올려보세요. 텍스트는 댓글은 받지만 공유는 안 했죠. 만약 "공유 + 댓글" 을 한 덩어리 약속으로 묶어놨다면, 텍스트는 공유를 안 하는데도 억지로 공유 빈칸까지 채워야 했을 거예요. 하지만 역할을 작게 쪼개뒀으니, 텍스트는 Commentable 만 골라 받고 Shareable 은 안 받으면 그만이에요. 필요한 역할만 조립하듯 끼우는 거죠.

작은 역할(약속) 여러 개로 쪼개두면, 클래스마다 필요한 것만 골라 끼울 수 있어서 훨씬 유연해져요. 이게 사실 "좋은 설계" 의 출발점이에요. 다음 시간엔 바로 이 "좋은 설계란 무엇인가" 를 본격적으로 이야기해볼 거예요. 오늘 역할을 쪼갠 경험이 그때 큰 힌트가 될 거예요.

자, 이걸로 인터페이스의 모든 조각을 직접 손으로 만들어봤어요. 처음엔 "추상 클래스랑 뭐가 다른 거지?" 싶었을 텐데, 이제는 둘을 나란히 놓고 "이건 정체, 저건 역할" 이라고 가를 수 있게 됐죠. 정말 수고 많으셨어요!


마무리

오늘은 "부모는 하나뿐" 이라는 추상 클래스의 벽을 넘는 여정이었어요. 한 콘텐츠가 여러 역할을 동시에 갖고 싶다는 요구에서 출발해, 인터페이스라는 약속표로 그걸 시원하게 풀어냈죠. 머릿속에 흩어진 조각들을 한 번에 모아 정리해볼게요.

오늘 배운 것 한눈에 정리

  • 인터페이스 = 역할 약속표interface 키워드로 만들고, "이런 일을 할 수 있어야 한다" 는 약속(빈칸)만 담아요. 추상 클래스가 "정체(~이다)" 라면 인터페이스는 "역할(~할 수 있다)" 이에요.
  • implements 로 다중 구현extends 는 부모 하나뿐이지만, implements Shareable, Commentable 처럼 여러 역할을 동시에 받을 수 있어요. 받은 약속은 @Override 로 다 채워야 하고, instanceof 로 "어떤 역할을 가졌는지" 구분할 수 있어요.
  • 역할 분화 — 콘텐츠 종류마다 필요한 역할만 골라 끼워요. 이미지·영상은 공유+댓글 둘 다, 텍스트는 댓글만 받았죠.
  • default 메서드 — 인터페이스가 기본 동작(share())을 미리 만들어 물려줘요. 클래스마다 다른 부분은 빈칸으로, 공통 부분은 default로 채우는 거죠.
  • static 메서드 & 상수 — 인터페이스 안의 변수(MAX_COMMENTS)는 자동으로 상수가 되고, static 메서드(isFull)는 객체 없이 Commentable.isFull(...) 로 바로 불러요.
  • 추상 클래스 vs 인터페이스 — 상태(필드)와 공통 동작을 물려줘야 하면 추상 클래스, 순수한 역할을 여러 겹 입혀야 하면 인터페이스. 둘은 함께 써도 좋아요.

인터페이스를 쓸까 말까, 이렇게 물어보세요

새 무언가를 설계하다 "이거 인터페이스로 할까, 추상 클래스로 할까?" 망설여질 때, 이 질문을 떠올리면 편해요.

  1. 이건 "~이다(정체)" 인가요, "~할 수 있다(역할)" 인가요? — 역할이면 인터페이스 후보예요.
  2. 객체마다 다른 데이터(상태)를 품어야 하나요? — 그렇다면 추상 클래스가 어울려요. 인터페이스는 상태를 못 가지니까요.
  3. 한 클래스에 여러 개를 겹쳐 입혀야 하나요? — 그렇다면 인터페이스밖에 답이 없어요.

💡 인터페이스의 핵심은 두 가지예요. 하나는 "여러 역할을 동시에 입힐 수 있다(다중 구현)", 또 하나는 "순수한 역할 약속이다(상태는 없고 약속·default·static만)". 한 클래스가 부모 하나(extends)와 역할 여럿(implements)을 함께 갖는 구조가 아주 자연스러운 그림이에요.

다음 시간 예고 — "좋은 설계란 무엇일까?"

오늘 우리는 "공유 가능", "댓글 가능" 처럼 역할을 작게 쪼개서 따로 만들었어요. 그래서 텍스트는 공유 역할을 빼고 댓글 역할만 골라 끼울 수 있었죠. 이렇게 작은 역할로 쪼개두면 필요한 것만 조립할 수 있어 유연하다는 걸 맛봤어요.

그런데 "그럼 대체 무엇이 좋은 설계인가?" 하는 더 큰 질문이 남아요. 역할은 얼마나 잘게 쪼개는 게 좋을까요? 새 기능이 생겼을 때 기존 코드를 안 건드리고 추가하려면 어떻게 짜야 할까요? 다음 시간엔 지금까지 배운 클래스·상속·다형성·추상 클래스·인터페이스를 한데 모아, "좋은 설계가 무엇인지" 를 차근차근 짚어볼 거예요. 오늘 역할을 쪼갠 경험이 그 출발점이 될 거예요. 그럼 다음 시간에 만나요!


과제

오늘 배운 interface, implements, 다중 구현, default·static 메서드, instanceof 로 역할 구분하기를 손에 익히는 과제 세 개예요. 모두 코드베이스의 Content·Shareable·Commentable·ImageContent 계열을 곁눈질로 참고하되, 새로 만드는 부분은 직접 손으로 짜보는 게 핵심이에요. 도메인 코드(Content 계열)는 한 줄도 안 고쳐도 새 역할을 입힐 수 있다는 걸 직접 확인해보세요.

과제 1: [기본] 새 역할 인터페이스 Playable 만들고 음성 콘텐츠에 입히기

오늘 Shareable·Commentable 두 역할을 봤죠. 이번엔 "재생할 수 있다" 는 새 역할을 직접 만들어, 음성 콘텐츠에 입히는 과제예요. 인스타에는 사진·영상뿐 아니라 음성 게시물도 있다고 상상해볼게요. 음성은 틀어서 들을 수 있고(재생), 댓글도 달리지만, 공유 링크는 없다고 정해볼 거예요.

해야 할 일:

새 역할 인터페이스 Playable 을 만들고, 음성 콘텐츠 AudioContentContent 를 물려받으면서 PlayableCommentable 두 역할을 받게 하세요.

요구사항:

  • 인터페이스 Playable 을 만드세요. 추상 메서드 int getDurationSeconds();(재생 길이 초) 를 약속(빈칸)으로 걸고, default 메서드 default String play() 를 만들어 "▶ 재생을 시작해요 (" + getDurationSeconds() + "초)" 를 돌려주게 하세요.
  • AudioContent 클래스를 만들어 extends Content implements Playable, Commentable 로 선언하세요. (Shareable 은 받지 않아요 — 음성은 공유 안 함.)
  • 부모의 빈칸 getType()"음성", preview() 는 작성자와 재생 시간이 보이는 문구(예: getAuthorName() + " 님의 음성 (" + durationSeconds + "초)")로 채우세요.
  • Playable 의 약속 getDurationSeconds()Commentable 의 약속 addComment()·getCommentCount()@Override 로 채우세요. (댓글 처리는 ImageContent 를 그대로 따라 하면 돼요.)
  • AudioContent 객체를 만들어 play() 를 호출하고, default 메서드가 따로 안 만들었는데도 동작하는지 확인하세요.

힌트:

  • ImageContent 를 옆에 두고 따라 만들면 쉬워요. 공유(getShareUrl) 대신 재생(getDurationSeconds)이 들어가는 정도의 차이예요.
  • 새 역할 Playable 을 만들고 새 클래스에 입히는 동안, 기존 Content·Shareable·Commentable 은 한 글자도 안 고쳐도 된다는 걸 확인해보세요. 역할을 따로 떼어 만드는 인터페이스의 힘이에요.

과제 2: [응용] 배열에서 "공유 가능한" 콘텐츠만 골라 링크 뽑기

여러 종류의 콘텐츠가 배열에 섞여 있을 때, 그중 "공유할 수 있는 것" 만 골라내는 과제예요. instanceof 로 역할을 확인하는 연습이죠.

해야 할 일:

Content[] 배열에 이미지·영상·텍스트·음성을 섞어 담고, 향상된 for 로 순회하면서 공유 가능한 콘텐츠만 골라 공유 링크를 출력하세요. 그리고 공유 가능한 콘텐츠가 몇 개인지 세는 메서드도 만들어보세요.

요구사항:

  • Content[] 배열에 ImageContent·VideoContent·TextContent·AudioContent(과제 1에서 만든 것)를 한 개씩 담으세요.
  • 향상된 for 안에서 if (content instanceof Shareable s) 로 공유 가능한 것만 걸러, s.getShareUrl() 을 출력하세요. 텍스트·음성은 Shareable 이 아니라서 자동으로 빠질 거예요.
  • 공유 가능한 콘텐츠 개수를 세는 메서드 public static int countShareable(Content[] feed) 를 만드세요. 컬렉션은 아직 안 배웠으니 int 카운터 하나로 세면 돼요.
  • 이미지·영상은 링크가 나오고, 텍스트·음성은 빠져서 결과가 2개로 세어지는지 확인하세요.

힌트:

  • instanceof Shareable s 는 Day 11에서 배운 패턴 변수 그대로예요. "공유 가능하면, 그걸 s 라는 이름으로 받아라" 는 뜻이죠.
  • "공유 가능한 것만" 처럼 역할 단위로 콘텐츠를 다룰 수 있다는 게 인터페이스의 큰 장점이에요. 그게 사진인지 영상인지는 신경 쓸 필요 없이, "공유할 수 있다" 는 역할만 보고 일을 처리하는 거죠.

과제 3: [심화] 콘텐츠가 가진 역할을 보고하는 도우미 만들기

콘텐츠 하나가 어떤 역할들을 갖고 있는지 한 줄로 정리해주는 도우미를 만드는 과제예요. 여러 역할을 instanceof 로 차례로 확인하는 연습이에요.

해야 할 일:

콘텐츠를 받아서 "공유 가능, 댓글 가능" 처럼 가진 역할을 쉼표로 이어붙여 돌려주는 메서드 describeRoles 를 만드세요.

요구사항:

  • public static String describeRoles(Content content) 메서드를 만드세요.
  • instanceofShareable·Commentable·Playable 을 차례로 확인해서, 가진 역할만 골라 문자열로 이어붙이세요. 순서는 공유 → 댓글 → 재생, 구분은 쉼표와 공백(", ")으로요.
  • 결과 예시: ImageContent"공유 가능, 댓글 가능", TextContent"댓글 가능", AudioContent"댓글 가능, 재생 가능".

힌트:

  • 컬렉션 없이 String 을 이어붙이려면, 결과 문자열이 비어 있으면 첫 역할을 그대로 넣고, 이미 뭔가 들어 있으면 앞에 ", " 를 붙여 잇는 작은 도우미 메서드를 하나 더 두면 깔끔해요.
  • 같은 객체라도 "어떤 역할들을 가졌는가" 는 instanceof 로 하나하나 물어보면 알 수 있어요. 역할을 정직하게 나눠둔 덕분에, 텍스트엔 "공유 가능" 이 안 붙고 음성엔 "재생 가능" 이 붙는 거죠.

생각해볼 주제

오늘 배운 인터페이스 너머의 이야기를 세 가지 던져드릴게요. 정답이 정해진 질문이 아니에요. 직접 코드를 떠올리며 본인의 답을 만들어보세요.

1. 추상 클래스로도 되고 인터페이스로도 될 것 같을 때, 무엇을 고를까?

오늘 우리는 Content 는 추상 클래스로, Shareable·Commentable 은 인터페이스로 만들었어요. 그런데 가끔은 "이건 추상 클래스로 해도 되고 인터페이스로 해도 될 것 같은데?" 싶은 애매한 경우를 만나요.

예를 들어 "결제 가능한 것" 을 표현한다고 해볼게요. 결제에 필요한 공통 데이터(잔액 같은)가 많고 그걸 자식들이 나눠 가져야 한다면 어느 쪽이 어울릴까요? 반대로 데이터는 없고 "결제한다" 는 행동 약속만 필요하다면요? "정체(~이다)냐 역할(~할 수 있다)이냐", "공통 데이터(상태)를 품어야 하느냐", "여러 개를 겹쳐 입혀야 하느냐" 라는 오늘의 기준으로 본인만의 판단 흐름을 정리해보세요.

2. default 메서드가 생겨서 좋기만 할까?

원래 인터페이스는 "약속만 담는 순수한 약속표" 였어요. 그런데 default 메서드가 생기면서 인터페이스도 동작을 가질 수 있게 됐죠. 덕분에 기존 코드를 안 깨고 새 기능을 더할 수 있는 큰 장점이 생겼어요.

그런데 이 편리함에는 다른 얼굴도 있어요. 인터페이스가 점점 많은 default 동작을 품기 시작하면, "인터페이스는 약속, 클래스는 동작" 이라는 깔끔한 경계가 흐려질 수 있어요. 또 한 클래스가 여러 인터페이스를 구현하는데 두 인터페이스가 똑같은 이름의 default 메서드를 갖고 있다면 어떤 혼란이 생길지도 상상해보세요. default 메서드를 어디까지 쓰는 게 적당할지, 그 경계에 대한 본인의 감을 잡아보는 게 목표예요.

3. 역할은 얼마나 잘게 쪼개는 게 좋을까?

오늘 우리는 "공유 가능"(Shareable)과 "댓글 가능"(Commentable)을 따로따로 만들었어요. 덕분에 텍스트는 댓글 역할만 골라 받을 수 있었죠. 그런데 만약 이 둘을 "콘텐츠 상호작용" 이라는 하나의 큰 인터페이스로 합쳐버렸다면 어땠을까요?

반대로, 역할을 너무 잘게 쪼개면 어떨까요? "좋아요 가능", "공유 가능", "댓글 가능", "저장 가능"… 인터페이스가 수십 개로 늘어나면 그것대로 관리가 번거로울 수 있어요. 역할을 합치면 골라 끼우는 유연함을 잃고, 너무 쪼개면 인터페이스가 넘쳐나죠. 이 사이의 적당한 균형을 어디서 잡을지, "필요한 만큼만 쪼갠다" 는 게 무슨 뜻일지 본인의 생각을 정리해보세요.

✅ 예시 답안정답 보기

오늘 배운 interface, implements, 다중 구현, default·static 메서드, instanceof 로 역할 구분하기를 손에 익히는 답안이에요. 정답이 하나뿐인 건 아니에요. 아래 코드는 "이렇게 풀면 깔끔하다" 는 모범 사례 중 하나로 봐주세요. 모두 코드베이스의 Content·Shareable·Commentable·ImageContent 계열을 곁눈질로 참고하며 짜면 돼요. 도메인 코드(Content 계열)는 한 줄도 안 고쳐도 새 역할을 입힐 수 있다는 걸 직접 확인해보세요.

과제 1 예시답안: 새 역할 인터페이스 Playable 만들고 음성 콘텐츠에 입히기

"재생할 수 있다" 는 새 역할 인터페이스 Playable 을 만들고, 음성 콘텐츠 AudioContentContent 를 물려받으면서 PlayableCommentable 두 역할을 받게 하는 과제예요.

핵심 접근

이 과제의 진짜 목표는 "기존 Content·Shareable·Commentable 을 한 글자도 안 고쳐도 새 역할이 추가된다" 를 눈으로 보는 거예요. 인터페이스 Playable 은 약속(getDurationSeconds()) 하나와 기본 동작(default play()) 하나로 만들면 돼요. AudioContentImageContent 를 옆에 두고 거의 그대로 따라 만들되, 공유(Shareable) 대신 재생(Playable)을 입히는 게 차이예요. 음성은 공유를 안 하니까 implementsShareable 은 안 적고 Playable, Commentable 만 적어요. 역할을 따로 떼어 만들어두면, 콘텐츠마다 필요한 역할만 골라 끼울 수 있다는 게 인터페이스의 힘이에요.

예시 구현

먼저 새 역할 인터페이스 Playable 이에요. 약속 하나와 default 동작 하나로 끝나요.

package com.instagram.javabasic.solution.day13;

// com/instagram/javabasic/solution/day13/Playable.java
// "재생할 수 있다" 는 역할(role)을 약속하는 인터페이스예요. Shareable·Commentable 과 마찬가지로
// 콘텐츠 부모(Content)와는 따로 노는 역할이라, 어떤 콘텐츠든 "재생 가능" 이라는 역할만
// 따로 받아갈 수 있어요. 음성·영상처럼 "틀어서 듣고 보는" 콘텐츠에 입혀요.
public interface Playable {

    // 구현 클래스가 반드시 채워야 할 약속 — 재생 길이(초).
    // 본문이 없는 빈칸이에요(세미콜론으로 끝나요). 채우는 건 구현 클래스의 몫이에요.
    int getDurationSeconds();

    // default 메서드 — 인터페이스가 "기본 동작" 을 미리 만들어 물려줘요.
    // 구현 클래스가 따로 만들지 않아도 이 동작을 그대로 쓸 수 있어요.
    // 위에서 약속한 getDurationSeconds() 를 불러 재생 안내 문구를 조립해요.
    default String play() {
        return "▶ 재생을 시작해요 (" + getDurationSeconds() + "초)";
    }
}

다음은 음성 콘텐츠 AudioContent 예요. Content 를 물려받고 Playable·Commentable 두 역할을 받아요.

package com.instagram.javabasic.solution.day13;

// com/instagram/javabasic/solution/day13/AudioContent.java
// 음성 콘텐츠 — 콘텐츠의 또 다른 종류예요. Content 를 물려받아(extends) 공통 정보(작성자·좋아요)는
// 그대로 쓰고, 부모가 남겨둔 빈칸(getType·preview) 두 개를 음성에 맞게 채워요.
// 여기가 역할 분화의 핵심이에요 — 음성은 재생도 되고(Playable) 댓글도 달 수 있지만(Commentable),
// 공유는 안 해요(Shareable 없음). 그래서 implements 에 Playable·Commentable 만 적어요.
// 새 역할(Playable)을 새로 만들어 입혔는데도 기존 Content 부모는 한 줄도 고치지 않았어요.
import com.instagram.javabasic.domain.content.Commentable;
import com.instagram.javabasic.domain.content.Content;

public class AudioContent extends Content implements Playable, Commentable {

    // 음성만 추가로 갖는 정보 — 재생 길이(초)
    private int durationSeconds;

    // 댓글 역할(Commentable)을 위해 필요한 정보 — 댓글 수와 마지막 댓글 내용
    private int commentCount;
    private String lastComment;

    // 생성자 — 첫 줄 super(...) 로 부모의 공통 필드(작성자·좋아요)를 먼저 채우고,
    // 그 다음 음성만의 필드를 채워요.
    public AudioContent(String authorName, int likeCount, int durationSeconds) {
        super(authorName, likeCount);
        this.durationSeconds = durationSeconds;
    }

    // 부모의 빈칸을 채워요 — 음성의 종류 이름
    @Override
    public String getType() {
        return "음성";
    }

    // 부모의 빈칸을 채워요 — 음성은 작성자와 재생 시간을 미리보기로 보여줘요
    @Override
    public String preview() {
        return getAuthorName() + " 님의 음성 (" + durationSeconds + "초)";
    }

    // Playable 의 약속을 채워요 — 재생 길이(초).
    // play() 는 default 라 따로 만들 필요 없이 자동으로 물려받아요.
    @Override
    public int getDurationSeconds() {
        return durationSeconds;
    }

    // Commentable 의 약속을 채워요 — 댓글 달기.
    // 한도에 찼으면(Commentable.isFull) 더 받지 않고 그냥 돌아가요 (ImageContent 와 같은 방식).
    @Override
    public void addComment(String text) {
        if (Commentable.isFull(commentCount)) return;
        this.lastComment = text;
        this.commentCount++;
    }

    // Commentable 의 약속을 채워요 — 현재 댓글 수
    @Override
    public int getCommentCount() {
        return commentCount;
    }

    // 마지막으로 달린 댓글을 확인하는 일반 메서드 (인터페이스 약속은 아니에요)
    public String getLastComment() {
        return lastComment;
    }
}

객체를 만들어 play() 를 불러보면, 따로 안 만든 default 메서드가 그대로 동작해요.

// com/instagram/javabasic/solution/day13/Day13SolutionMain.java
AudioContent audio = new AudioContent("minji", 50, 30);
System.out.println(audio.play());   // ▶ 재생을 시작해요 (30초)

// 음성은 댓글과 재생은 되지만 공유는 안 돼요 (역할 분화)
System.out.println(audio instanceof Commentable); // true
System.out.println(audio instanceof Playable);    // true
System.out.println(audio instanceof Shareable);   // false

코드 해설

여기서 눈여겨볼 건 세 가지예요.

첫째, implements Playable, Commentable 두 개만 적고 Shareable 은 안 적었어요. 음성은 공유를 안 하니까요. 그래서 audio instanceof Shareablefalse 가 나와요. 콘텐츠 종류마다 필요한 역할만 골라 입힐 수 있는 게 인터페이스를 따로 떼어둔 덕분이에요.

둘째, play()AudioContent 안에 만들지 않았는데도 동작해요. Playable 인터페이스가 default 로 기본 동작을 미리 만들어 물려줬거든요. play() 안에서 부르는 getDurationSeconds()AudioContent 가 채운 30 으로 불려서, "▶ 재생을 시작해요 (30초)" 가 나와요. 약속(getDurationSeconds)은 구현 클래스가 채우고, 그 약속을 활용한 기본 동작(play)은 인터페이스가 물려주는 분담이에요.

셋째, Content 부모와 기존 역할 Shareable·Commentable한 글자도 안 고쳤어요. 새 인터페이스 Playable 한 파일과 새 클래스 AudioContent 한 파일을 더한 게 전부예요.

실행 결과

▶ 재생을 시작해요 (30초)
true
true
false

play() 를 안 만들었는데도 첫 줄이 정확히 나왔죠? default 메서드를 그대로 물려받은 증거예요. 그리고 Commentable·Playabletrue, Shareablefalse 라 음성에 입힌 역할이 정확히 두 개라는 것도 보여요. 이 동작은 코드베이스 Day13SolutionTest 에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
인터페이스 정의 Playableinterface 로 만들고 getDurationSeconds() 를 본문 없는 약속(빈칸)으로 걸었는가
default 메서드 default String play()getDurationSeconds() 를 불러 안내 문구를 조립하는가
역할 선택 implements Playable, Commentable 만 적고 Shareable 은 빼서 "음성은 공유 안 함" 을 표현했는가
두 빈칸 구현 부모의 getType()·preview() 를 음성에 맞게 채웠는가
약속 채우기 getDurationSeconds()·addComment()·getCommentCount()@Override 로 채웠는가
default 미작성 확인 play() 를 클래스에 다시 만들지 않고도 동작하는 걸 확인했는가

흔한 실수

  • play()AudioContent 안에 또 만듦Playable 이 이미 default 로 물려준 동작이라 다시 만들 필요가 없어요. getDurationSeconds() 만 채우면 play() 는 자동으로 따라와요. 굳이 만들면 중복이에요.
  • Shareable 까지 implements 에 적음 → 그러면 getShareUrl() 까지 채우라고 강제돼요. 음성은 공유를 안 하니 Shareable 을 빼야 "이 역할은 없다" 가 코드로 드러나요. 안 쓰는 역할까지 받는 건 거짓 약속이에요.
  • getDurationSeconds() 를 안 채움Playable 의 약속이라 안 채우면 "abstract method getDurationSeconds() is not implemented" 컴파일 에러가 나요. 인터페이스의 약속은 구현 클래스가 반드시 채워야 클래스가 완성돼요.

실무 개선 포인트 (심화)

지금은 음성 재생을 "▶ 재생을 시작해요 (30초)" 라는 안내 문구로만 흉내 냈어요. 실제 서비스라면 play() 가 진짜로 오디오 스트림을 트는 동작을 하겠죠. 그래도 설계의 핵심은 그대로예요 — "재생할 수 있다" 는 역할을 Playable 로 따로 떼어두면, 음성이든 영상이든 "재생 가능한 것" 끼리 똑같이 다룰 수 있어요. 나중에 영상도 Playable 을 입히면, "재생 가능한 콘텐츠를 한 번에 재생목록으로 묶기" 같은 기능을 콘텐츠 종류와 상관없이 역할만 보고 만들 수 있게 돼요. 그게 역할을 인터페이스로 떼어두는 진짜 가치예요.


과제 2 예시답안: 배열에서 공유 가능한 콘텐츠만 골라 링크 뽑기

여러 종류의 콘텐츠가 섞인 배열에서 "공유할 수 있는 것" 만 골라 링크를 뽑고, 그 개수를 세는 과제예요. instanceof 로 역할을 확인하는 연습이죠.

핵심 접근

이 과제의 진짜 목적은 "그게 사진인지 영상인지는 신경 쓰지 않고, '공유할 수 있다' 는 역할만 보고 일을 처리하는" 감각을 익히는 거예요. 향상된 for 로 배열을 순회하면서 if (content instanceof Shareable s) 로 거르면, Shareable 을 입은 콘텐츠만 통과하고 s 라는 이름으로 받아져요. 텍스트·음성은 Shareable 이 아니라 자동으로 빠지죠. 개수 세기는 컬렉션을 아직 안 배웠으니 int 카운터 하나로 세면 돼요.

예시 구현

배열에 네 종류를 한 개씩 담고, 공유 가능한 것만 골라 링크를 뽑아요.

// com/instagram/javabasic/solution/day13/Day13SolutionMain.java
Content[] feed = {
        new ImageContent("minji", 120, "beach.jpg"),
        new VideoContent("jaehoon", 340, "trip.mp4", 45),
        new TextContent("seungwoo", 12, "오늘 점심 맛있었다"),
        new AudioContent("yujin", 8, 30)
};

// 향상된 for + instanceof Shareable 로 "공유 가능한 것" 만 걸러요.
// 텍스트·음성은 Shareable 이 아니라서 자동으로 빠져요.
for (Content content : feed) {
    if (content instanceof Shareable s) {
        System.out.println(s.getShareUrl());
    }
}
System.out.println("공유 가능한 콘텐츠 개수: " + countShareable(feed)); // 2

개수를 세는 메서드는 카운터 하나로 만들어요.

// 과제 2 도우미 — 배열에서 "공유 가능한(Shareable)" 콘텐츠가 몇 개인지 세요.
// 컬렉션을 아직 안 배웠으니 카운터(int) 하나로 세고 그 수를 돌려줘요.
public static int countShareable(Content[] feed) {
    int count = 0;
    for (Content content : feed) {
        if (content instanceof Shareable) {
            count++;
        }
    }
    return count;
}

코드 해설

핵심은 if (content instanceof Shareable s) 한 줄이에요. 이건 Day 11에서 배운 패턴 변수 그대로예요. "이 contentShareable 역할을 가졌으면, 그걸 s 라는 Shareable 타입 이름으로 받아라" 는 뜻이죠. 그래서 통과한 직후 바로 s.getShareUrl() 을 부를 수 있어요. 따로 형 변환(캐스팅)을 적을 필요가 없어요.

배열의 타입은 Content[] 인데, 그 안에는 이미지·영상·텍스트·음성이 섞여 있어요. for 문은 "그게 무슨 종류인지" 를 전혀 묻지 않아요. 오직 "공유할 수 있는가(Shareable)" 라는 역할만 묻죠. 이미지·영상은 Shareable 을 입었으니 통과하고, 텍스트·음성은 입지 않았으니 그냥 빠져요.

       Content[] feed (네 종류가 섞여 있음)
   ┌──────────┬──────────┬──────────┬──────────┐
   │ Image    │ Video    │ Text     │ Audio    │
   │ Shareable│ Shareable│  (없음)  │  (없음)  │
   └────┬─────┴────┬─────┴──────────┴──────────┘
        │ 통과     │ 통과       빠짐       빠짐
        ▼          ▼
   getShareUrl  getShareUrl    → 공유 가능 개수: 2

countShareable 도 똑같은 원리예요. 역할(Shareable)이 맞을 때만 카운터를 1 올리니, 이미지·영상 둘만 세어져 2 가 나와요.

실행 결과

instagram.com/p/img-minji
instagram.com/p/vid-jaehoon
공유 가능한 콘텐츠 개수: 2

이미지·영상의 공유 링크 두 줄만 나오고, 텍스트·음성은 한 줄도 안 나왔죠? 그리고 개수도 정확히 2 예요. "공유할 수 있는 것" 이라는 역할만 보고 골라낸 결과예요. 이 동작은 코드베이스 Day13SolutionTest 에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
패턴 변수 활용 if (content instanceof Shareable s) 로 거르고 s 로 바로 getShareUrl() 을 불렀는가
역할 기준 필터 콘텐츠 종류가 아니라 Shareable 역할 유무로 걸렀는가
카운터 메서드 public static int countShareable(Content[] feed)int 카운터로 만들었는가
배열 구성 이미지·영상·텍스트·음성 네 종류를 한 개씩 담았는가
결과 정확성 링크가 이미지·영상 두 줄만 나오고 개수가 2 인가
캐스팅 불필요 확인 s 로 바로 호출해 별도 형 변환을 적지 않았는가

흔한 실수

  • 종류로 일일이 거름if (content instanceof ImageContent || content instanceof VideoContent) 처럼 종류를 하나하나 나열하면, 나중에 공유 가능한 새 종류가 생길 때마다 이 if 를 또 고쳐야 해요. Shareable 역할 하나로 거르면 새 종류가 Shareable 만 입으면 자동으로 통과돼서 이 코드는 안 건드려도 돼요.
  • 패턴 변수 없이 캐스팅if (content instanceof Shareable) 만 쓰고 안에서 ((Shareable) content).getShareUrl() 로 내려받는 건 번거로워요. instanceof Shareable s 로 받으면 s 가 바로 Shareable 이라 캐스팅이 필요 없어요.
  • 카운터를 메서드 밖에서 셈countShareable 메서드 안에서 int count = 0 으로 시작해 세고 돌려줘야 재사용이 돼요. for 문 바깥에서 손으로 세면 다른 배열에 다시 쓸 수가 없어요.

실무 개선 포인트 (심화)

지금은 공유 가능한 것만 골랐지만, 실무에서는 "댓글 가능한 것만", "재생 가능한 것만" 처럼 역할별로 골라 다루는 일이 정말 많아요. 그때마다 똑같은 모양의 for + instanceof 가 반복되죠. 나중에 컬렉션과 람다를 배우면, "조건에 맞는 것만 걸러내기" 를 한 줄로 표현하는 더 깔끔한 방법을 만나게 돼요. 지금은 그 전 단계로, "종류가 아니라 역할로 거른다" 는 사고방식 자체를 손에 익히는 게 중요해요. 이 사고방식은 도구가 바뀌어도 그대로 쓰여요.


과제 3 예시답안: 콘텐츠가 가진 역할을 보고하는 도우미 만들기

콘텐츠 하나가 어떤 역할들을 갖고 있는지 한 줄로 정리해주는 도우미를 만드는 과제예요. 여러 역할을 instanceof 로 차례로 확인하는 연습이에요.

핵심 접근

이 과제의 진짜 목적은 "같은 객체라도 어떤 역할들을 가졌는지는 instanceof 로 하나씩 물어보면 알 수 있다" 를 체감하는 거예요. Shareable·Commentable·Playable 을 순서대로 물어보면서, 가진 역할만 골라 문자열로 이어붙이면 돼요. 컬렉션이 없으니 결과 문자열을 직접 이어붙이는데, "비어 있으면 그대로 넣고, 이미 뭔가 있으면 앞에 ", " 를 붙인다" 는 작은 도우미를 따로 두면 깔끔해져요.

예시 구현

역할을 차례로 확인해 이어붙이는 describeRoles 와, 이어붙이기를 맡는 작은 도우미 append 예요.

// com/instagram/javabasic/solution/day13/Day13SolutionMain.java
// 과제 3 도우미 — 콘텐츠 하나가 가진 역할을 보고 쉼표로 이어붙여 돌려줘요.
// 순서는 공유 → 댓글 → 재생. 없는 역할은 그냥 건너뛰어요.
public static String describeRoles(Content content) {
    String result = "";

    if (content instanceof Shareable) {
        result = append(result, "공유 가능");
    }
    if (content instanceof Commentable) {
        result = append(result, "댓글 가능");
    }
    if (content instanceof Playable) {
        result = append(result, "재생 가능");
    }
    return result;
}

// 글자를 쉼표로 이어붙이는 작은 도우미 — 첫 역할이면 그대로, 두 번째부터는 ", " 를 앞에 붙여요.
private static String append(String base, String role) {
    if (base.isEmpty()) {
        return role;
    }
    return base + ", " + role;
}

배열의 세 콘텐츠로 불러보면 역할이 정직하게 갈려요.

// com/instagram/javabasic/solution/day13/Day13SolutionMain.java
System.out.println(describeRoles(feed[0])); // 공유 가능, 댓글 가능
System.out.println(describeRoles(feed[2])); // 댓글 가능
System.out.println(describeRoles(feed[3])); // 댓글 가능, 재생 가능

코드 해설

describeRolesif 세 개를 순서대로 지나가요. 각 if 는 "이 역할을 가졌니?" 를 묻고, 가졌으면 그 이름을 결과에 이어붙여요. else if 가 아니라 독립된 if 세 개라는 게 중요해요. 이미지는 Shareable·Commentable 둘 다 가졌으니 두 if 를 모두 통과해 두 역할이 다 붙거든요.

이어붙이기에서 까다로운 게 쉼표 위치예요. 첫 역할 앞에는 쉼표가 없어야 하고, 두 번째부터만 ", " 가 붙어야 하죠. 그걸 append 도우미가 맡아요. 결과가 비어 있으면(base.isEmpty()) 첫 역할이니 그대로 넣고, 이미 뭔가 있으면 앞에 ", " 를 붙여요.

describeRoles(이미지) 의 진행:
   result = ""
   instanceof Shareable?  → 예 → append("", "공유 가능")   → "공유 가능"
   instanceof Commentable? → 예 → append("공유 가능", "댓글 가능") → "공유 가능, 댓글 가능"
   instanceof Playable?   → 아니오 → 건너뜀
   결과: "공유 가능, 댓글 가능"

음성(feed[3])은 Commentable·Playable 만 가졌으니 첫 if 를 건너뛰고 뒤의 두 if 만 통과해서 "댓글 가능, 재생 가능" 이 나와요. 역할을 정직하게 나눠둔 덕분에, 텍스트엔 "공유 가능" 이 안 붙고 음성엔 "재생 가능" 이 붙는 거예요.

실행 결과

공유 가능, 댓글 가능
댓글 가능
댓글 가능, 재생 가능

이미지는 두 역할, 텍스트는 댓글 하나, 음성은 댓글과 재생 두 역할이 정확히 보고됐죠. 콘텐츠마다 가진 역할이 다르게 나오는 건 역할을 인터페이스로 따로 떼어둔 덕분이에요. 이 동작은 코드베이스 Day13SolutionTest 에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
역할 순서 ShareableCommentablePlayable 순서로 확인했는가
독립 if 세 개 else if 가 아닌 독립 if 로 여러 역할을 동시에 붙일 수 있게 했는가
쉼표 처리 첫 역할엔 쉼표 없이, 두 번째부터 ", " 로 이어붙였는가 (append 도우미 등)
결과 정확성 이미지 "공유 가능, 댓글 가능", 텍스트 "댓글 가능", 음성 "댓글 가능, 재생 가능" 가 나오는가
메서드 시그니처 public static String describeRoles(Content content) 형태로 만들었는가
도우미 분리 이어붙이기 로직을 작은 도우미 메서드로 빼서 describeRoles 가 깔끔한가

흔한 실수

  • else if 로 묶음if ... else if ... 로 쓰면 첫 번째로 맞는 역할 하나만 붙고 끝나요. 이미지는 공유·댓글 둘 다 가졌는데 "공유 가능" 만 붙죠. 여러 역할을 다 붙이려면 독립된 if 세 개여야 해요.
  • 쉼표를 무조건 붙임result = result + ", " + role 처럼 항상 앞에 쉼표를 붙이면, 첫 역할 앞에도 ", 공유 가능" 처럼 쉼표가 생겨요. "비어 있으면 그대로, 아니면 쉼표 붙이기" 로 갈라야 깔끔해요.
  • 마지막에 쉼표가 남음role + ", " 처럼 뒤에 쉼표를 붙이면 마지막 역할 뒤에 "... 재생 가능, " 처럼 쉼표가 달랑 남아요. 쉼표는 뒤가 아니라 두 번째 역할부터 앞에 붙이는 게 안전해요.

실무 개선 포인트 (심화)

지금은 역할이 셋이라 if 세 개로 충분했어요. 그런데 역할이 열 개로 늘면 if 도 열 개가 되고, 새 역할이 생길 때마다 describeRoles 를 또 손봐야 해요. 나중에 컬렉션을 배우면, 역할 이름들을 모아두고 한 번에 이어붙이는 더 깔끔한 방법을 만나게 돼요. 그래도 핵심 아이디어는 같아요 — "객체가 어떤 역할을 가졌는지 instanceof 로 물어본다" 는 거죠. 참고로 실무에서는 instanceof 를 너무 많이 늘어놓기보다, 각 역할이 스스로 자기 이름을 답하게 만드는 설계를 더 선호하기도 해요. 이런 고민은 생각해볼 주제에서 더 풀어볼게요.


생각해볼 주제 예시답안

생각해볼 주제 1 예시답안: 추상 클래스로도 되고 인터페이스로도 될 것 같을 때, 무엇을 고를까?

[문제 상황 요약]

오늘 Content 는 추상 클래스로, Shareable·Commentable 은 인터페이스로 만들었어요. 그런데 가끔 "이건 추상 클래스로 해도 되고 인터페이스로 해도 될 것 같은데?" 싶은 애매한 경우를 만나요. 예를 들어 "결제 가능한 것" 을 표현한다면 어느 쪽이 어울릴까요? 둘 다 자식/구현을 갖는데, 무엇을 기준으로 갈라야 할까요?

[튜터의 가이드 및 해설]

판단 기준을 세 가지 질문으로 잡으면 헷갈리지 않아요.

첫째, "정체(~이다)냐 역할(~할 수 있다)이냐" 예요. Content 는 "이것은 콘텐츠다" 라는 정체를 나타내요. 이미지·영상·음성은 모두 "콘텐츠인 것" 이죠. 반면 Shareable 은 "공유할 수 있다" 는 역할이에요. 이미지가 공유할 수 있다고 해서 "이미지는 공유다" 는 말이 안 되죠. 정체를 표현하면 추상 클래스, 행동 약속을 표현하면 인터페이스가 자연스러워요.

둘째, "공통 데이터(상태)를 품어 자식에게 물려줘야 하느냐" 예요. Content 는 작성자·좋아요 같은 공통 필드를 갖고 자식이 super(...) 로 그걸 채워요. 이렇게 공통 상태를 물려줘야 하면 추상 클래스만 할 수 있어요. 인터페이스는 (오늘 본 MAX_COMMENTS 같은 상수 말고는) 인스턴스 필드를 가질 수 없거든요. 결제로 치면, 잔액·결제 한도 같은 공통 데이터가 많고 그걸 자식들이 나눠 가져야 한다면 추상 클래스가 어울려요.

셋째, "여러 개를 겹쳐 입혀야 하느냐" 예요. 자바는 extends 는 하나만, implements 는 여러 개를 받을 수 있어요. 이미지가 공유도 되고 댓글도 되듯, 한 객체에 역할을 여러 겹 입혀야 하면 인터페이스여야 해요. "결제도 되고 공유도 되고 재생도 되는" 식으로 여러 능력을 겹쳐야 한다면 인터페이스가 맞아요.

  • Option A — 추상 클래스: 정체(~이다)를 나타내고, 공통 데이터를 품어 자식에게 물려줘야 할 때. 예: 결제에 필요한 잔액·한도 같은 공통 상태가 많고 자식들이 그걸 공유해야 함. 장점은 상태와 공통 동작을 한 번에 물려줌. 단점은 하나만 물려받을 수 있어 다른 부모와 겹칠 수 없음.
  • Option B — 인터페이스: 순수한 행동 약속(~할 수 있다)이고, 여러 역할을 겹쳐 입혀야 할 때. 예: 데이터 없이 "결제한다" 는 행동 약속만 필요하고, 결제·공유·재생을 한 객체에 겹쳐 입혀야 함. 장점은 여러 개를 동시에 받음. 단점은 (인스턴스) 상태를 품을 수 없음.

현업 감각으로는 결론이 이래요. 공통 상태와 공통 동작을 물려줘야 하면 추상 클래스, 순수한 역할이고 여러 겹으로 입혀야 하면 인터페이스 예요. 그리고 둘 다 필요하면 둘을 같이 써요 — 오늘 AudioContentContent(추상 클래스)를 extends 하면서 Playable·Commentable(인터페이스)을 implements 한 것처럼요.

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

"추상 클래스와 인터페이스의 선택 기준을 저는 세 질문으로 가릅니다. '정체(~이다)냐 행동 약속(~할 수 있다)이냐', '공통 상태를 품어 물려줘야 하느냐', '여러 개를 겹쳐 입혀야 하느냐' 입니다. 공통 데이터와 공통 동작을 물려줘야 하면 추상 클래스, 순수한 역할이고 여러 겹으로 입혀야 하면 인터페이스를 씁니다. 그리고 둘은 배타적이지 않아서, 콘텐츠 한 종류가 추상 클래스를 상속하면서 동시에 여러 역할 인터페이스를 구현하는 식으로 함께 쓰는 게 실제 설계에서 가장 흔합니다."


생각해볼 주제 2 예시답안: default 메서드가 생겨서 좋기만 할까?

[문제 상황 요약]

원래 인터페이스는 "약속만 담는 순수한 약속표" 였어요. 그런데 default 메서드가 생기면서 인터페이스도 동작을 가질 수 있게 됐죠. 덕분에 기존 코드를 안 깨고 새 기능을 더할 수 있는 큰 장점이 생겼어요. 그런데 이 편리함에는 다른 얼굴은 없을까요? 또 두 인터페이스가 똑같은 이름의 default 메서드를 가지면 어떤 혼란이 생길까요?

[튜터의 가이드 및 해설]

먼저 default 메서드가 빛나는 경우예요. 핵심 장점은 "기존 구현 클래스를 안 깨고 인터페이스에 기능을 더할 수 있다" 는 거예요. 만약 Playable 에 새 동작을 일반 약속(빈칸)으로 추가하면, Playable 을 구현한 모든 클래스가 그걸 채워야만 컴파일이 돼요. 클래스가 많으면 전부 손봐야 하죠. 그런데 default 로 추가하면 기본 동작이 딸려오니, 기존 클래스는 한 글자도 안 고쳐도 새 동작을 그대로 물려받아요. 오늘 play() 가 바로 이 방식이에요.

그런데 다른 얼굴이 있어요. 인터페이스가 점점 많은 default 동작을 품으면, "인터페이스는 약속, 클래스는 동작" 이라는 깔끔한 경계가 흐려져요. 인터페이스만 봐서는 "이게 순수한 약속표인지, 동작 덩어리인지" 가 안 보이기 시작하죠. 그러면 추상 클래스와 인터페이스의 구분도 모호해져요.

또 하나 골치 아픈 경우가 있어요. 한 클래스가 두 인터페이스를 구현하는데, 그 둘이 똑같은 이름의 default 메서드 를 가지면 어떻게 될까요? 예를 들어 Playabledefault String describe() 를 갖고, 또 다른 인터페이스도 default String describe() 를 가지면, 구현 클래스는 둘 중 어느 걸 물려받아야 할지 알 수가 없어요. 이걸 다이아몬드 충돌이라고 불러요. 자바는 이걸 애매하게 넘기지 않고, 구현 클래스가 직접 describe() 를 오버라이드해서 결판을 내라고 강제 해요. 안 그러면 컴파일 에러가 나죠.

  • Option A — default 메서드를 적극 활용: 여러 구현 클래스가 똑같이 쓸 기본 동작이 있고, 기존 코드를 안 깨고 추가하고 싶을 때. 장점은 기존 클래스 무수정으로 기능 확장. 단점은 인터페이스가 동작을 많이 품으면 약속/동작 경계가 흐려지고, 이름 충돌 위험이 생김.
  • Option B — default 없이 순수한 약속만 유지: 인터페이스를 "약속표" 로만 깔끔하게 두고 동작은 클래스에 맡길 때. 장점은 인터페이스가 단순하고 경계가 또렷함. 단점은 새 약속을 추가하면 모든 구현 클래스를 손봐야 함.

현업에서는 보통 이렇게 봐요. default 메서드는 "이미 배포된 인터페이스를 안 깨고 진화시키기 위한 보조 수단" 이지, 동작을 마구 담는 그릇이 아니다 예요. 새 약속을 추가해야 하는데 기존 구현 클래스를 다 손볼 수 없을 때 default 로 기본값을 주는 건 좋지만, 인터페이스가 점점 동작 덩어리가 되는 건 경계해요. 그건 추상 클래스가 할 일에 가깝거든요.

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

"default 메서드의 진짜 가치는 '이미 배포된 인터페이스를 기존 구현 클래스를 깨지 않고 진화시키는 것' 입니다. 새 메서드를 일반 약속으로 추가하면 모든 구현 클래스가 깨지지만, default 로 기본 동작을 주면 기존 클래스는 그대로 둘 수 있죠. 다만 인터페이스가 동작을 너무 많이 품으면 '약속표' 라는 정체성이 흐려져 추상 클래스와 구분이 모호해집니다. 그리고 두 인터페이스가 같은 이름의 default 를 가지면 충돌이 나는데, 자바는 이걸 구현 클래스가 직접 오버라이드해 결판내도록 강제합니다. 그래서 default 는 API 진화용 보조 수단으로만 쓰고 남용하지 않는 게 제 원칙입니다."


생각해볼 주제 3 예시답안: 역할은 얼마나 잘게 쪼개는 게 좋을까?

[문제 상황 요약]

오늘 우리는 "공유 가능"(Shareable)과 "댓글 가능"(Commentable)을 따로따로 만들었어요. 덕분에 텍스트는 댓글 역할만 골라 받을 수 있었죠. 그런데 만약 이 둘을 "콘텐츠 상호작용" 이라는 하나의 큰 인터페이스로 합쳤다면 어땠을까요? 반대로 역할을 너무 잘게 쪼개면 또 어떤 문제가 생길까요?

[튜터의 가이드 및 해설]

먼저 합쳤을 때의 문제를 보죠. ShareableCommentable 을 "콘텐츠 상호작용" 하나로 합치면, 텍스트도 그 큰 인터페이스를 받아야 해요. 그런데 텍스트는 공유를 안 하잖아요. 그러면 텍스트는 안 쓰는 getShareUrl() 까지 억지로 채워야 하죠. 빈 문자열이나 null 을 돌려주는 가짜 구현을 넣게 되는데, 이건 "공유할 수 있다" 는 거짓 약속이에요. 역할을 합치면 안 쓰는 능력까지 강제로 떠안기는 부담 이 생겨요.

반대로 너무 잘게 쪼개면요? "좋아요 가능", "공유 가능", "댓글 가능", "저장 가능", "신고 가능"… 이렇게 인터페이스가 수십 개로 늘어나면, 콘텐츠 하나를 만들 때마다 implements 뒤에 줄줄이 적어야 하고, 어떤 역할이 어디 있는지 찾기도 번거로워져요. 너무 쪼개면 인터페이스가 넘쳐나서 관리가 힘들어져요.

그럼 기준은 뭘까요? 핵심은 "클라이언트(그 역할을 쓰는 쪽)가 실제로 골라 끼우는 단위로 쪼갠다" 예요. 오늘 ShareableCommentable 을 나눈 이유가 정확히 이거예요. 텍스트는 댓글은 쓰지만 공유는 안 쓰니까, 둘을 나눠둬야 텍스트가 댓글만 골라 받을 수 있죠. 만약 모든 콘텐츠가 공유와 댓글을 항상 함께 쓴다면, 굳이 둘로 나눌 필요가 없었을 거예요. "따로따로 쓰이는가" 가 쪼갬의 기준 이에요.

  • Option A — 역할을 합쳐 큰 인터페이스로: 모든 구현 클래스가 그 능력들을 항상 함께 쓸 때. 장점은 인터페이스 개수가 적어 관리가 단순함. 단점은 일부만 필요한 클래스도 안 쓰는 능력까지 가짜로 채워야 함.
  • Option B — 역할을 잘게 나눠 작은 인터페이스로: 능력들이 콘텐츠마다 따로따로 쓰일 때. 장점은 필요한 역할만 골라 끼울 수 있어 거짓 구현이 없음. 단점은 인터페이스가 너무 많아지면 관리가 번거로움.

현업에서는 "필요한 만큼만 쪼갠다" 가 균형점이에요. 그 기준은 "이 역할이 다른 역할과 따로 쓰이는 일이 실제로 있는가" 죠. 따로 쓰이면 나누고, 늘 함께 쓰이면 합쳐요. 텍스트가 댓글만 쓰는 일이 실제로 있으니 Commentable 을 따로 둔 거예요. 너무 적게 쪼개면 안 쓰는 능력을 떠안고, 너무 많이 쪼개면 인터페이스가 넘쳐나니, 그 사이에서 "실제로 따로 쓰이는 단위" 를 찾는 게 핵심이에요.

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

"역할을 쪼개는 기준은 '그 역할을 쓰는 쪽이 실제로 따로 골라 끼우는 일이 있는가' 입니다. 텍스트는 댓글은 쓰지만 공유는 안 쓰기 때문에 ShareableCommentable 을 나눴습니다. 둘을 합쳐버리면 텍스트가 안 쓰는 공유까지 가짜로 구현해야 하는 거짓 약속이 생기죠. 반대로 너무 잘게 쪼개면 인터페이스가 수십 개로 넘쳐 관리가 어려워집니다. 그래서 '필요한 만큼만', 즉 실제로 따로 쓰이는 단위로만 쪼개는 게 제 기준입니다. 작은 인터페이스로 나눠 클라이언트가 자기가 쓸 역할만 의존하게 하는 게 좋은 설계라고 봅니다."

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.