DECHIVE
DECHIVE
← Archive
Dev/

오버로딩과 오버라이딩은 대체 무슨 차이일까?

오버로딩과 오버라이딩은 둘 다 같은 이름의 메서드를 다룬다. 하지만 하나는 입력의 차이고, 하나는 동작의 재정의다. 그 차이를 코드로 확인한다.

이 두 개념이 헷갈리는 이유는 하나다. 둘 다 메서드 이름이 같다.

그런데 같은 이름을 쓰는 이유도, 방식도, 목적도 다르다.

오버로딩: 같은 이름, 다른 입력

오버로딩은 같은 이름의 메서드를 매개변수의 개수, 타입, 순서 차이로 여러 개 만드는 것이다. 주로 같은 클래스 안에서 보지만, 상속 관계에서도 부모 메서드 이름과 같고 매개변수만 다른 메서드를 자식 클래스에 추가하는 형태로 나타날 수 있다.

Java Language Specification 기준으로 메서드 시그니처는 메서드 이름과 형식 매개변수 타입으로 정의된다. 반환 타입은 시그니처에 포함되지 않는다. 그래서 반환 타입만 달라서는 오버로딩이 성립하지 않는다.

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }
}
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2));       // 3
System.out.println(calc.add(1.5, 2.5));  // 4.0
System.out.println(calc.add(1, 2, 3));   // 6

같은 이름의 add지만 자바는 매개변수의 차이를 보고 어떤 메서드를 호출할지 결정한다. 이 결정은 컴파일 시점에 이루어진다.

반환 타입만 다르면 오버로딩이 되지 않는다.

class Example {
    int getValue() { return 1; }
    double getValue() { return 1.0; } // 컴파일 에러
}

getValue()만 쓰면 어느 메서드를 써야 할지 컴파일러가 알 수 없다. 반환 타입은 메서드 시그니처에 포함되지 않기 때문이다.

실전 예시: 주문 금액 계산

class OrderCalculator {
    int calculate(int quantity) {
        return quantity * 10000;
    }

    int calculate(int quantity, int couponAmount) {
        return quantity * 10000 - couponAmount;
    }

    int calculate(int quantity, double discountRate) {
        return (int)(quantity * 10000 * (1 - discountRate));
    }
}
OrderCalculator calc = new OrderCalculator();
System.out.println(calc.calculate(3));             // 30000
System.out.println(calc.calculate(3, 5000));       // 25000
System.out.println(calc.calculate(3, 0.1));        // 27000

'주문 금액 계산'이라는 목적은 같지만 입력 조건이 다르다. 오버로딩은 이처럼 목적은 같고 입력이 달라질 때 같은 이름으로 묶는 방식이다.

함정 예시: 변수 타입이 다르면 다른 메서드가 호출된다

class MessagePrinter {
    void print(String message) {
        System.out.println("String: " + message);
    }

    void print(Object message) {
        System.out.println("Object: " + message);
    }
}
MessagePrinter printer = new MessagePrinter();

String text = "hello";
Object objectText = "hello";

printer.print(text);        // String: hello
printer.print(objectText);  // Object: hello

실제 값은 둘 다 문자열이지만 결과가 다르다. 오버로딩은 컴파일 시점의 변수 타입을 기준으로 메서드를 고른다. 런타임 실제 값이 아니라 선언된 타입이 기준이다.

오버라이딩: 상속받은 동작을 다시 정의하다

오버라이딩은 상속 관계에서 부모 클래스의 메서드를 자식 클래스가 같은 이름, 같은 매개변수로 다시 정의하는 것이다.

class Animal {
    void sound() {
        System.out.println("...");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹");
    }
}
Dog dog = new Dog();
Cat cat = new Cat();
dog.sound(); // 멍멍
cat.sound(); // 야옹

@Override는 컴파일러에게 부모 메서드를 재정의한다고 알리는 어노테이션이다. 생략해도 동작하지만 붙이면 메서드 이름을 잘못 썼을 때 컴파일 에러로 잡아준다.

실전 예시: 결제 수단별 결제 처리

class Payment {
    void pay(int amount) {
        System.out.println(amount + "원을 결제합니다.");
    }
}

class CardPayment extends Payment {
    @Override
    void pay(int amount) {
        System.out.println("카드로 " + amount + "원을 결제합니다.");
    }
}

class KakaoPayPayment extends Payment {
    @Override
    void pay(int amount) {
        System.out.println("카카오페이로 " + amount + "원을 결제합니다.");
    }
}
Payment payment = new CardPayment();
payment.pay(10000);  // 카드로 10000원을 결제합니다.

payment = new KakaoPayPayment();
payment.pay(15000);  // 카카오페이로 15000원을 결제합니다.

변수 타입은 Payment지만 실제 객체에 따라 실행되는 메서드가 달라진다. 오버라이딩은 런타임에 실제 객체 기준으로 동작한다. 이것이 동적 바인딩이고, 다형성의 핵심이다.

super를 활용한 예시

