J-S-04 정적 멤버(Static)와 상속(Inheritance)
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel1
1. 정적 멤버 (Static Member)
정적 멤버는 객체지향인 자바에서 '객체 없는 세상'을 다루는 독특한 요소입니다.
1.1. 개념과 특징
- 독립적 존재: 인스턴스(객체) 생성과 무관하게 독립적으로 존재합니다.
- 생성 시점: 프로그램 시작 후 해당 클래스가 로딩(Class Loading)되는 시점에 메모리에 딱 한 번 생성됩니다.
- 공유 자원: 모든 인스턴스가 하나의
static필드를 공유합니다. - 호출 관례:
참조변수.필드보다는클래스명.필드로 호출하는 것이 명확합니다. (예:Math.PI) this사용 불가: 객체가 생성되기도 전에 이미 존재하므로, '나 자신(Instance)'을 가리키는this키워드는 사용할 수 없습니다.
1.2. 심볼릭 상수 (Symbolic Constant)
static과 final을 조합하여 유지보수성과 효율성을 모두 잡는 패턴입니다.
- 가독성:
3.14같은 매직 넘버 대신PI라는 이름을 부여합니다. - 메모리 효율:
static으로 한 번만 할당하고,final로 불변성을 보장합니다. - 관례: 대문자와 언더바(
_)를 사용합니다. (예:MAX_VALUE,DEFAULT_TIMEOUT)
public class Constants {
// 메모리에 1개만 존재하며, 수정 불가능한 상수
public static final int MAX_user_COUNT = 100;
}
1.3. 메모리 구조 (Deep Dive)
- 위치: 정적 필드는 Heap이 아닌 Method Area (또는 Static Area)에 저장됩니다.
- GC 대상 아님: 프로그램 종료 시까지 유지되므로, 무분별한
static남용은 메모리 누수의 원인이 될 수 있습니다. - Tip (메서드의 비밀):
- 사실 모든 메서드(로직)는
static이든 아니든 Method Area에 한 번만 로딩되어 공유됩니다. - 인스턴스 메서드는 단지 '숨겨진 파라미터(
this)'를 넘겨받아 누구의 데이터를 처리할지 알 뿐, 코드 자체는 하나입니다.
2. 상속 (Inheritance), 그리고 관계
상속은 코드를 재사용하고 확장하기 위한 핵심 문법(extends)입니다.
2.1. 관계의 정의 (is-a vs has-a)
설계 시 가장 중요한 것은 두 객체 간의 관계를 정의하는 것입니다.
- is-a 관계 (상속): "~은 ~의 한 종류이다."
- 자식 클래스는 부모 클래스를 포함하는 개념입니다.
- 예:
Man extends Mammal(사람은 포유류다) - has-a 관계 (포함/조합): "~은 ~을 가지고 있다."
- 상속하지 않고, 클래스 내부에 다른 클래스를 멤버 변수로 가집니다.
- 예:
Car클래스 내부에Engine변수 존재 (자동차는 엔진을 가진다)
2.2. 생성자의 연쇄 호출
- 자식 클래스를
new로 생성하면, 논리적으로 부모 클래스도 함께 초기화되어야 합니다. - 자식 클래스 생성자의 맨 첫 줄에는 항상
super()가 생략되어 있으며, 이를 통해 부모의 생성자가 먼저 호출됩니다.
class Parent {
Parent() { System.out.println("부모 생성"); }
}
class Child extends Parent {
Child() {
// super(); // 컴파일러가 자동으로 삽입
System.out.println("자식 생성");
}
}
// 결과: "부모 생성" -> "자식 생성"
2.3. 추상자료형 (ADT)과 관점
- 추상화: 내부는 복잡하게 구현(
@Override)되어 있어도, 외부에서는 부모 타입의 단순한 인터페이스만 보고 사용합니다. - 은닉: "어떻게(How)" 구현했는지는 자식 클래스에 숨기고, "무엇을(What)" 하는지만 노출합니다.
2.4. 2차원적 코드 흐름
상속을 사용하면 디버깅이나 코드 분석 시 시각이 달라져야 합니다.
- 1차원: 일반 코드는 위에서 아래로 순차 실행됩니다.
- 2차원 (수직 이동): 메서드를 호출하거나 변수를 찾을 때, 현재 클래스에 없으면 부모 클래스(위쪽)로 거슬러 올라가며 탐색합니다. 실행 흐름이 수평(시간)과 수직(계층)을 오가게 됩니다.
2.5 자식 클래스의 생성자: 호출과 실행의 역설
상속 관계에서 가장 먼저 이해해야 할 부분은 바로 생성자의 동작 방식입니다. 자식 클래스의 인스턴스를 생성할 때, 자식 클래스의 생성자가 코드상으로는 가장 먼저 호출되지만, 실제 실행은 가장 늦게 완료됩니다.
이 순서는 반드시 기억해야 합니다. 호출은 '자식'부터 시작되지만, 초기화의 흐름은 부모 클래스로 거슬러 올라가 최상위 클래스(Object)부터 아래로 내려오며 실행되기 때문입니다.

