문서 읽는 데 74분 · day11

Day11: 다형성 — 부모 타입 하나로 여러 자식을 자유자재로 다루기

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

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

Day 11에 오신 걸 환영합니다! 지난 시간 끝에 제가 아주 신기한 장면 하나를 보여드렸어요. 기억나시나요?

관리자 회원(AdminMember)의 자기소개를 출력했더니, 분명 부모의 toString() 을 불렀는데 점수가 부모 버전(104점)이 아니라 자식이 오버라이딩한 버전(154점)으로 튀어나왔죠. "어? 부모를 불렀는데 자식 게 나오네?" 하는 신기함만 기억해두라고 했고요.

오늘은 바로 그 정체를 시원하게 풀어드릴게요. 그 신기한 동작에는 다형성(polymorphism) 이라는 이름이 붙어 있어요. "여러(poly) 모습(morph)" 이라는 뜻이에요. 같은 메서드를 불러도 객체에 따라 여러 모습으로 동작한다는 거죠.

지난 시간 예고에서 던져둔 떡밥이 네 개 있었어요. 오늘 이걸 차례로 회수합니다.

  • Member m = new AdminMember(...) 처럼 부모 타입 변수에 자식 객체를 담는 것 (= 업캐스팅)
  • 그렇게 담아도 m.calculateRecommendScore() 를 부르면 자식 버전(154점)이 불리는 이유 (= 동적 디스패치)
  • 어떤 변수에 담긴 객체가 진짜 관리자인지 알아보는 instanceof
  • Step 5에서 살짝 본 (Member) obj 같은 형 변환 (= 다운캐스팅)

앞의 두 개(업캐스팅, 동적 디스패치)를 오늘 Step 1~3에서 제대로 다루고, 뒤의 두 개(instanceof, 다운캐스팅)는 이어지는 Step에서 풀어드릴게요.

오늘의 주제는 "다형성 — 부모 타입 하나로 여러 자식을 자유자재로 다루기" 입니다.

🎯 학습 목표

  • 업캐스팅: 자식 객체를 부모 타입 변수에 담는 것이 무엇인지 이해하고 쓸 수 있다
  • 동적 디스패치: 부모 타입으로 메서드를 불러도 실제 객체의 오버라이딩 버전이 실행되는 원리를 설명할 수 있다
  • 다형성이 코드를 유연하게 만드는 이유(부모 배열 하나로 여러 자식을 일관되게 처리)를 안다
  • (이어지는 Step에서) instanceof 타입 판별과 다운캐스팅으로 자식 전용 기능을 안전하게 꺼내 쓰는 법을 미리 그려본다

Step 1. 업캐스팅 — 부모 타입 변수에 자식 담기

지난 시간에 우리는 Member 를 물려받은 두 자식, AdminMember(관리자)와 PremiumMember(프리미엄)를 만들었어요. 이 셋의 관계를 한 문장으로 정리하면 이래요. "관리자도 회원이다. 프리미엄도 회원이다."

영어로는 이 관계를 is-a 관계라고 불러요. "A is a B"(A는 B의 한 종류다)라는 뜻이에요. 관리자는 회원의 한 종류, 프리미엄도 회원의 한 종류죠.

그러면 이런 코드가 가능할까요?

Member m = new AdminMember("jaehoon", 1240, 42, 3, 90, "콘텐츠 관리자");

변수 타입은 Member(부모)인데, 실제로 담는 객체는 new AdminMember(...)(자식)예요. 놀랍게도 이게 형 변환을 한 글자도 안 쓰고 그냥 됩니다. 왜냐하면 "관리자는 회원이다" 가 참이니까요. 회원을 담는 변수에 회원의 한 종류인 관리자를 담는 건 전혀 이상하지 않죠.

이렇게 자식 객체를 부모 타입 변수에 담는 것을 업캐스팅(upcasting) 이라고 불러요. "위로(up) 올려 담는다" 는 느낌이에요. 자식에서 부모 쪽(더 넓은 쪽)으로 올라가니까요.

변수의 타입과 실제 객체의 타입은 다를 수 있어요

여기서 처음 만나는, 그리고 오늘 가장 중요한 구분이 하나 나와요. 변수에 적힌 타입실제로 담긴 객체의 타입 은 서로 다를 수 있다는 거예요.

명함꽂이에 비유해볼게요. 회사에 "직원" 이라고 적힌 명함꽂이가 있다고 해봐요. 거기엔 팀장 명함도 꽂을 수 있고, 인턴 명함도 꽂을 수 있어요. 팀장도 직원이고 인턴도 직원이니까요. 꽂이에는 "직원" 이라고 적혀 있지만, 실제로 꽂힌 명함은 팀장일 수도 인턴일 수도 있죠.

  Member m = new AdminMember(...);

  ┌─ 변수의 타입 ─┐        ┌─ 실제 객체의 타입 ─┐
  │   Member     │  ◄───  │   AdminMember     │
  │ (명함꽂이 라벨)│  담음  │  (꽂힌 진짜 명함)  │
  └──────────────┘        └───────────────────┘

  꽂이 라벨은 "Member" 지만,
  실제로 들어 있는 건 "AdminMember" 객체예요.

이 구분이 왜 중요하냐면, 오늘 배울 모든 게 여기서 출발하거든요. "변수 라벨" 과 "진짜 알맹이" 가 다를 수 있다는 것, 이것만 잡고 가면 나머지는 술술 풀려요.

부모 타입 변수로는 부모가 가진 메서드만 부를 수 있어요

업캐스팅을 하고 나면 한 가지 규칙이 생겨요. 부모 타입 변수(Member m)로는 부모(Member)가 가진 메서드만 부를 수 있어요.

명함꽂이 비유로 돌아가볼게요. 라벨이 "직원" 이라고만 적혀 있으면, 우리는 그 명함을 "직원이 할 수 있는 일" 기준으로만 다뤄요. 진짜 알맹이가 팀장이라도, 라벨만 보고는 "팀장 전용 결재" 같은 건 시키지 못하는 거죠.

코드로 확인해볼게요. 이 데모 클래스의 메서드들을 차례로 살펴볼 거예요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 부모 타입 변수로도 부모가 가진 메서드(getUsername)는 그대로 부를 수 있어요.
public String upcastAndGetUsername(Member member) {
    // admin 을 Member 자리에 그냥 담아요 — 이게 업캐스팅
    return member.getUsername();
}

// 부모 타입 변수로 calculateRecommendScore() 를 불러도,
// 실제 객체가 AdminMember 면 +50 된 자식 버전이 불려요 (다음 항목의 동적 디스패치).
public int upcastAndGetScore(Member member) {
    return member.calculateRecommendScore();
}

두 메서드 모두 매개변수 타입이 Member(부모)예요. 그런데 우리가 main 에서 호출할 땐 자식인 AdminMember 객체를 넘길 수 있어요. 바로 이 넘기는 순간이 업캐스팅이에요. 자식 객체가 부모 타입 매개변수(Member member)에 쏙 담기는 거죠.

getUsername()calculateRecommendScore() 는 둘 다 부모 Member 가 가진 메서드예요. 그래서 부모 타입 변수로 자유롭게 부를 수 있어요.

그럼 자식 전용 메서드는 어떨까요? 지난 시간에 봤던 AdminMember.deletePost(...) 는 부모 Member 에는 없고 관리자에게만 있는 행동이에요. 이건 부모 타입 변수(Member member)로는 부를 수 없어요. 라벨이 "Member" 라서, 자바는 "Member 가 할 줄 아는 일" 까지만 허용하거든요.

public int upcastAndGetScore(Member member) {
    member.calculateRecommendScore();  // OK — 부모가 가진 메서드
    member.deletePost("post-1");       // 컴파일 에러! Member 에는 deletePost 가 없어요
    ...
}

⚠️ 잠깐, 그러면 자식 전용 기능은 영영 못 쓰는 걸까요? 아니에요. 다시 자식 타입으로 "내려받아서" 꺼내 쓰는 방법이 있어요. 그게 바로 지난 시간 예고에서 말한 다운캐스팅이에요. 안전하게 내려받는 법은 이어지는 Step에서 차근차근 다룰 거니까, 지금은 "부모 타입 변수로는 부모 메서드만 된다" 만 잡고 가면 돼요.

이 동작은 코드베이스에서 upcastAndGetUsername, upcastAndGetScore 의 결과로 검증해두었어요.

💡 튜터의 결론

"관리자는 회원이다(is-a)" 라서 Member m = new AdminMember(...) 가 형 변환 없이 자동으로 돼요. 이게 업캐스팅이에요. 단, 부모 타입 변수로는 부모가 가진 메서드만 부를 수 있어요. 변수 라벨이 그 객체로 할 수 있는 일의 범위를 정하거든요.


Step 2. 동적 디스패치 (1) — 부모 타입으로 불러도 자식 메서드가 나온다

자, 이제 지난 시간 끝에 봤던 그 "154점 신기함" 을 정식으로 풀어드릴 시간이에요.

Step 1에서 우리는 Member member 라는 부모 타입 변수에 자식 객체를 담을 수 있다는 걸 배웠어요. 그러면 질문이 하나 생기죠. 그 변수로 calculateRecommendScore() 를 부르면, 부모 버전이 불릴까요, 자식 버전이 불릴까요?

라벨은 "Member" 인데 알맹이는 "AdminMember" 예요. 어느 쪽 점수 계산이 실행될까요? 직접 확인해봐요.

Member 배열에 여러 자식을 섞어 담기

지난 시간에 배운 배열을 활용할게요. Member[] 라는 부모 타입 배열을 만들면, 그 안에 일반 회원도, 관리자도, 프리미엄도 전부 섞어 담을 수 있어요. 셋 다 "회원" 이니까요.

이 데모 메서드를 봐요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 배열의 각 회원 점수를 모아서 돌려줘요. 같은 호출이지만 타입마다 다른 결과가 나와요.
public int[] collectScores(Member[] members) {
    int[] scores = new int[members.length];
    for (int i = 0; i < members.length; i++) {
        // members[i] 의 선언 타입은 Member 지만, 실제 객체 타입의 오버라이딩 버전이 불려요
        scores[i] = members[i].calculateRecommendScore();
    }
    return scores;
}

