개발자

JVM의 메소드 오버로딩 배워보기

Rafael del Nero | InfoWorld 2024.08.27
자바 개발자가 메소드 오버로딩을 사용하는 방법과 이유를 알아보고, 배운 내용을 자바 가상 머신 자체를 대상으로 테스트해보자.
 
메소드 오버로딩은 개발자가 같은 클래스에서 동일한 메소드 이름을 다양한 매개변수와 함께 여러 번 사용할 수 있게 해주는 프로그래밍 기법이다. 오버로딩이라는 단어로 인해 개발자들은 이 기법이 시스템에 과부하를 준다고 생각하는 경우가 종종 있지만 사실 그렇지 않다. 프로그래밍에서 메소드 오버로딩은 같은 메소드 이름을 다양한 매개변수와 함께 사용하는 것을 의미한다.
 
ⓒ Getty Images Bank

예시 1은 매개변수의 수와 유형, 순서가 다른 하나의 메소드를 보여준다.
 

예시 1. 메소드 오버로딩의 세 가지 유형


Number of parameters:
public class Calculator {
      void calculate(int number1, int number2) { }
      void calculate(int number1, int number2, int number3) { }
}

Type of parameters:
public class Calculator {
      void calculate(int number1, int number2) { }
      void calculate(double number1, double number2) { }
}

Order of parameters:
public class Calculator {
      void calculate(double number1, int number2) { }
      void calculate(int number1, double number2) { }
}
 

메소드 오버로딩과 프리미티브 유형

예시 1에서 프리미티브 유형 int와 double을 볼 수 있다. 이 유형과 다른 몇 가지 유형으로 더 많은 작업을 할 예정이므로 잠시 시간을 내서 자바의 프리미티브 유형을 살펴보자.

표 1. 자바의 프리미티브 유형
 
ⓒ Infoworld
 

메소드 오버로딩을 왜 사용하는가?

오버로딩은 코드를 더 깔끔하고 읽기 쉽게 만들어주며 프로그램의 버그를 피하는 데도 도움이 된다.
 
예시 1과 달리 이름이 calculate1, calculate2, calculate3 등인 여러 calculate() 메소드가 있다고 상상해 보자. 좋지 않다. calculate() 메소드를 오버로딩하면 같은 메소드 이름을 사용하면서 변경이 필요한 부분, 즉 매개변수만 변경할 수 있다. 또한 오버로딩된 메소드는 코드에서 그룹으로 묶이므로 찾기도 매우 쉽다.

오버로딩이 아닌 것
변수 이름을 바꾸는 것은 오버로딩이 아니다. 다음 코드는 컴파일되지 않는다.
 
public class Calculator {
    
    void calculate(int firstNumber, int secondNumber){}

void calculate(int secondNumber, int thirdNumber){}

}

또한 메소드 서명의 반환 유형을 바꾸는 것으로 메소드를 오버로딩할 수 없다. 다음 코드 역시 컴파일되지 않는다.
 
public class Calculator {
    double calculate(int number1, int number2){return 0.0;}
    long calculate(int number1, int number2){return 0;}
}

생성자 오버로딩
생성자도 메소드와 같은 방식으로 오버로딩할 수 있다.
 
public class Calculator {
    private int number1;
    private int number2;

    public Calculator(int number1) {this.number1 = number1;}
    
public Calculator(int number1, int number2) {
        this.number1 = number1;
        this.number2 = number2;
}

}
 

메소드 오버로딩 과제 풀어보기

다음 코드를 잘 살펴보자.

예시 2. 고급 메소드 오버로딩 과제
 
