[Java] Java의 static 키워드는 상속이 불가능한 이유와 Hiding
'켄트벡의 구현 패턴' 책을 다시 정독하던 날이었다.. 10장의 내용인 프레임워크 개발 패턴 읽다가 의문이 들어 작성하게 되었다
부모클래스의 static 메소드를 자식클래스에서 오버라이딩(Overriding)하지 못한다는 내용이었다. 즉, 정적 메소드는 오버라이딩 할 수 없다.
이 내용이 왜 그런지에 대한 개념이 제대로 잡혀있지 않는 것 같아 정리하고자 글을 작성한다
Dynamic Method Dispatch
= 다이나믹 메소드 디스패치
메소드 디스패치: 어떤 메소드를 실행시킬지 결정하고 실행시키는 과정
Dynamic하므로 컴파일러가 어떤 메소드를 실행시킬지 모르기 때문에 런타임 과정에서 호출할 메소드를 선택하는 것이다.
c.f) Static 메소드 디스패치도 있는데 가장 큰 예시론 자바의 main함수가 static하기 때문에 컴파일 과정중에 어떤 메소드를 실행시킬지 아는 것이다.
예를 들어보자
public class Exam{
public static void main(String[] args) {
Parent parent1 = new Parent();
Parent parent2 = new Child();
Child child = new Child();
parent1.getTest(); // parent
parent2.getTest(); // child
child.getTest(); // child
}
}
class Parent {
void getTest() {
System.out.println("parent");
}
}
class Child extends Parent {
@Override
void getTest() {
System.out.println("child");
}
}
parent2가 Parent 참조변수에 Child 인스턴스를 받았기 때문에 parent2는 Parent에 있는 getTest()를 받을 것이라고 생각할 수 있다
하지만, Child의 getTest()를 받아 Override하여 실행 결과가 나온 것을 확인할 수 있다
컴파일과정에선 parent2가 Parent클래스의 getTest()를 받는 것은 맞지만 런타임 과정에서 Dynamic Method Dipsatch가 발생했기 때문에 JVM이 인스턴스 타입을 확인하고 Child 객체의 getTest()를 실행시켜준 것이다.
그렇다면 static 키워드란 무엇인가??
사전적인 의미로는 '고정된', '움직이지 않는'이라는 의미를 가지고 있다 (이래서 고정되지 않은 오버라이딩은 안되는군이란 생각도 들었다)
정적 메소드와 인스턴스 메소드(non-static method)는 메모리에 올라가는 시점이 다르다
정적 메소드는 컴파일 시점에 올라가며 인스턴스 메소드는 런타임 시점에 메소드가 메모리에 올라간다
즉, 컴파일 시점에 올라가는 정적 메소드는 컴파일러가 어떤 메소드를 실행시킬지 컴파일 타임에 결정하는 것이고 - `static binding`
런타임 시점에 올라가는 인스턴스 메소드는 런타임에 JVM에 의해 어떤 메소드가 실행될지 런타임에 결정하는 것이다 - `dynamic binding`
런타임에 올라가는 인스턴스는 Heap 영역에 저장되고 static이 저장되는 영역은 method area이다 (저장 시점은 런타임, 컴파일타임)
이때, 인스턴스 메소드를 호출할 때엔 런타임에 해당 메소드를 구현하고 있는 실제 객체를 찾아서 호출한다 (다형성O)
하지만 컴파일러와 JVM은 static 메소드에 대해선 실제 객체를 찾지않는다.
따라서 컴파일 시점에 선언된 참조 타입의 메소드를 실행하는 것이다. (다형성X)
우리가 사용하는 Overriding은 결국 인스턴스 메소드라는 것이다. 그렇기 때문에 런타임에 실제 인스턴스 타입을 찾아 메소드를 호출하는 것이 가능하다.
반대로 부모 클래스의 static 메소드는 컴파일 타임에 정적으로 method area에 저장되기 때문에 자식 클래스에서 재정의하지 못하는 것이다
왜냐? static(움직이지 않는)하기 때문이다
그렇다면 Hiding은 무엇인가?
실제론 부모 클래스의 static 메소드를 자식 클래스에서 똑같은 이름을 가진 static 메소드로 정의가 가능하다
하지만 첫번째 사진과 같이 @Override 어노테이션이 붙으면 이것은 오버라이딩한 메소드가 아님을 알려준다
어노테이션을 제거했을 땐 두번째 사진과 같이 부모 클래스의 static 메소드를 오버라이딩 하지 못한다고 알려주고 있다
그래서 Child 클래스의 getTest메소드에 static을 추가하게 된다면 아래와 같은 경고 메시지가 사라진다
Child 클래스의 getTest 메소드에 static을 붙여 경고 메시지가 사라진다면 성공적으로 오버라이딩 된 것 처럼 보일 수 있다
하지만, 실제론 오버라이딩이 되지 않는 것이다
그렇다면 이렇게 진행할 수 있는 이유는 무엇일까? 이런걸 바로 "Hiding"이라고 한다.
Hiding이란 부모 클래스의 static 메소드를 자식 클래스에서 다시 정의하면 부모 클래스의 static 메소드가 "가려지게 된다"
가려진 상태는 존재가 지워지거나 삭제된 것이 아니라 해당 메소드의 호출 환경(부모 참조, 자식 참조)에 따라 2개의 메소드를 모두 사용할 수 있는 것이다
오버라이딩은 이와 달리 부모의 참조나 자식의 참조에 상관없이 오버라이딩된 메소드가 호출된다
즉, 상속 오버라이딩 재정의 된 메소드가 아닌 자식 클래스 독립적으로 가지는 static 메소드를 정의한 것이다
존재가 지워지거나 삭제된 것이 아닌 메소드의 호출 상태에 따라서 다르게 사용할 수 있기 때문에 Hiding이라고 의미한다
public class Exam {
public static void main(String[] args) {
Parent parent1 = new Parent();
Parent parent2 = new Child();
Child child = new Child();
parent1.getTest(); // parent
parent2.getTest(); // parent
child.getTest(); // child
}
}
class Parent {
static void getTest() {
System.out.println("parent");
}
}
class Child extends Parent {
static void getTest() {
System.out.println("child");
}
}
실제로 처음엔 parent2의 getTest()결과가 'child'가 나왔지만 현재는 'parent'가 나오고 있는 것을 확인할 수 있다.
오버라이딩 메소드라면 런타임 과정에서 child 인스턴스 메소드 타입을 찾았겠지만... (그래서 처음 예제엔 child가 출력 됨)
지금은 컴파일 타임에 정의된 Parent의 static getTest()를 호출한다.
static 메소드는 런타임 상관없이 항상 컴파일 타임에 의존하여 사용한다
따라서 부모 클래스의 static 메소드는 자식 클래스에서 상속받을 수 없다.
자식 클래스에서 부모 클래스의 static 메소드를 사용한다면?
public class Exam {
public static void main(String[] args) {
Child child = new Child();
child.childTest(); // parent
child.childTest2(); // parent
}
}
class Parent {
static void getTest() {
System.out.println("parent");
}
}
class Child extends Parent {
void childTest() {
getTest();
}
static void childTest2() {
getTest();
}
}
이 모습은 자식 클래스에서 부모 클래스의 static 메소드인 getTest()를 사용(상속말고)하는 것 처럼 보일 수 있다
class Child extends Parent {
public Child() {
super();
}
void childTest() {
super.getTest();
Parent.getTest();
}
static void childTest2() {
//super.getTest();
Parent.getTest();
}
}
실제로 Child 클래스의 생략된 코드를 모두 넣게된다면 위와 같은 모습이 된다고 한다
`super` 키워드를 사용하는 것을 보면 상속된 메소드를 사용하는 것을 알 수 있다
하지만, static 메소드인 childTest2()는 `super` 키워드를 사용할 수 없고 사용한다면 컴파일 에러가 발생하게 된다
즉, Parent로 부터 직접 getTest를 받아오는 것이다
따라서 자식 클래스의 static 메소드는 상속에 제한을 받는다는 사실을 알 수 있다
실제 상속으로 사용되는 메소드와 변수는 `super` 키워드를 사용해도 컴파일 에러가 발생하지 않아야 한다
자식 클래스의 static 메소드는 상속이 아니라 부모 클래스를 직접 호출해서 사용하는 것임을 알아야 한다 (=`Parent.getTest()`)
결론
Static 메소드는 컴파일 타임에 method area올라가 클래스에 종속적이게 되어 상속될 수 없다.
객체가 생성되기 전부터 메모리에 할당되어 있기 때문이다
자식 클래스의 static메소드에서 부모 클래스의 static메소드를 상속하는 것처럼 보이지만 이는 사실 상속이 아니라 "직접 호출"하는 것임을 기억하자
Reference