🎯1주차 Unit 2.3 — 상속과 생성자 체이닝

Psj·2026년 5월 7일

F-lab

목록 보기
25/142

🎯 Unit 2.3 — 상속과 생성자 체이닝

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

선수 지식: Unit 2.1 (메서드의 구조)
다음 Unit: 2.4 — 다형성 (★ OOP의 정점)

이 Unit의 의미: 다형성으로 가는 결정적 디딤돌. extends, super() 의 동작 원리를 정확히 이해해야 다음 Unit이 자연스럽다.


🌍 1. 세상 속 비유

상속 = "유산과 가풍을 물려받는 가족"

한 집안을 상상해보세요.

할아버지의 집 (부모 클래스):

  • 재산 (필드): 토지, 가구, 가전제품
  • 가풍 (메서드): 새해 인사 방식, 식사 예절, 전통

할아버지가 자식에게 집을 물려주면, 자식은 자동으로 할아버지의 모든 것을 가지게 됩니다:

  • 같은 토지 위에서 살고
  • 같은 가구를 쓰고
  • 같은 가풍을 따름

자식의 집 (자식 클래스):

  • 할아버지의 모든 재산 + 가풍 + 자기만의 새 것
  • 새 가구를 추가할 수도 있고 (새 메서드 추가)
  • 가풍을 살짝 바꿀 수도 있음 (메서드 오버라이드)
  • 그러나 할아버지의 기반 위에서

이게 상속. extends 는 "이 부모의 모든 것을 물려받는다" 는 선언.


생성자 체이닝 = "집 짓기 순서"

새 집을 짓는다고 합시다. 자식이 자기 집을 지을 때:

"잠깐, 할아버지 집의 기초 부터 다지자.
그래야 그 위에 내 집을 올릴 수 있지."

1. 할아버지 집 기초 다짐 (super 생성자 호출)
   ├── 땅 정리
   ├── 토대 콘크리트
   └── 1층 골조
        ↓
2. 자식 집 기초 추가 (자식 생성자)
   ├── 2층 추가
   ├── 자기만의 인테리어
   └── 완성

핵심: 자식 집은 할아버지 기초 없이 못 지음. 그래서 자바는 자식 생성자를 호출하기 전에 반드시 부모 생성자부터 호출 합니다.

이게 생성자 체이닝(constructor chaining).


더 일상적인 비유 — "회사 직급"

일반 사원 (Employee)
  - 가진 것: 사번, 이름, 월급
  - 할 줄 아는 것: 출근하기, 퇴근하기

[승진]
        ↓
대리 (Manager extends Employee)
  - 추가로 가진 것: 부하 직원 목록
  - 추가로 할 줄 아는 것: 업무 지시
  - 그러나 출근/퇴근은 여전히 함 (상속)

[또 승진]
        ↓
부장 (Director extends Manager)
  - 추가로 가진 것: 부서 예산
  - 추가로 할 줄 아는 것: 예산 결정
  - 출근/퇴근, 업무 지시도 여전히 함 (상속의 상속)

핵심:

  • 대리는 사원의 모든 것 + 자기만의 것
  • 부장은 사원 + 대리 + 자기만의 것
  • 한 사람이 사원이 되지 않고 바로 부장이 될 수 없듯 → 부모 생성자가 먼저

🔥 2. 탄생 배경

상속이 없던 시절 — 코드 중복 지옥

상속이 없는 언어 (또는 사용하지 않을 때) 의 모습:

public class Customer {
    private String name;
    private String email;
    private String phone;
    
    public void register() { /* 등록 */ }
    public void sendNotification(String msg) { /* 알림 */ }
    public String getName() { return name; }
    // ... 30개 메서드
}

public class VipCustomer {
    // 일반 고객의 모든 것을 다시 작성
    private String name;       // 중복
    private String email;      // 중복
    private String phone;      // 중복
    private int discountRate;  // VIP만의 것
    
    public void register() { /* 같은 코드 */ }              // 중복
    public void sendNotification(String msg) { /* 같은 */ } // 중복
    public String getName() { return name; }                  // 중복
    
    public int getDiscount() { return discountRate; }  // 새 것
    // ... 30개 + 1개 메서드
}

public class PartnerCustomer {
    // 또 모든 것을 다시 작성 ❌
    // ...
}

문제:

  • 코드 폭증 — 새 등급마다 모든 코드 복사
  • 수정 지옥 — 알림 로직 변경 시 모든 클래스 수정
  • 버그 위험 — 한 군데 빠뜨리면 일관성 깨짐

상속의 등장 — Simula 67 (1967)

이 문제를 해결하려고 Simula 67 (1967) 이라는 언어가 상속(inheritance) 을 도입:

public class Customer {
    private String name;
    private String email;
    
    public void register() { /* 등록 */ }
    public void sendNotification(String msg) { /* 알림 */ }
}

// VipCustomer는 Customer의 모든 것을 자동으로 가짐
public class VipCustomer extends Customer {
    private int discountRate;  // 추가만 작성
    
    public int getDiscount() { return discountRate; }
}

public class PartnerCustomer extends Customer {
    // 추가만 작성
}

효과:

  • 공통 코드 한 번만 작성
  • 자식은 추가/수정만
  • 변경 시 부모 한 곳만 수정 → 모든 자식에 반영

