자바 개발자라면 면접에서 수없이 질문 받았을 오버라이딩과 오버로딩에 대해서 공부해보려고 한다.
오버라이딩과 오버로딩은 객체 지향 프로그래밍에서 다형성을 구현하는 가장 핵심적인 개념이다.
오버라이딩은 상속 관계에서 부모 클래스가 가지고 있는 메서드를 자식 클래스가 물려받아 재정의하는 것을 말한다.
메서드의 이름, 매개변수, 리턴 타입이 부모 클래스와 완벽하게 동일해야 한다.
오버라이딩에는 일반적인 부모 클래스의 기능을 자식 클래스에서 수정하는 것과 추상 메서드를 상속 받아 실질적인 기능을 정의하는 방법이 있다.
객체 지향의 리스코프 치환 원칙(LSP) 때문에 부모 클래스가 사용되는 곳에 자식 클래스를 대체해서 넣더라도 문제가 없어야 한다.
💡 리스코프 치환 원칙이란?
객체 지향 설계의 5대 원칙(SOLID)중 하나로, 자식 클래스는 언제나 자신의 부모 클래스를 교체할 수 있어야 한다는 것이다. 자식 클래스는 부모 클래스의 기능을 확장해야 하기 때문이다.
부모 클래스가 사용되는 곳에 자식 클래스를 대체해서 넣더라도 문제가 없어야 한다. 따라서 public > protected > default > private의 범위에 따라 접근 제어자를 설정해주어야 한다.
오버라이딩 예시 코드
// 부모 클래스
class Camera {
// 접근 제어자가 protected (상속받은 곳이나 같은 패키지에서만 접근 가능)
protected void takePicture() {
System.out.println("사진을 찍습니다. (기본 화질)");
}
}
// 자식 클래스
class SmartPhone extends Camera {
// 부모의 메서드를 재정의 (Overriding)
// 1. 메서드 이름, 매개변수, 리턴 타입이 부모와 똑같음
// 2. 접근 제어자를 protected -> public으로 더 넓게 변경
@Override // 이 어노테이션을 붙이면 컴파일러가 오버라이딩 규칙을 검사해줌
public void takePicture() {
System.out.println("사진을 찍습니다. (AI 보정 + 고화질)");
}
}
public class OverridingTest {
public static void main(String[] args) {
// 1. 부모 타입 변수에 자식 객체를 담음 (다형성)
Camera myCam = new SmartPhone();
// 2. 메서드 호출
// 변수 타입은 Camera지만, 실제 담긴 객체는 SmartPhone
// 런타임(실행) 시점에 실제 객체를 확인하고 자식의 메서드를 실행 (동적 바인딩)
myCam.takePicture();
}
}
오버로딩은 한 클래스 내에서 같은 이름을 가진 메서드를 여러 개 만드는 것이다. 이때, 매개변수의 개수나 타입이 달라야 한다.
메서드 이름은 같지만, 들어오는 입력값에 따라 다르게 동작하도록 만든다. 리턴 타입만 다른 것은 오버로딩으로 인정되지 않는다.
오버로딩에는 1. 메서드 오버로딩과 2. 생성자 오버로딩이 있다. 메서드 오버로딩은 일반적인 메서드를 여러 개 정의하는 것이며, 생성자 오버로딩은 객체를 생성할 때 초기화하는 방법을 다양하게 제공하는 것이다.
오버로딩 예시 코드
class Calculator {
// 1. 기본 더하기 (정수 2개)
int add(int a, int b) {
System.out.println("1번 메서드 호출 (int, int)");
return a + b;
}
// 2. 매개변수 타입이 다름 (실수 2개)
// 이름은 add로 같지만, 입력 타입이 double이므로 오버로딩 성립
double add(double a, double b) {
System.out.println("2번 메서드 호출 (double, double)");
return a + b;
}
// 3. 매개변수 개수가 다름 (정수 3개)
// 이름은 같지만 개수가 다르므로 오버로딩 성립
int add(int a, int b, int c) {
System.out.println("3번 메서드 호출 (int, int, int)");
return a + b + c;
}
}
public class OverloadingTest {
public static void main(String[] args) {
Calculator calc = new Calculator();
// 컴파일 시점에 어떤 add()를 쓸지 이미 결정됨(정적 바인딩)
calc.add(10, 20); // 1번 실행
calc.add(1.5, 2.5); // 2번 실행
calc.add(1, 2, 3); // 3번 실행
}
}
| 비교 | 오버라이딩 | 오버로딩 |
|---|---|---|
| 의미 | 재정의 (덮어쓰기) | 중복 정의 (새로 추가) |
| 적용 | 상속 관계 (부모-자식) | 같은 클래스 내 |
| 메서드 이름 | 동일 | 동일 |
| 매개변수 | 동일 | 달라야 함 (개수 또는 타입) |
| 리턴 타입 | 동일 | 상관없음 |
| 접근 제어자 | 자식이 같거나 더 넓어야 함 | 상관없음 |
정적 바인딩이라고도 불리는데, 컴파일러가 코드를 번역할 때 미리 결정하며 실행(런타임) 속도에는 거의 영향을 주지 않는다.
동적 바인딩이라고도 불리며, 컴파일러는 단순히 부모 타입의 메서드를 호출하는 것으로 알지만, 실제 프로그램이 실행될 때(런타임) 실제 생성된 객체(자식 객체)가 무엇인지 확인하고 그 객체의 메서드를 찾아가서 실행한다.
이것이 부모 타입의 변수에 자식 객체를 담았을 때 자식의 메서드가 실행되는 이유이며 다형성의 핵심이 된다.
스터디 중 스터디원분이 설명해주신 용어로 처음 들어보는 개념이라 찾아보았다.
Animal myPet = new Dog(); // 부모 타입 변수에 자식 객체를 담음
myPet.cry(); // Dog의 메서드가 실행됨
컴파일러 입장에서는 myPet이 Animal 타입 변수기 때문에 Animal의 cry()를 실행해야 할 것 같지만, 런타임에 Dog의 cry()를 찾아가서 실행한다.
Animal 클래스도 vtable을 하나 가지고, Dog 클래스도 vtable을 하나 가진다. 이 테이블에는 메서드 이름 : 실제 코드의 메모리 주소가 매핑되어 있다. new Dog()로 객체를 생성하면, 해당 객체는 Dog 클래스가 가리키는 vtable을 본다.
Animal의 vtable
cry() : Animal.cry() 주소 가리킴eat() : Animal.eat() 주소 가리킴Dog의 vtable
cry() : Dog.cry() 주소로 덮어쓰기 됨 (오버라이딩)eat() : Animal.eat() 주소 그대로 (오버라이딩 안 함)=> 컴퓨터는 myPet.cry()를 호출할 때, myPet이 가리키는 vtable을 본다. 이것은 Dog의 vtable이며 해당 테이블에 적힌 cry() 주소를 따라간다.
★ 추가로 자바는 기본적으로 모든 메서드가 가상 함수(Virtual Method)이다. 따라서 자바에서 final 키워드를 메서드에 붙이면 오버라이딩을 못하기 때문에, vtable을 거치지 않아 최적화를 할 수 있다.