haileyjpark

[JAVA] 함수의 다형성(Polymorphism)과 오버로딩 & 오버라이딩 본문

소프트웨어 공학

[JAVA] 함수의 다형성(Polymorphism)과 오버로딩 & 오버라이딩

개발하는 헤일리 2023. 6. 4. 23:31
반응형

오늘은 JAVA를 배우면서 가장 기본적인 개념인 다형성과 이를 구현하는 방법 중 대표적인 오버로딩과 오버라이딩에 대해 알아보았습니다.

 

다형성이란?

다형성(Polymorphism)은 하나의 타입에서 여러 가지 타입으로 확장할 수 있는 성질을 말합니다.

다형성은 상속, 추상화와 더불어 객체지향 프로그래밍에서 중요한 특징 중 하나로, 다형성을 활용하면 기능을 확장하거나, 객체를 변경해야할 때 타입 변경 없이 객체 생성만으로 타입 변경이 일어나게 할 수 있습니다.

 

이러한 다형성을 구현하는 방법인 오버로딩과 오버라이딩을 개념과 단순한 사례를 통해 차근차근 살펴보겠습니다.

 

오버로딩이란?

  오버로딩(Overloading)은 자바의 한 클래스 내에 이미 사용하려는 이름과 같은 이름을 가진 메소드가 있더라도 매개변수의 개수 또는 타입이 다르면, 같은 이름을 사용해서 메소드를 정의할 수 있다는 것을 말합니다. 오버로딩의 특성 덕분에 함수 이름을 동일하게 사용하면서도 다양한 경우에 대응할 수 있습니다.

  오버로딩은 컴파일 시점에 함수의 인자를 바탕으로 적절한 함수를 찾아주기 때문에, 이를 정적 다형성이라고도 합니다.

 

오버로딩의 조건

  • 메서드 이름이 동일해야 합니다. 메서드의 이름이 동일하면, 코드의 일관성이 유지되고 개발자가 메서드의 기능을 쉽게 이해할 수 있습니다.
  • 매개변수의 타입 또는 개수가 달라야 합니다. 같은 이름의 메서드가 다양한 매개변수를 받아 처리할 수 있게 하여, 메서드의 사용 범위를 확장할 수 있습니다.
  • 반환 타입은 영향을 주지 않습니다. 즉, 반환 타입만 다르게 지정한 메서드를 오버로딩할 수 없습니다.
  • 접근 제어자도 자유롭게 지정해줄 수 있습니다. 각 메소드의 접근 제어자를 public, default, protected, private으로 다르게 지정해줘도 상관없습니다. 하지만, 접근 제어자만 다르게한다고 오버로딩이 가능하지는 않습니다.

 

오버로딩을 사용하는 이유와 장점

  • 다양한 데이터 타입을 처리할 수 있습니다
      오버로딩을 사용하면 동일한 이름의 메서드가 다양한 데이터 타입의 매개변수를 처리할 수 있기 때문에, 메서드의 사용 범위를 확장하고, 유연한 프로그래밍이 가능해집니다.
      예를 들어, 텍스트 출력 시 사용하는 println의 인자 값으로 int, double, boolean, String 등의 다양한 타입의 매개변수들을 집어넣어도 println 메서드는 각 타입에 맞게 데이터를 출력해줍니다.
  • 코드의 중복을 줄일 수 있습니다
      동일한 기능을 수행하는 메서드를 여러 개 정의하는 대신, 매개변수가 다른 여러 가지 상황에 대응할 수 있는 하나의 메서드를 정의함으로써 코드의 중복을 줄일 수 있습니다. 
      위의 예시에서 사용된 println메서드를 각각 정의하게 되면, printlnInt, printlnDouble, printlnBoolean 등 수많은 메소드가 만들어져서 코드의 중복이 생기고, 이 메서드들의 이름을 각각 정의해주기 때문에 복잡성이 가중됩니다.
  • 코드의 가독성을 향상시킵니다
      메서드의 이름이 일관되고 명확하면, 개발자가 코드를 더 쉽게 이해할 수 있습니다. 또한, 오버로딩을 사용하면 메서드의 이름을 기억하기 쉽고, 코드를 더 간결하게 작성할 수 있습니다.

 

