[Java] 다형성과 오버로딩, 오버라이딩

임재영·2025년 5월 22일
post-thumbnail

다형성(Polymorphism)이란?

"한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 하는 것"이다.
즉, 상위 클래스 타입의 참조 변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용하여 상위 클래스가 동일한 메시지로 하위 클래스들이 서로 다른 동작을 할 수 있도록 한다.

  • 여기서 참조 변수/참조 타입(Reperence Type)이란?
    먼저 기본 타입과 참조 타입에 대해 알 필요가 있다.
    기본 타입 (Primitive Type)
    - byte, short, char, long, float, double, boolean의 8개의 타입을 말한다.
    - 기본 타입 변수에는 값 자체가 저장된다.
    참조 타입 (Reference Type)
    - 기본 타입을 제외한 배열, 열거, 클래스, 인터페이스 등을 말한다.
    - 참조 타입의 변수에는 객체(메모리)의 주소가 저장된다.

다시 말해, 같은 코드에서 하나의 메서드나 변수가 다양한 타입의 객체에 대해 동작할 수 있도록 하는 것이다.
여기서 중요한 부분은 우리가 다형성에 대해 배울 때 하나의 자료형이 여러가지 자료형으로 변환될 수 있다고 배운다. (업캐스팅/다운캐스팅)
하지만 엄밀히 따지자면, 메서드가 여러가지 자료형을 인수로 받아서 동작하는 것도 다형성이라고 할 수 있는 것이다. (오버로딩/오버라이딩)

다형성의 조건

  • 상위 클래스와 하위 클래스는 상속관계여야 한다.
  • 다형성이 보장되기 위해 오버라이딩(하위 클래스 메서드의 재정의)이 반드시 필요하다.
  • 자식 클래스의 객체가 부모 클래스의 타입으로 형변환(업캐스팅)해야 한다.

다형성의 장점

  • 유지보수 : 여러 객체를 하나의 타입으로 관리할 수 있어 유지보수가 용이하다.
  • 재사용성 : 객체의 재사용이 쉬워 재사용성이 높아진다.
  • 느슨한 결합 : 클래스 간의 의존성을 줄여 확장성은 높아지고 결합도는 낮아진다.


👉 여기서 형변환(Casting)이란?