배열을 순회하면서 members[i].calculateRecommendScore() 를 부르고 있어요. members[i] 의 타입은 전부 Member(부모)예요. 코드 어디에도 "관리자면 +50, 프리미엄이면 +30" 같은 분기가 없어요. 그냥 똑같이 calculateRecommendScore() 한 줄만 부르죠.

그런데 결과가 갈려요. 직접 호출해볼게요.

public static void main(String[] args) {
    // 셋 다 팔로워를 1000으로 똑같이 맞춰요 (기본 점수는 1000/100 = 10점)
    Member ilban = new Member("ilban", 1000, 0, 0, 0);
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member premium = new PremiumMember("premium", 1000, 0, 0, 0, true);

    Member[] members = { ilban, admin, premium };

    PolymorphismDemo demo = new PolymorphismDemo();
    int[] scores = demo.collectScores(members);

    System.out.println(scores[0]);  // 10  (일반 회원: 기본 10점)
    System.out.println(scores[1]);  // 60  (관리자: 10 + 보너스 50)
    System.out.println(scores[2]);  // 40  (프리미엄: 10 + 보너스 30)
}

세 객체를 전부 Member 타입 변수에 담았어요(업캐스팅). 셋 다 팔로워 1000으로 맞췄으니 기본 점수는 똑같이 10점이어야 할 것 같죠? 그런데 결과는 10 / 60 / 40 으로 갈렸어요.

똑같은 calculateRecommendScore() 한 줄을 불렀는데, 실제 객체가 관리자면 +50된 60점이, 프리미엄이면 +30된 40점이 나온 거예요.

누가 결정할까? 변수 타입이 아니라 실제 객체 타입이에요

여기서 오늘의 핵심 한 문장이 나와요. 어떤 버전의 메서드가 불릴지는, 변수에 적힌 타입이 아니라 실제로 담긴 객체의 타입이 결정해요.

members[1] 의 라벨은 Member 지만, 알맹이는 AdminMember 예요. 그래서 관리자가 오버라이딩한 60점 버전이 불린 거죠. 이렇게 "실제 객체의 타입을 따라가서 알맞은 메서드를 골라 실행하는 동작" 을 동적 디스패치(dynamic dispatch) 라고 불러요. "디스패치" 는 "어느 메서드로 보낼지 배정한다" 는 뜻이에요. 실행 시점에(동적으로) 알맹이를 보고 배정하니까 "동적 디스패치" 죠.

  members 배열 (라벨은 전부 Member)

  index:    0           1              2
         [ Member ]  [ Member ]    [ Member ]   ← 변수 라벨
            │           │              │
            ▼           ▼              ▼
       (실제 알맹이)  (실제 알맹이)  (실제 알맹이)
         일반 회원   AdminMember   PremiumMember
            │           │              │
   같은 호출 calculateRecommendScore()  ← 한 줄로 똑같이 부름
            │           │              │
            ▼           ▼              ▼
          10점        60점           40점
                  (알맹이 따라 자식 버전이 골라짐 = 동적 디스패치)

택배 분류장을 떠올리면 쉬워요. 모든 상자에 "택배" 라는 똑같은 라벨이 붙어 있어도, 안에 든 게 책이냐 그릇이냐에 따라 가는 곳이 달라지죠. 겉라벨이 아니라 안에 든 알맹이가 목적지를 정하는 거예요. 동적 디스패치도 똑같아요. 변수 라벨이 아니라 실제 객체가 어떤 메서드를 부를지 정해요.

이게 바로 지난 시간 154점의 정체예요. 부모 toString() 안에서 calculateRecommendScore() 를 불렀을 때, 그 객체의 실제 알맹이가 관리자였기 때문에 자식 버전(보너스 포함)이 불렸던 거죠. "부모 코드 안에서 불러도 알맹이를 따라간다" 는 게 동적 디스패치의 위력이에요.

🙋 학생 질문 — "튜터님, 왜 변수 타입이 아니라 객체 타입을 따라가나요? 변수에 Member 라고 적었으면 Member 걸 부르는 게 자연스럽지 않나요?"

아주 좋은 질문이에요! 직관적으로는 "라벨에 적힌 대로" 가 자연스러워 보이죠.

하지만 잠깐 생각해보면, 만약 변수 라벨을 따라간다면 다형성이 아무 쓸모가 없어져요. Member 배열에 관리자를 담는 순간 "관리자가 관리자라는 사실" 을 잊어버리고 그냥 일반 회원처럼 행동한다면, 굳이 자식 클래스를 만들 이유가 없잖아요?

그래서 자바는 객체를 만들 때(new AdminMember(...)) "이 객체는 관리자다" 라는 정보를 그 객체 안에 새겨둬요. 나중에 그 객체가 Member 타입 변수에 담기든, 배열에 들어가든, 메서드 매개변수로 넘어가든 상관없어요. 객체 자신은 "나는 관리자야" 라는 걸 끝까지 기억하고 있어요.

그래서 메서드를 부르는 순간, 자바는 변수 라벨이 아니라 그 객체에 새겨진 진짜 정체를 보고 알맞은 버전을 골라줘요. "한 번 관리자는 영원한 관리자" 인 셈이죠. 어디에 담겨도 자기 정체를 잊지 않아요.

이 동작은 코드베이스에서 collectScores 의 결과(10 / 60 / 40)로 검증해두었어요.

💡 튜터의 결론

같은 calculateRecommendScore() 한 줄을 불러도, 실제 객체가 관리자면 60점, 프리미엄이면 40점이 나와요. 어느 버전이 불릴지는 변수 라벨이 아니라 실제 객체의 타입 이 결정해요. 이게 동적 디스패치, 즉 다형성의 심장이에요.


Step 3. 동적 디스패치 (2) — for 루프로 여러 회원 점수 한 번에

Step 2에서 동적 디스패치의 정체를 봤어요. 이제 이게 실제로 코드를 얼마나 편하게 만드는지 체감해볼 차례예요.

상황을 하나 그려볼게요. 회원 목록 전체의 추천 점수를 모두 더해서 "우리 서비스 회원들의 총 추천 점수" 를 구하고 싶어요. 회원 중엔 일반도, 관리자도, 프리미엄도 섞여 있고요.

다형성이 없다면 — 타입마다 if 분기 지옥

만약 동적 디스패치가 없다고 상상해봐요. 그러면 우리는 회원 하나하나가 무슨 타입인지 일일이 확인하고, 타입마다 점수 계산을 따로 해줘야 했을 거예요.

[ 다형성이 없다면... 머릿속으로만 그려보는 번거로운 코드 ]

  배열을 돌면서 한 명씩:
    만약 관리자라면   → 기본 점수 계산하고 + 50
    아니고 프리미엄이면 → 기본 점수 계산하고 + 30
    아니면 (일반)      → 기본 점수만

  회원 종류가 늘어날 때마다 이 if 분기가 계속 길어져요.
  "은퇴 회원", "기업 회원" 이 생기면? 또 분기 추가... 끝이 없어요.

회원 종류가 셋이면 if 분기가 셋, 다섯이면 다섯. 새 종류가 생길 때마다 이 합산 코드를 또 열어서 분기를 추가해야 해요. 게다가 빠뜨리기라도 하면 조용히 잘못된 점수가 나오죠.

다형성이 있으면 — 부모 배열 + 같은 호출 한 줄

이제 동적 디스패치가 있는 세상으로 돌아와요. 우리 데모의 이 메서드를 봐요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 배열 전체 점수의 합 — 각 요소가 자기 타입의 버전으로 계산돼요.
public int totalScore(Member[] members) {
    int total = 0;
    for (Member member : members) {
        total = total + member.calculateRecommendScore();
    }
    return total;
}

지난 시간에 배운 향상된 for문으로 배열을 한 바퀴 돌면서, 각 회원의 calculateRecommendScore() 를 더하기만 해요. 타입을 확인하는 if 분기가 단 하나도 없어요. "관리자면 +50" 같은 코드를 우리가 직접 쓰지 않았죠.

그런데도 각 회원은 알아서 자기 타입의 점수 버전으로 계산돼요. 관리자는 60점으로, 프리미엄은 40점으로요. 동적 디스패치가 알맹이를 보고 알맞은 버전을 골라주니까요.

public static void main(String[] args) {
    Member ilban = new Member("ilban", 1000, 0, 0, 0);
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member premium = new PremiumMember("premium", 1000, 0, 0, 0, true);

    Member[] members = { ilban, admin, premium };

    PolymorphismDemo demo = new PolymorphismDemo();
    System.out.println(demo.totalScore(members));  // 110  (10 + 60 + 40)
}

세 명을 합치면 10 + 60 + 40 = 110점이에요. 우리가 타입 분기를 한 줄도 안 썼는데, 각자 알아서 자기 점수를 계산해서 정확히 합쳐졌어요.

  Member[] members 를 향상된 for 로 순회

  ┌──────────────────────────────────────────┐
  │  for (Member member : members)            │
  │       │                                   │
  │       ▼  member.calculateRecommendScore() │ ← 분기 없이 같은 호출
  │                                           │
  │   일반 →  10  ┐                            │
  │   관리자 → 60  ├─ 각자 자기 버전으로 계산   │
  │   프리미엄 → 40 ┘                          │
  │                                           │
  │   total = 10 + 60 + 40 = 110              │
  └──────────────────────────────────────────┘

여기서 다형성의 진짜 힘이 보여요. 새 회원 종류(예: 기업 회원)가 생겨도 이 totalScore 메서드는 한 글자도 고칠 필요가 없어요. 새 클래스가 Member 를 상속하고 자기 점수 계산만 오버라이딩해두면, 이 메서드는 그냥 똑같이 calculateRecommendScore() 를 부르고, 동적 디스패치가 알아서 새 회원의 버전을 골라주거든요.

"부모 타입 하나로 받고, 같은 메서드를 부르고, 알맹이는 각자 알아서" — 이게 오늘 제목에 적은 "부모 타입 하나로 여러 자식을 자유자재로 다룬다" 의 진짜 의미예요. 코드가 회원 종류 수에 휘둘리지 않고 단정하게 유지되죠.

이 동작은 코드베이스에서 totalScore 의 결과(110)로 검증해두었어요.

💡 튜터의 결론

