CodeGym /행동 /JAVA 25 SELF /메서드 오버라이딩(overriding): 오버로딩과의 차이

메서드 오버라이딩(overriding): 오버로딩과의 차이

JAVA 25 SELF
레벨 18, 레슨 2
사용 가능

1. 메서드 오버라이딩

오버라이딩(overriding)은 하위 클래스에서 부모 클래스에 이미 있는 메서드의 자체 버전을 작성할 수 있는 기능입니다. 덕분에 프로그램 실행 시점(run-time)에 진정한 다형성이 동작합니다.

쉽게 말해:
기본 클래스에 메서드가 있고, 하위 클래스가 그 메서드를 자신만의 방식으로 수행하길 원한다면 하위 클래스에서 동일한 시그니처로 메서드를 선언하면 됩니다. 기본 타입의 참조로 메서드를 호출해도 실제 객체의 타입에 있는 메서드 버전이 호출됩니다.

생활 예시.
동물 팀이 있고 각자에게 “소리를 내라”라고 요청한다고 상상해 보세요. 모두에게는 makeSound()라는 같은 명령이지만, 개는 짖고, 고양이는 야옹하고, 소는 음메 하고 웁니다. 코드는 동일한 메서드 호출처럼 보이지만 결과는 다릅니다 — 이것이 바로 오버라이딩의 마법입니다!

오버라이딩 문법

기본 예제

class Animal {
    void makeSound() {
        System.out.println("동물이 어떤 소리를 냅니다");
    }
}

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

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("야옹!");
    }
}

여기서 Animal 클래스에 makeSound() 메서드가 정의되어 있습니다. 하위 클래스인 Dog와 Cat은 이 메서드를 오버라이드하여 자신만의 구현을 제공합니다.

애너테이션 @Override

Java에서는 오버라이드하는 메서드에 @Override 애너테이션을 붙이는 것을 권장합니다(좋은 습관입니다):

@Override
void makeSound() { ... }

코드가 동작하는 데 필수는 아니지만 다음과 같은 이점이 있습니다:

  • 정말로 메서드를 오버라이드하는지(이름이나 매개변수에 오타가 아닌지)를 컴파일러가 확인해 줍니다.
  • 코드 가독성이 향상됩니다 — 다른 개발자도 이 메서드가 부모의 메서드를 오버라이드한다는 것을 바로 알 수 있습니다.

오버라이드된 메서드 호출

Animal myDog = new Dog();
myDog.makeSound(); // 출력: 멍!

변수 타입은 Animal이지만 실제로는 Dog 객체를 가리키므로, Dog 클래스의 메서드가 호출됩니다. 이것이 동적(지연) 바인딩입니다.

2. 오버라이딩(overriding)과 오버로딩(overloading)의 차이

오버로딩(overloading)

  • 하나의 클래스(또는 같은 계층 내, 항상 “근처”).
  • 메서드 이름은 같지만 매개변수(타입, 개수, 순서)가 다릅니다.
  • 메서드 선택은 컴파일 시에 이루어집니다.
void print(int x) { ... }
void print(String s) { ... }

오버라이딩(overriding)

  • 서로 다른 클래스: 슈퍼클래스에 선언된 메서드를 서브클래스에서 오버라이드합니다.
  • 메서드 이름과 시그니처(매개변수와 반환 타입)가 동일합니다.
  • 메서드 선택은 실행 시(run-time)에 실제 객체 타입을 기준으로 이루어집니다.

비교 표

오버로딩 (overloading) 오버라이딩 (overriding)
어디서 하나의 클래스 내 슈퍼클래스와 서브클래스
메서드 이름 동일 동일
매개변수 다름 동일
반환 타입 달라도 됨 일치하거나 하위 타입이어야 함
선정 시점 컴파일 시 실행 시(run-time)
애너테이션 필요 없음 @Override (권장)

3. 메서드 오버라이딩 규칙

오버라이딩은 강력하지만 엄격한 규칙이 있습니다. 하나씩 살펴봅시다.

