JAVA/Effective Java

[아이템 52] 다중정의는 신중히 사용하라

꾸준함. 2024. 3. 10. 10:55

다중정의(Overloading)

다중정의란 오버로딩을 의미하며 리턴 타입과 메서드명은 같으나 파라미터의 개수나 타입이 다른 메서드를 작성하는 것을 의미합니다.

다중정의로 인해 예상과 달리 오동작하는 코드들이 있는데 원인은 다음과 같습니다.

  • 오버로딩한 메서드는 정적으로 선택
  • 오버라이딩한 메서드는 동적으로 선택

 

위 내용을 완벽하게 보여주는 예제 코드는 다음과 같습니다.

 

 

코드 부연 설명

  • 다중정의된 세 classify 메서드 중 어느 메서드를 호출할지가 컴파일 타임에 정해짐
    • 컴파일 시점에는 for 문 안의 c는 항상 Collection<?> 타입

 

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

 

의도한 대로 동작하기 위해서는 모든 classify 메서드를 하나로 합친 후 instanceof로 명시적으로 검사하면 말끔히 해결됩니다.

 

 

재정의(Overriding)

 

모두 알다시피, 메서드 재정의란 상위 클래스가 정의한 것과 똑같은 시그니처의 메서드를 하위 클래스에서 다시 정의하는 것을 말합니다.

메서드를 재정의한 다음 하위 클래스의 인스턴스에서 해당 메서드를 호출하면 재정의한 메서드가 실행됩니다.

앞서 언급했드시 오버라이딩한 메서드는 동적으로 선택되기 때문에 컴파일 타임에 그 인스턴스의 타입이 무엇이었냐는 상관없습니다.

 

 

코드 부연 설명

  • 오버라이딩한 메서드는 동적으로 실행되기 때문에 for문에서의 컴파일 시점 타입이 모두 Wine인 것에 무관하게 항상 `가장 하위에서 정의한` 재정의 메서드가 실행

 

매개변수 수가 같은 오버로딩을 하지 않는 것을 권장

 

CollectionClassifier 코드에서 확인한 것처럼, 매개변수 수가 같은 오버로딩을 적용할 경우 개발자의 의도와는 다르게 동작할 가능성이 높습니다.

특히, 가변인수를 사용하는 메서드라면 오버로딩을 아예 하지 말아야 합니다.

따라서 웬만하면 오버로딩을 하지 않는 것을 권장합니다.

매개변수 수가 동일한 메서드를 생성하려면 메서드명을 구분하기 위해 다른 이름을 사용하는 것이 좋습니다.

이 원칙은 java.io 패키지의 ObjectOutputStream 클래스에서 잘 적용되었습니다.

 

ObjectOutputStream

 

java.io 패키지의 ObjectInputStream 클래스 또한 위 원칙을 잘 적용했습니다.

 

ObjectInputStream

 

파라미터 개수만 같은 생성자들끼리는 어떻게 처리할까?

 

생성자는 이름을 다르게 지을 수 없으니 두 번째 생성자부터는 무조건 다중정의가 되지만 다음과 같은 이유로 크게 걱정하지 않아도 됩니다.

  • 하지만 아이템 1에서 다룬 정적 팩토리라는 대안을 활용할 수 있는 경우가 많고
  • 생성자는 재정의할 수 없으므로 다중정의와 재정의가 혼용될 걱정은 안 해도 됨

 

그럼에도 불구하고 여러 생성자가 같은 수의 파라미터를 받아야 하는 경우를 완전히 피해 갈 수 없으므로 그럴 때를 대비해 안전대책을 둬야 합니다.

  • 매개변수 중 하나 이상이 `근본적으로 다르다`면 헷갈릴 일이 없음
    • 근본적으로 다르다는 것은 null이 아닌 두 타입의 값을 서로 어느 쪽으로든 형변환할 수 없다는 뜻
    • 위 조건만 충족하면 컴파일 시점 타입에는 영향을 받지 않게 되고 혼란을 주는 주된 원인이 사리지며  어느 다중정의 메서드를 호출할지가 매개변수들의 런타임 타입만으로 결정
    • ex) ArrayList에는 int를 받는 생성자와 Collection을 받는 생성자

 

 

 

다중정의와 오토박싱으로 인해 의도와 달리 오동작하는 예

 

자바 5+ 버전부터 오토박싱이라는 개념이 도입되었습니다.

  • Wrapper 타입을 전달해도 기본 타입으로 자동 형변환
  • 기본 타입을 전달해도 Wrapper 타입으로 자동 형변환

 

설명에 앞서 예시 코드부터 소개하겠습니다.


 

위 코드를 얼핏 보면 -3 ~ 3까지 Set, List에 넣어준 뒤 각각 0 ~ 2를 삭제했으므로 결과를 다음과 같이 예상을 했을 것입니다.

  • [-3, -2, -1], [-3, -2, -1]

 

