🎯1주차 Unit 2.5 — instanceof와 형변환

Psj·2026년 5월 7일

F-lab

목록 보기
27/237

🎯 Unit 2.5 — instanceof와 형변환

F-lab Java 1주차 / Phase 2 / Unit 2.5 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Unit 2.4 (다형성)
다음 Unit: 2.6 — Nested/Inner/Anonymous 클래스

이 Unit의 의미: 다형성의 안전장치. ClassCastException을 방지하고, 자식의 고유 메서드를 안전하게 호출하는 도구.


🌍 1. 세상 속 비유

instanceof = "신분증 검사"

당신이 어른용 행사에 들어가려고 합니다. 입구의 보안 직원이 묻습니다:

"이분이 정말 성인이 맞는지 확인하겠습니다."

직원이 신분증을 보고 확인:

  • ✅ "성인 맞으시네요. 입장하세요."
  • ❌ "미성년자네요. 입장 불가."

핵심:

  • 사람의 겉모습 만으로는 모름
  • 신분증 으로 진짜 신원 확인
  • 확인 후에야 적절한 처리

instanceof 가 자바에서 하는 역할.


형변환 = "유니폼 갈아입기"

야구 선수가 경기 후 유니폼을 갈아입는다고 합시다.

상황:

  • 선수는 사람 (Person)
  • 경기 중에는 야구 선수 (BaseballPlayer) 유니폼
  • 일상에서는 시민 (Citizen) 옷

같은 사람 인데:

  • 야구 선수로서의 능력 (스윙, 던지기) 을 쓰려면 → 야구 유니폼
  • 시민으로서의 권리 (투표하기) 를 쓰려면 → 시민 옷

핵심:

  • 본질(사람)은 같지만 어떤 능력을 쓰느냐에 따라 형태를 바꿈
  • 잘못 입으면 (미성년자가 어른 옷) 안 어울림 → 거부

이게 형변환 (Type Casting).


둘을 합친 비유 — "병원 응급실"

응급실에 환자가 도착합니다. 의사가:

1. instanceof — 신원 확인
   "이 환자는 성인인가? 어린이인가?"
        ↓
2. 형변환 — 적절한 도구 준비
   "어린이라면 → 소아과 도구"
   "성인이라면 → 성인 도구"
        ↓
3. 자식 클래스만의 메서드 호출
   "어린이에게는 어린이용 약 처방"

잘못된 처리:

  • 어린이를 성인용 약으로 처방 → 위험 (ClassCastException)

instanceof로 먼저 확인 → 안전한 형변환 → 자식의 고유 메서드 사용


🔥 2. 탄생 배경

다형성의 한계 — "공통 메서드만 쓸 수 있다"

Unit 2.4 다형성을 복습:

public abstract class Animal {
    public abstract void makeSound();  // 모든 동물 공통
}

public class Dog extends Animal {
    @Override
    public void makeSound() { System.out.println("멍멍"); }
    
    public void bark() { System.out.println("BARK!"); }       // Dog만의 메서드
    public void wagTail() { System.out.println("꼬리 흔들기"); } // Dog만의 메서드
}

다형성의 강점:

Animal a = new Dog();
a.makeSound();  // ✅ "멍멍"

다형성의 한계 ⚠️ :

Animal a = new Dog();
a.bark();     // ❌ 컴파일 에러 — Animal에 bark() 없음
a.wagTail();  // ❌ 컴파일 에러 — Animal에 wagTail() 없음

부모 타입으로는 자식만의 메서드 사용 불가. 컴파일러는 변수 타입(Animal) 만 봄.


해결책 — 자식 타입으로 변환

자식의 메서드를 쓰려면 자식 타입으로 형변환 필요:

Animal a = new Dog();
Dog d = (Dog) a;  // 명시적 캐스팅
d.bark();          // ✅ 가능
d.wagTail();       // ✅ 가능

새로운 문제 — 잘못된 캐스팅의 위험 ⚠️

Animal a = new Cat();      // 실제는 Cat
Dog d = (Dog) a;            // ⚠️ Cat을 Dog로 캐스팅 시도
d.bark();                   // 💥 ClassCastException (런타임 에러)

문제:

  • 컴파일러는 "캐스팅하겠다" 만 확인
  • 실제 가능 여부는 런타임에 결정
  • 잘못 캐스팅 시 즉시 크래시