메서드 시그니처는 일치해야 한다

  • 메서드 이름, 매개변수 타입과 순서는 슈퍼클래스의 메서드와 동일해야 합니다.
  • 반환 타입은 동일하거나 공변적(즉, 부모 메서드 반환 타입의 하위 타입)이어야 합니다.

공변 반환 타입 예제:

class Animal {
    Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
    @Override
    Cat reproduce() { return new Cat(); } // OK! Cat은 Animal의 하위 타입
}

접근 제어자

오버라이드된 메서드의 접근 제어자는 슈퍼클래스의 메서드보다 더 제한적일 수 없습니다. 부모의 메서드가 public이면, 하위 클래스에서도 반드시 public이어야 합니다. 더 좁은(protected 또는 private) 접근으로 낮출 수 없습니다.

예:

class Parent {
    public void greet() { }
}
class Child extends Parent {
    // void greet() { } // 오류! 기본 접근 제한(package-private)은 public보다 범위가 좁습니다
    @Override
    public void greet() { } // OK
}

예외

  • 오버라이드된 메서드는 기본 메서드에 선언되지 않은 새로운 checked 예외를 던질 수 없습니다.
  • 더 적거나 동일한 예외를 던질 수는 있습니다.

예:

class Parent {
    void doWork() throws IOException { }
}
class Child extends Parent {
    @Override
    void doWork() throws FileNotFoundException { } // OK, FileNotFoundException은 IOException의 하위 타입
    // void doWork() throws SQLException { } // 오류! SQLException은 부모에 선언되어 있지 않습니다
}

static 메서드는 오버라이드되지 않는다

static 메서드는 숨길 수는 있지만(hidden), 오버라이드할 수는 없습니다. 하위 클래스에서 동일한 시그니처의 static 메서드를 선언해도 이것은 오버라이딩이 아닙니다! 메서드 숨김일 뿐이며, 폴리모orphism이 아닙니다.

class Animal {
    static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
    static void info() { System.out.println("Dog"); }
}

Dog.info()를 호출하면 "Dog"가 출력되지만, Animal 타입의 변수로 호출하면 Animal.info()가 호출됩니다. 이는 폴리모orphism이 아닙니다!

final 메서드는 오버라이드할 수 없다

슈퍼클래스의 메서드가 final로 선언되어 있으면 이를 오버라이드하려 하면 컴파일 오류가 발생합니다.

class Animal {
    final void sleep() { }
}
class Dog extends Animal {
    // @Override
    // void sleep() { } // 오류! final 메서드는 오버라이드할 수 없습니다
}

4. 실용 예제

오버라이딩이 어떻게 동작하는지, 그리고 오버로딩과 어떻게 다른지 실제로 살펴봅시다.

예제 1: 클래스 Shape와 그 하위 클래스

class Shape {
    void draw() {
        System.out.println("도형을 그립니다");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("원을 그립니다");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("사각형을 그립니다");
    }
}

다형성 사용:

public class Main {
    public static void main(String[] args) {
        Shape s1 = new Circle();
        Shape s2 = new Rectangle();
        s1.draw(); // 원을 그립니다
        s2.draw(); // 사각형을 그립니다
    }
}

변수는 Shape로 선언되어 있지만, 실제 객체의 클래스에 정의된 메서드가 호출됩니다.

예제 2: 오버로딩과의 차이

class Printer {
    void print(String s) {
        System.out.println("문자열: " + s);
    }

    void print(int n) {
        System.out.println("숫자: " + n);
    }
}

두 메서드의 이름은 모두 print이지만 매개변수가 다릅니다 — 이것은 오버로딩이지 오버라이딩이 아닙니다.

5. 오버라이딩과 부모 메서드 호출(super)

오버라이드된 메서드에서 먼저 부모의 로직을 실행한 뒤 자신의 로직을 추가하고 싶을 때가 있습니다. 이때 super 키워드를 사용합니다.

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        super.makeSound(); // 부모 메서드 호출
        System.out.println("멍!");
    }
}