부모의 동작을 완전히 버리지 않고 자식이 추가 동작을 붙이는 경우다.

class Notification {
    void send(String message) {
        System.out.println("알림 전송: " + message);
    }
}

class EmailNotification extends Notification {
    @Override
    void send(String message) {
        super.send(message);
        System.out.println("이메일 로그를 저장합니다.");
    }
}
EmailNotification email = new EmailNotification();
email.send("주문이 완료되었습니다.");
// 알림 전송: 주문이 완료되었습니다.
// 이메일 로그를 저장합니다.

super.send(message)는 부모 메서드를 먼저 실행하고 그 위에 자식의 동작을 얹는다.

static 메서드는 오버라이딩이 아니다

class Parent {
    static void hello() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    static void hello() {
        System.out.println("Child");
    }
}
Parent p = new Child();
p.hello(); // Parent

Parent.hello(); // Parent
Child.hello();  // Child

p.hello()는 오버라이딩처럼 보이지만, 실제로는 p의 컴파일 타임 타입인 Parent를 기준으로 해석된다. static 메서드는 동적 바인딩이 적용되지 않으므로 런타임 실제 객체가 Child여도 Parent의 메서드가 실행된다. 같은 이름으로 다시 선언해도 오버라이딩이 아니라 메서드 숨김에 해당한다.

실무에서는 이런 혼동을 피하기 위해 Parent.hello()처럼 클래스 이름으로 호출하는 것이 권장된다. 오버라이딩은 인스턴스 메서드에서만 동작한다.

차이를 한눈에

오버로딩오버라이딩
위치주로 같은 클래스 안, 상속 관계에서도 가능상속 관계
메서드 이름같음같음
매개변수달라야 함같아야 함
반환 타입반환 타입만으로 구분 불가같거나 더 구체적인 타입 가능
결정 시점컴파일 타임런타임

오버라이딩의 반환 타입이 "같아야 함"이 아닌 이유는 자바가 공변 반환 타입을 허용하기 때문이다. 부모 메서드의 반환 타입보다 더 구체적인 하위 타입으로 오버라이딩하는 것이 가능하다.

연습 문제

문제 1

다음 중 오버로딩으로 올바른 선언을 모두 고르시오.

class Test {
    void print(int a) {}          // (1)
    void print(int a, int b) {}   // (2)
    void print(double a) {}       // (3)
}

그리고 아래 선언을 같은 Test 클래스 안에 추가하면 오버로딩이 성립하는가?

int print(int a) {} // (4)

① (1), (2), (3) 모두 성립하며, (4)도 성립한다
② (1), (2), (3)은 성립하고, (4)는 컴파일 에러다
③ (3)은 성립하지 않는다
④ 반환 타입이 다르면 오버로딩이 항상 성립한다

<details> <summary>정답 보기</summary>

② (1), (2), (3)은 성립하고, (4)는 컴파일 에러다

(1), (2), (3)은 매개변수 타입 또는 개수가 달라 오버로딩이 성립한다. (4)는 반환 타입만 다르고 매개변수는 (1)과 동일하다. 반환 타입은 메서드 시그니처에 포함되지 않으므로 오버로딩이 되지 않고 컴파일 에러가 발생한다.

</details>

문제 2

다음 코드를 실행했을 때 출력 결과는?

class Parent {
    void show() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    @Override
    void show() {
        System.out.println("Child");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.show();
    }
}

① Parent
② Child
③ Parent
Child
④ 컴파일 에러

<details> <summary>정답 보기</summary>

② Child

변수 타입은 Parent지만 실제 객체는 Child다. 오버라이딩된 메서드는 런타임에 실제 객체 기준으로 호출된다(동적 바인딩). 따라서 Childshow()가 실행된다.

</details>

문제 3

다음 코드를 실행했을 때 출력 결과는?

class Printer {
    void print(String value) {
        System.out.println("String: " + value);
    }

    void print(Object value) {
        System.out.println("Object: " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        Printer printer = new Printer();

        String text = "hello";
        Object obj = "hello";

        printer.print(text);
        printer.print(obj);
    }
}

① String: hello
String: hello
② String: hello
Object: hello
③ Object: hello
Object: hello
④ 컴파일 에러

<details> <summary>정답 보기</summary>

② String: hello / Object: hello

오버로딩은 컴파일 시점의 변수 타입을 기준으로 메서드를 선택한다. textString 타입으로 선언됐으므로 print(String value)가 호출되고, objObject 타입으로 선언됐으므로 print(Object value)가 호출된다. 런타임 실제 값이 둘 다 문자열이어도 결정 기준은 선언된 타입이다.

</details>

오버로딩과 오버라이딩을 구분할 때 이름이 같다는 사실에서 시작하면 계속 헷갈린다. 입력이 다른지, 상속받은 동작을 다시 쓰는지를 먼저 보는 것이 더 정확하다. 오버로딩은 입력을 나누는 일이고, 오버라이딩은 동작을 다시 정하는 일이다.