Java_13) 오버로딩과 오버라이딩
2025. 11. 10. 17:33

Java 객체지향 프로그래밍에서 메서드를 다루는 핵심 기법에는 오버로딩(Overloading)과 오버라이딩(Overriding)이 존재한다. 두 개념은 이름이 유사하여 혼동되기 쉽지만, 각각이 해결하고자 하는 문제와 사용 목적이 명확히 구분된다. 오버로딩은 같은 기능을 다양한 입력 형태로 제공하여 사용 편의성을 높이는 기법이며, 오버라이딩은 상속받은 메서드를 자식 클래스의 요구사항에 맞게 재정의하여 확장성을 제공하는 기법이다.

학습 목표

  • 오버로딩의 정의와 성립 조건을 이해한다
  • 오버라이딩의 정의와 성립 조건을 이해한다
  • 오버로딩과 오버라이딩의 차이점을 명확히 구분한다
  • 실무에서 두 기법을 올바르게 적용하는 방법을 학습한다

<오버로딩>

- 오버로딩의 정의

오버로딩은 같은 이름의 메서드를 매개변수의 개수, 타입, 순서를 다르게 하여 한 클래스 내에서 여러 개 정의하는 것이다. 메서드의 이름은 곧 그 기능의 이름을 나타내며, 오버로딩을 통해 동일한 기능을 수행하는 메서드를 다양한 형태의 매개변수로 제공할 수 있다.

메서드는 함수의 이름, 매개변수, 반환형으로 구성된다. 함수의 이름은 기능을 나타내고, 매개변수는 함수에서 이용하는 데이터를 의미하며, 반환형은 처리 결과를 나타낸다. 오버로딩에서 중요한 점은 반환형은 오버로딩의 구분 기준이 아니라는 것이다. 컴파일러는 메서드를 호출할 때 매개변수의 타입과 개수만으로 어떤 메서드를 실행할지 결정하므로, 반환형만 다른 메서드는 오버로딩으로 인정되지 않는다.

- 오버로딩의 성립 조건

오버로딩이 성립하기 위해서는 다음 조건 중 하나를 만족해야 한다.

첫 번째, 매개변수의 개수가 달라야 한다. 예를 들어 정수 2개를 더하는 메서드와 정수 3개를 더하는 메서드는 매개변수 개수가 다르므로 오버로딩이 성립한다.

두 번째, 매개변수의 타입이 달라야 한다. 정수형 매개변수를 받는 메서드와 실수형 매개변수를 받는 메서드는 타입이 다르므로 오버로딩이 성립한다.

세 번째, 매개변수의 순서가 달라야 한다. method(int a, double b)method(double a, int b)는 매개변수 순서가 다르므로 오버로딩이 성립한다.

반면, 매개변수의 이름만 다르거나 반환형만 다른 경우는 오버로딩이 성립하지 않는다. 매개변수 이름은 메서드 시그니처에 포함되지 않으며, 반환형만으로는 호출 시점에 어떤 메서드를 실행할지 구분할 수 없기 때문이다.

- 오버로딩 구현

다음은 덧셈 기능을 수행하는 메서드를 오버로딩으로 구현한 예제이다.

public class MethodOverloading {

    // 정수 2개를 더하는 메서드
    public static int adder(int n1, int n2) {
        return n1 + n2;
    }

    // 실수 2개를 더하는 메서드 - 매개변수 타입이 다름
    public static int adder(double n1, double n2) {
        return (int)(n1 + n2);
    }

    // 정수 3개를 더하는 메서드 - 매개변수 개수가 다름
    public static int adder(int n1, int n2, int n3) {
        return n1 + n2 + n3;
    }

    // 오버로딩 성립 X - 매개변수 이름만 다른 경우
    // public static int adder(int n5, int n6) {
    //     return n5 + n6;
    // }

    // 오버로딩 성립 X - 반환형만 다른 경우
    // 호출 시점에 메서드를 구분할 수 없다
    // public static double adder(int n1, int n2) {
    //     return n1 + n2;
    // }
}

위 코드에서 adder 메서드는 세 가지 형태로 오버로딩되었다. 첫 번째 메서드는 정수 2개를 받고, 두 번째 메서드는 실수 2개를 받으며, 세 번째 메서드는 정수 3개를 받는다. 각 메서드는 매개변수의 타입이나 개수가 다르므로 컴파일러가 명확히 구분할 수 있다.

주석 처리된 부분은 오버로딩이 성립하지 않는 경우를 보여준다. 매개변수 이름만 바꾼 경우와 반환형만 바꾼 경우는 모두 컴파일 에러가 발생한다.

실행 결과:

System.out.println(adder(5, 10));           // 15
System.out.println(adder(5.5, 10.3));       // 15
System.out.println(adder(5, 10, 15));       // 30