instanceof 의 등장

이 문제를 해결하기 위한 안전장치:

Animal a = new Cat();

if (a instanceof Dog) {  // ← 진짜 Dog인지 확인
    Dog d = (Dog) a;     // 안전한 캐스팅
    d.bark();
} else {
    System.out.println("Dog 아님");
}

효과:

  • 캐스팅 전 타입 검증
  • ClassCastException 방어
  • 안전한 다형성 활용

핵심 통찰

"다형성은 강력하지만 자식의 고유 능력을 쓰려면 형변환이 필요하다.
그 형변환은 위험하므로 instanceof 라는 안전장치를 거쳐야 한다."

다형성, instanceof, 형변환은 세 가지가 함께 가는 한 세트.


💣 3. 없으면 생기는 문제

instanceof 없이 형변환을 남발하면 어떤 문제가 생기는지 보겠습니다.

시나리오: ILIC 운임 시스템

다양한 운임 종류 (StandardFare, UrgentFare, InternationalFare) 가 있고, 각자 고유 메서드를 가집니다.

public abstract class Fare { ... }

public class StandardFare extends Fare { ... }

public class UrgentFare extends Fare {
    public int getUrgentFee() { return urgentFee; }
}

public class InternationalFare extends Fare {
    public String getCurrency() { return currency; }
    public double getExchangeRate() { return exchangeRate; }
}

instanceof 없이 — 위험한 캐스팅

public class FareReportService {
    public void printReport(List<Fare> fares) {
        for (Fare fare : fares) {
            // 모든 운임을 UrgentFare로 캐스팅 시도 ❌
            UrgentFare urgent = (UrgentFare) fare;
            System.out.println("긴급비: " + urgent.getUrgentFee());
        }
    }
}

List<Fare> fares = List.of(
    new StandardFare(1L, 50000),
    new UrgentFare(2L, 50000, 10000),
    new InternationalFare(3L, 100, "USD", 1300.0)
);

reportService.printReport(fares);
// 첫 번째 항목 처리 시도:
// 💥 ClassCastException — StandardFare를 UrgentFare로 캐스팅 불가
// → 프로그램 죽음

문제:
1. 첫 다른 타입 만나면 즉시 크래시
2. 의도하지 않은 객체 처리 위험
3. 고객 데이터 처리 중 다운 → 서비스 장애


instanceof 없이 — 모든 메서드를 부모에 우겨넣기

대안으로 모든 메서드를 부모에 정의하는 사람이 있습니다:

// ❌ 잘못된 설계
public abstract class Fare {
    // 모든 자식의 메서드를 다 가짐
    public int getUrgentFee() { return 0; }  // 일반 운임은 0
    public String getCurrency() { return "KRW"; }  // 일반은 KRW
    public double getExchangeRate() { return 1.0; }  // 일반은 1.0
    // ... 모든 자식의 메서드 ❌
}

문제:

  • 부모가 자식들의 모든 정보를 알아야 함 (잘못된 의존)
  • 일반 운임에 의미 없는 메서드들 (getUrgentFee() 가 항상 0)
  • 새 자식 추가 시 부모 수정 필요 (OCP 위반)
  • 빈혈 모델 + God Class 안티패턴

instanceof 활용 — 안전한 처리

public class FareReportService {
    public void printReport(List<Fare> fares) {
        for (Fare fare : fares) {
            // 공통 정보
            System.out.println("운임 ID: " + fare.getId());
            System.out.println("총액: " + fare.calculateTotal());
            
            // 타입별 추가 정보
            if (fare instanceof UrgentFare) {
                UrgentFare urgent = (UrgentFare) fare;
                System.out.println("긴급비: " + urgent.getUrgentFee());
            } else if (fare instanceof InternationalFare) {
                InternationalFare intl = (InternationalFare) fare;
                System.out.println("통화: " + intl.getCurrency());
                System.out.println("환율: " + intl.getExchangeRate());
            }
        }
    }
}

reportService.printReport(fares);
// 모든 운임 안전하게 처리 ✅

효과:

  • 각 타입을 안전하게 식별
  • 자식의 고유 메서드 활용
  • ClassCastException 방어

그러나 — instanceof가 너무 많으면 다형성 외면 신호 ⚠️