public class AdvancedOverloadingChallenge3 {
  static String x = "";
  public static void main(String... doYourBest) {
     executeAction(1);
     executeAction(1.0);
     executeAction(Double.valueOf("5"));
     executeAction(1L);
    
     System.out.println(x);
  }
  static void executeAction(int ... var) {x += "a"; }
  static void executeAction(Integer var) {x += "b"; }
  static void executeAction(Object var)  {x += "c"; }
  static void executeAction(short var)   {x += "d"; }
  static void executeAction(float var)   {x += "e"; }
  static void executeAction(double var)  {x += "f"; }

이 코드의 출력은 무엇이라고 생각하는가?
befe bfce efce aecf

예시 2 과제의 정답은 3번, efce다.
 
무슨 일이 일어났는지 이해하기 위해서는 메소드 오버로딩에 대해 조금 더 깊게 들어가볼 필요가 있다.
 

JVM은 오버로딩된 메소드를 어떻게 컴파일하는가

예시 2를 이해하기 위해서는 JVM이 오버로딩된 메소드를 컴파일하는 방법에 대해 몇 가지를 알아야 한다.
 
우선 JVM은 지능적으로 게으르다. 즉, 메소드를 실행하기 위해 항상 가능한 최소한의 노력만 기울이다. 따라서 JVM이 오버로딩을 처리하는 방법에 대해 생각할 때는 다음의 세 가지 중요한 컴파일러 기법을 염두에 두어야 한다.
  1. 확대
  2. 박싱(오토박싱과 언박싱)
  3. 가변 인수
 
이 세 가지 기법을 접한 적이 없다면 예제를 통해 알아볼 수 있다. JVM은 주어진 순서대로 이를 처리한다.
 
확대의 예는 다음과 같다.
 
 
int primitiveIntNumber = 5;
 double primitiveDoubleNumber = primitiveIntNumber ;

확대된 프리미티브 유형의 순서는 다음과 같다.
 
그림 1. 확대 시 프리미티브 유형의 순서 ⓒ Rafael del Nero

다음은 오토박싱의 예다.
 
int primitiveIntNumber = 7;
 Integer wrapperIntegerNumber = primitiveIntNumber;

이 코드가 컴파일될 때 내부적으로 무슨 일이 일어나는지에 주목하라.
 
Integer wrapperIntegerNumber = Integer.valueOf(primitiveIntNumber);
 
다음은 언박싱이다.
 
Integer wrapperIntegerNumber = 7;
 int primitiveIntNumber= wrapperIntegerNumber;

이 코드가 컴파일될 때 내부적으로 무슨 일이 일어나는지 생각해 보라.
 
int primitiveIntNumber = wrapperIntegerNumber.intValue();

마지막으로, 가변 인수의 예다. varargs는 항상 마지막에 실행된다.
 
execute(int… numbers){}

이 구문의 용도가 궁금할 수 있다. 기본적으로 varargs는 가변 인수에 사용된다. 3점(…)에 의해 지정되는 값의 배열이다. 이 메소드에 원하는 만큼 많은 int 수를 전달할 수 있다.

다음은 그 예시다.
 

execute(1,3,4,6,7,8,8,6,4,6,88...); // We could continue…
 
 
가변 인수는 값을 메소드로 직접 전달할 수 있으므로 매우 유용하다. 배열을 사용했다면 값으로 배열을 인스턴스화해야 했을 것이다.
 

확대 : 실무 예제

executeAction 메소드에 숫자 1을 직접 전달하면 JVM은 자동으로 이를 int로 취급한다. 이 숫자가 executeAction(short var) 메소드로 가지 않는 이유가 이것이다.
 
마찬가지로, 숫자 1.0을 전달하면 JVM은 자동으로 이 숫자를 double로 인식한다.
 
물론 숫자 1.0은 float가 될 수도 있지만 유형은 사전에 정의되며, 그래서 예시 2에서는 executeAction(double var) 메소드가 호출된다.
 
Double 래퍼 유형을 사용하는 경우 두 가지 가능성이 있다. 래퍼 숫자는 프리미티브 유형으로 언박싱되거나 Object로 확대될 수 있다(자바의 모든 클래스는 Object 클래스임을 상기). 이 경우 JVM은 Double 유형을 Object로 확대하는 쪽을 선택한다. 앞서 설명한 바와 같이 언박싱보다 노력이 덜 들기 때문이다.
 
마지막으로 전달하는 숫자는 1L이다. 이번에는 변수 수형을 지정했으므로 long이 된다.
 

오버로딩에서 흔히 저지르는 실수

이제 메소드 오버로딩이 다소 헷갈릴 수 있다는 점을 알았으니, 실제로 직면할 수 있는 몇 가지 까다로운 시나리오를 생각해 보자.

래퍼를 사용한 오토박싱
자바는 강한 유형의 프로그래밍 언어이며, 래퍼에서 오토박싱을 사용할 경우 몇 가지 유의해야 할 점이 있다. 우선 다음 코드는 컴파일되지 않는다.
 

int primitiveIntNumber = 7;
Double wrapperNumber = primitiveIntNumber;

오토박싱은 double 유형에서만 작동한다. 이 코드를 컴파일하면 다음과 같은 일이 일어나기 때문이다.
 
 
Double number = Double.valueOf(primitiveIntNumber);
 
위 코드는 컴파일된다. 첫 int 유형은 double로 확대된 다음 Double로 박싱된다. 그러나 오토박싱을 할 때는 유형 확대가 없고 Double.valueOf의 생성자는 int가 아닌 double을 받는다. 이 경우 오토박싱은 다음과 같이 캐스트를 적용하는 경우에만 동작할 것이다.
 
 
Double wrapperNumber = (double) primitiveIntNumber;

Integer는 Long이 될 수 없고 Float는 Double이 될 수 없음을 기억하라. 상속은 없다. 이러한 각 유형(Integer, Long, Float, Double)은 Number와 Object다. 
 
확실치 않을 때는 래퍼 숫자는 Number 또는 Object로 확대가 가능하다는 점만 기억하면 된다. (래퍼에 대해서는 그 외에도 알아볼 내용이 많지만 다음에 다루도록 하겠다.)

JVM의 하드 코딩된 숫자 유형
숫자에 유형을 지정하지 않는 경우 JVM이 대신 해준다. 코드에서 숫자 1을 직접 사용하면 JVM은 이 숫자를 int로 생성한다. short를 받는 메소드에 1을 직접 전달하려고 하면 컴파일이 되지 않는다.

다음은 그 예시다.
 
class Calculator {
        public static void main(String… args) {
              // This method invocation will not compile
              // Yes, 1 could be char, short, byte but the JVM creates it as an int
              calculate(1);
        } 