위 실행 결과에서 볼 수 있듯이, 같은 adder 메서드 이름을 사용하지만 전달되는 인자의 타입과 개수에 따라 적절한 메서드가 자동으로 선택되어 실행된다.

- 오버로딩의 실무 활용

오버로딩은 Java 표준 라이브러리에서도 광범위하게 사용된다. 대표적인 예시가 System.out.println() 메서드이다. 이 메서드는 정수, 실수, 문자열, 객체 등 다양한 타입의 인자를 받을 수 있는데, 이는 모두 오버로딩으로 구현되어 있다. 개발자는 출력할 데이터의 타입을 신경 쓰지 않고 동일한 메서드 이름으로 다양한 타입을 출력할 수 있다.

StringBuilder 클래스의 append() 메서드 역시 오버로딩의 좋은 예시이다. 문자열, 정수, 실수, 문자 등 다양한 타입을 추가할 수 있도록 여러 버전의 append() 메서드가 오버로딩되어 있다.


<오버라이딩>

- 오버라이딩의 정의

오버라이딩은 자식 클래스가 상속받은 부모 클래스의 메서드를 재정의하는 것이다. 부모 클래스가 제공하는 기본 구현을 자식 클래스의 특성에 맞게 변경하여 사용할 수 있도록 하는 기법이다. 상속 관계에서 다형성을 구현하는 핵심 메커니즘으로, 같은 메서드 호출이라도 실제 객체의 타입에 따라 다른 동작을 수행하게 만든다.

- 오버라이딩의 성립 조건

오버라이딩이 성립하기 위해서는 다음 조건을 모두 만족해야 한다.

첫 번째, 부모 메서드의 메서드명과 동일해야 한다. 메서드 이름이 다르면 오버라이딩이 아닌 새로운 메서드를 정의하는 것이다.

두 번째, 매개변수의 개수, 자료형, 순서가 동일해야 한다. 매개변수가 다르면 오버라이딩이 아닌 오버로딩이 된다.

세 번째, 반환형이 동일해야 한다. Java 5 이상에서는 공변 반환 타입(covariant return type)을 지원하여 자식 타입의 반환이 가능하지만, 기본적으로는 동일한 반환형을 사용해야 한다.

네 번째, 부모 메서드의 접근제한자보다 범위가 같거나 넓어야 한다. 예를 들어 부모 메서드가 protected라면 자식 메서드는 protected 또는 public이어야 한다. private이나 default로 좁힐 수는 없다.

이러한 조건들은 메서드 시그니처의 일관성을 유지하여 다형성이 정상적으로 작동하도록 보장한다.

- Object 클래스 메서드 오버라이딩

Java의 모든 클래스는 Object 클래스를 최상위 부모로 갖는다. Object 클래스는 모든 객체가 공통으로 사용할 수 있는 기본 메서드를 제공하며, 이들 메서드를 오버라이딩하여 클래스의 특성에 맞게 재정의하는 것이 일반적이다.

public class Man {
    private String name;
    private String number;

    public Man(String name, String number) {
        this.name = name;
        this.number = number;
    }

    // Object 클래스의 toString() 메서드 오버라이딩
    @Override
    public String toString() {
        return "Man [name=" + name + ", number=" + number + "]";
    }

    // Object 클래스의 hashCode() 메서드 오버라이딩
    @Override
    public int hashCode() {
        return Objects.hash(name, number);
    }

    // Object 클래스의 equals() 메서드 오버라이딩
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Man man = (Man) obj;
        return number.equals(man.number);
    }

    public String getNumber() {
        return number;
    }
}

위 코드에서 Man 클래스는 Object 클래스의 세 가지 주요 메서드를 오버라이딩했다. toString() 메서드는 객체를 문자열로 표현하는 방법을 정의하고, hashCode() 메서드는 해시 기반 컬렉션에서 사용할 해시값을 생성하며, equals() 메서드는 객체 간 동등성 비교 기준을 정의한다.

@Override 어노테이션은 컴파일러에게 이 메서드가 오버라이딩임을 명시적으로 알려준다. 만약 부모 클래스에 해당 메서드가 없거나 시그니처가 다르면 컴파일 에러가 발생하여 실수를 방지할 수 있다.

실행 결과:

Man m1 = new Man("jjw", "111111-1111111");
System.out.println(m1);  // Man [name=jjw, number=111111-1111111]

toString() 메서드를 오버라이딩하지 않았다면 Man@해시코드 형태로 출력되었겠지만, 오버라이딩 후에는 의미 있는 정보가 출력된다.

- 상속 관계에서의 오버라이딩

상속 계층 구조에서 오버라이딩은 다형성을 구현하는 핵심 메커니즘이다.

// 부모 클래스
public class Animal {
    public void speak() {
        System.out.println("동물이 소리낅니다.");
    }
}