부모 타입 배열 하나 + 같은 메서드 호출 한 줄이면, 타입별 if 분기 없이도 각 자식이 자기 점수를 알아서 계산해요. 새 회원 종류가 생겨도 합산 코드는 그대로. 이 유연함이 다형성을 객체지향에서 가장 강력한 무기로 만들어주는 이유예요.


Step 4. instanceof 기초 — 이 객체가 진짜 관리자인지 물어보기

Step 1에서 한 가지 답답한 규칙을 만났던 거 기억나세요? 부모 타입 변수(Member member)로는 부모가 가진 메서드만 부를 수 있어서, 관리자 전용인 deletePost(...) 같은 자식 전용 메서드는 못 부른다고 했죠. 라벨이 "Member" 라서 자바가 "Member 가 할 줄 아는 일" 까지만 허용한다고요.

그런데 자식 전용 기능을 꼭 써야 하는 상황이 생겨요. "이 회원이 관리자라면 게시물 삭제 권한을 주고, 아니면 안 주고" 같은 처리요. 그러려면 먼저 해야 할 일이 하나 있어요. "이 변수에 담긴 알맹이가 진짜 관리자가 맞아?" 부터 확인하는 거예요.

명함꽂이 비유로 다시 가볼게요. 라벨에는 "직원" 이라고만 적혀 있어요. 알맹이가 팀장인지 인턴인지는 라벨만 봐선 몰라요. 그래서 명함을 꺼내기 전에 먼저 들여다보고 "어, 이건 팀장 명함이네?" 하고 확인하는 거죠. 자바에서 이 "들여다보고 확인하는" 도구가 바로 instanceof 예요.

instanceof — "이 객체가 ~의 한 종류인가?"

instanceof 는 "이 객체가 ~의 인스턴스(한 종류)인가?" 를 true/false 로 물어보는 연산자예요. instance 는 "객체 한 개", of 는 "~의" 라는 뜻이니, member instanceof AdminMember 는 그대로 읽으면 "member 가 AdminMember 의 한 종류인가?" 가 돼요.

데모의 이 메서드를 봐요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 실제 타입을 사람이 읽기 좋은 이름으로 돌려줘요.
public String describeType(Member member) {
    if (member instanceof AdminMember) {
        return "관리자";
    } else if (member instanceof PremiumMember) {
        return "프리미엄 회원";
    } else {
        return "일반 회원";
    }
}

매개변수 타입은 Member(부모) 하나예요. 어떤 회원이 들어올지는 모르죠. 그래서 member instanceof AdminMember 로 "이 알맹이가 관리자야?" 를 먼저 물어봐요. 맞으면 "관리자" 를 돌려주고, 아니면 다음 줄에서 "프리미엄이야?" 를 물어보는 식이에요.

여기서 중요한 점 하나. instanceof 가 보는 건 변수 라벨이 아니라 실제 알맹이 예요. Step 2에서 배운 "메서드는 알맹이를 따라간다" 와 똑같은 원리죠. member 의 라벨이 Member 라도, 안에 든 진짜 객체가 관리자면 member instanceof AdminMembertrue 가 돼요.

  member instanceof AdminMember 의 판별 흐름

  member (라벨: Member)
       │
       ▼  "알맹이가 AdminMember 의 한 종류야?"
   ┌───────────────┐
   │ 실제 알맹이는? │
   └───────┬───────┘
           │
     ┌─────┴───────────┬──────────────┐
     ▼                 ▼              ▼
 AdminMember     PremiumMember     일반 Member
   = true          = false          = false
   "관리자"      "프리미엄 회원"    "일반 회원"

boolean 하나만 돌려주는 더 단순한 버전

타입 이름까지는 필요 없고, 그냥 "관리자인지 아닌지" true/false 만 알고 싶을 때도 있어요.

// 이 회원이 관리자인지 true/false 로 알려줘요.
public boolean isAdmin(Member member) {
    return member instanceof AdminMember;
}

member instanceof AdminMember 자체가 이미 true/false 를 내놓는 식이라, 그 결과를 그대로 돌려주기만 하면 돼요. if 문도 필요 없죠.

직접 호출해서 확인해볼게요.

public static void main(String[] args) {
    Member ilban = new Member("ilban", 1000, 0, 0, 0);
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member premium = new PremiumMember("premium", 1000, 0, 0, 0, true);

    PolymorphismDemo demo = new PolymorphismDemo();

    System.out.println(demo.describeType(ilban));    // 일반 회원
    System.out.println(demo.describeType(admin));     // 관리자
    System.out.println(demo.describeType(premium));   // 프리미엄 회원

    System.out.println(demo.isAdmin(admin));          // true
    System.out.println(demo.isAdmin(ilban));          // false
}

세 회원을 전부 Member 타입 변수에 담았는데도, describeType 이 각자 진짜 정체를 정확히 알아냈죠. 알맹이를 들여다봤으니까요.

여기서 비전공자분들이 자주 놀라는 사실 하나를 짚고 갈게요. admin instanceof Membertrue 예요. 관리자도 회원의 한 종류니까요(Step 1의 is-a 관계!). 즉 한 객체가 여러 타입에 동시에 "속할" 수 있어요. 관리자는 AdminMember 이면서 동시에 Member 이기도 하거든요. 그래서 더 좁은 타입(AdminMember)부터 물어봐야 정확하게 갈래가 나뉘어요. describeType 에서 AdminMember 를 가장 먼저 확인한 이유예요.

💡 튜터의 결론

instanceof 는 "이 객체가 ~의 한 종류인가?" 를 true/false 로 물어보는 연산자예요. 변수 라벨이 아니라 실제 알맹이를 들여다봐요. 자식이면 부모 타입에도 속해서 admin instanceof Membertrue 예요. 그래서 좁은 타입부터 먼저 확인하는 게 안전해요.


Step 5. 다운캐스팅 — instanceof 로 확인하고 자식 기능 꺼내 쓰기

Step 1 끝에서 제가 ⚠️ 박스에 약속 하나를 남겨뒀어요. "부모 타입 변수로는 자식 전용 메서드를 못 부르지만, 다시 자식 타입으로 내려받아서 꺼내 쓰는 방법이 있다" 고요. 그 약속을 이제 지킬 시간이에요.

Step 4에서 instanceof 로 "이 알맹이가 관리자야?" 를 확인하는 법을 배웠죠. 관리자가 맞다는 걸 확인했으면, 이제 그 회원을 다시 관리자 타입으로 내려받아서 관리자 전용 기능(deletePost)을 꺼내 쓸 수 있어요.

업캐스팅이 위로 올렸다면, 다운캐스팅은 아래로 내린다

Step 1에서 자식 객체를 부모 타입 변수에 담는 걸 업캐스팅 이라고 했어요. "위로(up) 올려 담는다" 였죠. 이번엔 반대예요. 부모 타입 변수를 다시 자식 타입으로 내려받는 걸 다운캐스팅(downcasting) 이라고 불러요. "아래로(down) 내린다" 는 느낌이에요.

  업캐스팅 (위로 ↑)              다운캐스팅 (아래로 ↓)

      Member                        Member member
        ▲                              │
        │ 자동 (형 변환 없이)            │ (AdminMember) member
        │                              ▼
    AdminMember                    AdminMember
  (자식을 부모 자리에)            (부모 변수를 다시 자식으로)

  올릴 땐 안전해서 자동,          내릴 땐 "진짜 그 자식 맞아?"
  형 변환을 안 써도 됨            확인이 필요해서 형 변환을 명시

업캐스팅은 항상 안전해요. "관리자는 회원이다" 가 무조건 참이니까 자식을 부모 자리에 올리는 건 형 변환 없이 자동으로 됐죠. 하지만 다운캐스팅은 조심해야 해요. "회원 자리에 담긴 이 알맹이가 진짜 관리자 맞아?" 가 항상 참은 아니거든요. 일반 회원이 담겨 있을 수도 있잖아요. 그래서 자바는 다운캐스팅할 땐 (AdminMember) 처럼 형 변환을 명시적으로 적게 해요. "내가 책임지고 관리자로 내려받을게" 라고 선언하는 거죠.

택배로 비유하면, "택배" 라벨 상자를 받아서 안에 있는 책을 꺼내려면 먼저 열어보고 진짜 책이 맞는지 확인해야 하는 것과 같아요. 그릇이 들어 있는데 책인 줄 알고 책장에 꽂으려 하면 사고가 나니까요.

향상된 instanceof 패턴 — 검사와 내려받기를 한 줄에

그래서 안전한 순서는 이래요. 먼저 instanceof 로 확인하고, 통과하면 그때 다운캐스팅 한다. 자바는 이 두 동작을 한 줄로 합쳐주는 편리한 문법을 제공해요. 데모의 이 메서드를 봐요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 관리자일 때만 자식 전용 메서드 deletePost 를 불러요.
public String safeDeletePost(Member member, String postId) {
    // member 가 AdminMember 면, 검사 통과와 동시에 admin 변수로 내려받아요
    if (member instanceof AdminMember admin) {
        return admin.deletePost(postId);
    }
    return "관리자만 게시물을 삭제할 수 있어요.";
}

if (member instanceof AdminMember admin) 이 한 줄이 핵심이에요. 잘 보면 instanceof AdminMember 뒤에 admin 이라는 변수 이름이 붙어 있죠? 이게 바로 검사와 형 변환을 한 번에 처리하는 문법이에요.

이 한 줄이 두 가지 일을 동시에 해요. 첫째, "member 가 관리자야?" 를 검사해요. 둘째, 검사를 통과하면 그 즉시 admin 이라는 관리자 타입 변수에 내려받아 담아줘요. 그래서 if 블록 안에서는 admin.deletePost(postId) 처럼 관리자 전용 메서드를 마음껏 쓸 수 있어요. 이미 adminAdminMember 타입이니까요.

관리자가 아니라서 if 검사를 통과 못 하면, 아래의 "관리자만 게시물을 삭제할 수 있어요." 가 돌려져요. 일반 회원이나 프리미엄 회원이 게시물을 지우려 하면 권한 안내를 받는 거죠.

프리미엄 전용 기능도 똑같은 방식이에요.

// 프리미엄일 때만 자식 전용 메서드 isAdProtected 를 읽어요.
public boolean checkAdProtected(Member member) {
    if (member instanceof PremiumMember premium) {
        return premium.isAdProtected();
    }
    return false;
}