주의해야 할 필드 정의
자식 클래스를 설계할 때 한 가지 원칙을 꼭 지켜야 합니다. 바로 "자식 클래스에서 부모 클래스의 필드를 절대 재정의하지 말 것" 입니다.
부모와 동일한 이름의 변수를 자식에서 다시 선언하는 것은 대부분 좋지 않은 설계이며, 의도치 않은 버그를 유발합니다.
이를 변수섀도잉이라고도 부릅니다.
변수 섀도잉(Variable Shadowing) 변수 섀도잉은 상위 스코프(부모 클래스)에서 정의된 변수와 동일한 이름의 변수를 하위 스코프(자식 클래스)에서 다시 선언했을 때 발생합니다. 이 경우, 자식 클래스 내에서는 부모의 변수가 자식의 변수에 의해 "가려지는(Shadowed)" 현상이 나타납니다.
상속 관계에서의 메모리 구조는 아래 그림을 참고하면 이해가 빠릅니다. 
현재와 미래의 대화
부모와 자식 클래스 간의 관계를 시점의 관점에서 바라보면 흥미롭습니다.
부모 클래스는 현재 작성되는 코드이고, 자식 클래스는 미래에 작성될 코드입니다.
따라서 우리는 항상 미래에 만들어질 파생(자식) 클래스를 고려하여 현재의 클래스를 설계해야 합니다.
다만, 상속에 상속이 꼬리를 무는 깊은 상속 구조는 유지 보수 측면에서 바람직하지 않으니 주의해야 합니다.

2.6 super: 부모를 부르는 이름
super는 자식 클래스에서 부모 클래스를 지칭할 때 사용하는 키워드입니다.
주로 자식 클래스의 생성자 내부에서 부모 클래스의 생성자를 호출해야 할 때 super()와 같은 형태로 사용합니다.
이를 통해 부모 객체가 먼저 온전하게 초기화되도록 보장할 수 있습니다.
class Parent {
String name;
// 기본 생성자 없음 (매개변수가 있는 생성자만 존재)
Parent(String name) { this.name = name; }
}
class Child extends Parent {
int age;
Child(String name, int age) {
super(name); // [필수] 부모의 생성자를 명시적으로 호출해야 함!
this.age = age;
}
}
2.7 메서드 재정의(Override): 확장의 핵심
상속의 꽃은 바로 메서드 재정의(Overriding)입니다.
이는 부모 클래스가 가진 기존 메서드의 기능을 자식 클래스에서 대체하거나, 새로운 기능을 덧붙이는 것을 목적으로 합니다.
실체(Instance)가 우선이다
메서드 재정의에서 가장 중요한 규칙은 "실제 생성된 인스턴스가 우선" 이라는 점입니다.
변수의 타입이 부모 클래스(Parent)라고 할지라도, 실제 new 키워드로 생성한 인스턴스가 자식 클래스(Child)라면 실행 시에는 자식 클래스에서 재정의한 메서드가 호출됩니다.
물론, 자식 클래스가 메서드를 함부로 변경하지 못하게 하고 싶다면 final 키워드를 사용하여 재정의를 막을 수 있습니다.
class Parent {
// final 키워드를 사용하여 자식 클래스에서 수정을 금지함
public final void secureMethod() {
System.out.println("이 메서드는 자식 클래스에서 재정의할 수 없습니다.");
}
public void generalMethod() {
System.out.println("이 메서드는 재정의가 가능합니다.");
}
}
class Child extends Parent {
// 아래 주석을 해제하면 컴파일 에러가 발생합니다.
/*
@Override
public void secureMethod() {
System.out.println("변경 시도!");
}
*/
}
@Override 어노테이션의 활용
재정의할 때는 반드시 @Override 어노테이션을 사용하는 습관을 들여야 합니다.
- 메타데이터: 코드에 대한 정보를 제공합니다.
- 안전장치: 컴파일러에게 "이 메서드는 재정의된 것이다"라고 알립니다. 만약 메서드 이름이나 파라미터가 틀렸다면 컴파일러가 오류를 잡아주어 실수를 방지합니다.
프레임워크와 제어의 역전 (Called by Framework)
앞서 언급했듯 재정의는 '미래'에 일어날 일을 미리 대비하는 것입니다.
부모 클래스(라이브러리나 프레임워크)는 전체적인 흐름만 제어하고, 구체적인 동작은 비워두거나 기본 동작만 정의해 둡니다.
이후 개발자가 자식 클래스에서 메서드를 재정의하면, 프레임워크가 정해진 시점에 이 메서드를 호출하게 됩니다. 이를 'Called by Framework'라고 합니다.
- OnXXX() 메서드: 보통
onCreate,onInit과 같은 이름의 메서드들이 여기에 해당합니다. - 흐름의 확장: 부모 클래스가 구조와 흐름을 잡고, 자식 클래스는 그 흐름 속에서 자신만의 코드를 끼워 넣어 기능을 완성합니다.
abstract class FrameworkBase {
// 1. 전체적인 실행 흐름을 정의 (자식이 변경 못하게 final 사용)
public final void start() {
System.out.println("프레임워크가 준비를 시작합니다.");
onInit(); // 자식이 구현한 초기화 로직 호출
onCreate(); // 자식이 구현한 생성 로직 호출
System.out.println("프레임워크가 실행을 완료했습니다.");
}
// 2. 자식이 재정의하여 기능을 끼워 넣을 메서드들 (Hook Method)
protected abstract void onInit();
protected abstract void onCreate();
}
class MyApp extends FrameworkBase {
@Override
protected void onInit() {
System.out.println("[MyApp] 설정 파일을 로드합니다.");
}
@Override
protected void onCreate() {
System.out.println("[MyApp] 화면 UI를 생성합니다.");
}
}