🔗 자바 인터페이스
🔗 super를 사용한 매개변수 있는 부모 생성자 호출 (예시)
1. 상속의 기본 개념과 문법
- 자바에서는 상속을 구현할 때 extends 키워드를 사용한다.
- 부모 클래스 (Parent/Super/Base Class): 상속을 해주는 상위 클래스
- 자식 클래스 (Child/Sub/Derived Class): 상속을 받는 하위 클래스
// 부모 클래스
class Animal {
String name;
void eat() {
System.out.println(name + "이(가) 음식을 먹습니다.");
}
}
// 자식 클래스 (Animal을 상속받음)
class Dog extends Animal {
void bark() {
System.out.println(name + "이(가) 멍멍 짖습니다.");
}
}
💡 핵심 포인트
Dog 클래스는 Animal 클래스의 name 변수와 eat() 메서드를 물려받기 때문에, 따로 선언하지 않아도 바로 사용할 수 있음.
자식 클래스는 부모의 멤버(필드, 메서드) 외에 자신만의 멤버를 추가(확장)하여 사용할 수 있다.
2. 상속의 특징
- 단일 상속만 지원 (다중 상속 불가능)
- 자바는 클래스 간의 다중 상속을 지원하지 않음. 즉, extends 뒤에는 오직 하나의 클래스만 올 수 있다.
- 다중 상속으로 발생할 수 있는 메서드 충돌 등의 모호성을 차단하기 위함.
- 대신 '인터페이스'를 통해 다중 구현을 지원.
- Object 클래스 상속
- 자바에서 상속을 받지 않는(클래스 선언 시 extends 키워드를 쓰지 않음) 모든 클래스는 자동으로 최상위 루트 클래스인 java.lang.Object 클래스를 상속받음.
- 컴파일러가 자동으로 extends Object를 붙여줌
- 자바의 모든 클래스는 Object 클래스의 자식.
-
아래는 계층 구조 시각화
Object (최상위 루트 클래스) └── Animal (Dog의 직속 부모 클래스) └── Dog (최하위 자식 클래스)
- 자바에서 상속을 받지 않는(클래스 선언 시 extends 키워드를 쓰지 않음) 모든 클래스는 자동으로 최상위 루트 클래스인 java.lang.Object 클래스를 상속받음.
- 생성자와 초기화 블록은 상속되지 않음
- 상속되는 멤버: 부모 클래스의 멤버 필드(변수)와 멤버 메서드
- 자식 클래스 코드에 아무 내용이 없어도 부모의 필드와 메서드를 내 것처럼 바로 사용할 수 있음
- 부모 클래스의 멤버 변수와 메서드만 상속되며, 생성자는 상속되지 않음.
- 대신 자식 클래스의 생성자 내부에서 부모 클래스의 생성자를 호출하여 사용.
- 상속되는 멤버: 부모 클래스의 멤버 필드(변수)와 멤버 메서드
- private 멤버의 진실 (캡슐화 유지)
- 자바 공식 관점에선 상속이 안됨. (조건부 상속 없음)
- 부모의 private 멤버도 물리적으로 상속은 되지만, 자식 클래스에서 직접 접근할 수 없음. (캡슐화 유지)
- 자식 객체(Dog)를 생성할 때: 미리 계산한 총용량 공간만큼 한번에 통째로 확보한 다음, 컴퓨터 메모리 앞부분에 부모(Animal)의 멤버 변수가 자리 잡고, 그 뒤를 이어 자식의 멤버 변수들이 배치되는 구조 (하나의 대형 주소 공간 안에 층(Layer)이 나뉘어 있는 형태)
- 접근하려면 부모가 제공하는 public 또는 protected 메서드(Getter/Setter 등)를 거쳐서 돌아가야 함.
💡 다중 상속 지원하는 언어에서 다이아몬드 문제가 발생시 해결 방안
C++ 해결책: C++에서는 개발자가 virtual 키워드(가상 상속)나 명시적 스코프(부모클래스::메서드)를 지정해 이 문제를 직접 해결해야 함 (개발자 제어권을 극대화한 대신, 복잡도가 높음).
Python 해결책: MRO(Method Resolution Order, 메서드 탐색 순서)라는 확실한 규칙으로, super()를 호출하면 파이썬이 미리 계산한 내부 순서(C3 선형화 알고리즘)에 따라 알아서 순차적으로 부모들을 방문
💡다이아몬드 문제 (Diamond Problem)
다중 상속시 두 부모 클래스에이름이 같은 메서드가 있으면 어느 것을 실행해야 할지 모호해지는 문제
ℹ️ 또한 final 키워드가 붙은 클래스(주로 래퍼 클래스)는 상속 받지 못한다 (부모가 될 수 없음)
3. 상속에서 꼭 알아야 할 키워드와 디폴트 생성자
1) 오버라이딩 (Overriding, 메서드 재정의)
- 부모 클래스로부터 물려받은 메서드의 내용을 자식 클래스에 맞게 변경하는 것.
- 메서드의 선언부(이름, 매개변수, 반환타입)는 부모의 것과 완전히 같아야 한다.
- @Override 어노테이션을 붙여서 컴파일러에게 재정의를 검증받는 것이 안전 (생략 가능)
- "부모 클래스에 정의된 메서드와 동일한 시그니처를 자식 클래스에서 재정의했는지" 만을 기준으로 판단한다.
- 어노테이션의 존재 유무는 컴파일 조건이나 동작(바이트코드) 방식에 전혀 영향을 주지 않음.
더보기
ℹ️ JDK 실제 소스 코드에서의 @Override
JDK 내부 소스 코드(특히 String처럼 자바 극초기부터 존재했던 핵심 클래스들)에는 @Override가 빠져 있는 경우가 종종 있다.
- 역사적 이유 (JDK 버전): String 클래스와 equals() 메서드는 Java 1.0부터 존재. 하지만 @Override 어노테이션은 Java 5 (JDK 1.5)에 처음 도입되었다. 기존에 잘 작성되어 동작하던 소스코드를 가급적 건드리지 않고 유지보수해왔기 때문에 여전히 지워진 채 남아있는 경우가 있음.
- 인터페이스 구현 메서드: Java 5 시절에는 인터페이스의 추상 메서드를 구현할 때는 @Override를 붙일 수 없었고(Java 6부터 가능해짐), 이런 역사적 흐름 때문에 표준 라이브러리 곳곳에 어노테이션이 생략된 형태가 남아 있다.
2) super 키워드 (직속 부모)
- super: 자식 클래스에서 부모 클래스의 멤버(필드/메서드)를 참조할 때 사용. (자신의 멤버를 가리키는 this와 대비)
- 단일 상속 원칙으로 자식 클래스의 직속 부모는 오직 하나.
- super(): 부모 클래스의 생성자를 호출할 때 사용. 자식 클래스의 생성자 첫 줄에는 반드시 부모 클래스의 생성자가 호출되어야 하며, 생략 시 컴파일러가 자동으로 super();를 추가 (부모 공간이 먼저 초기화)
- 바로 위 직속 부모의 생성자만 호출할 수 있으며, 할아버지 클래스의 생성자를 자식이 직접 호출할 수는 없다. (계층을 타고 올라가며 순차적으로 호출됨)
- Dog이 생성될 때: Dog() 생성자 호출 ➡️ Animal() 생성자 호출 ➡️ Object() 생성자 호출 순으로 거슬러 올라간 뒤, Object ➡️ Animal ➡️ Dog 순서로 생성자가 실행 완료.
- this()처럼 매개변수가 없는 기본 생성자가 호출되며, 인자(arguement)를 줄 수 있다.
class Dog extends Animal {
@Override
void eat() {
super.eat(); // 부모의 eat() 메서드 호출
System.out.println("강아지가 찹찹 먹습니다."); // 기능 재정의
}
}
3) 디폴트 생성자
- 매개변수가 없음: () 안에 아무런 인자도 받지 않는 형태
- 클래스명과 동일한 이름: 일반 생성자와 마찬가지로 클래스 이름과 완전히 같아야 하며, 반환 타입(void 등)이 없음
- 컴파일러의 자동 생성: 개발자가 클래스 내부에 생성자를 하나도 정의하지 않으면, 자바 컴파일러가 컴파일 시점에 자동으로 디폴트 생성자를 만들어 줌
- 컴파일러가 넣어주는 디폴트 생성자는 내부적으로 인스턴스 변수들을 각 타입의 기본값으로 초기화한다.
- 아래는 실제 컴파일러가 만드는 디폴트 생성자의 형태
public Dog() { super(); // 부모 클래스(Object)의 생성자를 호출함 }
- 프레임워크(Spring, Hibernate 등)나 라이브러리를 사용할 때는 내부적으로 디폴트 생성자를 요구하는 경우가 많으므로, 커스텀 생성자를 만들 때 디폴트 생성자도 함께 선언해 주는 것이 안전
💡 자바 데이터 타입별 기본값
- 참조 타입: null (String, 객체 등)
- 정수 타입: 0 (int, long 등)
- 실수 타입: 0.0 (float, double 등)
- 논리 타입: false (boolean)
Case A : 디폴트 생성자가 자동으로 생성되는 경우
class Dog {
String name;
// 생성자가 하나도 없음 -> 컴파일러가 invisible하게 디폴트 생성자 삽입
}
public class Main {
public static void main(String[] args) {
// 컴파일러가 디폴트 생성자를 만들어줬기 때문에 에러 없이 작동함
Dog myDog = new Dog();
}
}
Case B : 디폴트 생성자가 생성되지 않는 경우 (⚠️주의!)
class Dog {
String name;
// 매개변수가 있는 생성자를 직접 정의함
Dog(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
// ❌ 컴파일 에러 발생!
// 매개변수가 없는 디폴트 생성자가 존재하지 않기 때문
Dog myDog = new Dog();
// ⭕ 이렇게 매개변수를 넣어서 호출해야 함
Dog myDog2 = new Dog("맥스");
}
}
💡 Tip: 만약 매개변수가 있는 생성자도 쓰고, 인자가 없는 생성자도 같이 쓰고 싶다면 클래스 내부에 디폴트 생성자(public Dog() {})를 수동으로 직접 작성 필요
4. 상속의 장단점
1) 상속의 장점
- 코드 재사용: 상속의 목적. 기존 코드를 재사용하여 개발 시간과 비용을 줄임. (코드 생산성 극대화)
- 의미: 이미 잘 만들어진 부모 클래스의 코드를 자식이 그대로 물려받기 때문에, 똑같은 코드를 다시 타이핑할 필요가 없음.
- 예: 상용차, 스포츠카, 트럭 클래스를 만들 때, 시동 걸기(start()), 브레이크(brake()) 같은 공통 기능은 부모인 Car 클래스에 한 번만 만듦. → 새로운 차종을 추가할 때 개발 시간(생성성)이 획기적으로 줄어듦
- 유지보수성 향상: 공통적인 부분을 부모 클래스에서 한 번에 관리함.
- 의미: 버그가 발생하거나 기능 수정이 필요할 때 부모 클래스만 고치면 됨.
- 예: 모든 자동차의 브레이크 방식을 수정해야 한다면, 100개의 자식 클래스를 일일이 수정하는 것이 아닌, 부모인 Car 클래스의 brake() 메서드 딱 하나만 수정하면 100개의 자식에게 자동으로 적용
- 다형성 (Polymorphism) 구현: 부모 타입의 참조변수로 자식 객체를 다룰 수 있음.
- 의미: Is-a 관계이기 때문에 부모 타입의 변수 하나로 여러 가지 형태의 자식 객체를 유연하게 다룰 수 있음.
- 예: 게임을 만들 때 Unit이라는 부모 아래 Warrior, Mage, Healer 자식이 있다고 가정. Unit 타입의 배열에 자식 객체를 담아서 관리 가능.
// 다형성이 없다면? 각각 따로 관리해야 함 Warrior w = new Warrior(); Mage m = new Mage(); // 다형성이 있다면? 부모 배열 하나로 묶어서 한 번에 관리 가능! Unit[] party = { new Warrior(), new Mage(), new Healer() }; for(Unit u : party) { u.attack(); // 각 자식의 직업에 맞는 공격이 나감 }
2) 상속의 단점
- 강한 결합도(Tight Coupling): 부모 클래스의 변경이 모든 자식 클래스에 영향을 줌.
- 의미: 부모 클래스와 자식 클래스가 너무 끈끈하게 연결되어 있어서, 부모를 살짝만 건드려도 자식 클래스들이 수정이 필요한 경우가 발생.
- 예: 부모 클래스의 메서드 이름을 `eat()`에서 `consume()`으로 바꾸는 순간, 이 부모를 상속받던 수십 개의 자식 클래스 코드 전체에서 에러가 터짐. (부모의 변화에 자식이 너무 취약)
- 설계의 유연성 저하: 자식 클래스가 부모 클래스의 내부 구현에 종속됨.
- 의미: 자식 클래스가 부모 클래스의 내부 논리 구조를 완벽하게 알고 있어야 하거나, 부모의 규칙에 꽁꽁 묶이게 됨.
- 예: 부모 클래스에 A라는 메서드가 있고 내부적으로 B라는 메서드를 호출하고 있다면, 자식이 A를 오버라이딩할 때 부모의 내부 동작(B를 호출한다는 사실)까지 신경 써서 코드를 짜야 함. 캡슐화(내부 감추기) 원칙이 깨지고, 부모의 그늘에서 벗어나 자유롭게 설계를 변경하기가 어려워짐
⚠️ 주의할 점
상속은 단순히 코드를 재사용하기 위한 수단으로만 사용하면 안 됨. 반드시 두 클래스 사이에 '자식은 부모이다 (IS-A 관계)'가 성립할 때만 상속을 사용 권장. (예: Dog is an Animal ⭕, Gun is a Police ❌)
5. 상속과 접근 제어자 (캡슐화와 패키지 설계)
1) 예제 구조 정의
- 패키지 1: Animal 클래스, Main1 클래스
- Animal 내부 변수: A (package-private), B (protected), C (private) + 각 변수의 public getter 메서드
- 패키지 2: Dog 클래스 (Animal을 상속받음), Main2 클래스
- Main1, Main2의 메서드에서 만든 Dog의 참조 변수를 각각 dog1, dog2
| 패키지 | 클래스 / 메인 메서드 | 변수 및 접근 제어자 |
| package1 | Animal (부모) Main1 (메서드) |
A (package-private) B (protected) C (private) A, B, C의 getter 메서드 (public) |
| package2 | Dog (자식) Main2 (메서드) |
Animal을 상속받음 |
2) Dog 클래스가 물려받은 '상속 여부'
- Dog가 패키지가 다른 Animal을 상속받을 때, 접근 제어자에 따라 실제 자식 클래스 내부로 상속(전달)이 되었는가의 여부
- 변수 A (package-private / default): 상속 안 됨
- 패키지가 다르기 때문에 Dog 클래스는 A를 물려받지 못함
- 부모 클래스 공간 안에 package-private 변수 A가 물리적으론 있음 (메모리에 같이 올라가고, 자식 객체의 일부가 되기 때문에)
- 부모(Animal)와 같은 패키지였다면 this.A가 가능 (= 상속)
- 패키지가 다르기 때문에 Dog 클래스는 A를 물려받지 못함
- 변수 B (protected): 상속 됨
- 패키지가 달라도 상속 관계가 성립하므로, Dog 내부로 상속
- 변수 C (private): 상속 안 됨
- private은 오직 자기 클래스(Animal) 내부에서만 존재하므로 상속되지 않음
- 부모 클래스 공간 안에 private 변수 C도 분명히 함께 생성됨
- Dog 클래스 안에서 this.C라고 쓰는 순간 컴파일 에러 (접근 권한이 없음)
- private은 오직 자기 클래스(Animal) 내부에서만 존재하므로 상속되지 않음
3) Animal 클래스의 멤버 필드들에 대한 접근 방법
① Dog(Animal과 다른 패키지)에서의 접근성
- 상황: Dog 내부 메서드에서 부모의 변수들을 쓰려고 할 때
- 변수 A (package-private):
- 직접 접근 (this.A): 패키지가 다르기 때문에 불가능.
- 간접 접근: 부모가 물려준 public getter 메서드를 통해서만 접근 가능 (getA()).
- 변수 B (protected):
- 직접 접근 (this.B): 패키지가 다르더라도 상속 관계(자식 클래스 내부)이기 때문에 가능
- Dog 클래스 안에서는 마치 자기 변수인 것처럼 B = 10; 이나 this.B 형태로 직접 접근하여 값을 바꾸거나 읽을 수 있음.
- 직접 접근 (this.B): 패키지가 다르더라도 상속 관계(자식 클래스 내부)이기 때문에 가능
- 변수 C (private):
- 직접 접근 (this.C): 불가능.
- private은 무조건 Animal 클래스 안에서만 사용 가능.
- 자식 클래스 내부라 할지라도 this.C라고 적으면 컴파일러가 막음.
- 접근 방법: 부모가 물려준 public getter 메서드를 통해서만 접근 가능 (getC()).
- 직접 접근 (this.C): 불가능.
ℹ️ 상속 계층이 아무리 깊어져도 접근 제한자가 허용되는 필드라면 자식 클래스에서 this.필드명 으로 모두 접근 가능
② Main1(Animal과 같은 패키지)에서의 접근성
- 상황: package1에 있는 Main1 내부 메서드에서 Dog dog1 = new Dog();를 만들었을 때
- 변수 A (package-private):
- 직접 접근 (dog1.A): 가능. Dog가 A를 상속받지는 못했지만, Main1과 Animal이 같은 패키지에 있기 때문에 dog1 내부에 존재하는 부모(Animal)의 package-private 멤버에 직접 접근 가능.
- 간접 접근: Animal에서 물려준 public getter 메서드로도 접근 가능
- 변수 B (protected):
- 직접 접근 (dog1.B): 가능. protected는 '같은 패키지' 또는 '자식 클래스'에게만 접근을 허용. Main1이 Animal과 같은 패키지이므로 직접 접근이 가능합니다.
- 간접 접근: Animal에서 물려준 public getter 메서드로도 접근 가능
- 변수 C (private):
- 직접 접근 (dog1.C): 불가능. private은 무조건 Animal 클래스 내부에서만 접근할 수 있음(직접 노출 X).
- 간접 접근: Animal에서 물려준 public getter 메서드를 통해서만 값을 가져올 수 있음 (teacher1.getC()).
③ Main2(Dog와 같은 패키지)에서의 접근성
- 상황: package2에 있는 Main2 내부 메서드에서 Dog dog2 = new Dog();를 만들었을 때
- 변수 A (package-private):
- 직접 접근 (dog2.A): 불가능. 패키지가 다르기 때문에 접근할 수 없음
- 간접 접근: Animal에서 물려준 publicgetter 메서드를 통해서만 접근 가능 (dog2.getA()).
- 변수 B (protected):
- 직접 접근 (dog2.B): ❌ 불가능. (⚠️가장 많이 헷갈리는 부분!)
- protected는 자식 클래스 '내부(상속받은 코드 안)'에서 사용할 수 있음. 다른 패키지의 일반 클래스(Main2)에서 자식 클래스 인스턴스(dog2)가 직접 점(.)을 찍어 접근할 수 없음
- Main2 클래스 자체는 Animal을 상속받은 게 아니기 때문
- 간접 접근: Animal에서 물려준 public getter 메서드를 통해서만 접근 가능 (dog2.getB()).
- 직접 접근 (dog2.B): ❌ 불가능. (⚠️가장 많이 헷갈리는 부분!)
- 변수 C (private):
- 직접 접근 (dog2.C): 불가능.
- 간접 접근: Animal에서 물려준 public getter 메서드를 통해서만 접근 가능 (dog2.getC()).
📊 자바 접근 제어자 허용 범위 표
| 클래스 내부 | 같은 패키지 | 자식 클래스 | 외부 | |
| public (getter 메서드) | ⭕ | ⭕ | ⭕ | ⭕ (누구나 접근 가능) |
| protected (A) | ⭕ | ⭕ | ⭕ (다른 패키지여도 가능) | ❌ |
| package-private (B) (default / 아무것도 안 적음) |
⭕ | ⭕ | ❌ (다른 패키지면 자식도 불가) | ❌ |
| private (C) | ⭕ | ❌ | ❌ | ❌ (오직 내 클래스만) |
💡핵심 결론: 객체 지향 프로그래밍(OOP)의 캡슐화 원칙에 따라, 변수에 직접 점(.)을 찍고 접근하기보다는 public getter 메서드를 제공하여 접근하는 것이 두 메인 메서드 모두에서 가장 안전하고 확실한 방법
'Language & Tool > Java' 카테고리의 다른 글
| ☕Java 제어문 총정리 (+ 레이블) (0) | 2026.06.08 |
|---|---|
| ☕OOP 핵심: 클래스가 반드시 구현해야 하는 설계도를 정의하는 인터페이스(Interface) (0) | 2026.06.08 |
| 🌱변수 생성시 위치별 초기화 규칙 (0) | 2025.09.14 |
| 🌱JAVA 패키지 정리 모음 (0) | 2025.09.08 |
| 🌱JAVA 형변환 정리 (0) | 2025.09.08 |