member 가 프리미엄 회원이면 premium 변수로 내려받아서 프리미엄 전용인 isAdProtected()(광고 차단 여부)를 읽어요. 프리미엄이 아니면 그냥 false 를 돌려주고요.

직접 호출해서 결과를 봐요.

public static void main(String[] args) {
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member ilban = new Member("ilban", 1000, 0, 0, 0);
    Member premium = new PremiumMember("premium", 1000, 0, 0, 0, true);

    PolymorphismDemo demo = new PolymorphismDemo();

    // 관리자는 삭제 권한이 있어요
    System.out.println(demo.safeDeletePost(admin, "post-1"));
    // [관리자] admin 가 게시물 post-1 을(를) 삭제했어요.

    // 일반 회원은 권한 안내를 받아요
    System.out.println(demo.safeDeletePost(ilban, "post-1"));
    // 관리자만 게시물을 삭제할 수 있어요.

    // 프리미엄 회원의 광고 차단 여부
    System.out.println(demo.checkAdProtected(premium));  // true
    System.out.println(demo.checkAdProtected(ilban));    // false
}

같은 safeDeletePost 메서드를 불렀는데, 관리자를 넘기면 삭제가 실행되고 일반 회원을 넘기면 권한 안내가 나와요. instanceof 가 알맹이를 보고 갈래를 정확히 나눠준 덕분이에요.

그리고 한 가지 회수할 게 있어요. 지난 시간에 우리가 객체를 비교할 때 (Member) obj, (Post) obj 처럼 괄호로 타입을 적어서 형 변환했던 거 기억나세요? 그게 바로 오늘 배운 다운캐스팅 이었어요. Object 타입(가장 넓은 부모)으로 받은 걸 다시 MemberPost 같은 자식 타입으로 내려받았던 거죠. 그땐 이름을 몰랐지만, 이제 정체를 알게 됐네요.

💡 튜터의 결론

부모 타입을 다시 자식 타입으로 내려받는 게 다운캐스팅이에요. 잘못 내리면 위험하니 (AdminMember) 처럼 형 변환을 명시해요. 안전한 순서는 "먼저 instanceof 로 확인, 통과하면 내려받기". if (member instanceof AdminMember admin) 한 줄이면 검사와 내려받기가 동시에 돼요.


Step 6. 다형성으로 타입별 처리를 깔끔하게 — 공통은 동적 디스패치, 특화는 instanceof

지금까지 두 가지 도구를 손에 넣었어요. 하나는 Step 2~3의 동적 디스패치 — 부모 타입으로 같은 메서드를 불러도 알맹이를 따라 자식 버전이 알아서 실행되는 것. 또 하나는 Step 4~5의 instanceof + 다운캐스팅 — 알맹이가 어떤 자식인지 확인하고 자식 전용 기능을 꺼내 쓰는 것.

이 둘을 언제 어떻게 나눠 쓰는지 감을 잡으면, 타입이 섞인 데이터를 아주 깔끔하게 처리할 수 있어요. 실전 같은 상황을 하나 그려볼게요.

시나리오 — 회원 목록을 한 바퀴 돌며 리포트 출력하기

회원 배열을 쭉 순회하면서 회원마다 이런 정보를 출력하고 싶어요.

  • 공통 정보: 회원의 추천 점수 (모든 회원이 가진 공통 동작)
  • 타입별 특화 정보: 관리자라면 게시물 삭제 권한 안내, 프리미엄이라면 광고 차단 여부

여기서 두 도구의 역할이 자연스럽게 나뉘어요. 공통 정보(점수)는 모든 회원이 똑같이 가진 동작이니 동적 디스패치 로 처리하면 돼요. calculateRecommendScore() 한 줄이면 각자 자기 버전으로 계산되니까요. 타입별 특화 정보는 "관리자에게만 있는 것", "프리미엄에게만 있는 것" 이라 instanceof 로 알맹이를 확인하고 꺼내야 해요.

우리가 Step 4~5에서 검증한 메서드들(describeType, safeDeletePost, checkAdProtected)을 조합해서 main 에서 리포트를 만들어볼게요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java (main 조합 시연)

public static void main(String[] args) {
    Member ilban = new Member("ilban", 1000, 0, 0, 0);
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member premium = new PremiumMember("premium", 1000, 0, 0, 0, true);

    Member[] members = { ilban, admin, premium };

    PolymorphismDemo demo = new PolymorphismDemo();

    for (Member member : members) {
        // 1) 공통 정보 — 동적 디스패치로 각자 자기 점수 계산
        System.out.println("[" + demo.describeType(member) + "] "
                + member.getUsername()
                + " 점수: " + member.calculateRecommendScore());

        // 2) 타입별 특화 정보 — instanceof 로 알맹이 확인 후 꺼내기
        System.out.println("  삭제권한 안내: " + demo.safeDeletePost(member, "post-1"));
        System.out.println("  광고차단 여부: " + demo.checkAdProtected(member));
    }
}

실행하면 회원마다 이렇게 리포트가 출력돼요.

[일반 회원] ilban 점수: 10
  삭제권한 안내: 관리자만 게시물을 삭제할 수 있어요.
  광고차단 여부: false
[관리자] admin 점수: 60
  삭제권한 안내: [관리자] admin 가 게시물 post-1 을(를) 삭제했어요.
  광고차단 여부: false
[프리미엄 회원] premium 점수: 40
  삭제권한 안내: 관리자만 게시물을 삭제할 수 있어요.
  광고차단 여부: true

배열을 딱 한 번 순회했을 뿐인데, 회원마다 점수도 제각각 정확히 계산되고(동적 디스패치), 관리자에게만 삭제 권한이 안내되고 프리미엄에게만 광고 차단이 true 로 나왔어요(instanceof). 두 도구가 각자 맡은 일을 하며 협업한 결과예요.

두 도구의 역할 분담을 정리하면

  회원 배열을 한 바퀴 순회

  ┌─────────────────────────────────────────────┐
  │  공통 동작 (모든 회원이 가진 것)               │
  │    → 동적 디스패치                            │
  │    member.calculateRecommendScore()          │
  │    같은 호출, 알맹이 따라 자식 버전 자동 선택   │
  ├─────────────────────────────────────────────┤
  │  특화 동작 (특정 자식에게만 있는 것)           │
  │    → instanceof + 패턴 변수                   │
  │    if (member instanceof AdminMember admin)   │
  │    확인 후에만 자식 전용 메서드 꺼내 쓰기       │
  └─────────────────────────────────────────────┘

기준은 간단해요. 모든 회원이 공통으로 하는 동작이면 동적 디스패치, 특정 자식에게만 있는 동작이면 instanceof 예요. 가능하면 공통 동작(동적 디스패치)으로 풀어내는 게 코드가 더 깔끔해요. Step 3에서 봤듯이 if 분기 없이 새 회원 종류에도 끄떡없으니까요. instanceof 는 "이 자식에게만 있는 특별한 기능을 꺼내야 할 때" 만 조심스럽게 쓰는 거예요.

💡 튜터의 결론

공통 동작은 동적 디스패치로, 특화 동작은 instanceof + 패턴 변수로 — 이 역할 분담이 타입 섞인 데이터를 깔끔하게 처리하는 핵심이에요. 되도록 공통 동작(동적 디스패치)으로 풀고, instanceof 는 자식 전용 기능이 꼭 필요할 때만 쓰는 게 좋은 습관이에요.


Step 7. 안전한 형 변환 마무리 — ClassCastException 과 다음 시간 예고

오늘의 마지막 Step이에요. Step 5에서 "다운캐스팅은 조심해야 한다, 그래서 instanceof 로 먼저 확인한다" 고 했죠. 그런데 만약 그 확인을 건너뛰면 실제로 무슨 일이 벌어질까요? 직접 보여드릴게요. 왜 안전 순서가 중요한지 몸으로 느끼고 마무리하면 좋거든요.

검사를 건너뛴 위험한 다운캐스팅

데모에는 일부러 위험하게 만든 메서드가 하나 있어요. Step 5의 안전한 safeDeletePost 와 짝을 이루는 비교용이에요.

// com/instagram/javabasic/domain/member/PolymorphismDemo.java

// 일부러 위험하게 — 검사 없이 바로 캐스팅해요. 관리자가 아니면 예외가 터져요.
public String unsafeDeletePost(Member member, String postId) {
    // 검사를 건너뛰었기 때문에, member 가 관리자가 아니면 이 줄에서 ClassCastException 발생
    AdminMember admin = (AdminMember) member;
    return admin.deletePost(postId);
}

safeDeletePost 와 다른 점이 보이시나요? 여기엔 instanceof 검사가 아예 없어요. 무턱대고 (AdminMember) member 로 강제로 내려받아버려요. "이거 무조건 관리자일 거야" 라고 가정하고 밀어붙이는 거죠.

관리자를 넘기면 운 좋게 잘 돌아가요. 하지만 일반 회원이나 프리미엄 회원을 넘기면?

public static void main(String[] args) {
    Member admin = new AdminMember("admin", 1000, 0, 0, 0, "콘텐츠 관리자");
    Member ilban = new Member("ilban", 1000, 0, 0, 0);

    PolymorphismDemo demo = new PolymorphismDemo();

    // 관리자를 넘기면 잘 돌아가요
    System.out.println(demo.unsafeDeletePost(admin, "post-1"));
    // [관리자] admin 가 게시물 post-1 을(를) 삭제했어요.

    // 일반 회원을 넘기면? 실행 중에 ClassCastException 이 터져요!
    System.out.println(demo.unsafeDeletePost(ilban, "post-1"));
    // 💥 여기서 프로그램이 예외를 던지며 멈춰요
}

일반 회원(ilban)을 unsafeDeletePost 에 넘기면, (AdminMember) member 줄에서 프로그램이 ClassCastException 이라는 예외를 던지며 멈춰요. 이름을 풀어보면 "Class(클래스) Cast(형 변환) Exception(예외)" — "이 객체를 그 클래스로 형 변환할 수 없어요!" 라는 자바의 비명이에요. 일반 회원은 관리자가 아닌데 억지로 관리자로 내려받으려 했으니, 자바가 "이건 관리자 명함이 아니라 일반 직원 명함이잖아요!" 하고 거부한 거죠.