하지만 Set은 예상한 대로 출력한 반면, List는 예상과 달리 [-2, 0, 2]를 출력합니다.

 

 

[-2, 0, 2]를 출력한 이유

  • 원래 바랬던 것은 원소 내용을 기준으로 없애는 remove(Object) 메서드를 호출하는 것이었으나
  • for문 안의 타입이 int이기 때문에 인덱스를 기준으로 없애는 remove(int)가 컴파일 시점에 정해졌음
  • 자바 4 버전까지는 제네릭도 없었고 오토박싱도 없었으나, 자바 5 이후로는 제네릭과 오토박싱이 도입되어 Object와 int가 `근본적으로 다른 타입`이 아니기 때문 (서로 형변환이 가능)
  • 예상한 바를 이루기 위해서는 remove(Integer(i))와 같이 형변환이 필요

 

 

 

다중정의, 람다, 그리고 메서드 참조로 인해 의도와 달리 오동작하는 예

 

다음은 자바 8 이상 버전에서 도입된 람다와 메서드 참조로 인해 발생할 수 있는 다중 정의 시 혼란을 초래하는 코드 예시입니다.


 

코드 부연 설명

  • 1번과 2번이 모습은 비슷하지만 2번은 컴파일 오류 발생
    • 넘겨진 인수는 모두 System.out::pringln으로 똑같고, 양쪽 모두 Runnable을 받는 형제 메서드를 다중정의하고 있음
    • 다만 ExecutorService의 submit 메서드의 경우 오버로딩이 적용되어 파라미터로 Callable을 받는 메서드와 Runnable을 받는 메서드가 존재
    • 모든 println이 void를 반환하니 반환값이 있는 Callable과 헷갈릴 리는 없다고 추론할 수 있겠지만 다중정의 메서드를 찾는 알고리즘인 resolution은 추론한 대로 동작하지 않음
      • 기술적으로 말하자면 System.out::println은 부정확한 메서드 참조 (inexact method reference)
      • 임시적 타입 람다식이나 부정확한 메서드 참조 같은 이수 표현식은 목표 타입이 선택되기 전에는 그 의미가 정해지지 않기 때문에 적용성 테스트 때 무시되고 이 것이 resolution이 추론한대로 동작하지 않은 이유

 

 

핵심을 짚자면 다중정의된 메서드 혹은 생성자들이 함수형 인터페이스를 인수로 받을 때 비록 서로 다른 함수형 인터페이스라도 인수 위치가 같으면 혼란이 생깁니다.

따라서 메서드를 다중정의할 때 서로 다른 함수형 인터페이스라도 같은 위치의 인수로 받아서는 안되며 이유는 다음과 같습니다.

  • 서로 다른 함수형 인터페이스라도 서로 `근본적으로 다르지 않음`
  • 즉, 함수형 인터페이스끼리는 서로 캐스팅 가능

 

자바에서 제공하는 클래스 중 이번 아이템을 잘 따르지 않는 예

 

1. 아이템을 따르지 않지만 문제없는 예

 

  • String은 자바 4 버전부터 contentEquals(StringBuffer) 메서드를 정의
  • 자바 5+ 버전부터 StringBuffer, StringBuilder, String, CharBuffer 등의 비슷한 부류의 타입을 위한 공통 인터페이스인 CharSequence가 등장했고 이에 따라 String 또한 contentEquals(CharSequence) 메서드를 정의하며 오버로딩 적용
  • 그럼에도 불구하고 두 메서드는 완전히 같은 작업을 수행하기 때문에 오동작할 일은 없음
  • 결론은 어떤 다중정의 메서드가 호출되는지 몰라도 기능이 똑같다면 신경 쓸 필요가 없음

 

2. 아이템을 따르지 않음에 따라 문제가 발생하는 예

 

  • 앞서 다루었던 List의 remove 메서드
  • String 클래스의 valueOf(char[])과 valueOf(Object)는 같은 객체를 건네더라도 전혀 다른 일을 수행

 

 

정리

  • 자바가 오버로딩을 허용한다고 해서 오버로딩을 반드시 활용하란 뜻은 아님
  • 일반적으로 매개변수 수가 같을 때는 다중정의를 피하는 것을 권장
  • 매개변수 수가 같은데도 불구하고 다중정의를 꼭 사용해야 한다면 오동작을 방지하기 위해 다중정의한 메서드들이 모두 동일하게 동작하도록 처리해야 함
  • 위 원칙을 지키지 못할 경우 개발자들은 다중정의된 메서드나 생성자를 효과적으로 사용하지 못할 것이고, 의도대로 동작하지 않은 이유를 디버깅하는데 굉장히 오랜 시간이 걸릴 것

 

참고

이펙티브 자바

반응형