오버로딩의  예시

package JavaProgramming.src.Java;

public class OverloadingExample {
    public int sum(int a, int b) {
        return a + b;
    }

    public double sum(double a, double b) {
        return a + b;
    }

    public int sum(int a, int b, int c) {
        return a + b + c;
    }

    public String sum(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        OverloadingExample example = new OverloadingExample();
        System.out.println(example.sum(3, 5)); // 8
        System.out.println(example.sum(3.5, 5.5)); // 9.0
        System.out.println(example.sum(3, 5, 7)); // 15
        System.out.println(example.sum("Hello, ", "world!")); // "Hello, world!"
    }
}

예시 코드의 수행 결과

 

위의 예시 코드에서 각각의 sum 메서드는 동일한 이름을 가지지만 매개변수의 개수나 타입이 다르기 때문에 다른 동작을 수행합니다.

 

여기서 OverloadingExample 클래스에는 다음과 같은 sum 메서드들이 있습니다:

  • sum(int a, int b): 정수형 매개변수 a와 b를 받아서 두 정수의 합을 반환합니다.
  • sum(double a, double b): 실수형 매개변수 a와 b를 받아서 두 실수의 합을 반환합니다.
  • sum(int a, int b, int c): 정수형 매개변수 a, b, c를 받아서 세 정수의 합을 반환합니다.
  •  sum(String a, String b): 문자열 매개변수 a b 받아서  문자열을 연결한 결과를 반환합니다.

 

오버라이딩이란?

  오버라이딩(Overriding)은 상속 관계에서 자식 클래스가 부모 클래스의 메소드를 재정의하는 것을 말합니다. 상속받은 메소드를 그대로 사용할 수도 있지만, 자식 클래스에서 상황에 맞게 변경해야하는 경우 오버라이딩할 필요가 생깁니다. 

 

@Override라는 어노테이션은 오버라이딩을 검증하는 기능을 합니다. 코드를 검사했을 때 오버라이딩이 실제로 시행되지 않았다면 컴파일시에 오류를 출력합니다.

 

오버라이딩은 런타임에 객체의 타입에 따라 적절한 메소드를 동적으로 호출하기 때문에, 이를 동적 다형성이라고도 합니다.

 

오버라이딩의 조건

  • 상속 관계
    오버라이딩은 부모 클래스와 자식 클래스 사이에서 발생합니다. 
  • 메서드 시그니처 
    오버라이딩할 메서드는 부모 클래스의 메서드와 동일한 이름, 매개변수 리스트, 반환 타입을 가져야 합니다.
  • 접근제어자
    접근제어자를 설정할 때, 자식 클래스에서 오버라이딩하는 메서드의 접근제어자는 부모클래스보다 더 좁게 설정할 수 없습니다.
    또한, 예외는 부모 클래스의 메서드보다 더 많이 선언할 수 없습니다.
    Static 메서드는 인스턴스의 메서드로 또는 그 반대로 바꿀 수 없습니다. 부모클래스의 static 메서드를 자식에서 같은 이름으로 정의할 수 있지만 이것은 다시 정의하는 것이 아니라 같은 이름의 static 메서드를 새롭게 정의하는 것입니다.

 

오버라이딩을 사용하는 이유와 장점

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

 

오버라이딩의  예시

package JavaProgramming.src.Java;

public class OverridingExample {
    static class Animal {
        public void makeSound() {
            System.out.println("The animal makes a sound");
        }
    }

    static class Dog extends Animal {
        @Override
        public void makeSound() {
            System.out.println("The dog barks");
        }
    }

    static class Cat extends Animal {
        @Override
        public void makeSound() {
            System.out.println("The cat meows");
        }
    }

    static class Cow extends Animal {
        @Override
        public void makeSound() {
            System.out.println("The cow moos");
        }
    }