// ⚠️ 너무 많은 instanceof — 다형성을 활용 못함
public void process(Fare fare) {
    if (fare instanceof StandardFare) { ... }
    else if (fare instanceof UrgentFare) { ... }
    else if (fare instanceof InternationalFare) { ... }
    else if (fare instanceof DomesticFare) { ... }
    // ... 매번 추가
}

다형성 (Unit 2.4) 으로 해결할 수 있는 문제를 instanceof로 풀고 있다는 신호.

진짜 해결:

public abstract class Fare {
    public abstract void printDetail();  // 각 자식이 알아서
}

public void process(Fare fare) {
    fare.printDetail();  // ✅ 다형성!
}

instanceof 가 코드에 많이 등장하면 설계 재검토.


✅ 4. 해결책 — instanceof와 형변환 문법

instanceof 기본 문법

객체 instanceof 타입

반환값: boolean

  • true: 객체가 그 타입 (또는 자손) 임
  • false: 그렇지 않음

기본 사용

Animal a = new Dog();

a instanceof Dog;     // true — Dog 인스턴스
a instanceof Animal;  // true — Animal의 자손이기도 함
a instanceof Object;  // true — 모든 객체는 Object의 자손
a instanceof Cat;     // false — Cat이 아님
a instanceof String;  // 컴파일 에러 — 무관한 타입

핵심 규칙 ⭐ :

  • 자기 타입과 모든 부모 타입 에 대해 true
  • 무관한 타입 은 컴파일 에러

null 처리

Animal a = null;
a instanceof Dog;     // false (null은 어떤 타입도 아님)
a instanceof Animal;  // false

instanceof 는 null 안전. NullPointerException 안 남.

활용:

// null 체크 + 타입 체크를 한 번에
if (obj instanceof String) {
    // obj는 null이 아니고, String임
    String s = (String) obj;
}

// vs 안 좋은 코드
if (obj != null && obj.getClass() == String.class) {
    // 더 길고 명시적
}

형변환 (Type Casting) ⭐

두 종류:

1. 업캐스팅 (Upcasting) — 자식 → 부모

Dog d = new Dog();
Animal a = d;  // ✅ 암시적 캐스팅 (자동)

규칙:

  • 암시적 (자동) — 명시적 캐스팅 표시 불필요
  • 항상 안전 — 자식은 부모의 모든 것 가짐

다형성의 토대:

List<Animal> animals = new ArrayList<>();
animals.add(new Dog());  // Dog → Animal 자동 업캐스팅
animals.add(new Cat());

2. 다운캐스팅 (Downcasting) — 부모 → 자식

Animal a = new Dog();
Dog d = (Dog) a;  // ✅ 명시적 캐스팅 필요

규칙:

  • 명시적(타입) 표시 필요
  • 위험 — 잘못하면 ClassCastException
  • 사전 instanceof 검사 권장

안전한 다운캐스팅 패턴 ⭐

Animal a = getAnimal();

if (a instanceof Dog) {
    Dog d = (Dog) a;  // 안전
    d.bark();
}

Java 16+ 패턴 매칭 ⭐⭐ (현대적 문법)

Java 16부터 instanceof + 캐스팅을 한 줄로:

// 옛 방식
if (a instanceof Dog) {
    Dog d = (Dog) a;
    d.bark();
}

// Java 16+ 패턴 매칭
if (a instanceof Dog d) {  // ← 한 번에!
    d.bark();
}

더 간결한 사용:

// 부정 조건과 결합
if (!(a instanceof Dog d)) {
    return;  // Dog 아니면 종료
}
d.bark();  // 여기서는 d 사용 가능

// 조건 결합
if (a instanceof Dog d && d.getAge() > 3) {
    d.bark();
}

Java 21+ Switch 패턴 매칭 (최신)

public String describe(Animal a) {
    return switch (a) {
        case Dog d -> "개: " + d.getBreed();
        case Cat c -> "고양이: " + c.getName();
        case null -> "동물 없음";
        default -> "알 수 없는 동물";
    };
}

→ if-else 지옥을 깔끔하게 해결.

ILIC 활용 가능:

public String describeFare(Fare fare) {
    return switch (fare) {
        case UrgentFare u -> "긴급 (긴급비: " + u.getUrgentFee() + ")";
        case InternationalFare i -> "국제 (" + i.getCurrency() + ")";
        case StandardFare s -> "일반";
        default -> "알 수 없는 운임";
    };
}

