F-lab Java 1주차 / Phase 2 / Unit 2.3 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 2.1 (메서드의 구조)
다음 Unit: 2.4 — 다형성 (★ OOP의 정점)이 Unit의 의미: 다형성으로 가는 결정적 디딤돌.
extends,super()의 동작 원리를 정확히 이해해야 다음 Unit이 자연스럽다.
한 집안을 상상해보세요.
할아버지의 집 (부모 클래스):
할아버지가 자식에게 집을 물려주면, 자식은 자동으로 할아버지의 모든 것을 가지게 됩니다:
자식의 집 (자식 클래스):
→ 이게 상속. extends 는 "이 부모의 모든 것을 물려받는다" 는 선언.
새 집을 짓는다고 합시다. 자식이 자기 집을 지을 때:
"잠깐, 할아버지 집의 기초 부터 다지자.
그래야 그 위에 내 집을 올릴 수 있지."
1. 할아버지 집 기초 다짐 (super 생성자 호출)
├── 땅 정리
├── 토대 콘크리트
└── 1층 골조
↓
2. 자식 집 기초 추가 (자식 생성자)
├── 2층 추가
├── 자기만의 인테리어
└── 완성
핵심: 자식 집은 할아버지 기초 없이 못 지음. 그래서 자바는 자식 생성자를 호출하기 전에 반드시 부모 생성자부터 호출 합니다.
→ 이게 생성자 체이닝(constructor chaining).
일반 사원 (Employee)
- 가진 것: 사번, 이름, 월급
- 할 줄 아는 것: 출근하기, 퇴근하기
[승진]
↓
대리 (Manager extends Employee)
- 추가로 가진 것: 부하 직원 목록
- 추가로 할 줄 아는 것: 업무 지시
- 그러나 출근/퇴근은 여전히 함 (상속)
[또 승진]
↓
부장 (Director extends Manager)
- 추가로 가진 것: 부서 예산
- 추가로 할 줄 아는 것: 예산 결정
- 출근/퇴근, 업무 지시도 여전히 함 (상속의 상속)
핵심:
상속이 없는 언어 (또는 사용하지 않을 때) 의 모습:
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) 이라는 언어가 상속(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 다중 상속 시)
만약 Dog 와 Cat 모두 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; // ← 자기 필드
}
→ 이게 생성자 체이닝. 상속의 자연스러운 귀결.
상속과 생성자 체이닝이 없거나 잘못 사용했을 때의 문제를 보겠습니다.
ILIC가 다양한 운임 타입을 지원한다고 합시다:
// ❌ 모든 종류를 한 클래스에 우겨넣음
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. 무결성 X — isUrgent=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 = 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'
자바가 "부모 초기화 없이 자식만 만들 수 없다" 를 강제. 이게 자바의 안전장치.
extendspublic class 자식클래스 extends 부모클래스 {
// 부모의 모든 필드/메서드를 자동으로 상속
// 추가/수정만 작성
}
상속 O:
상속 X:
예시:
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. 부모에 매개변수 없는 생성자가 없으면 반드시 명시적 호출
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 생성자");
}
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(...):
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); // 부모 초기화
}
}
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 를 상속합니다:
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의 메서드).
자식이 부모의 메서드를 오버라이드(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 필수.
ILIC 운임 시스템에서 상속을 활용한 실전 예시들.
// 부모 — 모든 운임의 공통
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 타입으로 다루기)// 부모
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() 로 수렴 해서 부모 초기화.
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() ⭐ — 부모의 메서드를 명시적으로 호출:
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 생성자 (자동)
→ 모든 부모가 순서대로 초기화됨.
public class Child extends Parent {
public Child() {
System.out.println("자식 시작");
super(); // ❌ 컴파일 에러: 첫 줄이어야 함
}
}
규칙: super() 또는 this() 는 반드시 생성자의 첫 줄.
올바른 코드:
public Child() {
super();
System.out.println("자식 시작");
}
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() 시도하지만 매개변수 없는 생성자가 없어서 실패.
이전에 다룬 함정 다시 강조:
// ❌ 잘못된 상속
public class Stack extends ArrayList { ... }
원칙: is-a 관계 만 상속, 그 외에는 합성.
→ Unit 1.1의 "상속 남용" 참고.
public class Parent {
protected int internalState; // ⚠️ 자식에서 직접 접근 가능
public void process() {
// internalState 검증 후 변경
}
}
public class Child extends Parent {
public void manipulate() {
internalState = -99999; // ❌ 검증 우회!
}
}
문제:
해결:
public class Parent {
private int internalState;
protected void setInternalState(int value) {
validate(value);
this.internalState = value;
}
}
// ❌ 오타 발생 가능
public class Dog extends Animal {
public void makesound() { // 오타: 소문자 's'
System.out.println("멍멍");
}
}
Animal a = new Dog();
a.makeSound(); // "동물 소리" — 오버라이드 안 됨!
해결 — 모든 오버라이드에 @Override:
@Override
public void makesound() { // 컴파일 에러 즉시 발견
...
}
→ @Override 는 선택이 아니라 필수.
상속받은 객체를 컬렉션에 쓸 때:
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주차에서 더 깊이 다룰 주제. 지금은 "상속 시 챙겨야 할 것" 으로만 기억.
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
해결:
→ Effective Java의 유명한 항목. 면접 고급 질문.
[Unit 2.1: 메서드의 구조]
↓
[Unit 2.2: 가변인자]
↓
[Unit 2.3: 상속과 생성자 체이닝] ← 지금 여기
↓
[Unit 2.4: 다형성] ★★★ — OOP의 정점
↓
[Unit 2.5: instanceof와 형변환]
1주차 내:
미래 주차:
List<? extends Animal>)| 상속 (extends) | 합성 (composition) | 인터페이스 (implements) | |
|---|---|---|---|
| 관계 | is-a | has-a | can-do |
| 결합도 | 강함 | 약함 | 약함 |
| 다중 가능 | X (1개만) | ○ | ○ (N개) |
| 실무 빈도 | 적당히 | 가장 많음 | 매우 많음 |
| 용도 | 명확한 분류 | 행동 조립 | 능력 표현 |
현대 트렌드:
"상속보다 합성을, 클래스보다 인터페이스를"
| 질문 | 이 Unit에서의 답 |
|---|---|
| "상속이란?" | extends로 부모의 모든 것을 자식이 물려받음 |
| "다중 상속이 안 되는 이유?" | 다이아몬드 문제 — 자바는 단일 상속만 |
| "super() 의 역할?" | 부모 생성자 호출, 첫 줄이어야 함 |
| "부모 생성자 자동 호출?" | super() 생략 시 자바가 자동으로 매개변수 없는 super() 추가 |
| "@Override 의 역할?" | 오버라이드 명시 + 컴파일 시 오타 검증 |
| "Object 클래스란?" | 모든 클래스의 최상위 부모 |
1️⃣ 상속은 "부모의 모든 것을 자식이 자동으로 가진다" 는 선언이다.
extends키워드로 표현하며, 부모의 public/protected 멤버를 자식이 그대로 쓸 수 있다. 자바는 클래스 단일 상속 만 허용 (다이아몬드 문제 회피), 인터페이스는 다중 구현 가능. 모든 클래스는 자동으로Object를 상속한다.2️⃣ 생성자 체이닝은 "부모부터 차례로 초기화" 하는 안전장치다.
자식 생성자는 반드시 첫 줄에서
super(...)호출 (생략 시 자동으로super()추가). 부모에 매개변수 있는 생성자만 있으면 자식이 명시적으로super(...)호출해야 함 (안 하면 컴파일 에러). 객체 메모리에는 부모 필드 + 자식 필드 가 함께 존재.3️⃣ 상속은 강력하지만 위험하다 — 함정을 알고 써야 한다.
@Override누락 (오타 시 오버라이드 안 됨), 생성자에서 오버라이드 가능한 메서드 호출 (NPE 위험), protected 남용 (캡슐화 깨짐), 무분별한 상속 (Stack extends ArrayList 같은 안티패턴). 의심스러우면 합성 을 먼저 검토.
extends 키워드의 의미를 한 문장으로 설명할 수 있다super() 가 반드시 첫 줄이어야 함을 안다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 다중 상속)
Dog 와 Cat 모두 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++ 의 다중 상속 복잡도를 피하고 단순한 모델 선택.
Animal a = new Dog(); 가 가능한가" 가 궁금하다