상속은 "코드 재사용 + 확장" 의 혁명적 발명.


자바의 선택 — 단일 상속 ⭐ (면접 단골)

자바는 클래스 단일 상속만 허용 합니다. 한 클래스는 부모를 1개만 가질 수 있어요.

public class A { ... }
public class B { ... }

public class C extends A { ... }       // ✅ OK
public class D extends A, B { ... }    // ❌ 컴파일 에러

왜 단일 상속만?다이아몬드 문제 (Diamond Problem) ⚠️

        Animal
       /      \
   Dog        Cat
       \      /
        Hybrid (Dog + Cat 다중 상속 시)

만약 DogCat 모두 eat() 을 다르게 오버라이드 했다면?

  • Hybrid.eat() 은 어느 쪽?
  • 모호함 → 컴파일러도 결정 불가

C++ 의 사례: 다중 상속 허용 → 다이아몬드 문제로 악명 높음 → 복잡도 폭발.

자바의 결정: 클래스는 단일 상속, 인터페이스는 다중 구현 가능.

public class Person extends Mammal implements Worker, Citizen { ... }
//                  ↑ 클래스 1개 ↑ 인터페이스 N개

자바가 단순함을 선택한 결과. 이게 자바의 핵심 설계 철학 중 하나.


생성자 체이닝의 필요성

상속이 도입되니 새 문제 가 생겼습니다:

"자식 객체를 만들 때, 부모의 필드는 누가 초기화 하지?"

public class Customer {
    private String name;
    
    public Customer(String name) {
        this.name = name;
    }
}

public class VipCustomer extends Customer {
    private int discountRate;
    
    public VipCustomer(int discountRate) {
        // name은 누가 초기화?
        this.discountRate = discountRate;
    }
}

해결책:

  • 자식 생성자가 부모 생성자를 먼저 호출 해서 부모 필드 초기화
  • 그 후 자식만의 필드 초기화
public VipCustomer(String name, int discountRate) {
    super(name);  // ← 부모 생성자 호출 (먼저)
    this.discountRate = discountRate;  // ← 자기 필드
}

이게 생성자 체이닝. 상속의 자연스러운 귀결.


💣 3. 없으면 생기는 문제

상속과 생성자 체이닝이 없거나 잘못 사용했을 때의 문제를 보겠습니다.

시나리오: ILIC 운임 시스템의 다양한 운임 종류

ILIC가 다양한 운임 타입을 지원한다고 합시다:

  • 일반 운임 (Fare)
  • 긴급 운임 (UrgentFare) — 일반 + 긴급 처리비
  • 국제 운임 (InternationalFare) — 일반 + 환율 처리

상속 없이 — 거대한 한 클래스

// ❌ 모든 종류를 한 클래스에 우겨넣음
public class Fare {
    private Long id;
    private int amount;
    private FareStatus status;
    
    // 긴급 운임만 사용하는 필드
    private boolean isUrgent;
    private int urgentFee;
    
    // 국제 운임만 사용하는 필드
    private String currency;
    private double exchangeRate;
    private String origin;
    private String destination;
    
    // 모든 종류의 처리를 한 메서드에서
    public int calculateTotal() {
        int total = amount;
        if (isUrgent) {
            total += urgentFee;
        }
        if (currency != null) {
            total = (int)(total * exchangeRate);
        }
        return total;
    }
}

문제:
1. 메모리 낭비 — 일반 운임도 긴급/국제 필드를 가짐
2. null 체크 지옥 — 매 메서드마다 if (isUrgent), if (currency != null)
3. 무결성 XisUrgent=true 인데 urgentFee=0 같은 잘못된 상태 가능
4. 타입 안전성 X — 컴파일러가 "이건 일반, 이건 긴급" 구별 못함
5. 확장 불가 — 새 운임 종류 추가 시 또 필드 추가


상속을 잘못 사용 — 코드 복사

// ❌ 비슷한 코드 매번 작성
public class Fare {
    protected Long id;
    protected int amount;
    
    public int calculateTotal() { return amount; }
}

public class UrgentFare {  // extends 안 함, 코드 복사
    private Long id;
    private int amount;
    private int urgentFee;
    
    public int calculateTotal() { return amount + urgentFee; }
}

public class InternationalFare {  // 또 복사
    private Long id;
    private int amount;
    private String currency;
    private double exchangeRate;
    
    public int calculateTotal() { return (int)(amount * exchangeRate); }
}

문제:

  • 공통 필드 (id, amount) 가 매 클래스에 중복
  • Fare 의 메서드 변경 시 다른 클래스도 수정 필요
  • 다형성 활용 불가 (한 통합 타입으로 다룰 수 없음)

상속 + 생성자 체이닝의 올바른 사용

// ✅ 부모 클래스
public class Fare {
    protected Long id;
    protected int amount;
    protected FareStatus status;
    
    public Fare(Long id, int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("음수 불가");
        }
        this.id = id;
        this.amount = amount;
        this.status = FareStatus.DRAFT;
    }
    
    public int calculateTotal() {
        return amount;
    }
}

// ✅ 자식 — 긴급 운임
public class UrgentFare extends Fare {
    private int urgentFee;
    