🏗️ 5. 내부 동작 원리

instanceof 의 내부 동작

JVM이 어떻게 instanceof를 검사하는지 보겠습니다.

Animal a = new Dog();
boolean result = a instanceof Dog;

JVM 내부 흐름:

1. a가 가리키는 객체의 클래스 정보 확인
        ↓
2. 그 클래스가 Dog인지 확인
        ↓
3. 아니면 부모 클래스로 거슬러 올라가며 확인
   Dog → Animal → Object
        ↓
4. 비교 대상(Dog)을 발견하면 true
   끝까지 못 찾으면 false

바이트코드:

INSTANCEOF Dog  ← JVM 명령어

→ JVM이 직접 처리하는 단순하고 빠른 연산.


형변환의 내부 동작

Animal a = new Dog();
Dog d = (Dog) a;

JVM 내부 흐름:

1. a가 가리키는 객체의 실제 클래스 확인
        ↓
2. 캐스팅 대상(Dog)과 호환되는지 검증
   - a가 Dog 또는 Dog의 자식? → OK
   - 아니면 → ClassCastException
        ↓
3. d 변수에 같은 객체 참조 할당
   - 객체 자체는 변하지 않음 ⭐
   - 단지 변수의 타입만 다름

바이트코드:

CHECKCAST Dog  ← JVM이 검증 + 캐스팅

핵심 통찰 — "캐스팅은 객체를 변환 X" ⭐

Animal a = new Dog();
Dog d = (Dog) a;

// a와 d는 같은 객체를 가리킴
System.out.println(a == d);  // true ✅

의미:

  • 캐스팅은 객체를 새로 만들지 않음
  • 단지 변수의 타입을 변경 할 뿐
  • Heap의 객체는 그대로

비유:

  • 같은 사람이 직책에 따라 다른 명함을 가짐
  • 명함은 다르지만 사람은 같음

메모리 관점

Animal a = new Dog();  // Heap에 Dog 인스턴스 생성, a는 Animal 타입 참조
Dog d = (Dog) a;        // d도 같은 Dog 인스턴스를 가리킴

메모리 구조:

[Stack]
  a (Animal 타입 참조) ─┐
  d (Dog 타입 참조)    ─┤
                       ↓
[Heap]
  Dog 인스턴스 (1개만 존재)
   - name
   - breed

참조 변수만 여러 개, 객체는 하나.


컴파일 시 캐스팅 검증

String s = "hello";
Integer i = (Integer) s;  // ❌ 컴파일 에러

왜?:

  • String과 Integer는 상속 관계 없음
  • 컴파일러가 "절대 캐스팅 불가" 판단
Animal a = new Dog();
Dog d = (Dog) a;  // ✅ 컴파일 OK (가능성 있음)
                  // 단, 런타임에 검증

getClass() vs instanceof

두 가지 방법 비교:

Animal a = new Dog();

// 방법 1: getClass()
a.getClass() == Dog.class;  // true (정확히 Dog)
a.getClass() == Animal.class;  // false (Dog는 Animal이 아님)

// 방법 2: instanceof
a instanceof Dog;     // true
a instanceof Animal;  // true (자손 관계)

핵심 차이 ⭐ :

getClass()instanceof
비교 방식정확히 같은 클래스자손 포함
다형성XO
사용 빈도낮음매우 높음
사용 케이스equals() 구현 등일반적인 타입 검증

equals() 에서의 논쟁:

// 방식 1 — instanceof (Joshua Bloch 권장)
@Override
public boolean equals(Object o) {
    if (!(o instanceof Customer)) return false;
    // 자식도 equals 가능 (대칭성 깨질 수 있음)
}

// 방식 2 — getClass() (대칭성 보장)
@Override
public boolean equals(Object o) {
    if (o == null || getClass() != o.getClass()) return false;
    // 정확히 같은 클래스만 같다고 판단
}

상황에 따라 다름. 6주차에서 자세히.


💻 6. 실전 코드 예시

예시 1: ILIC 운임 처리 — 안전한 다운캐스팅

public class FareReportService {
    