상속받은 객체에 대한 형변환이 의미하는 것은, 객체에 속한 "멤버들에 대한 사용 범위가 달라진다는 것을 의미"한다.

  • 업캐스팅(upcasting)
    (자식 클래스의 인스턴스에 대한) 자식 클래스의 타입의 참조 변수를 부모 클래스 타입으로 형변환 하는 것이다.

    • 자식 => 부모 (up) 변환.

    • 타입 변환 구문 생략 가능. 자동 형변환 됨.

    • 단, 부모 클래스로 캐스팅된다는 것은 멤버 갯수의 감소를 의미한다. 이는 곧 자식 클래스에서만 있는 속성과 메서드는 실행하지 못한다는 뜻이다. (범위 제한)

    • 업캐스팅을 하고 메서드를 실행할 때, 만일 자식 클래스에서 오버라이딩한 메서드가 있을 경우, 부모 클래스의 메서드가 아닌 오버라이딩 된 메서드가 실행되게 된다.

      객체 지향(OOP)의 “참조 다형성”
      업캐스팅의 이해는 객체 지향의 참조 다형성에 대해 알고 있다면 크게 어렵지 않다.
      쉽게 말하면, 한번에 대입하느냐 변수에 나눠 대입하느냐의 차이가 있을 뿐이다.

      Parent p1 = new Child(); // 참조 다형성
      // ---------------------------------------------
      Child c1 = new Child();
      Parent p1 = c1; // 변수 업캐스팅(upcasting)

  • 다운캐스팅(downcasting)
    (자식 클래스의 인스턴스에 대한) 부모 클래스 타입의 참조 변수를 자식 클래스 타입으로 형변환 하는 것이다.

    • 부모 => 자식 (down) 변환.

    • 타입 변환 구문 생략 불가. 형변환 타입을 명시해야됨.

    • 다운캐스팅의 목적은 업캐스팅한 객체를 다시 자식 클래스 타입의 객체로 되돌리는데 목적을 둔다. (범위 복구)

      다운 캐스팅은 부모 클래스를 자식 클래스로 캐스팅하는, 단순히 업캐스팅의 반대 개념이 아니다.
      다운 캐스팅의 진정한 의미는 부모 클래스로 업캐스팅된 자식 클래스를 복구하여, 본인의 필드와 기능을 회복하기 위해 있는 것이다.
      즉, 원래 있던 기능을 회복하기 위해 다운캐스팅을 하는 것이다.

      구분(사진2) 다운캐스팅(사진3) 업캐스팅
      요약부모 객체인 p1이 자식 객체로 다운캐스팅 후, 자식 객체인 d1이 부모 객체인 p1으로 업캐스팅됨.자식 객체인 d1이 부모 객체인 p1으로 업캐스팅 됨.
      설명p1은 자식 객체로 생성되었지만 형태는 부모 객체이므로 사용 범위가 부모(p1)이지만,
      d1은 부모인 p1의 자식 객체로 캐스팅되므로 범위 또한 자식(d1)으로 볼 수 있음.
      d1은 자식 객체이므로 사용 범위가 자식(d1)이지만,
      p1은 자식인 d1의 부모 객체로 캐스팅되므로 범위 또한 부모(p1)로 제한됨.

  • 형변환(Casting)의 장점
    여러 자식 클래스 타입의 참조 변수들을 하나의 부모 클래스 타입의 참조 변수로 다룰 수 있는 것이다.
    만약 아래와 같이, Person이라는 부모 클래스를 가진 자식 클래스 Dancer, Singer, Actor가 있을 경우에 부모 클래스 타입으로 형변환하여 사용하게 되면 아래와 같은 장점이 있다.

    • 여러 자식 클래스 객체를 하나의 배열로 다룰 수 있다.

    • 메서드의 매개변수를 부모 클래스 타입 하나로 전달받아 사용할 수 있다.

      활용 예시 코드

      // 서로 다른 타입을 각각 정의해서 사용
      Dancer[] d = new Dancer[];
      d[0] = new Dancer();
      d[1] = new Dancer();
      
      Singer[] s = new Singer[];
      s[0] = new Singer();
      s[1] = new Singer();
      
      Actor[] a = new Actor[];
      a[0] = new Actor();
      a[1] = new Actor();
      
      //--------------------------------
      
      // 부모 클래스로 묶어서 사용
      Person[] p = new Person[];
      p[0] = new Dancer[];
      p[1] = new Dancer[];
      p[2] = new Singer[];
      p[3] = new Singer[];
      p[4] = new Actor[];
      p[5] = new Actor[];

      위 코드에서 보면 알듯이, 하나의 자료형으로 관리하면 코드량도 훨씬 줄고 가독성 및 유지보수성 또한 좋아진다.

  • 이때, 자식 클래스에만 있는 고유한 메서드를 실행하려면 어떻게 해야 할까?
    오버라이딩 한 메서드가 아닌 이상, 업캐스팅한 부모 클래스 타입에서 자식 클래스의 고유 메서드는 실행할 수 없다.
    따라서, 업캐스팅한 객체를 다시 자식 클래스 타입으로 되돌리는 다운캐스팅(downcasting)이 필요한 것이다.