    public UrgentFare(Long id, int amount, int urgentFee) {
        super(id, amount);  // ← 부모 생성자 호출 (필수)
        this.urgentFee = urgentFee;
    }
    
    @Override
    public int calculateTotal() {
        return super.calculateTotal() + urgentFee;
    }
}

// ✅ 자식 — 국제 운임
public class InternationalFare extends Fare {
    private String currency;
    private double exchangeRate;
    
    public InternationalFare(Long id, int amount, String currency, double rate) {
        super(id, amount);  // ← 부모 생성자 호출
        this.currency = currency;
        this.exchangeRate = rate;
    }
    
    @Override
    public int calculateTotal() {
        return (int)(super.calculateTotal() * exchangeRate);
    }
}

효과:

  • 공통 코드 한 곳에 (Fare)
  • 각 자식은 자기만의 추가만 작성
  • 부모 생성자 체이닝으로 무결성 보장
  • 다형성 활용 가능 (Fare fare = new UrgentFare(...);)

이게 상속의 진짜 가치.


만약 생성자 체이닝을 빠뜨리면? — 자바가 막아줌

public class UrgentFare extends Fare {
    private int urgentFee;
    
    public UrgentFare(int urgentFee) {
        // super(...) 안 부름
        this.urgentFee = urgentFee;
    }
}

컴파일 에러: There is no default constructor available in 'Fare'

자바가 "부모 초기화 없이 자식만 만들 수 없다" 를 강제. 이게 자바의 안전장치.


✅ 4. 해결책 — 상속과 생성자 체이닝 문법

상속 문법 — extends

public class 자식클래스 extends 부모클래스 {
    // 부모의 모든 필드/메서드를 자동으로 상속
    // 추가/수정만 작성
}

상속되는 것 vs 안 되는 것 ⭐

상속 O:

  • ✅ public 필드/메서드
  • ✅ protected 필드/메서드
  • ✅ default (같은 패키지 내)

상속 X:

  • ❌ private 필드/메서드 (직접 접근 불가, 그러나 부모 인스턴스에는 존재)
  • ❌ 생성자 (생성자는 상속 X — 호출만 가능)
  • ❌ static 멤버 (상속이라기보다 "공유")

예시:

public class Parent {
    public int publicField = 1;
    protected int protectedField = 2;
    int defaultField = 3;
    private int privateField = 4;
    
    public void publicMethod() { ... }
    private void privateMethod() { ... }
}

public class Child extends Parent {
    public void test() {
        System.out.println(publicField);     // ✅ 1
        System.out.println(protectedField);  // ✅ 2
        System.out.println(defaultField);    // ✅ 3 (같은 패키지면)
        // System.out.println(privateField); // ❌ 컴파일 에러
        
        publicMethod();      // ✅
        // privateMethod(); // ❌
    }
}

⚠️ 중요한 함정: private 도 자식 객체에 존재 합니다. 단지 직접 접근만 불가. (메모리에는 존재하니 부모의 메서드를 통해 접근 가능)


생성자 체이닝 문법 — super()

public class Child extends Parent {
    public Child(...) {
        super(...);  // ← 부모 생성자 호출 (반드시 첫 줄)
        // 자식 초기화
    }
}

규칙 ⭐ :
1. super(...)생성자의 첫 줄 이어야 함
2. 생략하면 자바가 자동으로 super() (매개변수 없는) 추가
3. 부모에 매개변수 없는 생성자가 없으면 반드시 명시적 호출


자동 super() 호출 — 자바의 친절

public class Parent {
    public Parent() {
        System.out.println("Parent 생성자");
    }
}

public class Child extends Parent {
    public Child() {
        // super() 안 썼지만
        // 자바가 자동으로 super() 추가
        System.out.println("Child 생성자");
    }
}

new Child();
// 출력:
// Parent 생성자
// Child 생성자

컴파일러가 자동 변환:

public Child() {
    super();  // ← 자동 추가
    System.out.println("Child 생성자");
}

부모에 매개변수 있는 생성자만 있을 때 ⭐⭐ (자기 점검 Q1)

public class Parent {
    private String name;
    
    // 매개변수 없는 생성자 X, 매개변수 있는 것만
    public Parent(String name) {
        this.name = name;
    }
}

public class Child extends Parent {
    public Child() {
        // 자바가 자동으로 super() 시도
        // → Parent에 매개변수 없는 생성자 없음 → ❌ 컴파일 에러!
    }
}

컴파일 에러 메시지:

There is no default constructor available in 'Parent'

해결 — 명시적 super(...) 호출:

public class Child extends Parent {
    public Child() {
        super("기본 이름");  // ✅ 명시적 호출
    }
    
    public Child(String name) {
        super(name);  // ✅ 인자 전달
    }
}

자기 점검 Q1의 답이 여기에.


this() 와 super() 의 관계

같은 클래스의 다른 생성자를 호출하는 this(...):

public class Customer {
    private String name;
    private int age;
    
    public Customer(String name) {
        this(name, 0);  // ← 다른 생성자 호출
    }
    