    public void printDetailedReport(List<Fare> fares) {
        for (Fare fare : fares) {
            // 공통 정보 (다형성)
            System.out.println("=== 운임 #" + fare.getId() + " ===");
            System.out.println("총액: " + fare.calculateTotal() + "원");
            
            // 타입별 추가 정보 (instanceof + 캐스팅)
            if (fare instanceof UrgentFare) {
                UrgentFare urgent = (UrgentFare) fare;
                System.out.println("긴급비: " + urgent.getUrgentFee() + "원");
                System.out.println("긴급 처리 사유: " + urgent.getUrgentReason());
            } else if (fare instanceof InternationalFare) {
                InternationalFare intl = (InternationalFare) fare;
                System.out.println("통화: " + intl.getCurrency());
                System.out.println("환율: " + intl.getExchangeRate());
                System.out.println("출발지: " + intl.getOrigin());
                System.out.println("도착지: " + intl.getDestination());
            }
        }
    }
}

예시 2: Java 16+ 패턴 매칭 사용

public class FareReportService {
    
    public void printDetailedReport(List<Fare> fares) {
        for (Fare fare : fares) {
            System.out.println("=== 운임 #" + fare.getId() + " ===");
            System.out.println("총액: " + fare.calculateTotal() + "원");
            
            // Java 16+ 패턴 매칭 — 한 줄로 검사 + 캐스팅
            if (fare instanceof UrgentFare urgent) {
                System.out.println("긴급비: " + urgent.getUrgentFee() + "원");
            } else if (fare instanceof InternationalFare intl) {
                System.out.println("통화: " + intl.getCurrency<());
                System.out.println("환율: " + intl.getExchangeRate());
            }
        }
    }
}

변수 선언이 한 번에 끝남.


예시 3: Java 21+ Switch 패턴 매칭

public class FareDescriptionService {
    
    public String describe(Fare fare) {
        return switch (fare) {
            case UrgentFare u -> 
                String.format("긴급 운임 (긴급비: %d원, 사유: %s)",
                    u.getUrgentFee(), u.getUrgentReason());
            case InternationalFare i -> 
                String.format("국제 운임 (%s, 환율 %.2f)",
                    i.getCurrency(), i.getExchangeRate());
            case StandardFare s -> 
                "일반 운임";
            case null -> 
                "운임 없음";
            default -> 
                "알 수 없는 운임 타입";
        };
    }
}

if-else 지옥 → 깔끔한 switch.


예시 4: 컬렉션의 안전한 처리

public class CustomerService {
    
    public void processCustomers(List<Customer> customers) {
        int vipCount = 0;
        int totalDiscount = 0;
        
        for (Customer customer : customers) {
            // 공통 처리
            customer.register();
            
            // VIP만의 추가 처리
            if (customer instanceof VipCustomer vip) {
                vipCount++;
                totalDiscount += vip.getExtraVipDiscount();
                vip.sendVipNotification();
            }
            
            // 파트너만의 추가 처리
            if (customer instanceof PartnerCustomer partner) {
                partner.notifyPartnerTeam();
            }
        }
        
        System.out.println("VIP 수: " + vipCount);
        System.out.println("VIP 추가 할인 총액: " + totalDiscount);
    }
}

예시 5: 안티패턴 — instanceof 남발

// ❌ 안 좋은 코드 — 다형성을 외면
public class FareCalculator {
    public int calculate(Fare fare) {
        if (fare instanceof StandardFare s) {
            return s.getAmount();
        } else if (fare instanceof UrgentFare u) {
            return u.getAmount() + u.getUrgentFee();
        } else if (fare instanceof InternationalFare i) {
            return (int)(i.getAmount() * i.getExchangeRate());
        }
        return 0;
    }
}

문제:

  • 새 운임 타입 추가 시 매번 수정
  • 다형성을 사용하지 않음

해결 — 다형성 활용:

public abstract class Fare {
    public abstract int calculateTotal();  // 각 자식이 구현
}

public class FareCalculator {
    public int calculate(Fare fare) {
        return fare.calculateTotal();  // ✅ 다형성!
    }
}

instanceof 가 자주 보이면 다형성으로 리팩토링.


예시 6: instanceof가 정당한 경우

언제 instanceof가 적합한가?

Case 1: 외부 API 응답 처리