여기서 Step 4의 instanceof 가 왜 필요했는지가 분명해져요. safeDeletePostif (member instanceof AdminMember admin) 으로 먼저 확인했기 때문에, 일반 회원이 들어오면 그냥 안내 문구를 돌려주고 조용히 넘어갔어요. 반면 unsafeDeletePost 는 확인을 건너뛰어서 프로그램 자체가 멈춰버렸고요.

⚠️ 무턱대고 다운캐스팅하면 ClassCastException으로 프로그램이 멈춰요

검사 없이 (AdminMember) member 로 강제 캐스팅하면, 실제 알맹이가 관리자가 아닐 때 실행 도중 ClassCastException이 터지며 프로그램이 중단돼요. 그래서 다운캐스팅 전에는 항상 instanceof 로 먼저 확인 하는 게 철칙이에요. if (member instanceof AdminMember admin) 패턴을 쓰면 확인과 내려받기가 한 줄로 안전하게 처리돼요.

오늘 배운 네 가지를 한 번에 정리하면

지난 시간 끝에 던져둔 네 개의 떡밥을 오늘 전부 회수했어요. 한 문단으로 압축할게요. 업캐스팅 으로 자식 객체를 부모 타입 변수에 자동으로 담을 수 있고(Member m = new AdminMember(...)), 그 변수로 메서드를 부르면 동적 디스패치 가 변수 라벨이 아니라 실제 알맹이를 따라 자식 버전을 골라줘요(154점의 정체!). 알맹이가 어떤 자식인지 궁금하면 instanceoftrue/false 를 물어보고, 확인이 끝나면 다운캐스팅 으로 자식 타입으로 내려받아 자식 전용 기능을 꺼내 써요. 이 네 가지가 맞물려서 "부모 타입 하나로 여러 자식을 자유자재로 다루는" 다형성이 완성돼요.

다음 시간 예고 — 직접 만들면 안 되는 '뼈대 부모'

오늘 우리는 부모(Member)와 자식(AdminMember, PremiumMember)을 자유자재로 섞어 썼어요. 부모 타입 변수에 담고, 배열에 섞고, 알맹이를 확인하고요. 그런데 여기서 한 가지 생각해볼 게 있어요. 우리는 지금까지 new Member(...) 로 일반 회원을 직접 만들 수 있었죠.

만약에 말이에요. "이 부모(Member)는 그 자체로는 불완전한 뼈대라서, 직접 만들면 안 되고 반드시 자식 중 하나로만 써야 한다" 면 어떨까요? 예를 들어 "도형" 이라는 부모가 있다고 해봐요. "도형" 은 너무 막연하잖아요. 넓이를 구하려 해도 원인지 사각형인지 모르면 계산을 못 하죠. 그래서 "도형" 은 직접 만들지 못하게 막고, 반드시 "원" 이나 "사각형" 같은 구체적인 자식으로만 쓰게 하고 싶을 수 있어요.

이런 "직접 만들 수 없고 뼈대 역할만 하는 부모" 를 만드는 게 바로 추상 클래스(abstract class) 예요. abstract 라는 키워드 하나로 "이 클래스는 직접 객체로 만들지 마, 반드시 자식으로 상속해서 써" 라고 못 박는 거죠. 다음 시간에 이 추상 클래스로 더 단단한 설계를 배워볼게요.


과제

오늘 배운 업캐스팅, 동적 디스패치, instanceof, 다운캐스팅을 손에 익히는 과제 세 개예요. 모두 코드베이스의 Member·AdminMember·PremiumMember 와 같은 패키지에서 연습하면 돼요. PolymorphismDemo 의 메서드들을 곁눈질로 참고하되, 카운트나 통합 처리는 직접 손으로 짜보는 게 핵심이에요.

과제 1: [기본] 업캐스팅 + 동적 디스패치 직접 확인

Step 2~3에서 "부모 타입 배열에 자식들을 섞어 담아도, 점수를 부르면 각자 자기 버전이 나온다" 는 동적 디스패치를 봤죠. 이번엔 그 장면을 직접 손으로 만들어 눈으로 확인하는 과제예요.

해야 할 일:

서로 다른 종류의 회원을 Member 타입 배열 하나에 섞어 담고, 향상된 for 로 순회하며 각자의 추천 점수를 출력하세요.

요구사항:

  • 일반 회원(new Member(...)), 관리자(new AdminMember(...)), 프리미엄(new PremiumMember(...))을 각각 하나씩 만드세요.
  • 이 셋을 Member[] members = { ... }; 처럼 Member 타입 배열 하나에 담으세요. (자식을 부모 타입 자리에 담는 게 바로 업캐스팅이에요.)
  • 향상된 for(for (Member m : members))로 순회하며 각 회원의 calculateRecommendScore() 를 출력하세요.
  • 같은 calculateRecommendScore() 호출인데도 타입마다 점수가 다르게 나오는지 확인하세요.

힌트:

  • followers 같은 값을 회원마다 다르게 줘서, 기본 점수가 어떻게 달라지는지도 관찰해보세요.
  • 관리자는 부모 점수에 +50, 프리미엄은 +30 보너스가 자동으로 붙어요. 같은 followers 를 줘도 세 회원의 점수가 다르게 나온다면 동적 디스패치가 제대로 동작하는 거예요.
  • PolymorphismDemocollectScores·totalScore 가 바로 이 패턴이에요. 구조만 참고하고, 출력은 직접 짜보세요.

과제 2: [응용] instanceof 로 회원 종류별 카운트

Step 4에서 describeType 으로 회원의 실제 종류를 알아냈죠. 이번엔 배열을 쭉 훑으면서 종류별로 몇 명인지 직접 세어보는 과제예요.

해야 할 일:

Member 타입 배열을 받아, 관리자 수 / 프리미엄 회원 수 / 일반 회원 수를 각각 세는 메서드를 직접 만드세요.

요구사항:

  • 메서드 시그니처는 예를 들어 void countByType(Member[] members) 처럼 직접 정하세요.
  • 세 개의 카운터 변수(adminCount, premiumCount, normalCount)를 0으로 시작하세요.
  • 향상된 for 로 순회하며 instanceof 로 분기해, 해당하는 카운터를 1씩 늘리세요.
  • 마지막에 세 카운트를 출력하세요.

힌트:

  • Step 4의 교훈을 떠올리세요. 자식이면 부모 instanceoftrue 라서, 좁은 타입(AdminMember, PremiumMember)부터 먼저 확인해야 해요.
  • 일반 회원은 "관리자도 아니고 프리미엄도 아니면" 으로 마지막 else 에서 세면 돼요.
  • describeTypeif / else if / else 구조를 그대로 카운트에 옮기면 자연스러워요. isAdmin 처럼 instanceof 결과를 바로 활용해도 좋아요.

과제 3: [심화] 안전한 다운캐스팅으로 관리자만 권한 부여

Step 5에서 safeDeletePost 가 향상된 instanceof 패턴 변수로 "관리자일 때만" 자식 전용 메서드를 안전하게 불렀죠. 이번엔 그 패턴을 배열 순회와 합쳐, 종류에 따라 다른 권한을 통합 처리 하는 과제예요.

해야 할 일:

Member 타입 배열을 순회하면서, 관리자에게만 게시물 삭제를 시키고 프리미엄에게만 광고 차단 여부를 확인하는 메서드 하나를 만드세요. ClassCastException 없이 안전하게요.

요구사항:

  • 메서드 시그니처는 예를 들어 void processMembers(Member[] members) 처럼 직접 정하세요.
  • 향상된 for 로 순회하며, if (m instanceof AdminMember admin) 처럼 향상된 instanceof 패턴 변수 로 검사와 내려받기를 한 줄에 처리하세요.
  • 관리자일 때만 admin.deletePost("post-1") 의 결과를 출력하세요.
  • 프리미엄일 때만 premium.isAdProtected() 의 결과를 출력하세요.
  • 일반 회원은 둘 다 해당 안 되니 자연스럽게 건너뛰게 하세요.

힌트:

  • 절대 검사 없는 강제 캐스팅((AdminMember) m)은 쓰지 마세요. Step 7에서 봤듯 알맹이가 다르면 ClassCastException 으로 프로그램이 멈춰요.
  • 향상된 instanceof 패턴 변수가 "이 타입이 맞는지 확인" 과 "맞으면 그 타입 변수로 내려받기" 를 한 줄에 해줘요. 그래서 if 블록 안에서는 마음 놓고 자식 전용 메서드를 부를 수 있어요.
  • 여유가 되면 관리자 분기 안에서 admin.suspendMember("someone") 도 함께 불러보세요. 관리자 전용 행동을 하나 더 다뤄보는 연습이에요.

생각해볼 주제

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

1. instanceof 분기가 자꾸 늘어나면? — 다형성과 타입 검사의 균형

오늘 우리는 두 가지 방법을 같이 봤어요. 하나는 동적 디스패치예요. 부모 타입으로 calculateRecommendScore() 만 부르면, 관리자든 프리미엄이든 알아서 자기 버전이 불렸죠. 분기(if)가 한 줄도 없었고요. 다른 하나는 instanceof 분기예요. describeType 처럼 "관리자면 이렇게, 프리미엄이면 저렇게" 를 if / else if 로 갈라줬죠.

여기서 상상해보세요. 회원 종류가 기업 회원, 인플루언서, 청소년 회원… 계속 늘어난다면 어떨까요? instanceof 분기로 짠 코드는 종류가 늘 때마다 else if 를 한 줄씩 계속 덧붙여야 해요. 그것도 종류별 처리가 흩어진 여러 곳에서요. 반면 동적 디스패치로 푼 코드는 새 자식이 calculateRecommendScore() 를 오버라이딩만 해두면, 부모 타입으로 부르는 쪽은 한 줄도 안 고쳐도 돼요. 그렇다고 instanceof 가 나쁜 것도 아니에요. "관리자만 할 수 있는 삭제" 처럼 특정 자식에게만 있는 행동은 동적 디스패치로 풀 수 없으니까요. 언제 공통 메서드 오버라이딩(동적 디스패치)으로 풀고, 언제 instanceof 검사를 쓰는 게 좋을지 본인의 기준을 정리해보세요.