    public Customer(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

규칙 ⭐ :

  • super()this()동시 사용 불가 (둘 다 첫 줄이어야 하는데 첫 줄은 1개만)
  • this() 를 사용하면 결국 그 호출된 생성자가 super() 를 호출
public class Child extends Parent {
    public Child() {
        this("기본");  // 이 호출이 결국 아래의 super를 통해 Parent 초기화
    }
    
    public Child(String name) {
        super(name);  // 부모 초기화
    }
}

🏗️ 5. 내부 동작 원리

객체 생성 시 메모리에서 일어나는 일 ⭐

public class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
}

public class Dog extends Animal {
    private String breed;
    
    public Dog(String name, String breed) {
        super(name);
        this.breed = breed;
    }
}

Dog dog = new Dog("뽀삐", "푸들");

JVM 내부 흐름 ⭐ :

1. new Dog(...) 호출
        ↓
2. JVM: Heap에 Dog 인스턴스용 메모리 할당
   - name 필드 자리 (Animal에서 상속)
   - breed 필드 자리 (Dog 자기 것)
        ↓
3. Dog 생성자 실행 시작
        ↓
4. super(name) 실행
        ↓
5. Animal 생성자 실행
   - this.name = "뽀삐"
        ↓
6. Animal 생성자 종료
        ↓
7. Dog 생성자 계속
   - this.breed = "푸들"
        ↓
8. Dog 생성자 종료
        ↓
9. dog 변수에 인스턴스 참조 반환

메모리 구조 (Heap):

[Dog 인스턴스]
  ├── name = "뽀삐"  ← Animal로부터 상속받은 부분 (먼저 초기화)
  └── breed = "푸들" ← Dog 자기 부분 (나중에 초기화)

핵심 통찰:

"부모가 먼저, 자식이 나중에 초기화"
"객체 하나에 부모 필드 + 자식 필드가 모두 들어있음"


Object — 모든 클래스의 최상위 부모 ⭐

자바에서 모든 클래스는 자동으로 Object 를 상속합니다:

public class Customer { ... }

// 컴파일러가 자동 변환
public class Customer extends Object { ... }

상속 체인:

Object  (모든 클래스의 부모)
  ↓
Animal
  ↓
Dog
  ↓
Puppy

새 Puppy 객체 생성 시 — 체이닝이 끝까지 올라감:

public Puppy() {
    super();  // Dog 생성자 호출
}

public Dog() {
    super();  // Animal 생성자 호출
}

public Animal() {
    super();  // Object 생성자 호출
}

public Object() {
    // JVM 내부 초기화 (가장 위)
}

실행 순서:

Object 생성자 → Animal 생성자 → Dog 생성자 → Puppy 생성자
   (가장 먼저)                                    (가장 나중)

모든 객체는 Object의 후손. 그래서 모든 객체에 toString(), equals(), hashCode() 가 있음 (Object의 메서드).


메서드 오버라이드의 내부 동작 — Virtual Method Table ⭐

자식이 부모의 메서드를 오버라이드(override) 하면?

public class Animal {
    public void makeSound() {
        System.out.println("동물 소리");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

Animal animal = new Dog();
animal.makeSound();  // "멍멍!" — Dog의 메서드 호출됨

왜 Animal 타입인데 Dog의 메서드가?VMT (Virtual Method Table) 덕분.

Animal의 VMT:
[makeSound → Animal.makeSound]
[toString → Object.toString]
[equals → Object.equals]

Dog의 VMT (상속받음 + 오버라이드):
[makeSound → Dog.makeSound]  ← 오버라이드된 것 우선 ⭐
[toString → Object.toString]
[equals → Object.equals]

호출 흐름:

1. animal.makeSound() 호출
2. JVM: animal이 가리키는 실제 객체는?
   → Dog 인스턴스
3. JVM: Dog의 VMT 확인
4. makeSound → Dog.makeSound 발견
5. Dog.makeSound() 실행 → "멍멍!"

핵심: 참조 타입(Animal)이 아닌 실제 객체 타입(Dog)의 메서드 호출

→ 이게 다음 Unit (2.4 다형성) 의 핵심. 미리보기.


@Override 어노테이션 ⭐

public class Dog extends Animal {
    @Override  // ← 이 메서드는 부모의 것을 오버라이드함을 명시
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

역할:

  • 컴파일러에게 "이건 오버라이드다" 명시
  • 부모에 같은 시그니처 메서드가 없으면 컴파일 에러

왜 필요한가 (이거 안 쓰면 위험):

public class Animal {
    public void makeSound() { ... }
}

public class Dog extends Animal {
    public void makesound() {  // 오타: 's' 가 소문자
        System.out.println("멍멍!");
    }
}

Animal animal = new Dog();
animal.makeSound();  // "동물 소리" 출력 — 오버라이드 안 됨! ❌

@Override 가 있었다면 컴파일러가 오타 즉시 잡아줌:

@Override
public void makesound() {  // ❌ 컴파일 에러: 'makesound' 메서드는 부모에 없음
    ...
}

모든 오버라이드에 @Override 필수.


💻 6. 실전 코드 예시

ILIC 운임 시스템에서 상속을 활용한 실전 예시들.

예시 1: 운임 종류별 상속 구조

// 부모 — 모든 운임의 공통
public abstract class Fare {
    protected Long id;
    protected int amount;
    protected FareStatus status;
    protected Long customerId;
    
    public Fare(Long id, int amount, Long customerId) {
        validateAmount(amount);
        this.id = id;
        this.amount = amount;
        this.customerId = customerId;
        this.status = FareStatus.DRAFT;
    }
    
    // 공통 검증
    protected void validateAmount(int amount) {
        if (amount < 0) throw new IllegalArgumentException("음수 불가");
    }
    
    // 추상 메서드 — 자식이 반드시 구현
    public abstract int calculateTotal();
    
    // 공통 상태 전이
    public void submit() {
        if (status != FareStatus.DRAFT) {
            throw new IllegalStateException("DRAFT 상태에서만 제출 가능");
        }
        this.status = FareStatus.SUBMITTED;
    }
}

// 자식 1 — 일반 운임
public class StandardFare extends Fare {
    public StandardFare(Long id, int amount, Long customerId) {
        super(id, amount, customerId);  // 부모 생성자 호출
    }
    
    @Override
    public int calculateTotal() {
        return amount;
    }
}

// 자식 2 — 긴급 운임
public class UrgentFare extends Fare {
    private int urgentFee;
    
    public UrgentFare(Long id, int amount, Long customerId, int urgentFee) {
        super(id, amount, customerId);  // 부모 먼저
        if (urgentFee < 0) throw new IllegalArgumentException("긴급비 음수 불가");
        this.urgentFee = urgentFee;
    }
    
    @Override
    public int calculateTotal() {
        return amount + urgentFee;  // 일반 + 긴급비
    }
    
    public int getUrgentFee() { return urgentFee; }
}

// 자식 3 — 국제 운임
public class InternationalFare extends Fare {
    private String currency;
    private double exchangeRate;
    private String origin;
    private String destination;
    
    public InternationalFare(Long id, int amount, Long customerId,
                              String currency, double rate,
                              String origin, String destination) {
        super(id, amount, customerId);  // 부모 먼저
        this.currency = currency;
        this.exchangeRate = rate;
        this.origin = origin;
        this.destination = destination;
    }
    
    @Override
    public int calculateTotal() {
        return (int)(amount * exchangeRate);
    }
}

사용:

Fare standard = new StandardFare(1L, 50000, 100L);
Fare urgent = new UrgentFare(2L, 50000, 100L, 10000);
Fare international = new InternationalFare(3L, 100, 100L, "USD", 1300.0, "Seoul", "Tokyo");

System.out.println(standard.calculateTotal());        // 50000
System.out.println(urgent.calculateTotal());          // 60000
System.out.println(international.calculateTotal());   // 130000

// 공통 메서드는 모두에서 사용 가능
standard.submit();
urgent.submit();
international.submit();

효과:

  • 공통 로직 (검증, 상태 전이) 한 곳에
  • 각 종류만의 계산은 각자
  • 다형성으로 통일된 처리 가능 (Fare 타입으로 다루기)

예시 2: 생성자 체이닝의 올바른 패턴

// 부모
public class User {
    protected String email;
    protected String name;
    protected LocalDateTime createdAt;
    
    public User(String email, String name) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("유효하지 않은 이메일");
        }
        this.email = email;
        this.name = name;
        this.createdAt = LocalDateTime.now();
    }
}

// 자식 — 다양한 생성자 패턴
public class AdminUser extends User {
    private int adminLevel;
    private List<String> permissions;
    