public class Parent {
   public static void main(String[] agrs) {

       Parent p1 = new Parent();
       Parent p2 = new Child();
       Parent p3 = new ChildOther();
       
...

위를 보면, 세 개의 객체 모두 클래스 타입은 Parent이다.
하지만 각각 다른 클래스의 생성자를 호출하고 있다.
자바에는 instanceOf라는 메서드를 통해 객체의 클래스 타입을 알 수 있다.
아래 예제를 통해 확인해보자.

instanceOf란?
자바에서 객체의 타입을 확인하는 연산자이다. 형변환 가능 여부를 확인하며, true/false를 반환한다. 주로 상속 관계에서 부모 객체인지 자식 객체인지 확인하는데 사용된다.
쉽게 말해, instanceOf"해당 클래스가 자기집이 맞는지 확인해주는 것" 이라고 생각하면 될 것이다.

다형성(polymorphism) 예제 코드

public class Parent {
    public static void main(String[] agrs) {

        Parent p1 = new Parent();
        Parent p2 = new Child();
        Parent p3 = new ChildOther();

        Parent[] arr = {p1, p2, p3};
        
        for(Parent item : arr) {
        	System.out.println("---------------");
        	if(item instanceof Child) {
        		System.out.println("is Child Type");
        	} 
        	if(item instanceof ChildOther) {
        		System.out.println("is ChildOther Type");
        	} 
        	if(item instanceof Parent) {
        		System.out.println("is Parent Type");
        	}
        }
    }
}

출력

---------------
is Parent Type
---------------
is Child Type
is Parent Type
---------------
is ChildOther Type
is Parent Type

부모 클래스를 제외한 자식 클래스들은 두 가지의 클래스 타입을 가진 것을 볼 수 있다.
이러한 것이 다형성(polymorphism)이다.



오버로딩(Overloading)이란?

같은 클래스 내에서 동일한 이름의 메서드를 매개변수의 개수나 자료형이 다른 여러 개의 메서드로 정의할 수 있는 메서드 정의 기법.

오버로딩은 하나의 클래스 내에서 같은 이름으로 다양한 기느을 수행하는 메서드를 정의하는 기법이다. 여기서 중요한 부분은 매개변수로 다양한 자료형을 받을 수 있으며, 자료형에 따라 메서드의 결과를 다르게 가져올 수 있기도 하다.

오버로딩(overloading) 예시 코드

class OverLoadingTest {

   public void test() {
     System.out.println("No Parameter");
   }
   
   public void test(int count) {
     System.out.println("Parameter: " + cont);
   }
   
}

public class OverLoading {
  public static void main(String[] args) {
  
    OverLoadingTest olt = new OverLoadingTest();
    olt.test();
    olt.test(5);
  }
}

출력

No Parameter
Parameter: 5

하지만 위와 같이 오버로딩(Overloading)을 사용하기 위해서는 다음의 조건을 만족시켜야 한다.

오버로딩의 조건

  • 오버로딩(Overloading)할 메서드의 매개변수의 타입개수는 달라야 한다.
  • 오버로딩(Overloading)할 메서드의 반환형은 같아야 한다.
    -> 메서드의 이름만 같을 뿐, input과 내부 기능은 다름. (= 새로운 메서드 생성)
  • 반환형과 접근 제어자 다르게 한다고 해서 오버로딩을 할 순 없다.
    -> 각 메서드의 접근 제어자를 public, default, protected, private으로 다르게 지정해줘도 상관은 없음.

오버로딩의 장점

  • 다양한 데이터 타입을 처리할 수 있다.
    동일한 이름의 메서드가 다양한 데이터 타입의 매개변수를 처리할 수 있기 때문에, 메서드의 사용 범위를 확장하고, 유연한 프로그래밍이 가능해진다.
  • 코드의 중복을 줄이고 가독성을 향상시킨다.
    동일한 기능을 수행하는 메서드를 여러 개 정의하는 대신, 매개변수가 다른 여러 가지 상황에 대응할 수 있는 하나의 메서드를 정의함으로써 코드의 중복을 줄일 수 있다.
    또한 메서드의 이름이 일관되고 명확하면 개발자가 코드를 더 쉽게 이해하고 간결하게 작성하기 쉽다.



오버라이딩(Overriding)이란?

상속 또는 구현 관계에 있는 상위 객체의 메서드를 하위 객체 또는 구현 객체가 재정의하여 사용하는 메서드 정의 기법

오버라이딩은 상위 객체가 가지고 있는 메서드의 시그니처를 그대로 받아와 재정의하여 사용한다. 이로 인해 하위 클래스는 상위 클래스와 동일한 인터페이스를 제공하며, 상위 클래스와는 다른 기능을 제공할 수 있다.


👉 업캐스팅 오버라이딩 메서드

업캐스팅을 하고 메서드를 실행할 때, 만일 자식 클래스에서 오버라이딩한 메서드가 있을 경우, 부모 클래스의 메서드가 아닌 오버라이딩 된 메서드가 실행되게 된다.

위의 설명을 그냥 보았을 땐 자식 클래스에서 업캐스팅 되었기 때문에 부모 클래스에 정의된 메서드를 사용할 것 같다.
하지만 실제로 보면, 오버라이딩된 자식 클래스의 메서드를 사용하는 것을 볼 수 있다.

-> 이유가 뭘까?
이는 오버라이딩 특성상 코드가 실행하는 "런타임 환경"에서 동적으로 바인딩 되었기 때문이다.

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

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

...

Animal myAnimal = new Dog(); // 업캐스팅
myAnimal.sound();
// 출력 : "Bark"
  1. 컴파일 시점에서 myAnimalAnimal타입으로, Animal클래스 내에 있는 sound() 메서드를 호출하는 것으로 인식된다.
  1. 실행 시점에서는 myAnimal이 실제로 참조하는 객체는 Dog 클래스의 인스턴스이다.
    자바의 동적 바인딩 때문에, 실행 시 실제 객체의 타입을 확인하고 오버라이딩된 Dog 클래스의 sound() 메서드가 호출된다.

-> 동적 바인딩의 이유
동적 바인딩은 실행 시점에 객체의 실제 타입을 기준으로 메서드를 결정하기 때문에 프로그램의 유연성을 증가시킨다.
컴파일 시점에서는 해당 변수의 타입만 알 수 있고, 실제 어떤 객체를 참조할지는 실행할 때까지 알 수 없기 때문이다.

업캐스팅 활용 시 주의사항 재정리

  • 업캐스팅을 하면 멤버 갯수가 제한 되어 자식 클래스에만 있는 멤버는 사용할 수 없게 된다.
  • 업캐스팅 했지만 오버라이딩된 메서드는 자식 클래스의 메서드로 실행이 된다.


오버라이딩(Overriding) 예시 코드

class OverRidingParent {
   public void test() {
     System.out.println("Parent Method");
   }
}

class OverRidingSon extends OverRidingParent {
   public void test() {
     System.out.println("Child Method Overriding");
   }
}

public class Overriding {
  public static void main(String[] args) {
  
    OverRidingSon ors = new OverRidingSon();
    ors.test();
  }
}

출력

Child Method Overriding

오버라이딩도 오버로딩에서 처럼 아래의 조건을 만족시켜야 한다.

오버라이딩의 조건

  • 메서드명매개변수가 같아야 한다.
  • 반환형이 같아야 한다.
    -> 내부 기능만 다를 뿐, 메서드의 input과 output의 형식은 같다고 보면 됨. (= 메서드 재정의)
  • 자식 클래스에서 오버라이딩하는 메서드의 접근제어자는 부모 클래스보다 더 좁게 설정할 수 없다.

오버라이딩의 장점

  • 다형성 구현
    오버라이딩을 통해 부모 클래스의 메서드를 자식 클래스에서 재정의할 수 있기 때문에 다형성을 구현할 수 있다.
    부모 클래스 타입으로 선언된 객체에 실제 자식 클래스의 인스턴스를 할당하여, 실행 시에 동적으로 적절한 메서드가 호출된다.
  • 확장성과 유연성
    부모 클래스의 메서드를 수정하지 않고 자식 클래스에서만 변경된 동작을 정의할 수 있기 때문에 코드의 유지보수성이 향상된다.
    또한, 상속 관계에서 동일한 이름의 메서드를 사용하므로 코드의 일관성과 간결성을 유지할 수 있다.



정리하자면,

오버로딩은 결과적으로 새로운 메서드의 생성이고,
오버라이딩은 부모로부터 상속 받은 메서드를 재정의하는 것이다.




[참고]
https://velog.io/@ung6860/JAVA%EB%8B%A4%ED%98%95%EC%84%B1-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9%EC%9D%98-%EC%B0%A8%EC%9D%B4
https://velog.io/@ovan/Overriding-and-Polymorphism
https://ittrue.tistory.com/132
→ 다형성의 장점 및 조건
https://kadosholy.tistory.com/99
→ 형변환 이해 & 예제 (+사진)
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%97%85%EC%BA%90%EC%8A%A4%ED%8C%85-%EB%8B%A4%EC%9A%B4%EC%BA%90%EC%8A%A4%ED%8C%85-%ED%95%9C%EB%B0%A9-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
→ 형변환 업캐스팅/다운캐스팅 개념 이해 & 예제

0개의 댓글