// 자식 클래스 - Dog
public class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("멍멍!");
    }
}

// 자식 클래스 - Cat
public class Cat extends Animal {
    @Override
    public void speak() {
        System.out.println("야옹!");
    }
}

위 코드는 전형적인 오버라이딩 구조를 보여준다. Animal 클래스의 speak() 메서드를 DogCat 클래스에서 각각 자신의 특성에 맞게 재정의했다.

실행 결과:

Animal dog = new Dog();
Animal cat = new Cat();

dog.speak();  // 멍멍!
cat.speak();  // 야옹!

변수 타입은 Animal이지만 실제 객체는 DogCat이므로, 각 객체의 오버라이딩된 메서드가 호출된다. 이것이 바로 런타임 다형성(runtime polymorphism)이다.

- 오버라이딩 시 주의사항

오버라이딩에서 흔히 발생하는 실수는 매개변수를 다르게 정의하는 것이다.

class Parent {
    public void show(int a) {
        System.out.println("Parent: " + a);
    }
}

class Child extends Parent {
    // 이것은 오버라이딩이 아닌 오버로딩이다
    public void show(double a) {
        System.out.println("Child: " + a);
    }
}

위 코드에서 Child 클래스의 show() 메서드는 매개변수 타입이 double로 다르므로 오버라이딩이 아닌 오버로딩이다. 따라서 Child 클래스는 두 개의 show() 메서드를 갖게 된다.

올바른 오버라이딩은 다음과 같다.

class Child extends Parent {
    @Override
    public void show(int a) {
        System.out.println("Child: " + a);
    }
}

매개변수의 타입과 개수가 동일해야 오버라이딩이 성립한다. @Override 어노테이션을 사용하면 이러한 실수를 컴파일 시점에 발견할 수 있다.


<오버로딩과 오버라이딩 비교>

오버로딩과 오버라이딩은 여러 측면에서 차이를 보인다.

첫 번째, 개념적 차이가 존재한다. 오버로딩은 같은 이름의 메서드를 여러 개 정의하는 것이고, 오버라이딩은 상속받은 메서드를 재정의하는 것이다.

두 번째, 적용 범위가 다르다. 오버로딩은 같은 클래스 내에서 이루어지며, 오버라이딩은 부모-자식 클래스 간에 이루어진다.

세 번째, 메서드 시그니처 요구사항이 다르다. 오버로딩은 메서드명은 동일하지만 매개변수가 달라야 하고, 오버라이딩은 메서드명과 매개변수가 모두 동일해야 한다.

네 번째, 반환형에 대한 제약이 다르다. 오버로딩에서 반환형은 메서드 구분 기준이 아니므로 상관없지만, 오버라이딩에서는 반환형이 동일해야 한다.

다섯 번째, 접근제한자 제약이 다르다. 오버로딩에서는 접근제한자가 자유롭지만, 오버라이딩에서는 부모 메서드보다 같거나 더 넓은 범위여야 한다.

여섯 번째, 목적이 다르다. 오버로딩은 다양한 입력 형태를 지원하여 편의성을 높이고, 오버라이딩은 부모 기능을 자식에 맞게 수정하여 확장성을 제공한다.

일곱 번째, 어노테이션 사용이 다르다. 오버로딩에는 특별한 어노테이션이 없지만, 오버라이딩에는 @Override 어노테이션을 사용하여 명시적으로 표시한다.


정리

오버로딩과 오버라이딩은 Java 객체지향 프로그래밍의 핵심 개념이다.

오버로딩은 편의성을 위한 기법이다. 같은 기능을 다양한 방식으로 사용할 수 있도록 하여 코드의 일관성과 가독성을 높인다. System.out.println()처럼 다양한 타입을 동일한 메서드 이름으로 처리할 수 있게 한다.

오버라이딩은 확장성을 위한 기법이다. 부모 클래스의 기능을 자식 클래스의 요구사항에 맞게 변경하여 상속의 유연성을 극대화한다. 이를 통해 다형성을 구현하고 객체지향 설계의 개방-폐쇄 원칙(OCP)을 실현한다.

두 개념을 명확히 구분하고 적절히 활용하면 유지보수가 용이하고 확장 가능한 코드를 작성할 수 있다. 오버로딩은 같은 기능의 다양한 인터페이스를 제공할 때, 오버라이딩은 상속 계층에서 특화된 동작을 구현할 때 사용한다는 점을 기억해야 한다.

'Java' 카테고리의 다른 글

Java_15)Java Object 클래스와 다형성 완벽 정리  (0) 2025.11.13
Java_14) Garbage Collection  (1) 2025.11.11
Java_12) Inheritance - 상속  (4) 2025.08.27
Java_11) Access Modifier, ObjectArray  (1) 2025.08.27
Java_10) Class - 객체 (Object)  (4) 2025.08.27