2. 업캐스팅하면 자식 정보가 사라질까?

오늘 Member m = new AdminMember(...) 로 관리자를 부모 타입 변수에 담았더니, m.deletePost(...) 를 부를 수 없었죠. 컴파일러가 "Member 에는 그런 메서드 없는데?" 하고 막았으니까요. 그럼 부모 타입에 담는 순간, 이 객체가 관리자라는 정보는 어디로 사라진 걸까요?

한번 곱씹어볼 만한 게 있어요. 우리는 분명 같은 객체를 다운캐스팅(instanceof 로 확인 후 admin 변수로 받기)하면 deletePost 를 다시 부를 수 있었어요. 정보가 진짜로 사라졌다면, 되살릴 수도 없어야 하잖아요? 즉, 객체 안의 알맹이(관리자라는 사실, adminRole, deletePost 라는 행동)는 그대로 남아 있고, 단지 Member 라는 변수 라벨이 "지금은 부모가 약속한 것만 보여줄게" 하고 시야를 좁힌 것에 가까워요. "정보는 그대로 있는데 접근만 제한된다" 는 이 감각을, Step 1~5를 다시 떠올리며 본인의 말로 정리해보세요. 변수의 타입(겉면)과 객체의 실제 타입(알맹이)이 다를 수 있다는 게 다형성의 핵심이거든요.

3. 다형성은 왜 '코드 변경에 강한' 설계일까?

오늘 totalScore(Member[] members) 는 배열을 순회하며 member.calculateRecommendScore() 를 부르기만 했어요. 안에 어떤 자식이 들었는지는 전혀 신경 쓰지 않았죠. 그런데 만약 다음 달에 기업 회원(BusinessMember)이라는 새 자식이 생긴다면 어떨까요?

BusinessMemberMember 를 상속하고 calculateRecommendScore() 를 자기 방식으로 오버라이딩하기만 하면, totalScore한 줄도 안 고쳐도 기업 회원의 점수까지 알아서 합산해줘요. 부모 타입으로만 다루고 있었으니까요. 이걸 "변하는 것과 변하지 않는 것의 분리" 라는 눈으로 보면 재밌어요. 자식마다 다른 점수 계산법은 "변하는 것" 이고, 배열을 돌며 점수를 모으는 공통 처리는 "변하지 않는 것" 이죠. 다형성은 이 둘을 깔끔하게 갈라놓아요. 새 종류가 추가돼도 기존 코드를 안 건드려도 되는 이 성질이 왜 큰 프로그램에서 그렇게 소중한지, 본인이 만들 법한 프로그램을 상상하며 떠올려보세요.

✅ 예시 답안정답 보기

오늘 배운 업캐스팅, 동적 디스패치, instanceof, 안전한 다운캐스팅을 손에 익히는 답안이에요. 정답이 하나뿐인 건 아니에요. 아래 코드는 "이렇게 풀면 깔끔하다" 는 모범 사례 중 하나로 봐주세요. 모두 코드베이스의 Member·AdminMember·PremiumMember 와 같은 패키지에서, PolymorphismDemo 의 메서드를 곁눈질로 참고하며 짜면 돼요.

과제 1 예시답안: 업캐스팅 + 동적 디스패치 직접 확인

핵심 접근

서로 다른 종류의 회원을 Member 타입 배열 하나에 섞어 담는 게 출발이에요. 자식을 부모 타입 자리에 담는 게 바로 업캐스팅이에요. 그다음 향상된 for 로 순회하며 calculateRecommendScore() 를 부르면, 같은 호출인데도 실제 객체 타입의 버전 이 불려요. 이게 동적 디스패치예요. 같은 followers 를 줘도 관리자는 +50, 프리미엄은 +30 보너스가 자동으로 붙어 점수가 갈리는 걸 눈으로 확인하는 게 핵심이에요.

예시 구현

// com/instagram/javabasic/domain/member/PolymorphismDemoMain.java
// 과제 1 — 부모 타입 배열에 자식을 섞어 담고 점수가 갈리는지 확인

public class PolymorphismDemoMain {

    public static void main(String[] args) {

        // 같은 스탯(팔로워 1000명 등)으로 세 회원을 만들어 비교해요.
        // 기본 점수는 같지만, 보너스가 달라 최종 점수가 갈리는 걸 보려고요.
        Member normal  = new Member("minji", 1000, 0, 0, 0);
        Member admin   = new AdminMember("jaehoon", 1000, 0, 0, 0, "콘텐츠 관리자");
        Member premium = new PremiumMember("seungwoo", 1000, 0, 0, 0, true);

        // 셋을 Member 타입 배열 하나에 담아요 — 자식을 부모 자리에 담는 게 업캐스팅
        Member[] members = { normal, admin, premium };

        // 향상된 for 로 순회하며 같은 메서드를 불러요.
        // 호출은 똑같은데, 실제 객체 타입의 버전이 불려 점수가 갈려요 (동적 디스패치)
        for (Member m : members) {
            System.out.println(m.getUsername() + " 추천 점수: " + m.calculateRecommendScore());
        }
    }
}

실행하면 이렇게 나와요.

minji 추천 점수: 10
jaehoon 추천 점수: 60
seungwoo 추천 점수: 40

세 변수의 선언 타입은 모두 Member 로 똑같은데, 점수는 왜 갈릴까요? 그림으로 보면 이래요.

 Member[] members = { normal, admin, premium };
        │              │        │         │
   선언 타입은       (일반)   (관리자)   (프리미엄)
   전부 Member        │        │         │
        │             ▼        ▼         ▼
   m.calculateRecommendScore() 를 부르면...
        │
        └─ "변수 타입(Member)" 이 아니라 "실제 객체 타입" 의 버전이 불려요
             일반   → 기본 10
             관리자 → 기본 10 + 50 = 60
             프리미엄 → 기본 10 + 30 = 40

팔로워 1000명이면 기본 점수가 1000 / 100 = 10 점이에요. 일반 회원은 거기서 끝, 관리자는 +50 해서 60, 프리미엄은 +30 해서 40이 나와요. 변수는 다 Member 인데 결과가 셋 다 다르다는 것 — 이게 동적 디스패치가 동작하는 증거예요. 이 동작은 코드베이스 PolymorphismDemoTest 에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
Member 타입 배열 자식들을 Member[] 한 배열에 담아 업캐스팅을 보이는가
동적 디스패치 확인 같은 calculateRecommendScore() 호출인데 점수가 타입별로 갈리는가
향상된 for 순회 for (Member m : members) 로 부모 타입으로만 순회하는가
보너스 값 정확성 관리자 +50, 프리미엄 +30 이 결과에 반영되는가
출력 확인 세 점수를 실제로 찍어 갈리는 걸 눈으로 보이는가

흔한 실수

  • AdminMember[] 처럼 자식 타입 배열을 따로 만듦 → 그러면 종류별로 배열을 쪼개야 해서 동적 디스패치의 장점이 안 보여요. 핵심은 Member[] 하나에 섞어 담는 거예요.
  • 점수가 다 같게 나옴 (보너스 미반영)AdminMember·PremiumMembercalculateRecommendScore() 오버라이딩이 빠졌거나 super 점수에 보너스를 안 더한 경우예요. Day 10에서 만든 오버라이딩을 다시 확인해보세요.
  • m.deletePost(...) 를 배열 순회 안에서 바로 부르려 함 → 변수 타입이 Member 라 컴파일러가 막아요. 자식 전용 메서드는 과제 3의 안전한 다운캐스팅에서 다뤄요. 과제 1은 공통 메서드(calculateRecommendScore)만으로 충분해요.

실무 개선 포인트 (심화)

지금은 회원을 코드 안에서 new 로 직접 만들어 배열에 손으로 담았어요. 실제 서비스라면 회원이 수십, 수백 명이라 이렇게 일일이 못 담죠. 그래서 여러 객체를 한 줄로 척척 담고 꺼내는 도구(나중에 배울 컬렉션)를 쓰면 훨씬 깔끔해져요. 지금은 배열로 충분하니 "더 편한 방법이 따로 있구나" 정도만 알아두면 돼요.


과제 2 예시답안: instanceof 로 회원 종류별 카운트

핵심 접근

Member 배열을 쭉 훑으면서 관리자 수 / 프리미엄 수 / 일반 수를 각각 세는 과제예요. 가장 중요한 건 검사 순서 예요. 자식이면 부모 instanceoftrue 라서, 좁은 타입(AdminMember, PremiumMember)부터 먼저 물어봐야 해요. 일반 회원은 "관리자도 프리미엄도 아니면" 이라서 마지막 else 에서 세면 돼요. PolymorphismDemo.describeTypeif / else if / else 구조를 그대로 카운트에 옮기면 자연스러워요.

예시 구현

// com/instagram/javabasic/domain/member/MemberCountMain.java
// 과제 2 — 배열을 훑으며 회원 종류별로 몇 명인지 센다

public class MemberCountMain {

    // Member 배열을 받아 종류별 인원을 세고 출력하는 메서드
    public static void countByType(Member[] members) {
        int adminCount   = 0;
        int premiumCount = 0;
        int normalCount  = 0;

        for (Member m : members) {
            // ★ 좁은 타입(자식)부터 먼저 검사한다.
            //   AdminMember 도 결국 Member 라서, Member 부터 물으면 전부 일반으로 빠져버려요.
            if (m instanceof AdminMember) {
                adminCount++;
            } else if (m instanceof PremiumMember) {
                premiumCount++;
            } else {
                // 관리자도 프리미엄도 아니면 일반 회원
                normalCount++;
            }
        }

        System.out.println("관리자: " + adminCount + "명");
        System.out.println("프리미엄: " + premiumCount + "명");
        System.out.println("일반: " + normalCount + "명");
    }

    public static void main(String[] args) {
        Member[] members = {
                new Member("minji", 1000, 0, 0, 0),
                new AdminMember("jaehoon", 1000, 0, 0, 0, "콘텐츠 관리자"),
                new PremiumMember("seungwoo", 1000, 0, 0, 0, true),
                new AdminMember("hyein", 500, 0, 0, 0, "신고 처리"),
                new Member("doyoon", 200, 0, 0, 0)
        };

        countByType(members);
    }
}