public void handleEvent(Object event) {
    if (event instanceof OrderCreatedEvent ev) {
        processOrder(ev);
    } else if (event instanceof PaymentCompletedEvent ev) {
        processPayment(ev);
    }
}

→ 이벤트 시스템처럼 타입을 미리 알 수 없는 경우.

Case 2: 플러그인 / 옵션 기능

public void register(Object service) {
    services.add(service);
    
    // 추가 인터페이스 구현 여부 확인
    if (service instanceof Closeable closeable) {
        cleanupHandlers.add(closeable);
    }
    
    if (service instanceof Healthchecker checker) {
        healthCheckers.add(checker);
    }
}

선택적 기능 을 위한 instanceof.

Case 3: 라이브러리 / 프레임워크 통합

public void serialize(Object obj) {
    if (obj instanceof Serializable) {
        // 직렬화 가능
    } else {
        throw new IllegalArgumentException("직렬화 불가");
    }
}

마커 인터페이스 확인.


예시 7: 테스트 코드에서의 instanceof

@Test
void 운임_생성_시_긴급_운임이라면_긴급비가_추가된다() {
    Fare fare = fareService.create(new UrgentFareRequest(...));
    
    assertThat(fare).isInstanceOf(UrgentFare.class);  // 타입 검증
    
    UrgentFare urgent = (UrgentFare) fare;
    assertThat(urgent.getUrgentFee()).isPositive();
}

→ AssertJ는 isInstanceOf() 를 제공.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: ClassCastException 방치

// ❌ instanceof 검사 없이 캐스팅
public void process(Animal a) {
    Dog d = (Dog) a;  // a가 Cat이면 즉시 폭발
    d.bark();
}

해결:

public void process(Animal a) {
    if (a instanceof Dog d) {
        d.bark();
    }
}

실수 2: instanceof 남발 — 다형성 외면 ⭐

// ❌ 다형성을 안 씀
public int calculate(Shape shape) {
    if (shape instanceof Circle c) {
        return (int)(Math.PI * c.getRadius() * c.getRadius());
    } else if (shape instanceof Rectangle r) {
        return r.getWidth() * r.getHeight();
    } else if (shape instanceof Triangle t) {
        return t.getBase() * t.getHeight() / 2;
    }
    return 0;
}

해결 — 다형성:

public abstract class Shape {
    public abstract int calculateArea();  // 각 자식이 구현
}

public int calculate(Shape shape) {
    return shape.calculateArea();  // ✅
}

instanceof 분기가 3개 이상이면 다형성으로 리팩토링 검토.


실수 3: 무관한 타입 비교

String s = "hello";
if (s instanceof Integer) {  // ❌ 컴파일 에러
    // ...
}

→ String과 Integer는 상속 관계 없음. 컴파일러가 잡음.

예외 — Object 변수:

Object obj = "hello";
if (obj instanceof Integer) {  // ✅ 컴파일 OK (런타임에 false)
    // ...
}

→ Object 타입은 어떤 객체든 가리킬 수 있어서 컴파일은 통과.


실수 4: null 체크 없이 캐스팅

Animal a = null;
Dog d = (Dog) a;  // ✅ 컴파일 OK, 런타임 OK (d는 null)
d.bark();          // 💥 NullPointerException

해결:

if (a instanceof Dog d) {  // null이면 false
    d.bark();
}

instanceof 가 자동으로 null 처리해줌.


실수 5: 순서 잘못 — 가장 구체적 타입부터 ⭐ (자기 점검 Q1 관련)

public class Cat extends Animal { ... }
public class Persian extends Cat { ... }

Animal a = new Persian();

// ❌ 잘못된 순서
if (a instanceof Cat c) {
    System.out.println("고양이 처리");
} else if (a instanceof Persian p) {  // 절대 도달 X (이미 Cat에서 잡힘)
    System.out.println("페르시안 처리");
}

// ✅ 올바른 순서 — 가장 구체적인 것부터
if (a instanceof Persian p) {
    System.out.println("페르시안 처리");
} else if (a instanceof Cat c) {
    System.out.println("고양이 처리");
}

규칙: 자식 → 부모 순서.


실수 6: instanceof와 equals() 잘못된 결합

// ❌ 위험한 equals
@Override
public boolean equals(Object o) {
    if (o instanceof Customer) {
        // 자식도 통과 → 대칭성 깨질 수 있음
    }
}