    // 패턴 1 — 기본 생성자
    public AdminUser(String email, String name) {
        this(email, name, 1);  // this()로 다른 생성자 호출
    }
    
    // 패턴 2 — 레벨 지정
    public AdminUser(String email, String name, int adminLevel) {
        this(email, name, adminLevel, new ArrayList<>());  // this() 체이닝
    }
    
    // 패턴 3 — 모든 정보
    public AdminUser(String email, String name, int adminLevel, List<String> permissions) {
        super(email, name);  // ← super() — 부모 호출
        this.adminLevel = adminLevel;
        this.permissions = permissions;
    }
}

호출 흐름new AdminUser("a@b.com", "Alice"):

1. AdminUser(String, String) 진입
2. this("a@b.com", "Alice", 1) 호출
3. AdminUser(String, String, int) 진입
4. this("a@b.com", "Alice", 1, new ArrayList<>()) 호출
5. AdminUser(String, String, int, List) 진입
6. super("a@b.com", "Alice") 호출
7. User 생성자 실행 — email, name, createdAt 초기화
8. User 생성자 종료
9. AdminUser 생성자 계속 — adminLevel, permissions 초기화
10. 모든 생성자 체인 종료

핵심: 결국 모든 경로가 super() 로 수렴 해서 부모 초기화.


예시 3: 메서드 오버라이드 활용

public class Notification {
    protected String message;
    protected String recipient;
    
    public Notification(String message, String recipient) {
        this.message = message;
        this.recipient = recipient;
    }
    
    public void send() {
        System.out.println("[Default] " + recipient + ": " + message);
    }
}

public class EmailNotification extends Notification {
    private String subject;
    
    public EmailNotification(String subject, String message, String recipient) {
        super(message, recipient);
        this.subject = subject;
    }
    