        void calculate(short number) {}
  
  } 

숫자 1.0을 사용할 때도 같은 규칙이 적용된다. float일 수 있지만 JVM은 이 숫자를 double로 취급한다.
  
  class Calculator {
        public static void main(String… args) {
               // This method invocation will not compile
               // Yes, 1 could be float but the JVM creates it as double
               calculate(1.0);
        } 

        void calculate(float number) {}  
  }

또 다른 흔한 실수는 Double 또는 다른 래퍼 유형이 double을 받는 메소드에 더 적합하다는 생각이다. 사실 JVM 관점에서는 Double 래퍼를 double 프리미티브 유형으로 언박싱하는 것보다 Object로 확대하는 편이 노력이 덜 든다.
 
정리하면, 자바 코드에서 직접 사용될 때 1은 int가 되고 1.0은 double이 된다. 확대는 실행에 이르는 가장 게으른 경로이며 그 다음이 박싱 또는 언박싱이고 마지막은 항상 varargs다.

char 유형

흥미로운 사실 하나를 살펴보자. char 유형이 숫자를 받는다는 사실을 알고 있었는가? 예를 들어 char anyChar = 127;이라고 하면 이상해 보이지만 컴파일이 된다.
 

오버로딩에 대해 기억해야 할 점

오버로딩은 동일한 메소드 이름을 서로 다른 매개변수와 함께 사용해야 하는 시나리오에서 매우 강력한 기법이다. 코드에 사용된 적절한 이름은 가독성 측면에서 큰 차이로 이어지므로 이는 유용한 기능이다. 메소드를 중복하고 코드를 어지럽히는 대신 간단히 오버로딩하면 된다. 코드가 깔끔하게 유지되고 읽기도 더 쉽고 중복된 메소드로 인해 시스템에 문제가 발생할 위험도 줄어든다. 

유의할 점: 메소드를 오버로딩할 때 JVM은 가장 노력이 덜 드는 길을 택한다. 실행에 이르는 가장 게으른 경로의 순서는 다음과 같다.
  • 첫 번째는 확대
  • 두 번째는 박싱
  • 세 번째는 가변 인수
 
주의할 점 : 숫자를 직접 선언할 때 까다로운 상황이 발생한다. 1은 int가 되고 1.0은 double이 된다.
 
또한 float의 경우 1F 또는 1f, double의 경우 1D 또는 1d 구문을 사용해서 명시적으로 이러한 유형을 선언할 수 있다는 점도 기억해 두자.
 
지금까지 메소드 오버로딩에서 JVM의 역할에 대해 알아봤다. JVM은 태생적으로 게으르며 항상 실행에 이르는 가장 게으른 경로를 택한다는 것을 알아둬야 한다.
 

동영상 과제! 메소드 오버로딩 디버깅


디버깅은 코드를 개선하면서 프로그래밍 개념을 완전히 이해하는 가장 쉬운 방법이다. 이 영상에서 필자가 메소드 오버로딩 과제를 디버깅하고 설명하는 과정을 보며 따라해볼 수 있다.
editor@itworld.co.kr 

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.