VipCustomer v = new VipCustomer(1L);
Customer c = new Customer(1L);

v.equals(c);  // true (Vip이 Customer로 비교)
c.equals(v);  // 결과가 다를 수 있음 ⚠️ — 대칭성 깨짐

→ 6주차에서 깊이 다룰 주제.


실수 7: 옛날 스타일 사용 (Java 16+ 무시)

// 옛 스타일 — 변수 두 번 선언
if (obj instanceof String) {
    String s = (String) obj;
    // ...
}

// 현대 스타일 (Java 16+)
if (obj instanceof String s) {
    // 한 번에 끝
}

→ Java 16+ 환경이면 패턴 매칭 적극 활용.


실수 8: ClassCastException을 catch로 처리

// ❌ 안티패턴
try {
    Dog d = (Dog) a;
    d.bark();
} catch (ClassCastException e) {
    // 무시
}

문제:

  • 예외는 느림 (Stack trace 생성 비용)
  • 의도가 불명확
  • instanceof로 사전 예방이 표준

해결:

if (a instanceof Dog d) {
    d.bark();
}

🔗 8. 연관 개념 맵

직접 이어지는 학습

[Unit 2.4: 다형성]
        ↓
[Unit 2.5: instanceof와 형변환]  ← 지금 여기
        ↓
[Unit 2.6: Nested/Inner/Anonymous]

이 Unit의 개념이 활용되는 곳

1주차 내:

  • Phase 3 (SOLID): LSP — 안전한 부모-자식 치환
  • Phase 6 (컬렉션): equals/hashCode 구현 시 instanceof

미래 주차:

  • 3주차 (제네릭): Bounded Type Parameter — <T extends Number>
  • 5주차 (Spring): 빈 타입 체크, ApplicationContextAware
  • 8-9주차 (AOP): Proxy 객체 처리
  • 15주차 (Spring MVC): HandlerMethod 처리
  • 17주차 (이벤트 시스템): ApplicationEvent 처리

다형성, instanceof, 형변환의 삼각관계 ⭐

[다형성 (Unit 2.4)]
"부모 타입으로 자식 객체 처리"
        ↓
   하지만 자식 고유 메서드는?
        ↓
[형변환 (Unit 2.5)]
"부모 타입을 자식 타입으로"
        ↓
   잘못 캐스팅하면 위험
        ↓
[instanceof (Unit 2.5)]
"안전하게 타입 확인"

세 가지가 한 세트 — 하나만 알아도 부족.


면접 단골 질문 매핑

질문이 Unit에서의 답
"instanceof가 뭔가요?"객체가 특정 타입인지 검사하는 연산자
"다운캐스팅이 위험한 이유?"ClassCastException 가능 — instanceof 사전 검사 필요
"instanceof와 getClass()의 차이?"instanceof는 자손 포함, getClass()는 정확한 타입
"패턴 매칭이 뭔가요?"Java 16+ 의 instanceof + 변수 선언 결합 문법
"instanceof 남발의 문제?"다형성을 활용 못함 — 설계 재검토 신호

📝 9. 핵심 요약 — 3줄 정리

1️⃣ instanceof는 다형성의 안전장치다.

부모 타입으로 자식 객체를 다루다가 자식의 고유 메서드 를 쓰려면 다운캐스팅이 필요한데, 잘못 캐스팅하면 ClassCastException이 발생한다. instanceof사전 타입 검증 후 안전하게 캐스팅하는 게 표준 패턴이다.

2️⃣ Java 16+ 패턴 매칭으로 한 줄에 끝낸다.

옛 방식 (if (a instanceof Dog) { Dog d = (Dog) a; ... }) 대신 if (a instanceof Dog d) { d.bark(); } 로 검사 + 캐스팅 + 변수 선언을 한 번에. Java 21+에서는 switch 패턴 매칭으로 if-else 지옥을 깔끔하게 해결.

3️⃣ instanceof가 많으면 다형성을 외면한다는 신호다.

분기가 3개 이상이면 다형성으로 리팩토링 검토. 단, 외부 이벤트 처리, 옵션 인터페이스 확인, 마커 인터페이스 검사 같은 정당한 사용 케이스는 있다. 또한 다운캐스팅 시 가장 구체적인 자식 타입부터 검사해야 정확하다.