    @Override
    public void send() {
        // 부모 동작 + 추가 동작
        System.out.println("[Email] To: " + recipient);
        System.out.println("Subject: " + subject);
        System.out.println("Body: " + message);
    }
}

public class SmsNotification extends Notification {
    public SmsNotification(String message, String phone) {
        super(message, phone);
    }
    
    @Override
    public void send() {
        // 완전히 다른 동작
        System.out.println("[SMS] " + recipient + ": " + message);
    }
}

public class UrgentEmailNotification extends EmailNotification {
    public UrgentEmailNotification(String subject, String message, String recipient) {
        super("[긴급] " + subject, message, recipient);
    }
    
    @Override
    public void send() {
        System.out.println("⚠️ 긴급 알림 ⚠️");
        super.send();  // ← 부모(EmailNotification)의 send() 호출
        System.out.println("⚠️ 즉시 확인 요망 ⚠️");
    }
}

super.method() ⭐ — 부모의 메서드를 명시적으로 호출:

  • 자식에서 오버라이드하면서도 부모 동작을 활용하고 싶을 때

예시 4: ILIC Customer 상속 구조

public abstract class Customer {
    protected Long id;
    protected String name;
    protected String email;
    protected LocalDateTime registeredAt;
    
    public Customer(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.registeredAt = LocalDateTime.now();
    }
    
    // 공통 메서드
    public void register() {
        System.out.println(name + " 등록 완료");
    }
    
    // 추상 메서드 — 등급별 다름
    public abstract int calculateDiscountRate();
    public abstract String getCustomerType();
}

public class IndividualCustomer extends Customer {
    private String phone;
    
    public IndividualCustomer(Long id, String name, String email, String phone) {
        super(id, name, email);  // 부모 호출
        this.phone = phone;
    }
    
    @Override
    public int calculateDiscountRate() { return 0; }
    
    @Override
    public String getCustomerType() { return "INDIVIDUAL"; }
}

public class CorporateCustomer extends Customer {
    private String companyName;
    private String businessRegistrationNumber;
    
    public CorporateCustomer(Long id, String name, String email,
                              String companyName, String registrationNumber) {
        super(id, name, email);
        this.companyName = companyName;
        this.businessRegistrationNumber = registrationNumber;
    }
    
    @Override
    public int calculateDiscountRate() { return 10; }  // 기업 10% 할인
    
    @Override
    public String getCustomerType() { return "CORPORATE"; }
}

public class VipCorporateCustomer extends CorporateCustomer {
    private int annualVolume;
    
    public VipCorporateCustomer(Long id, String name, String email,
                                  String companyName, String registrationNumber,
                                  int annualVolume) {
        super(id, name, email, companyName, registrationNumber);  // 부모 호출 (CorporateCustomer)
        this.annualVolume = annualVolume;
    }
    
    @Override
    public int calculateDiscountRate() {
        return 20;  // VIP 기업 20% 할인
    }
    
    @Override
    public String getCustomerType() {
        return "VIP_CORPORATE";
    }
}

3단 상속 구조:

Customer (추상)
   ↓
CorporateCustomer
   ↓
VipCorporateCustomer

호출 흐름new VipCorporateCustomer(...):

VipCorporateCustomer 생성자
  ↓ super(...)
CorporateCustomer 생성자
  ↓ super(...)
Customer 생성자
  ↓ super()
Object 생성자 (자동)

→ 모든 부모가 순서대로 초기화됨.


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

실수 1: super() 호출 위치

public class Child extends Parent {
    public Child() {
        System.out.println("자식 시작");
        super();  // ❌ 컴파일 에러: 첫 줄이어야 함
    }
}

규칙: super() 또는 this()반드시 생성자의 첫 줄.

올바른 코드:

public Child() {
    super();
    System.out.println("자식 시작");
}

실수 2: 부모에 매개변수 있는 생성자만 → super() 빠뜨림 ⭐ (자기 점검 Q1)

public class Parent {
    private String name;
    
    public Parent(String name) {  // 매개변수 있는 생성자만
        this.name = name;
    }
}

public class Child extends Parent {
    public Child() {
        // super() 안 부름
        // 자바가 자동 super() 시도
        // → Parent에 매개변수 없는 생성자 없음
    }
}

컴파일 에러: There is no default constructor available in 'Parent'

해결 — 명시적 호출:

public class Child extends Parent {
    public Child() {
        super("기본");  // ✅
    }
}

자기 점검 Q1의 답: 컴파일 에러 발생. 자바가 자동으로 super() 시도하지만 매개변수 없는 생성자가 없어서 실패.


실수 3: 무분별한 상속 ⚠️ (Unit 1.1 참고)

이전에 다룬 함정 다시 강조:

// ❌ 잘못된 상속
public class Stack extends ArrayList { ... }

원칙: is-a 관계 만 상속, 그 외에는 합성.

→ Unit 1.1의 "상속 남용" 참고.


실수 4: protected 남용

public class Parent {
    protected int internalState;  // ⚠️ 자식에서 직접 접근 가능
    
    public void process() {
        // internalState 검증 후 변경
    }
}

public class Child extends Parent {
    public void manipulate() {
        internalState = -99999;  // ❌ 검증 우회!
    }
}

문제:

  • protected는 자식이 부모의 내부에 직접 접근 가능
  • 캡슐화가 깨짐

해결:

  • 가능한 한 private + protected 메서드로 노출
  • 자식이 직접 필드를 만지지 않도록
public class Parent {
    private int internalState;
    
    protected void setInternalState(int value) {
        validate(value);
        this.internalState = value;
    }
}

실수 5: @Override 누락

// ❌ 오타 발생 가능
public class Dog extends Animal {
    public void makesound() {  // 오타: 소문자 's'
        System.out.println("멍멍");
    }
}

Animal a = new Dog();
a.makeSound();  // "동물 소리" — 오버라이드 안 됨!

해결 — 모든 오버라이드에 @Override:

@Override
public void makesound() {  // 컴파일 에러 즉시 발견
    ...
}

@Override 는 선택이 아니라 필수.


실수 6: equals() / hashCode() 안 챙김

상속받은 객체를 컬렉션에 쓸 때:

public class Customer {
    private Long id;
    // equals(), hashCode() 안 만듦 → Object의 기본 사용 (참조 비교)
}

Customer a = new Customer(1L);
Customer b = new Customer(1L);

a.equals(b);  // false ❌ (참조가 다름)

Set<Customer> set = new HashSet<>();
set.add(a);
set.contains(b);  // false ❌

해결:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Customer)) return false;
    Customer other = (Customer) obj;
    return Objects.equals(this.id, other.id);
}

@Override
public int hashCode() {
    return Objects.hash(id);
}

→ 6주차에서 더 깊이 다룰 주제. 지금은 "상속 시 챙겨야 할 것" 으로만 기억.


실수 7: 생성자에서 오버라이드 가능한 메서드 호출 ⚠️ (고급 함정)

public class Parent {
    public Parent() {
        init();  // ⚠️ 위험!
    }
    
    public void init() {
        System.out.println("Parent 초기화");
    }
}

public class Child extends Parent {
    private String name = "Alice";
    
    @Override
    public void init() {
        System.out.println("Child 초기화: " + name.length());  // NullPointerException!
    }
}

new Child();

왜 NPE?:
1. new Child() 시 Parent 생성자부터 실행
2. Parent 생성자에서 init() 호출
3. 다형성으로 Child의 init() 이 실행됨
4. 그러나 이 시점에는 Child의 필드가 아직 초기화 안 됨 (name = null)
5. name.length() → NPE

해결:

  • 생성자에서 오버라이드 가능한 메서드 호출 X
  • 또는 그 메서드를 final, private, static 으로

→ Effective Java의 유명한 항목. 면접 고급 질문.


🔗 8. 연관 개념 맵

직접 이어지는 학습

[Unit 2.1: 메서드의 구조]
        ↓
[Unit 2.2: 가변인자]
        ↓
[Unit 2.3: 상속과 생성자 체이닝]  ← 지금 여기
        ↓
[Unit 2.4: 다형성] ★★★ — OOP의 정점
        ↓
[Unit 2.5: instanceof와 형변환]

이 Unit의 개념이 활용되는 곳

1주차 내:

  • Unit 2.4 (다형성): 상속이 다형성의 토대
  • Unit 2.5 (instanceof): 상속 관계 확인
  • Phase 3 (SOLID): LSP (리스코프 치환) — 상속의 진짜 규칙
  • Phase 4 (JVM 메모리): 객체에 부모 + 자식 필드 모두 존재

미래 주차:

  • 3주차 (제네릭): 상속 관계와 제네릭 타입의 상호작용 (List<? extends Animal>)
  • 5주차 (Spring): 의존성 주입 시 상속 관계 활용
  • 8-9주차 (AOP): 프록시가 상속으로 만들어짐
  • 11-12주차 (JPA): Entity 상속 (@Inheritance)
  • 15주차 (Spring MVC): HandlerInterceptor 등 프레임워크 확장

상속 vs 합성 vs 인터페이스 ⭐

상속 (extends)합성 (composition)인터페이스 (implements)
관계is-ahas-acan-do
결합도강함약함약함
다중 가능X (1개만)○ (N개)
실무 빈도적당히가장 많음매우 많음
용도명확한 분류행동 조립능력 표현

현대 트렌드:

"상속보다 합성을, 클래스보다 인터페이스를"


면접 단골 질문 매핑

질문이 Unit에서의 답
"상속이란?"extends로 부모의 모든 것을 자식이 물려받음
"다중 상속이 안 되는 이유?"다이아몬드 문제 — 자바는 단일 상속만
"super() 의 역할?"부모 생성자 호출, 첫 줄이어야 함
"부모 생성자 자동 호출?"super() 생략 시 자바가 자동으로 매개변수 없는 super() 추가
"@Override 의 역할?"오버라이드 명시 + 컴파일 시 오타 검증
"Object 클래스란?"모든 클래스의 최상위 부모

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

1️⃣ 상속은 "부모의 모든 것을 자식이 자동으로 가진다" 는 선언이다.

extends 키워드로 표현하며, 부모의 public/protected 멤버를 자식이 그대로 쓸 수 있다. 자바는 클래스 단일 상속 만 허용 (다이아몬드 문제 회피), 인터페이스는 다중 구현 가능. 모든 클래스는 자동으로 Object 를 상속한다.

2️⃣ 생성자 체이닝은 "부모부터 차례로 초기화" 하는 안전장치다.