    public static void main(String[] args) {
        Animal myAnimal = new Animal();
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        Animal myCow = new Cow();

        myAnimal.makeSound(); // "The animal makes a sound"
        myDog.makeSound(); // "The dog barks"
        myCat.makeSound(); // "The cat meows"
        myCow.makeSound(); // "The cow moos"
    }
}

예시 코드의 수행 결과

위 예시 코드는 오버라이딩(Overriding)을 보여주는 예제입니다. 코드를 살펴보면 다음과 같은 특징을 가지고 있습니다:

  • Animal 클래스: Animal 클래스는 makeSound라는 메서드를 가지고 있습니다. 이 메서드는 “The animal makes a sound”라는 문구를 출력합니다.
  • Dog, Cat, Cow 클래스: Dog, Cat, Cow 클래스는 각각 Animal 클래스를 상속받습니다. 각 클래스에서는 makeSound 메서드를 오버라이딩하여 동물의 소리를 다르게 출력합니다.
  • main 메서드: main 메서드에서는 Animal 클래스를 상속받은 객체들을 생성하고, 각 객체의 makeSound 메서드를 호출합니다.

 

 

오버라이딩의 관점에서 코드를 설명하면 다음과 같습니다.

  • 오버라이딩
    Dog, Cat, Cow 클래스에서는 Animal 클래스의 makeSound 메서드를 재정의(오버라이딩)하여 동물의 소리를 다르게 출력합니다. 이렇게 오버라이딩된 메서드는 부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용하는 것을 의미합니다.
  • 다형성
    Animal 클래스로 선언된 객체인 myAnimal과 Dog, Cat, Cow 클래스로 선언된 객체인 myDog, myCat, myCow는 모두 Animal 타입으로 선언되었습니다. 그러나 실제로는 각각의 객체는 다른 클래스의 인스턴스를 가지고 있기 때문에, myDog, myCat, myCow 객체는 오버라이딩된 makeSound 메서드를 호출하므로 각각의 동물 소리를 출력합니다.
  • 동적 바인딩
    메서드 오버라이딩은 실행 시에 동적으로 적절한 메서드가 호출됩니다. 예를 들어, myDog.makeSound() 호출하면 Dog 클래스에서 오버라이딩된 makeSound 메서드가 호출되어 “The dog barks” 출력됩니다. 이는 프로그램 실행 중에 객체의 실제 타입을 확인하여 적절한 메서드를 호출하는 동적 바인딩의 특성을 나타냅니다.

 

다형성이 지원되지 않을 경우

  만약 다형성이 지원되지 않는다면, 프로그래머들은 같은 동작을 하는 함수들을 명확히 구분해야 할 것입니다. 이로 인해 코드가 불필요하게 길어지고, 중복이 발생하며, 코드 관리가 어려워질 수 있습니다. 또한, 클래스 상속 관계에서 부모 클래스의 메소드를 재사용하거나 확장하는 것이 어렵게 되어 코드 재사용성이 떨어지게 됩니다.

 

package JavaProgramming.src.Java;

public class NoPolymorphismExample {
    public int sumIntegers(int a, int b) {
        return a + b;
    }

    public double sumDoubles(double a, double b) {
        return a + b;
    }

    public int sumThreeIntegers(int a, int b, int c) {
        return a + b + c;
    }

    public String sumStrings(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        NoPolymorphismExample example = new NoPolymorphismExample();
        System.out.println(example.sumIntegers(3, 5)); // 8
        System.out.println(example.sumDoubles(3.5, 5.5)); // 9.0
        System.out.println(example.sumThreeIntegers(3, 5, 7)); // 15
        System.out.println(example.sumStrings("Hello, ", "world!")); // "Hello, world!"
    }
}

예시 코드의 수행 결과

   위의 예시에서는 sum() 함수의 매개변수에 따라 서로 다른 이름의 함수를 만들어야 했습니다. 이로 인해 코드가  길어지고, 이해하기 어렵게 되었습니다. 이와 같이 다형성이 지원되지 않으면 개발자는 동일한 동작을 수행하는 함수를 명시적으로 구분해야 하며, 이는 코드 관리와 재사용성 측면에서 불편함을 초래합니다.