🎓 학습 자기 점검

기본 이해

  • instanceof의 반환 타입과 동작 방식을 안다
  • 업캐스팅과 다운캐스팅의 차이를 안다
  • ClassCastException이 발생하는 상황을 안다
  • Java 16+ 패턴 매칭 문법을 작성할 수 있다

실전 적용

  • ILIC 코드에서 instanceof를 안전하게 사용할 수 있다
  • instanceof 남발 코드를 다형성으로 리팩토링할 수 있다
  • 가장 구체적인 자식 타입부터 검사하는 순서를 지킨다
  • 옛 instanceof + 캐스팅을 패턴 매칭으로 변환할 수 있다

면접 대비 (1-2분 답변)

  • "instanceof와 형변환의 관계는?" 답변 가능
  • "ClassCastException을 어떻게 방지하나요?" 답변 가능
  • "instanceof와 getClass()의 차이?" 답변 가능
  • "instanceof를 안 써야 하는 경우는?" 답변 가능

자기 점검 질문 답변

Q1: 부모 타입을 자식 타입으로 캐스팅하기 전에 왜 instanceof 검사가 필요한가?

한 문장: ClassCastException을 방지하기 위해.

상세 설명:

다운캐스팅 (부모 → 자식) 은 컴파일러가 검증할 수 없습니다:

Animal a = getAnimal();  // 실제 객체는 런타임에 결정
Dog d = (Dog) a;          // 컴파일러: "캐스팅하겠다"만 확인

컴파일러는 "Animal 변수를 Dog로 캐스팅하겠다" 만 보고, 실제로 가능한지는 모름. 실제 가능 여부는 런타임에 JVM이 검증.

잘못 캐스팅 시:

Animal a = new Cat();  // 실제는 Cat
Dog d = (Dog) a;        // 💥 ClassCastException
                        // — JVM이 런타임에 "Cat은 Dog 아님" 발견

instanceof 검사로 사전 방어:

Animal a = new Cat();

if (a instanceof Dog) {  // false → 캐스팅 시도 안 함
    Dog d = (Dog) a;
    d.bark();
} else {
    // 안전한 대체 처리
}

Java 16+ 패턴 매칭으로 더 깔끔하게:

if (a instanceof Dog d) {  // 검사 + 캐스팅을 한 번에
    d.bark();
}

핵심 통찰 ⭐ :

"컴파일러는 다운캐스팅의 가능성만 확인하고, 실제 가능 여부는 런타임에 결정된다.
그래서 instanceof로 사전 검증이 안전한 패턴이다."

예외 — 100% 확실한 경우:

public void process(Object obj) {
    String s = obj.toString();  // toString() 결과는 항상 String
    // 캐스팅 불필요
}

→ 메서드의 반환 타입이 명확하면 캐스팅도 불필요.


Q2: Object instanceof String 이 false일 수 있는 경우는?

가능한 모든 경우:

1. Object 변수가 String이 아닌 다른 타입

Object obj = 42;  // Integer
obj instanceof String;  // false

2. Object 변수가 null

Object obj = null;
obj instanceof String;  // false (null은 어떤 타입도 아님)

3. Object 변수가 String의 부모 타입

Object obj = new Object();  // 정확히 Object
obj instanceof String;  // false (Object는 String의 부모이지 자손이 아님)

왜 false인가instanceof 의 동작 규칙:

"객체가 그 타입 또는 그 타입의 자손인 경우 에만 true"

핵심 정리:

Object obj 의 실제 타입obj instanceof String
nullfalse
Object (정확히)false
Integerfalse
Stringtrue
String의 자식 (없음 — final 클래스)true

비유:

  • "이 사람이 박씨 후손인가?" 물으면
  • 본인이 박씨 → true
  • 부모/조부모가 박씨이지만 본인은 다른 성씨 → false

→ instanceof는 자기 또는 자손 만 true.

참고 — String의 특수성:

  • String은 final 클래스 → 자식 클래스 없음
  • 그래서 instanceof String 이 true면 정확히 String

다음 Unit으로

  • Nested/Inner/Anonymous 클래스 를 학습할 준비 완료
  • "클래스 안에 또 클래스를?" 가 궁금하다
  • 람다의 전신을 만날 준비 완료
profile
Software Developer

0개의 댓글