자식 생성자는 반드시 첫 줄에서 super(...) 호출 (생략 시 자동으로 super() 추가). 부모에 매개변수 있는 생성자만 있으면 자식이 명시적으로 super(...) 호출해야 함 (안 하면 컴파일 에러). 객체 메모리에는 부모 필드 + 자식 필드 가 함께 존재.

3️⃣ 상속은 강력하지만 위험하다 — 함정을 알고 써야 한다.

@Override 누락 (오타 시 오버라이드 안 됨), 생성자에서 오버라이드 가능한 메서드 호출 (NPE 위험), protected 남용 (캡슐화 깨짐), 무분별한 상속 (Stack extends ArrayList 같은 안티패턴). 의심스러우면 합성 을 먼저 검토.


🎓 학습 자기 점검

기본 이해

  • extends 키워드의 의미를 한 문장으로 설명할 수 있다
  • 자바가 단일 상속만 허용하는 이유를 안다 (다이아몬드 문제)
  • super() 가 반드시 첫 줄이어야 함을 안다
  • 모든 클래스가 Object 를 상속한다는 사실을 안다

실전 적용

  • ILIC 코드의 상속 구조를 그릴 수 있다
  • 부모-자식 생성자 체이닝을 작성할 수 있다
  • 어떤 멤버가 상속되고 안 되는지 분류할 수 있다
  • @Override를 모든 오버라이드에 사용한다

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

  • "상속과 생성자 체이닝의 동작 원리?" 답변 가능
  • "다중 상속이 자바에 없는 이유?" 답변 가능
  • "Object 클래스의 의미?" 답변 가능

자기 점검 질문 답변

Q1: 부모 클래스에 매개변수 있는 생성자만 있고 자식에서 super를 안 쓰면 무슨 일이 일어나는가?

컴파일 에러 발생.

상세 흐름:
1. 자식 생성자 작성 시 super() 안 쓰면
2. 자바 컴파일러가 자동으로 super() (매개변수 없는) 추가 시도
3. 그런데 부모에 매개변수 없는 생성자가 없음
4. → 컴파일 에러: "There is no default constructor available in 'Parent'"

예시:

public class Parent {
    public Parent(String name) {  // 매개변수 있는 것만
        this.name = name;
    }
}

public class Child extends Parent {
    public Child() {
        // 컴파일러: super() 추가 시도
        // → 매개변수 없는 Parent 생성자 없음 → 에러
    }
}

해결 방법 (3가지):

1. 자식에서 명시적 super(...) — 가장 흔함

public Child() {
    super("기본 이름");
}

2. 부모에 매개변수 없는 생성자 추가

public class Parent {
    public Parent() { ... }  // 추가
    public Parent(String name) { this.name = name; }
}

3. 자식에서도 매개변수 받기

public Child(String name) {
    super(name);  // 부모로 전달
}

핵심 통찰:

"자바는 부모 초기화 없이 자식만 만들 수 없다" 는 안전장치를 강제. 이걸 어기려는 시도를 컴파일러가 차단.


Q2: 다중 상속이 자바에서 금지된 이유는?

다이아몬드 문제 (Diamond Problem) 때문.

다이아몬드 문제 시나리오:

        Animal
       /      \
     Dog      Cat
       \      /
        DogCat (Dog + Cat 다중 상속)

DogCat 모두 Animal.eat() 을 다르게 오버라이드 했다고 가정:

public class Animal {
    public void eat() { System.out.println("동물 식사"); }
}

public class Dog extends Animal {
    @Override
    public void eat() { System.out.println("개 식사"); }
}

public class Cat extends Animal {
    @Override
    public void eat() { System.out.println("고양이 식사"); }
}

// 자바에서는 불가, C++ 에서 다중 상속 가능하다 가정
public class DogCat extends Dog, Cat {
    // ...
}

DogCat dc = new DogCat();
dc.eat();  // ❓ 어느 것? Dog의 것? Cat의 것?

모호함 (Ambiguity). 컴파일러도 결정 불가.

자바의 해결책 ⭐ :

1. 클래스는 단일 상속만:

public class Child extends OneParent { ... }  // OK
public class Child extends A, B { ... }       // ❌

2. 인터페이스는 다중 구현 가능 (메서드 정의만 가질 수 있어 모호함 적음):

public class Person extends Mammal implements Worker, Citizen { ... }

3. Java 8+ default 메서드 등장 시 다이아몬드 위험:

public interface A {
    default void method() { ... }
}

public interface B {
    default void method() { ... }
}

public class C implements A, B {
    // 컴파일 에러 — 어느 것?
}

→ Java 8은 이 경우 명시적 오버라이드 강제:

public class C implements A, B {
    @Override
    public void method() {
        A.super.method();  // 명시적 선택
    }
}

자바의 설계 철학:

"복잡함보다 단순함을" — C++ 의 다중 상속 복잡도를 피하고 단순한 모델 선택.


다음 Unit으로

  • 다형성 을 학습할 준비 완료
  • "왜 Animal a = new Dog(); 가 가능한가" 가 궁금하다
  • OOP의 정점인 다형성의 마법을 만날 준비 완료
profile
Software Developer

0개의 댓글