new Dog().makeSound() 호출 시 출력:

동물이 소리를 냅니다
멍!

동적 바인딩(late binding)은 어떻게 동작하나

기본 타입의 참조로 메서드를 호출할 때, Java는 실행 시점에 그 참조가 가리키는 실제 객체가 어느 클래스인지 확인하고, 그 클래스에 정의된 메서드 버전을 호출합니다.

Animal a = new Cat();
a.makeSound(); // Animal.makeSound()가 아니라 Cat.makeSound()가 호출됩니다

이것이 Java에서의 다형성의 근간입니다.

6. 여러분의 애플리케이션과의 연관성

우리의 학습용 애플리케이션(예: 직원 관리 시스템)에서 기본 클래스 Employee에 work() 메서드를 두고, 하위 클래스 Manager와 Developer가 각자 방식으로 이 메서드를 구현할 수 있습니다:

class Employee {
    void work() {
        System.out.println("직원이 일합니다");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("매니저가 관리합니다");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("개발자가 코드를 작성합니다");
    }
}

이제 모든 직원을 하나의 배열이나 리스트에 담아 처리할 수 있습니다:

Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
    e.work(); // 각 객체마다 다른 출력!
}

7. 메서드 오버라이딩에서 흔한 실수

오류 №1: 메서드 이름이나 매개변수에 오타. 이름이나 매개변수를 실수하면 오버라이딩이 아니라 새로운 메서드를 만들게 됩니다. 그 결과 다형성이 동작하지 않습니다. 그래서 항상 @Override 애너테이션을 사용하세요 — 컴파일러가 즉시 잡아줍니다.

오류 №2: 더 엄격한 접근 제어자. 부모 메서드가 public인데, 자식 클래스에서 protected나 기본 접근으로 선언하면 컴파일 오류가 발생합니다.

오류 №3: static 또는 final 메서드를 오버라이드하려는 시도. static 메서드는 오버라이드되지 않으며, final 메서드는 아예 오버라이드할 수 없습니다. 시도하면 컴파일러가 막습니다.

오류 №4: 호환되지 않는 반환 타입으로 변경. 하위 클래스의 반환 타입이 슈퍼클래스의 타입과 일치하지 않거나 그 하위 타입이 아니면 컴파일러가 오버라이딩을 허용하지 않습니다.

오류 №5: 새로운 checked 예외 추가. 오버라이드된 메서드는 기본 메서드 선언에 없는 새로운 checked 예외를 던질 수 없습니다. 그렇게 하면 컴파일 오류가 납니다.

오류 №6: super를 잊었다. 오버라이드된 메서드에서 부모의 일부 동작을 유지하고 싶다면 super.methodName()을 명시적으로 호출하는 것을 잊지 마세요. Java가 자동으로 해주지 않습니다.

이제 메서드 오버라이딩이 어떻게 동작하는지, 오버로딩과 무엇이 다른지, 그리고 이를 통해 Java에서 다형성이 어떻게 구현되는지 알게 되었습니다. 다음 강의에서는 컬렉션, 배열, 실제 과제와 함께 다형성을 어떻게 적용하는지 살펴보겠습니다!

1
과제
JAVA 25 SELF, 레벨 18, 레슨 2
잠금
동물원의 목소리: 서브클래스에서 메서드 재정의
동물원의 목소리: 서브클래스에서 메서드 재정의
1
과제
JAVA 25 SELF, 레벨 18, 레슨 2
잠금
출력의 마법: 오버라이딩과 오버로딩의 차이
출력의 마법: 오버라이딩과 오버로딩의 차이
1
과제
JAVA 25 SELF, 레벨 18, 레슨 2
잠금
그리기의 예술: @Override 애노테이션 사용
그리기의 예술: @Override 애노테이션 사용
1
과제
JAVA 25 SELF, 레벨 18, 레슨 2
잠금
반려동물의 음악 공연: super로 부모 메서드 호출
반려동물의 음악 공연: super로 부모 메서드 호출
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION