꾸준한 스터디
article thumbnail

컬렉션을 집합, 리스트, 그 외로 구분하고자 Overroading을 적용한 프로그램

 

"집합", "리스트", "그 외"를 차례로 출력하는 프로그램을 예상했지만 실제 동작은 "그 외"만 연달아 출력한다.

오버로딩은 classify 메서드 중 어느 메서드를 호출할지 컴파일타임에 정해지기 때문이다.

컴파일타임에서 for 문안의 collection은 항상 Collection<?> 타입이다.

런타임에는 타입이 매번 달라지지만, 호출할 메서드를 선택하는데는 영향을 주지 못한다. 따라서 컴파일타임의 매개변수 타입을 기준으로 항상 세번째 메서드인 classify(Collection<?>)만 호출하는 것이다.

이처럼 직관과 어긋나는 이유는 재정의한 메서드는 동적으로 선택되고, 다중정의한 메서드는 정적으로 선택되기 때문이다.

 

메서드를 재정의 했다면 해당 객체의 런타임 타입이 어떤 메서드를 호출할지의 기준이 된다.

컴파일 타입에 그 인스턴스의 타입이 무엇이었냐는 상관없다.

오버로딩과 오버라이딩이 정적 바인딩(Static Binding)과 동적 바인딩(Dynamic Binding)을 사용하는 이유는 자바의 다형성(polymorphism) 개념을 구현하기 위해서입니다. 다형성은 객체 지향 프로그래밍의 중요한 특징으로, 동일한 이름의 메서드가 다양한 상황에서 다른 동작을 수행할 수 있도록 해줍니다.

정적 바인딩 (Static Binding) - 오버로딩: 정적 바인딩은 컴파일타임에 메서드 호출이 결정되는 것을 의미합니다. 오버로딩된 메서드는 컴파일 시점에서 어떤 메서드가 호출될지 컴파일러가 결정하게 됩니다. 컴파일러는 메서드 호출 시 전달된 인자의 개수와 타입에 따라 적절한 메서드를 선택합니다.
오버로딩은 메서드 이름이 동일하지만 매개변수의 개수와 타입이 다른 경우에 사용됩니다. 컴파일러는 정적으로 어떤 오버로딩된 메서드가 호출될지를 결정하므로 런타임에는 메서드 호출의 타겟이 변경되지 않습니다.

동적 바인딩 (Dynamic Binding) - 오버라이딩: 동적 바인딩은 런타임에 메서드 호출이 결정되는 것을 의미합니다. 오버라이딩된 메서드는 런타임 시점에 메서드 호출의 타겟이 됩니다. 이는 상속과 관련되어 있습니다. 하위 클래스가 상위 클래스의 메서드를 동일한 시그니처로 재정의(오버라이딩)하면, 런타임에는 객체의 실제 타입에 따라 오버라이딩된 메서드가 호출됩니다.
동적 바인딩은 상속 관계에서 객체의 실제 타입에 따라 메서드 호출을 결정하므로 실행 중에 메서드 호출이 변경될 수 있습니다.
다형성을 실현하기 위해서는 정적 바인딩과 동적 바인딩이 함께 사용됩니다. 오버로딩은 정적 바인딩을 사용하여 컴파일타임에 적절한 메서드를 선택하고, 오버라이딩은 동적 바인딩을 사용하여 런타임에 객체의 실제 타입에 따라 오버라이딩된 메서드를 호출합니다. 이를 통해 다양한 타입의 객체에 대해 일관된 방식으로 메서드를 호출할 수 있고, 유연하고 확장 가능한 코드를 작성할 수 있습니다.

 

재정의한 메서드는 for 문의 컴파일 타입이 모두 Wine인 것에 무관하게 항상 '가장 하위에서 정의한' 재정의 메서드가 실행된다.

 

오버로딩된 메서드 사이에서는 객체의 런타임 타입은 전혀 중요치 않다. 선택은 컴파일 타임에 오직 매개변수의 컴파일타임 타입에 의해 이뤄진다.

 

원래 의도한 대로 오버로딩하여 각 객체에 맞게 출력하려면 정적 메서드를 사용해도 좋다면 CollectionClassfier의 모든 classify 메서드를 하나로 합친 후 instanceof로 명시적으로 검사하면 된다.

 

 

다중정의한 메서드는 기대한 대로 동작하지 않을 수 있으니 헷갈릴 수 있는 코드는 작성하지 않는게 좋다.

특히나 공개 API라면 사용자가 어떤 다중정의 메서드가 호출될지 몰라 런타임에 이상하게 행동할 것이며 프로그램이 오동작하기 쉽다.

API 사용자들은 문제를 진단하느라 긴 시간을 허비할 것이니 다중정의가 혼동을 일으키는 상황을 피해야 한다.

안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자.

가변인수를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.

 

다중정의 하는 대신 메서드 이름을 다르게 지어주는 방법도 있다.

ObjectOutputStream 클래스이다. 이 방식이 다중정의보다 나은 점은 read 메서드의 이름과 짝을 맞추기 좋다는 것이다.

 

 

생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 된다.

하지만 정적 팩터리라는 대안을 활용할 수 있는 경우가 많다.

또한 생성자는 재정의할 수 없으니 다중정의와 재정의가 혼용될 걱정은 없다.

그래도 여러 생성자가 같은 수의 매개변수를 받아야 하는 경우를 완전히 피해갈 수 없으니 대비하여 안전 대책을 배워두면 도움이 된다.

 

매개변수 수가 같은 다중정의 메서드가 많아도 매개변수 중 하나 이상이 근본적으로 다르다면 헷갈릴 일이 없다.

근본적으로 다르다는 건 두 타입의 값을 서로 어느쪽으로든 형변환할 수 없다는 뜻이다.

 

자바4까지는 모든 기본 타입이 모든 참조 타입과 근본적으로 달랐지만 자바 5에서 오토박싱이 도입되면서 주의를 기울여야 하는 경우가 생겼다.

"[-3, -2, -1] [-3, -2, -1]" 을 출력하리라 예상한 프로그램이 실제로는 "[-3, -2, -1] [-2, 0, 2]"을 출력한다.

set.remove(i)의 시그니쳐는 remove(Object)이다.

다중정의된 다른 메서드가 없으니 기대한 대로 동작하여 집합에서 0 이상의 수를 제거한다.

 

list.remove(i)는 동작해야했던 remove(Object)가 아닌 다중정의된 remove(int index)를 선택한다. list의 인덱스 위치를 제거하는 기능이어서 "[-3, -2, -1, 0, 1, 2]" 리스트의 0번째 요소 -3을 삭제하고 "[-2, -1, 0, 1, 2]" 리스트에서 1번째 요소인 -1을 삭제하고.. 그 다음 2번째 요소인 1을 삭제하여 "[-2, 0, 2]"가 남는다.

이 문제는 list.remove의 인수를 Integer로 형변환하여 올바른 다중정의 메서드를 선택하게 하면 해결된다.

 

자바 8에서 도입한 람다와 메서드 참조 역시 다중정의시의 혼란을 키웠다.

해당 코드에서 발생하는 오류는 자바 컴파일러가 submit 메서드를 호출할 때, 두 개의 메서드 중 어떤 것을 사용해야 하는지 모호하다는 것을 의미한다. 두 메서드는 각각 다른 매개변수 타입을 가지고 있다: <T> submit(java.util.concurrent.Callable<T> task): Callable을 인자로 받는 메서드입니다.submit(java.lang.Runnable task): Runnable을 인자로 받는 메서드입니다. 컴파일러는 람다식 System.out::println을 보고 어떤 메서드를 호출해야 할지 판단하지 못하여 모호한 참조 오류가 발생합니다. 이 두 메서드의 시그니처가 람다식과 일치하기 때문입니다.

1번과 2번이 모습은 비슷하지만 2번만 컴파일 오류가 난다.

넘겨진 인수는 모두 System.out::println으로 똑같고, 양쪽 모두 Runnable을 받는 형제 메서드를 다중정의하고 있다.

2번만 실패하는 원인은 바로 submit 다중정의 메서드 중에는 Callable<T>를 받는 메서드도 있다는데 있다.

하지만 모든 println이 void를 반환하니 반환값이 있는 Callable과 헷갈릴리 없을거라 생각하지만 다중정의 해소(적절한 다중정의 메서드를 찾는 알고리즘)는 이렇게 동작하지 않는다.

println 메서드도 다중정의 된 메서드인데 단 하나만 존재했다면 submit메서드 호출이 제대로 컴파일 됐을 것이다.

지금은 참조된 메서드(println)와 호출한 메서드(submit) 양족 다 다중정의되어 다중정의 해소 알고리즘이 우리의 기대처럼 동작하지 않는 상황이다.

 

핵심은 다중정의된 메서드 혹은 생성자들이 함수형 인터페이스를 인수로 받을 때, 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생긴다는 것이다. 따라서 메서드를 다중정의할 때, 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안 된다.

 

다중정의된 메서드 중 하나를 선택하는 규칙은 매우 복잡하며, 자바가 버전업될수록 더 복잡해지고 있어, 이 모두를 이해하고 사용하는 프로그래머는 극히 드물다.

 

이미 만들어진 클래스가 이번 챕터의 지침을 어기는 경우도 있다.

String은 자바4 버전에서 contentEquals(StringBuffer) 메서드를 가지고 있었다.

자바 5에서 StringBuffer, StringBuilder, String, CharBuffer등 비슷한 부류 타입을 위한 공통 인터페이스로 CharSequence가 등장하였고, 자연스럽게 String에도 CharSequence를 받은 contentEquals가 다중정의 되었다.

 

다행히 이 두 메서드는 같은 객체를 입력하면 완전히 같은 작업을 수행해주니 해로울건 전혀 없다. 이처럼 어떤 다중정의 메서드가 불리는지 몰라도 기능이 똑같다면 신경쓸게 없다.

 

실패한 사례로 String 클래스의 valueOf(char[]) 와 valueOf(Object)는 같은 객체를 건네더라도 전혀 다른 일을 수행한다. 이렇게 해야 할 이유가 없었음에도, 혼란을 불러올 수 있는 잘못된 사례로 남게되었다.

 

핵심정리

프로그래밍 언어가 다중정의를 허용한다고 해서 다중정의를 꼭 활용하란 뜻은 아니다.

일반적으로 매개변수 수가 같을 때는 다중정의를 피하는게 좋다. 상황에 따라 특히 생성자라면 이 조언을 따르기가 불가능할 수 있다. 그럴 때는 헷갈릴 만한 매개변수는 형변환하여 정확한 다중정의 메서드가 선택되도록 해야 한다.

이것이 불가능하면, 예컨대 기존 클래스를 수정해 새로운 인터페이스를 구현해야할 때는 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하도록 만들어야 한다.

그렇지 못하면 프로그래머들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작하지 않는 이유를 이해하지도 못할 것이다.

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!