오버라이딩과 오버로딩으로 구현하는 자바 다형성
자바의 오버라이딩과 오버로딩이 다형성을 구현하는 방식, 시그니처 규칙과 상속·API 설계에서의 활용 기준을 정리한다.
2026-08-14 · 최초 발행 2025-05-23
같은 이름의 메소드가 다른 동작을 갖는 방식
다형성(Polymorphism)은 하나의 인터페이스나 연산이 문맥에 따라 서로 다른 구현으로 이어지는 성질이다. 객체지향 코드에서 재사용성과 확장성을 확보하는 기반이기도 하다.
이 성질은 바인딩 시점에 따라 나눠 볼 수 있다. 오버로딩은 컴파일 타임 다형성, 오버라이딩은 런타임 다형성을 구현하는 대표적인 방식이다. 이름은 비슷하지만, 상속 관계의 필요 여부와 메소드 선택 시점이 다르다.
상속받은 동작을 바꾸는 오버라이딩
오버라이딩(Overriding)은 상위 클래스에 선언된 메소드를 하위 클래스가 다시 정의하는 것이다. 객체가 실제로 어떤 하위 클래스인지에 따라 실행할 메소드가 결정되므로 동적 바인딩의 형태로 동작한다.
오버라이딩이 성립하려면 다음 조건을 따라야 한다.
- 메소드 이름은 같아야 한다.
- 매개변수의 타입, 개수, 순서는 같아야 한다.
- 반환 타입은 같거나 하위 클래스 타입이어야 한다. Java 5 이상에서는 공변 반환 타입을 지원한다.
- 접근 제어자는 상위 클래스 메소드보다 더 제한적으로 지정할 수 없다.
- 예외는 상위 클래스 메소드의 예외와 같거나 더 구체적이어야 한다.
Java에서는 @Override 어노테이션으로 재정의 의도를 드러낼 수 있다.
// 상위 클래스
class Animal {
public void makeSound() {
System.out.println("동물이 소리를 냅니다.");
}
}
// 하위 클래스
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("멍멍!"); // 상위 클래스 메소드 재정의
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // 출력: "멍멍!"
}
}
UI 컴포넌트의 기본 그리기 메소드를 바꿔 사용자 정의 UI를 만들거나, 프레임워크의 기본 클래스를 확장해 특정 동작을 맞춤화할 때 이 방식을 쓴다. 데이터베이스 연결 라이브러리의 기본 동작을 특정 DB에 맞게 최적화하는 경우도 같은 맥락이다.
입력 형태에 따라 호출을 나누는 오버로딩
오버로딩(Overloading)은 하나의 클래스 안에 동일한 이름의 메소드를 여러 개 선언하는 방식이다. 매개변수의 타입·개수·순서가 달라야 하며, 컴파일러가 호출에 맞는 메소드를 선택한다. 상속 관계는 필요하지 않다.
오버로딩에서는 다음을 확인해야 한다.
- 메소드 이름은 같아야 한다.
- 매개변수 목록에서 타입, 개수, 순서 중 하나는 달라야 한다.
- 반환 타입만 달리한 선언은 오버로딩으로 인정되지 않는다.
- 접근 제어자는 오버로딩 성립 여부와 무관하다.
다양한 입력 조합을 하나의 일관된 이름으로 처리할 수 있다는 점이 장점이다.
public class Calculator {
// 정수 두 개를 더하는 메소드
public int add(int a, int b) {
return a + b;
}
// 실수 두 개를 더하는 메소드
public double add(double a, double b) {
return a + b;
}
// 정수 세 개를 더하는 메소드
public int add(int a, int b, int c) {
return a + b + c;
}
// 문자열 두 개를 연결하는 메소드
public String add(String a, String b) {
return a + b;
}
}
// 사용 예
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3)); // 8
System.out.println(calc.add(4.5, 3.2)); // 7.7
System.out.println(calc.add(1, 2, 3)); // 6
System.out.println(calc.add("Hello, ", "World!")); // "Hello, World!"
}
}
생성자에 여러 초기화 옵션을 제공하거나, 자바의 println()처럼 여러 데이터 타입을 출력해야 할 때 오버로딩이 쓰인다. 수학 라이브러리에서 다양한 타입의 연산을 지원하는 경우에도 같은 패턴을 볼 수 있다.
메소드 선택 기준의 차이
| 특성 | 오버라이딩 (Overriding) | 오버로딩 (Overloading) |
|---|---|---|
| 정의 | 상위 클래스 메소드를 하위 클래스에서 재정의 | 동일 클래스에서 같은 이름의 메소드를 다른 매개변수로 정의 |
| 다형성 유형 | 런타임 다형성 (동적 바인딩) | 컴파일 타임 다형성 (정적 바인딩) |
| 발생 시점 | 실행 시 결정 | 컴파일 시 결정 |
| 메소드 시그니처 | 동일해야 함 (반환 타입은 공변 가능) | 매개변수가 달라야 함 |
| 필요 조건 | 상속 관계 필수 | 상속 관계 불필요 |
| 목적 | 상속받은 기능 확장/수정 | 동일 이름으로 다양한 기능 수행 |
| 바인딩 | 동적 바인딩 | 정적 바인딩 |
클래스 확장과 API 설계에서의 선택
알고리즘의 골격을 상위 클래스에 두고 특정 단계를 하위 클래스에서 구체화하는 템플릿 메소드 패턴에는 오버라이딩이 적합하다. 프레임워크가 제공하는 기본 클래스의 기능을 확장하거나, 기존 코드를 수정하지 않고 기능을 더하는 OCP(Open-Closed Principle) 구현에도 활용할 수 있다. Spring의 JpaRepository 인터페이스 구현이 한 예다.
오버로딩은 API 사용성을 높이는 데 유용하다. Java의 String.valueOf()처럼 여러 입력 형식을 받는 API를 제공할 수 있고, 빌더 패턴에서 다양한 매개변수 조합으로 객체 생성 방식을 열어 둘 수도 있다. StringBuilder의 append() 메소드가 이에 해당한다. 기본 매개변수를 갖는 메소드와 추가 매개변수를 받는 메소드를 연결하는 방식으로도 사용할 수 있다.
재정의와 중복 정의에서 지킬 점
오버라이딩에서는 @Override 어노테이션을 사용해 실수를 줄이고, 상위 클래스의 동작을 충분히 이해한 뒤 재정의해야 한다. 필요하다면 super 키워드로 상위 클래스 메소드를 호출할 수 있다. 부모 클래스의 불변 조건을 깨지 않도록 리스코프 치환 원칙도 함께 고려한다.
오버로딩은 매개변수 타입이 비슷할수록, 특히 자동 형변환이 개입할수록 호출 의도를 혼동하기 쉽다. 목적이 불분명한 오버로드를 계속 늘리면 코드 복잡성이 커진다. 각 메소드는 명확한 역할을 가져야 하며, 같은 이름으로 묶인 메소드들은 일관된 동작 방식을 유지해야 한다.
상속 계층의 동작을 바꿔야 할 때는 오버라이딩을, 같은 기능에 여러 입력 경로를 열어야 할 때는 오버로딩을 선택한다. 두 방식을 구분해 적용하면 DRY(Don't Repeat Yourself)와 OCP(Open-Closed Principle)를 따르면서 읽기 쉽고 확장 가능한 코드를 설계할 수 있다.