실행하면 이렇게 나와요.

관리자: 2명
프리미엄: 1명
일반: 2명

검사 순서가 왜 그렇게 중요한지 그림으로 보면 이래요.

 관리자 jaehoon 을 검사할 때:

 [좁은 타입부터 — 올바름]            [부모부터 — 잘못됨]
   instanceof AdminMember? ✅          instanceof Member? ✅
   → 관리자 카운트++                    → 일반 카운트++ (?!)
   (정확히 관리자로 셈)                  관리자가 일반에 섞여 들어감 😱

관리자도 결국 Member 의 한 종류라서 m instanceof Membertrue 예요. 그래서 부모부터 물으면 관리자·프리미엄이 죄다 일반으로 빨려 들어가요. 좁은 타입(자식)부터 묻고, 일반은 맨 마지막 else 에서 거른다 — 이 순서가 핵심이에요. 이 동작은 코드베이스 PolymorphismDemoTest 의 종류 판별 케이스에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
검사 순서 AdminMember/PremiumMember 같은 좁은 타입을 부모보다 먼저 검사하는가
일반은 else 일반 회원을 마지막 else 에서 세는가 (instanceof Member 로 안 세는가)
카운터 초기화 세 카운터를 0으로 시작하는가
향상된 for for (Member m : members) 로 부모 타입으로 순회하는가
결과 출력 마지막에 세 카운트를 출력하는가

흔한 실수

  • 부모(Member)부터 검사if (m instanceof Member) 를 맨 위에 두면 모든 회원이 여기 걸려서 관리자·프리미엄이 한 명도 안 잡혀요. 위 그림처럼 자식부터 물어야 해요.
  • 일반 회원도 instanceof 로 셈 → 일반 회원은 "관리자도 프리미엄도 아닌 나머지" 라서 따로 검사할 타입이 없어요. else 로 거르는 게 가장 깔끔해요.
  • else if 대신 if 를 연달아 씀 → 따로따로 if 만 쓰면 한 회원이 여러 카운터에 잡힐 수 있어요(예외적으로 겹치는 경우). if / else if / else 로 묶어 한 회원이 한 칸에만 들어가게 하는 게 안전해요.

실무 개선 포인트 (심화)

instanceof 분기로 종류를 세는 건 잘 동작하지만, 회원 종류가 기업·인플루언서·청소년… 계속 늘면 else if 가 끝없이 길어져요. 실무에서는 종류마다 "내 종류 이름을 돌려주는" 공통 메서드를 두고, 그 결과로 세는 식으로 분기를 줄이기도 해요. 이건 아래 생각해볼 주제 1에서 더 풀어볼게요. 지금은 "instanceof 분기가 자꾸 늘어나면 한 번쯤 의심해보자" 정도만 느껴두면 충분해요.


과제 3 예시답안: 안전한 다운캐스팅으로 관리자만 권한 부여

핵심 접근

배열을 순회하면서 관리자에게만 게시물 삭제를, 프리미엄에게만 광고 차단 여부 확인을 시키는 과제예요. 핵심은 if (m instanceof AdminMember admin) 같은 향상된 instanceof 패턴 변수 예요. "이 타입이 맞는지 확인" 과 "맞으면 그 타입 변수로 내려받기" 를 한 줄에 해줘요. 그래서 if 블록 안에서는 ClassCastException 걱정 없이 자식 전용 메서드를 마음 놓고 부를 수 있어요. 일반 회원은 둘 다 해당 안 되니 자연스럽게 건너뛰어요.

예시 구현

// com/instagram/javabasic/domain/member/MemberProcessMain.java
// 과제 3 — 종류에 따라 다른 권한을 안전하게 통합 처리

public class MemberProcessMain {

    public static void processMembers(Member[] members) {
        for (Member m : members) {

            // 향상된 instanceof 패턴 변수:
            // "AdminMember 맞아?" 확인 + "맞으면 admin 변수로 받기" 를 한 줄에
            if (m instanceof AdminMember admin) {
                // 여기서는 admin 이 확실히 관리자라 자식 전용 메서드를 안전하게 부를 수 있어요
                System.out.println(admin.deletePost("post-1"));
                // 여유 연습 — 관리자 전용 행동 하나 더
                System.out.println(admin.suspendMember("spammer123"));

            } else if (m instanceof PremiumMember premium) {
                // 프리미엄일 때만 광고 차단 여부를 확인
                System.out.println(premium.getUsername()
                        + " 광고 차단? " + premium.isAdProtected());
            }
            // 일반 회원은 두 조건 모두 해당 안 됨 → 아무것도 안 하고 건너뜀
        }
    }

    public static void main(String[] args) {
        Member[] members = {
                new Member("minji", 1000, 0, 0, 0),
                new AdminMember("jaehoon", 1000, 0, 0, 0, "콘텐츠 관리자"),
                new PremiumMember("seungwoo", 1000, 0, 0, 0, true)
        };

        processMembers(members);
    }
}

실행하면 이렇게 나와요.

[관리자] jaehoon 가 게시물 post-1 을(를) 삭제했어요.
[관리자] @jaehoon 가 @spammer123 을(를) 정지시켰어요.
seungwoo 광고 차단? true

minji(일반 회원)는 출력이 한 줄도 없죠? 두 instanceof 검사를 모두 통과 못 해서 자연스럽게 건너뛴 거예요.

향상된 instanceof 패턴 변수가 한 줄로 두 가지 일을 어떻게 해내는지 그림으로 보면 이래요.

 if (m instanceof AdminMember admin) { ... }
        │            │          │
   부모 타입 변수   "관리자 맞아?"  맞으면 그 자리에서
   (실제는 관리자)    검사          admin 으로 내려받기
        │            │          │
        └── 검사 통과한 경우에만 admin 사용 가능 ──┘
              → if 블록 안은 100% 관리자라 deletePost 안전!

검사와 내려받기가 한 줄에 묶여 있어서, 검사를 통과 못 하면 admin 변수는 아예 만들어지지도 않아요. 그래서 if 블록 바깥에서 실수로 admin 을 쓸 일도 없고, 안에서는 안심하고 자식 전용 메서드를 부를 수 있어요. 이 안전한 다운캐스팅 패턴은 코드베이스 PolymorphismDemoTest 에서 검증되어 있어요.

채점 포인트

포인트 무엇을 봐야 하는가 배점 가중
향상된 패턴 변수 if (m instanceof AdminMember admin) 형태로 검사+내려받기를 한 줄에 하는가
안전성 검사 없는 강제 캐스팅((AdminMember) m)을 쓰지 않는가
종류별 분기 관리자만 deletePost, 프리미엄만 isAdProtected 를 부르는가
일반 회원 건너뜀 일반 회원이 두 분기에 안 걸려 자연스럽게 넘어가는가
결과 출력 deletePost·isAdProtected 결과를 실제로 출력하는가
여유 연습 관리자 분기에서 suspendMember 까지 다뤘는가 (선택, 있으면 가점)

흔한 실수

  • 검사 없는 강제 캐스팅AdminMember admin = (AdminMember) m; 를 검사 없이 바로 쓰면, m 이 일반 회원일 때 ClassCastException 으로 프로그램이 멈춰요. 반드시 instanceof 로 먼저 확인하세요.
  • 업캐스팅 변수로 자식 메서드 직접 호출m.deletePost(...) 처럼 부모 타입 변수로 자식 전용 메서드를 바로 부르면 컴파일 에러가 나요. 변수 라벨이 Member 라 컴파일러가 "그런 메서드 없는데?" 하고 막거든요. 패턴 변수 admin 으로 내려받은 뒤에 불러야 해요.
  • 검사 순서를 거꾸로 → 과제 2와 같은 함정이에요. PremiumMemberAdminMember 보다 먼저 검사해도 결과는 같지만, 만약 한 분기에서 부모(Member)부터 검사하면 자식이 엉뚱한 분기에 걸려요. 좁은 타입부터 묻는 습관을 들이세요.
  • 패턴 변수를 if 밖에서 사용admin 은 검사를 통과한 if 블록 안에서만 살아 있어요. 블록 밖에서 쓰면 컴파일 에러가 나요. 이건 버그가 아니라, 안전을 위한 설계예요.

실무 개선 포인트 (심화)

지금은 메서드가 결과를 System.out.println 으로 화면에 찍기만 해요. 실제 서비스라면 "삭제" 가 진짜 데이터에 반영되고, 누가 언제 삭제했는지 기록도 남아야 하죠. 또 하나, 관리자·프리미엄·일반처럼 종류별로 다른 처리가 계속 늘어나면 instanceof 분기도 같이 길어져요. 이럴 때 "각 종류가 자기 처리를 알아서 하게" 만드는 방법(공통 메서드 오버라이딩, 다음 시간에 배울 추상 클래스)으로 분기 자체를 줄여나갈 수 있어요. 다만 "관리자만 할 수 있는 삭제" 처럼 특정 자식에게만 있는 행동은 instanceof 로 풀 수밖에 없어요. 둘을 어떻게 나눠 쓸지는 아래 생각해볼 주제 1에서 이어가요.


생각해볼 주제 예시답안

생각해볼 주제 1 예시답안: instanceof 분기 vs 동적 디스패치, 어떻게 균형을 잡을까?

[문제 상황 요약]

오늘 우리는 두 가지 방법을 같이 봤어요. 하나는 동적 디스패치예요. 부모 타입으로 calculateRecommendScore() 만 부르면 관리자든 프리미엄이든 알아서 자기 버전이 불렸고, if 분기가 한 줄도 없었죠. 다른 하나는 instanceof 분기예요. describeType 처럼 "관리자면 이렇게, 프리미엄이면 저렇게" 를 if / else if 로 갈라줬어요. 그럼 언제 어느 쪽을 쓰는 게 좋을까요?

[튜터의 가이드 및 해설]

먼저 동적 디스패치의 매력을 떠올려볼게요. totalScore(Member[] members) 는 그냥 배열을 돌며 member.calculateRecommendScore() 만 불렀어요. 안에 어떤 자식이 들었는지 전혀 묻지 않았죠. 그런데도 각자 자기 점수가 계산됐어요. if 가 한 줄도 없으니 코드가 짧고, 무엇보다 새 회원 종류가 생겨도 이 코드는 안 고쳐도 돼요.

