오버로딩 & 오버라이딩(Overloading & Overriding)

쓰리원·2022년 5월 14일
0

java 개념

목록 보기
1/10
post-thumbnail

1. 객체 지향에서의 다형성

즉, 다형성이란 하나의 타입에서 여러 가지 타입으로 확장할 수 있는 성질이라는 것 입니다. 다형성을 활용하면 기능을 확장하거나, 객체를 변경해야할 때 타입 변경 없이 객체 생성만으로 타입 변경이 일어나게 할 수 있습니다.

위와 같은 다형성을 구현하는 방법에는 여러 가지가 있습니다. 예시를 들어 어떻게 사용할 수 있는지 대표적인 예로 오버라이딩, 오버로딩을 알아보겠습니다.

2. 오버라이딩(Overriding)

오버라이딩이란 부모-자식 상속 관계에 있는 클래스에서 상위 클래스의 메서드를 하위 클래스에서 재정의하는 것을 말합니다.

코드에 적힌 메서드 이름과 그 메서드의 바이트코드가 저장된 메모리가 대응 관계를 갖는 것을 두 대상이 binding된다고 합니다. 자식이 override한 이 메서드는 부모 클래스에 정의되어 있기 때문에 컴파일 시점에는 부모 클래스의 메모리에 코드의 메서드 이름이 대응됩니다.

하지만 런타임(메서드의 실행 시점)에는 자식 클래스의 namespace에 있는 메서드가 실행됩니다. 이렇게 컴파일 시점과 런타임의 바인딩 양상이 다르기 때문에 이 현상을 dynamic binding이라고 합니다.

즉, 동적 바인딩이란 메서드가 실행 시점에서 성격이 결정되는 바인딩입니다. 프로그램의 컴파일 시점에 부모 클래스는 자신의 멤버 함수밖에 접근할 수 없지만, 실행 시점에 동적 바인딩이 일어나 부모 클래스가 자식 클래스의 멤버함수를 접근하여 실행할 수 있습니다.

아래 예시로 보인 추상 클래스 Figure(도형)에는 하위 클래스에서 오버라이드 해야 할 메서드가 정의되어 있습니다.

public abstract class Figure {
    protected int dot;
    protected int area;

    public Figure(final int dot, final int area) {
        this.dot = dot;
        this.area = area;
    }

    public abstract void display();
}

Figure을 상속받은 하위 클래스인 Triangle 객체는 해당 객체에 맞는 기능을 구현합니다.

public class Triangle extends Figure {
    public Triangle(final int dot, final int area) {
        super(dot, area);
    }

    @Override
    public void display() {
        System.out.printf("넓이가 %d인 삼각형입니다.", area);
    }
}

만약 사각형 객체를 추가하고 싶다면, 같은 방식으로 Figure을 상속받되 메서드 부분에서 사각형에 맞는 display 메서드를 구현해주면 됩니다. 이렇게 하면 추후 도형 객체가 계속 추가되어도 도형 객체에 실제로 사용되는 비즈니스 로직의 변경을 최소화할 수 있습니다.

public static void main(String[] args) {
    Figure figure = new Triangle(3, 10); 
    figure.display();
}

만약 여기서 다형성을 사용하지 않고 도형 객체를 추가하는 로직을 생각해 본다면 아마 다음과 같이 if-else분기가 늘어나게 될 것 입니다.

public static void main1(String[] args) {
    int dot = SCANNER.nextInt();

    if (dot == 3) {
        Triangle triangle = new Triangle(3, 10);
        triangle.display();       
    } 
    else if(dot == 4) {
        Rectangle rectangle = new Rectangle(4, 20);
        rectangle.display();
    }
	  ....
}

오버라이드 다형성 방식을 잘 활용하면, 위의 예와 같이 기능의 확장과 객체의 수정에 유연한 구조를 가져갈 수 있습니다. 이유는 업캐스팅하여 객체를 선언, 부모 클래스 객체로 자식 메서드 호출을 할 수 있기 때문입니다.

public class Triangle extends Figure {
    public Triangle(final int dot, final int area) {
        super(dot, area);
    }

    @Override
    public void display() {
        System.out.printf("넓이가 %d인 삼각형입니다.", area);
    }
}
public static void main(String[] args) {
    Figure triangle = new Triangle(3, 10); 
	Figure rectangle = new Rectangle(4, 10);
    triangle.display();
    rectangle.display();
}

출력 결과

넓이가 10인 삼각형입니다.
넓이가 10인 사각형입니다.

결과적으로 위와 같이 다형성 중 하나인 오버라이드을 활용하면 부모 클래스로 객체를 선언했으나 실행시점에 동적 바인딩되어 자식클래스의 멤버함수가 호출되는 것을 볼 수 있습니다. 객체 타입 확인은 instanceof로 할 수 있습니다. 아래에 instanceof의 사용법을 알아보겠습니다. instanceof 연산자는 해당 객체가 타입이 맞다면 true, 아니면 false를 반환합니다.

if(triangle instanceof Triangle){
    triangle.print(); //true
}
if(rectangle instanceof Rectangle){
    rectangle.print(); //true
}

오버라이딩 요약

  • 유지보수가 쉽다
    개발자가 여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이합니다.
    (업캐스팅하여 객체 선언, 부모 클래스 객체로 자식 메서드 호출)
  • 재사용성 증가
    오버라이딩을 활용하면 객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높집니다.
  • 느슨한 결합
    오버라이딩을 활용하면 클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 유지보수에 용의하고 코드의 안전성이 높아집니다.

3. 오버로딩(Overloading)

1. 기본 개념

두번째로 메서드 오버로딩 입니다. 자바의 PrintStream.class에 정의되어 있는 println이라는 함수는 다음과 같이 매개변수만 다른 여러 개의 메서드가 정의되어 있습니다. 매개변수로 배열을 넣을 때, 문자열을 넣을 때, 그리고 객체를 넣을 때 모두 println이라는 메서드 시그니처를 호출하여 원하는 내용을 출력하는 기능을 수행합니다.

public class PrintStream {
	...
	public void println() {
		this.newLine();
	}

	public void println(boolean x) {
  		synchronized(this) {
      	this.print(x);
      	this.newLine();
  		}
	}

	public void println(char x) {
    	synchronized(this) {
        	this.print(x);
        	this.newLine();
    	}
	}

	public void println(int x) {
    	synchronized(this) {
        	this.print(x);
        	this.newLine();
    	}
	}
	...
}

오버로딩은 여러 종류의 타입을 받아들여 결국엔 같은 기능을 하도록 만들기 위한 작업입니다. 오버로딩도 메서드를 동적으로 호출할 수 있으니 다형성이라고 할 수 있습니다.

하지만 메서드를 오버로딩하는 경우 요구사항이 변경되었을 때 모든 메서드에서 수정이 수반되므로 필요한 경우에만 적절히 고려하여 사용하는 것이 좋습니다.

2. 주의할 점

public class Main {
    public static void main(String args []) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };
        for (Collection<?> c : collections) {
            System.out.println(classify(c));
        }
    }
    
    public static String classify(Set<?> s) {
        return "Set";
    }
    
    public static String classify(List<?> s) {
        return "List";
    }
    
    public static String classify(Collection<?> s) {
        return "Collection";
    }
}

첫번째 예를 보면 위의 결과를 예상했을 때 "Set" "List" "Collection" 순서로 출력될 것 같지만 "Collection"만 3개가 출력됩니다.

그 이유는 오버로딩된 메서드 가운데 어떤 메서드를 호출할 것인지는 컴파일 시점에서 결정이 됩니다. 위의 Collection을 보면 모두 Collection으로 동일합니다. 그렇기 때문에 컴파일 시점에 Collection 타입이였던 객체 모두 Collection을 파라미터로 가지는 메서드가 실행되게 됩니다.

반대로 오버라이딩은 동적으로 선택이 되기 때문에, 자식클래스에서 재정의한 메서드가 실행시간에도 호출이 되게 됩니다. 위의 메서드의 문제를 해결하기 위해서는 굳이 오버로딩을 사용하지말고 instanceof를 사용하여 해결할 수 있습니다.

public class Main {
    public static void main(String args []) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };
        for (Collection<?> c : collections) {
            System.out.println(classify(c));
        }
    }
 
    public static String classify(Collection<?> c) {
        return c instanceof Set ? "SET" : c instanceof List ? "List" : "Collection";
    }
}

위와 같은 문제가 발생될 수 있기때문에 같은 인자의 수를 갖는 메서드를 오버로딩을 하는 것을 피해야 합니다. 두번째로

	public static void main(String args []) throws IOException {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();
    
        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        
        System.out.println("set : "+set);
        System.out.println("list : "+list);
        
        for (int i = 0; i < 3; i++) {
            set.remove(i);
            System.out.println("set : "+set);
            list.remove(i);
            System.out.println("list : "+list);
        }
    }
    
결과

set : [-3, -2, -1, 0, 1, 2]
list : [-3, -2, -1, 0, 1, 2]
set : [-3, -2, -1, 1, 2]
list : [-2, -1, 0, 1, 2]
set : [-3, -2, -1, 2]
list : [-2, 0, 1, 2]
set : [-3, -2, -1]
list : [-2, 0, 2]

위의 코드를 살펴보면 set과 list에 -3, -2, -1, 0, 1, 2 가 들어가게 되고 0, 1, 2 세개의 데이터를 지우도록 코드가 작성되어 있습니다. 그런데 [-3, -2, -1][-2, 0, 2]의 결과가 출력됩니다. 둘의 출력 값이 다릅니다. 그 이유는 List 인터페이스에 정의된 remove는 remove(E)와 remove(int) 두 가지가 존재하기 때문입니다.

그래서 Integer로 들어간 데이터가 자동객체화(autoboxing)을 통해서 int로 변경되었고 첫 번째, 두 번째, 세 번재 위치의 요소가 지워지게 된 것 입니다. 이와 같이 오버로딩에는 다양한 오류를 생성할 수 있습니다. 그래서 이런 문제를 야기하지 않기 위해서 될 수 있으면 오버로딩을 피하고 메서드를 따로 만들거나 instanceof 를 통해 구분하여 사용하는 것이 안정적으로 코딩하는 방법이 될 수 있습니다.

4. reference

http://wiki.hash.kr/index.php?title=%EB%8B%A4%ED%98%95%EC%84%B1&mobileaction=toggle_view_mobile
https://www.grainpowder.net/posts/programming/java/polymorphism/

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글