이걸 instanceof 로 풀었다면 어땠을까요? "관리자면 +50, 프리미엄이면 +30, …" 을 if / else if 로 일일이 갈라야 했을 거예요. 회원 종류가 기업·인플루언서·청소년… 늘어날 때마다 else if 를 한 줄씩 덧붙여야 하고, 그것도 점수 세는 곳·등급 매기는 곳·통계 내는 곳 등 여기저기 흩어진 분기를 전부 찾아 고쳐야 해요. 하나라도 빠뜨리면 새 종류만 점수가 엉뚱해지죠.

그렇다고 instanceof 가 나쁜 건 아니에요. "관리자만 할 수 있는 게시물 삭제" 처럼 특정 자식에게만 있는 행동 은 동적 디스패치로 풀 수가 없어요. 부모 Member 에는 deletePost 가 아예 없으니까요. 부모에 없는 행동을 부모 타입으로 부를 방법은 없어요. 이럴 땐 instanceof 로 "관리자인지" 확인하고 내려받아 부르는 게 맞아요.

그래서 판단 기준을 이렇게 정리해볼 수 있어요.

  • Option A — 동적 디스패치 (공통 메서드 오버라이딩): 모든 종류가 공통으로 가진 행동인데 종류마다 방식만 다를 때. 예: 모든 회원이 추천 점수를 갖지만 계산법이 다름. 장점은 분기 없이 깔끔하고 새 종류에 강함. 단점은 부모에 그 메서드가 정의돼 있어야 함.
  • Option B — instanceof 분기: 특정 종류에만 있는 행동 을 다룰 때. 예: 관리자만 삭제, 프리미엄만 광고 차단. 장점은 자식 전용 기능을 안전하게 부름. 단점은 종류가 늘면 분기가 길어짐.

현업에서는 보통 "모두가 가진 행동이면 동적 디스패치, 특정 종류만의 행동이면 instanceof" 로 갈라요. 그리고 instanceof 분기가 자꾸 길어지면 "이거 사실 공통 메서드로 묶을 수 있는 거 아냐?" 하고 한 번 의심해봐요. 분기가 길다는 건 설계를 다시 볼 신호일 때가 많거든요.

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

"둘은 푸는 문제가 다릅니다. 모든 종류가 공통으로 가진 행동인데 방식만 다르면 동적 디스패치로 풀어 if 를 없애고, 특정 종류에만 있는 행동은 instanceof 로 확인 후 다룹니다. 동적 디스패치는 새 종류가 추가돼도 호출하는 쪽 코드를 안 고쳐도 되는 게 가장 큰 강점이고, 그래서 저는 instanceof 분기가 자꾸 길어지면 '공통 메서드로 묶을 수 있는 신호' 로 보고 설계를 다시 점검합니다."


생각해볼 주제 2 예시답안: 업캐스팅하면 자식 정보가 사라질까?

[문제 상황 요약]

오늘 Member m = new AdminMember(...) 로 관리자를 부모 타입 변수에 담았더니, m.deletePost(...) 를 부를 수 없었어요. 컴파일러가 "Member 에는 그런 메서드 없는데?" 하고 막았으니까요. 그럼 부모 타입에 담는 순간, 이 객체가 관리자라는 정보는 어디로 사라진 걸까요?

[튜터의 가이드 및 해설]

곱씹어볼 단서가 하나 있어요. 우리는 분명 같은 객체를 다운캐스팅(instanceof 로 확인 후 admin 변수로 받기)하면 deletePost 를 다시 부를 수 있었어요. 정보가 진짜로 사라졌다면, 되살릴 수도 없어야 하잖아요? 그러니 정보는 사라진 게 아니에요.

비유로 잡아볼게요. 택배 상자에 "도서" 라고만 라벨을 붙여 보냈다고 해봐요. 상자 안에는 사실 "한정판 사인본" 이 들어 있어요. 라벨이 "도서" 라고 해서 안의 사인본이 일반 책으로 바뀐 게 아니죠. 그냥 겉 라벨이 "도서까지만 보여줄게" 하고 시야를 좁힌 것 뿐이에요. 상자를 열어 확인하면(다운캐스팅) 사인본이 그대로 거기 있어요.

업캐스팅도 똑같아요. Member m = new AdminMember(...) 에서, 객체 안의 알맹이(관리자라는 사실, adminRole, deletePost 라는 행동)는 메모리에 그대로 살아 있어요. 단지 Member 라는 변수 라벨이 "지금은 부모가 약속한 것만 보여줄게" 하고 접근 범위를 좁힌 거예요. 그래서 m.deletePost(...) 가 막히는 거고요. 정보가 없어서가 아니라, 라벨이 안 보여줘서요.

증거는 동적 디스패치에서도 보여요. m.calculateRecommendScore() 를 부르면, 변수 라벨은 Member 인데도 관리자 버전(+50)이 불려요. 만약 업캐스팅하는 순간 관리자라는 정보가 정말 지워졌다면, 부모 버전 점수가 나와야 맞겠죠. 그런데 자식 버전이 나온다는 건 알맹이가 여전히 "나는 관리자야" 를 기억하고 있다 는 뜻이에요.

  • 변수의 타입 (겉 라벨): Member — "이 변수로는 부모가 약속한 것만 쓸 수 있어" 하고 접근을 제한.
  • 객체의 실제 타입 (알맹이): AdminMember — 메모리 안에 그대로. 동적 디스패치도, 다운캐스팅도 이 알맹이를 보고 동작.

현업 감각으로 보면, 이 "겉 라벨과 알맹이가 다를 수 있다" 가 다형성의 핵심이에요. 우리는 일부러 라벨을 부모로 좁혀서 여러 자식을 똑같이 다루다가, 자식 전용 행동이 필요한 순간에만 instanceof 로 알맹이를 확인하고 라벨을 다시 넓혀요(다운캐스팅).

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

"업캐스팅은 객체의 정보를 지우는 게 아니라, 변수 라벨로 접근 범위를 좁히는 것뿐입니다. 알맹이는 그대로 메모리에 남아 있죠. 그 증거가 동적 디스패치예요. 변수 타입은 부모인데도 자식 버전 메서드가 불린다는 건, 객체가 자기 실제 타입을 여전히 기억한다는 뜻이니까요. 그래서 instanceof 로 확인 후 다운캐스팅하면 좁혔던 시야를 다시 넓혀 자식 전용 기능을 안전하게 꺼낼 수 있습니다."


생각해볼 주제 3 예시답안: 다형성은 왜 '코드 변경에 강한' 설계일까?

[문제 상황 요약]

오늘 totalScore(Member[] members) 는 배열을 순회하며 member.calculateRecommendScore() 를 부르기만 했어요. 안에 어떤 자식이 들었는지는 전혀 신경 쓰지 않았죠. 그런데 만약 다음 달에 기업 회원(BusinessMember)이라는 새 자식이 생긴다면, 이 코드를 얼마나 고쳐야 할까요?

[튜터의 가이드 및 해설]

놀랍게도 한 줄도 안 고쳐도 돼요. BusinessMemberMember 를 상속하고 calculateRecommendScore() 를 자기 방식으로 오버라이딩하기만 하면, totalScore 는 기업 회원의 점수까지 알아서 합산해줘요. 왜냐하면 totalScore 는 처음부터 부모 타입(Member)으로만 다뤘지, "관리자면 이렇게, 프리미엄이면 저렇게" 같은 종류별 분기를 한 줄도 안 갖고 있었거든요.

이걸 "변하는 것과 변하지 않는 것의 분리" 라는 눈으로 보면 재밌어요. 자식마다 다른 점수 계산법은 "변하는 것" 이에요. 새 종류가 생길 때마다 늘어나죠. 반면 배열을 돌며 점수를 모으는 공통 처리는 "변하지 않는 것" 이에요. 회원 종류가 몇 개든 "하나씩 돌며 점수를 더한다" 는 흐름은 그대로니까요. 다형성은 이 둘을 깔끔하게 갈라놔요.

대비되는 방식을 한번 떠올려볼게요.

  • Option A — 다형성으로 분리: totalScoremember.calculateRecommendScore() 만 부르고, 점수 계산법은 각 자식이 알아서 책임짐. 새 종류는 상속 + 오버라이딩만 추가하면 끝. 기존 코드는 안 건드림.
  • Option B — instanceof 분기로 다 처리: totalScore 안에서 "관리자면 +50, 프리미엄이면 +30, …" 을 if / else if 로 직접 계산. 새 종류가 생기면 이 메서드를 열어 else if 를 추가해야 함. 게다가 점수 세는 곳이 여러 군데면 전부 찾아 고쳐야 함.

Option B 가 위험한 진짜 이유는 "코드가 늘어서" 가 아니에요. 잘 돌고 있던 기존 코드를 건드려야 한다 는 점이에요. 멀쩡히 동작하던 totalScore 를 새 종류 추가 때마다 열어 고치면, 그때마다 기존 회원들 점수 로직까지 망가뜨릴 위험이 생겨요. 반면 Option A 는 새 파일(BusinessMember)만 추가하고 기존 코드는 그대로 둬요. 안 건드린 코드는 망가질 일이 없죠.

현업에서는 이걸 "기능을 추가할 땐 기존 코드를 고치지 말고 새 코드를 더하는 쪽으로 설계하라" 는 원칙으로 정리해요(개방-폐쇄 원칙이라고도 불러요). 다형성이 바로 이걸 가능하게 해주는 대표 도구예요. 큰 프로그램일수록 "건드린 코드 = 망가질 수 있는 코드" 라서, 안 건드리고 늘릴 수 있다는 게 그렇게 소중한 거예요.

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

"다형성이 변경에 강한 이유는, 변하는 것(종류별 계산법)과 변하지 않는 것(공통 처리 흐름)을 분리해주기 때문입니다. 새 종류는 상속하고 오버라이딩만 추가하면 되고, 부모 타입으로 다루던 기존 코드는 한 줄도 안 고쳐도 됩니다. 기능을 추가할 때 기존 코드를 수정하지 않고 새 코드를 더하는 쪽으로 가는 것 — 이게 다형성이 주는 가장 큰 실무 가치이고, 흔히 개방-폐쇄 원칙으로 부릅니다."

더 배우려면

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

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