메서드 핸들 소개

메서드 핸들 소개

메서드 핸들(Method Handle)은 메서드를 찾고 호출하기 위한 저수준 메커니즘이에요. 흔히 리플렉션과 비교되곤 하는데, 둘 다 메서드·생성자 호출과 필드 접근 수단을 제공하기 때문이에요. 이 장에서는 메서드 핸들이 무엇이고, 어떻게 조회하고, 어떻게 변환해서 쓸 수 있는지 차근차근 살펴봐요.

출처: dev.java

본문

메서드 핸들이란

메서드 핸들은 메서드 조회와 호출에 쓰이는 저수준 메커니즘이에요. Reflection API와 메서드 핸들 모두 메서드, 생성자, 필드에 접근하는 수단을 제공하기 때문에 흔히 리플렉션과 비교되곤 해요.

정확히 메서드 핸들이 뭘까요? 그것은 밑에 있는 메서드, 생성자, 필드를 가리키는 **직접 호출 가능한 참조(directly invocable reference)**예요. Method Handle API는 메서드를 가리키는 단순한 포인터 위에 조작을 얹어서, 인자를 삽입하거나 재배열하고, 반환 값을 변환하는 등의 작업을 가능하게 해 줘요.

메서드 핸들이 무엇을 제공하는지, 어떻게 효과적으로 쓸 수 있는지 자세히 살펴볼게요.

접근 검사

메서드 핸들 호출의 접근 검사는 Reflection API와 다르게 수행돼요. 리플렉션에서는 매 호출마다 호출자에 대한 접근 검사가 이뤄져요. 하지만 메서드 핸들에서는 접근이 메서드 핸들이 생성될 때만 검사돼요.

메서드 핸들이 비공개(non-public) 멤버에 접근할 수 있는 문맥에서 생성되었는데, 그 핸들이 바깥으로 전달되면 여전히 그 비공개 멤버에 접근할 수 있다는 점을 꼭 기억해야 해요. 그 결과 비공개 멤버가 접근해서는 안 되는 코드에서 접근될 가능성이 생겨요. 그런 메서드 핸들을 자신의 문맥 안에 비공개로 유지하는 것은 개발자의 책임이에요. 또는 적절한 lookup 객체를 사용해 처음부터 접근 제한이 있는 채로 메서드 핸들을 만들 수도 있어요.

메서드 핸들 조회

메서드 핸들을 만들려면 먼저 Lookup 객체를 만들어야 해요. Lookup은 메서드 핸들을 만드는 팩토리 역할을 해요. lookup 객체 자체나 메서드 핸들을 어떻게 사용할지에 따라 접근 수준을 제한할지 결정할 수 있어요.

예를 들어 private 메서드를 가리키는 메서드 핸들을 만들었는데 그 핸들이 바깥에서 접근 가능하다면, private 메서드도 마찬가지로 접근 가능해져요. 보통은 그런 상황을 피하고 싶죠. 한 가지 방법은 lookup 객체와 메서드 핸들도 private으로 만드는 거예요. 또 다른 방법은 MethodHandles.publicLookup 메서드로 lookup 객체를 만들어, 무조건적으로 export된 패키지 안의 public 클래스에 있는 public 멤버만 찾게 하는 거예요.


MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

lookup 객체와 메서드 핸들을 private으로 유지할 거라면, private과 protected를 포함한 어떤 멤버에든 접근 권한을 주는 것이 안전해요.


MethodHandles.Lookup lookup = MethodHandles.lookup();

메서드 타입

메서드 핸들을 조회하려면 메서드나 필드의 타입 정보도 제공해야 해요. 메서드 타입 정보는 MethodType 객체로 표현돼요. MethodType을 만들려면 첫 번째 파라미터로 반환 타입을, 그다음에 모든 인자 타입을 제공하면 돼요.


MethodType methodType = MethodType.methodType(int.class /* the method returns integer */,
String.class /* and accepts a single String argument*/);

LookupMethodType 인스턴스를 갖추면 메서드 핸들을 조회할 수 있어요. 인스턴스 메서드는 Lookup.findVirtual을, static 메서드는 Lookup.findStatic을 쓰면 돼요. 두 메서드 모두 같은 인자를 받아요. 메서드가 있는 클래스(Class), 메서드 이름(String), 그리고 MethodType 인스턴스예요.

아래 예시에서는 Lookup.findVirtualString.replace 인스턴스 메서드를 조회해요. 이 메서드는 char 인자 두 개를 받아 String을 반환해요.


MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);

다음 예시에서는 Lookup.findStatic으로 static 메서드 String.valueOf를 조회해요. 이 메서드는 Object를 받아 String을 반환해요.


MethodType valueOfMethodType = MethodType.methodType(String.class, Object.class);
MethodHandle valueOfMethodHandle = lookup.findStatic(String.class, "valueOf", valueOfMethodType);

비슷하게 Lookup.findConstructor 메서드로 어떤 생성자를 가리키는 메서드 핸들도 조회할 수 있어요.

마지막으로 메서드 핸들을 얻었다면 그 밑에 있는 메서드를 호출할 수 있어요.

메서드 핸들 호출

호출도 여러 방식으로 할 수 있어요.

호출을 도와주는 모든 메서드는 결국 끝에서 호출되는 단일 메서드 하나로 모여요. 바로 **MethodHandle.invokeExact**예요. 이름이 시사하듯이 invokeExact에 제공되는 인자는 메서드 핸들의 타입과 **엄격하게 일치(strictly match)**해야 해요.

예를 들어 String.replace 메서드를 호출한다면, 인자는 엄격하게 String 반환 타입과 char 인자 두 개에 대응해야 해요.


MethodType replaceMethodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMethodHandle = lookup.findVirtual(String.class, "replace", replaceMethodType);
String result = (String) replaceMethodHandle.invokeExact("dummy", 'd', 'm');

MethodHandle.invoke는 더 관대해요. 제공된 인자의 타입과 엄격하게 일치하도록 조정된 타입의 새 메서드 핸들을 얻으려 시도해요. 그다음 조정된 메서드 핸들을 invokeExact로 호출할 수 있게 돼요.


String result = (String) replaceMethodHandle.invoke((Object)"dummy", (Object)'d', (Object)'m'); // would fail with `invokeExact`

메서드 핸들을 호출하는 또 다른 대안은 MethodHandle.invokeWithArguments예요. 이 메서드 호출의 결과는 invoke와 동일한데, 유일한 차이는 모든 인자를 객체의 배열이나 리스트로 제공할 수 있다는 점이에요.

이 메서드의 흥미로운 특징 하나는, 제공된 인자의 개수가 예상 개수를 초과하면 남은 인자들이 모두 마지막 인자에 압축되어 그게 배열로 취급된다는 거예요.

필드 접근

필드에 대한 읽기·쓰기 접근 권한을 가진 메서드 핸들을 만드는 것도 가능해요. 인스턴스 필드에는 findGetterfindSetter 메서드가, static 필드에는 findStaticGetterfindStaticSetter 메서드가 그걸 도와줘요. MethodType 인스턴스는 제공할 필요 없고, 대신 필드의 타입인 단일 타입 하나만 제공하면 돼요.

예를 들어 Example 클래스에 static 필드 magic이 있다고 해 볼게요.


private static String magic = "initial value static field";

Lookup 객체를 만들었다고 하면,


MethodHandles.Lookup lookup = MethodHandles.lookup();

setter와 getter 메서드 핸들을 각각 만들어 따로 호출할 수 있어요.


MethodHandle setterStaticMethodHandle = lookup.findStaticSetter(Example.class, "magic", String.class);
MethodHandle getterStaticMethodHandle = lookup.findStaticGetter(Example.class, "magic", String.class);

setterStaticMethodHandle.invoke("new value static field");
String staticFieldResult = (String) getterStaticMethodHandle.invoke(); // staticFieldResult == `new value static field`

Example 클래스에 인스턴스 필드 abc가 있다고 해 볼게요.


private String abc = "initial value";

인스턴스 필드를 읽고 쓰는 메서드 핸들도 비슷하게 만들 수 있어요.


MethodHandle setterMethodHandle = lookup.findSetter(Example.class, "abc", String.class);
MethodHandle getterMethodHandle = lookup.findGetter(Example.class, "abc", String.class);

인스턴스 필드에 setter와 getter 메서드 핸들을 쓰려면, 먼저 그 필드가 속한 클래스의 인스턴스를 얻어야 해요.


Example example = new Example();

그다음 setter와 getter 호출에 Example 인스턴스를 제공해야 해요.


setterMethodHandle.invoke(example, "new value");
String result = (String) getterMethodHandle.invoke(example); // result == `new value`

메서드 핸들로 필드 값을 읽고 쓰는 것이 가능하긴 하지만, 흔한 관행은 아니에요. 필드에는 VarHandle을 쓰는 것이 더 적합한데, findVarHandlefindStaticVarHandle 메서드로 만들 수 있어요.

배열 다루기

MethodHandles 클래스에는 미리 준비된 메서드 핸들을 제공하는 메서드들이 있어요. 배열 조작을 가능하게 하는 메서드 핸들도 포함돼요. 이런 메서드 핸들을 만드는 데는 접근 검사가 필요 없으므로 lookup 객체가 필요하지 않아요.

arrayConstructorString 5개를 담은 배열을 만들어 볼게요.


MethodHandle arrayConstructor = MethodHandles.arrayConstructor(String[].class);
String[] arr = (String[]) arrayConstructor.invoke(5);

단일 요소를 수정하려면 arrayElementSetter를 쓰는데, 대상 배열에 대한 참조, 요소의 인덱스, 새로운 값을 제공하면 돼요.


MethodHandle elementSetter = MethodHandles.arrayElementSetter(String[].class);
elementSetter.invoke(arr, 4, "test");

단일 요소의 값을 읽으려면 arrayElementGetter 메서드 핸들을 쓰는데, 배열에 대한 참조와 요소 인덱스를 제공해요.


MethodHandle elementGetter = MethodHandles.arrayElementGetter(String[].class);
String element = (String) elementGetter.invoke(arr, 4); // element == "test"

arrayLength가 제공하는 메서드 핸들로 배열 길이를 정수로 얻을 수도 있어요.


MethodHandle arrayLength = MethodHandles.arrayLength(String[].class);
int length = (int) arrayLength.invoke(arr); // length == 5

예외 처리

invokeExactinvoke는 모두 Throwable을 던지므로, 밑에 있는 메서드가 던질 수 있는 예외에 제한이 없어요. 메서드 핸들을 호출하는 메서드는 Throwable을 명시적으로 던지거나 잡아야 해요.

MethodHandles API에는 예외 처리를 더 쉽게 만드는 특정 메서드들이 있어요. 몇 가지 예시를 살펴볼게요.

catch 래퍼

MethodHandles.catchException 메서드는 주어진 메서드 핸들을 제공된 예외 처리기 메서드 핸들 안에 감쌀 수 있어요.

비즈니스 로직을 수행하는 problematicMethod와 특정 IllegalArgumentException을 처리하는 exceptionHandler 메서드가 있다고 해 볼게요. 예외 처리기 메서드는 원래 메서드와 같은 반환 타입을 가져야 해요. 그리고 첫 번째 인자로 우리가 관심 있는 Throwable을 받고, 그다음에 원래 받았던 나머지 인자들이 이어져요.


public static int problematicMethod(String argument) throws IllegalArgumentException {
    if ("invalid".equals(argument)) {
        throw new IllegalArgumentException();
    }
    return 1;
}

public static int exceptionHandler(IllegalArgumentException e, String argument) {
    // log exception
    return 0;
}

두 메서드 모두에 대한 메서드 핸들을 조회하고, problematicMethodexceptionHandler 안에 감쌀 수 있어요. 결과로 얻은 MethodHandle은 호출 시 IllegalArgumentException을 적절히 처리하고, 다른 예외가 발생하면 계속 던져요.


MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.findStatic(Example.class, "problematicMethod", MethodType.methodType(int.class, String.class));
MethodHandle handler = lookup.findStatic(Example.class, "exceptionHandler",
        MethodType.methodType(int.class, IllegalArgumentException.class, String.class));
MethodHandle wrapped = MethodHandles.catchException(methodHandle, IllegalArgumentException.class, handler);

IO.println(wrapped.invoke("valid")); // outputs "1"
IO.println(wrapped.invoke("invalid")); // outputs "0"

finally 래퍼

MethodHandles.tryFinally 메서드는 비슷하게 동작하지만, 예외 처리기 대신 try-finally 블록을 추가해 대상 메서드를 감싸요.

정리 로직을 담은 별도의 cleanupMethod 메서드가 있다고 해 볼게요. 이 메서드의 반환 타입은 대상 메서드의 반환 타입과 같아야 해요. 인자로 Throwable, 그다음 대상 메서드에서 나온 결과 값, 그리고 마지막으로 모든 인자를 받아야 해요.


public static int cleanupMethod(Throwable e, int result, String argument) {
    IO.println("inside finally block");
    return result;
}

이전 예시의 메서드 핸들을 try-finally 블록 안에 이렇게 감쌀 수 있어요.


MethodHandle cleanupMethod = lookup.findStatic(Example.class, "cleanupMethod",
        MethodType.methodType(int.class, Throwable.class, int.class, String.class));

MethodHandle wrappedWithFinally = MethodHandles.tryFinally(methodHandle, cleanupMethod);

IO.println(wrappedWithFinally.invoke("valid")); // outputs "inside finally block" and "1"
IO.println(wrappedWithFinally.invoke("invalid")); // outputs "inside finally block" and throws java.lang.IllegalArgumentException

메서드 핸들 변환

앞선 예시에서 보았듯이 메서드 핸들은 단순히 밑에 있는 메서드를 가리키는 것보다 더 많은 동작을 캡슐화할 수 있어요. 대상 메서드 핸들을 감싸서 인자 재배열, 사전 삽입, 반환 값 필터링 같은 특정 동작을 추가하는 **어댑터 메서드 핸들(adapter method handle)**을 얻을 수 있어요.

그런 변환 중 몇 가지를 살펴볼게요.

타입 변환

메서드 핸들의 타입은 asType 메서드로 새 타입에 맞춰 적응시킬 수 있어요. 만약 그런 타입 변환이 불가능하다면 WrongMethodTypeException이 발생해요. 변환을 적용할 때 실제로는 메서드 핸들이 두 개 있는 셈이에요. 원래 메서드 핸들이 추가 로직으로 감싸진 거죠. 이 경우 래퍼는 인자를 받아서 원래 메서드 핸들의 인자와 일치하도록 변환하려 시도해요. 원래 메서드 핸들이 일을 마치고 결과를 반환하면, 래퍼는 그 결과를 주어진 타입으로 캐스트하려 시도해요.

Object를 받아 String을 반환하는 test 메서드가 있다고 가정해 볼게요. 이 메서드를 String처럼 더 구체적인 인자 타입을 받도록 적응시킬 수 있어요.


MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test", 
        MethodType.methodType(String.class, Object.class));
MethodHandle adapter = targetMethodHandle.asType(
        MethodType.methodType(String.class, String.class));
String originalResult = (String) targetMethodHandle.invoke(111); // works
String adapterResult = (String) adapter.invoke("aaaaaa"); // works
adapterResult = (String) adapter.invoke(111); // fails

사실 MethodHandle에서 invoke를 쓸 때마다 가장 먼저 일어나는 일이 asType 호출이에요. invokeObject(들)를 받고 반환하는데, 이것들이 더 구체적인 타입으로 변환되려 시도돼요. 이 구체적인 타입들은 우리 코드에서 파생돼요. 즉 우리가 인자로 전달하는 정확한 값들과 반환 값을 캐스트한 타입이죠. 타입 변환이 성공적으로 끝나면, 그 구체적인 타입들에 대해 invokeExact 메서드가 호출돼요.

인자 순서 바꾸기(permute arguments)

인자가 재배열된 어댑터 메서드 핸들을 얻으려면 MethodHandles.permuteArguments를 쓰면 돼요.

예를 들어 서로 다른 타입의 인자를 여럿 받는 test 메서드를 만들어 볼게요.


public static void test(int v1, String v2, long v3, boolean v4) {
    IO.println(v1 + v2 + v3 + v4);
}

그리고 이에 대한 메서드 핸들을 조회해요.


MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

permuteArguments 메서드는 다음을 받아요.

  • 대상 메서드 핸들 (여기서는 test 메서드를 가리키는 것)
  • 원하는 순서대로 모든 인자를 재배열한 새 MethodType
  • 인자의 새 순서를 지정하는 인덱스 배열

MethodHandle reversedArguments = MethodHandles.permuteArguments(targetMethodHandle,
        MethodType.methodType(void.class, boolean.class, long.class, String.class, int.class), 3, 2, 1, 0);
reversedArguments.invoke(false, 1L, "str", 123); // outputs: "123str1false"

인자 삽입(insert arguments)

MethodHandles.insertArguments 메서드는 하나 이상의 바인딩된 인자(bound argument)를 가진 MethodHandle을 제공해요.

예를 들어 이전 예시의 메서드 핸들을 다시 볼게요.


MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

Stringlong 인자를 미리 바인딩한 어댑터 MethodHandle을 쉽게 얻을 수 있어요.


MethodHandle boundArguments = MethodHandles.insertArguments(targetMethodHandle, 1, "new", 3L);

결과 어댑터 메서드 핸들을 호출하려면 미리 채워지지 않은 인자만 제공하면 돼요.


boundArguments.invoke(1, true); // outputs: "1new3true"

이미 미리 채워진 인자를 전달하려 하면 WrongMethodTypeException으로 실패해요.

인자 필터링(filter arguments)

MethodHandles.filterArguments를 사용하면 대상 메서드 핸들을 호출하기 전에 인자에 변환을 적용할 수 있어요. 동작하게 하려면 다음을 제공해야 해요.

  • 대상 메서드 핸들
  • 변환할 첫 번째 인자의 위치
  • 각 인자의 변환을 위한 메서드 핸들들

특정 인자가 변환을 필요로 하지 않으면 null을 넘겨 건너뛸 수 있어요. 인자의 일부만 변환하고 싶다면 나머지 인자들을 아예 건너뛰는 것도 가능해요.

앞 섹션의 메서드 핸들을 재사용해서, 호출 전에 인자 몇 개를 필터링해 볼게요.


MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "test",
MethodType.methodType(void.class, int.class, String.class, long.class, boolean.class));

그다음 어떤 boolean 값이든 부정(negate)해서 변환하는 메서드를 만들어요.


private static boolean negate(boolean original) {
    return !original;
}

또한 주어진 정수 값을 증가시키는 메서드도 만들어요.


private static int increment(int original) {
    return ++original;
}

이 변환 메서드들에 대한 메서드 핸들을 얻을 수 있어요.


MethodHandle negate = lookup.findStatic(Example.class, "negate", MethodType.methodType(boolean.class, boolean.class));
MethodHandle increment = lookup.findStatic(Example.class, "increment", MethodType.methodType(int.class, int.class));

그리고 이것들을 사용해 인자가 필터링된 새 메서드 핸들을 얻어요.


// applies filter 'increment' to argument at index 0, 'negate' to the last argument, 
// and passes the result to 'targetMethodHandle'
MethodHandle withFilters = MethodHandles.filterArguments(targetMethodHandle, 0, increment, null, null, negate);
withFilters.invoke(3, "abc", 5L, false); // outputs "4abc5true"

인자 접기(fold arguments)

MethodHandle 호출 전에 하나 이상의 인자에 대한 전처리를 하고 싶을 때, MethodHandles.foldArguments를 사용할 수 있어요. 이 메서드에 어떤 선호 위치에서든 인자를 받기 시작하는 컴바이너(combiner) 메서드의 메서드 핸들을 제공하면 돼요.

대상 메서드가 있다고 가정해 볼게요.


private static void target(int ignored, int sum, int a, int b) {
    System.out.printf("%d + %d equals %d and %d is ignored%n", a, b, sum, ignored);
}

foldArguments를 사용하면 인자 중 일부를 전처리하고, 그 결과로 나온 값을 또 다른 인자로 삽입한 다음 대상 메서드 실행을 진행할 수 있어요.

우리 예시에서는 인자 int a, int b가 끝에 있어요. 원하는 만큼 인자를 전처리할 수 있지만, 모두 끝에 있어야 해요. 이 두 값 ab의 합을 계산하고 싶다고 해 볼게요. 그 메서드를 만들어 봅시다.


private static int sum(int a, int b) {
    return a + b;
}

결과 값은 정확히 어디로 갈까요? 그것은 대상 메서드의 인자 중 하나에 삽입돼요. 접으려는 인자들 바로 앞의 인자여야 하는데, 예시에서는 int sum 인자예요. folding 결과를 위해 예약된 인자는 다른 위치에 있을 수 없어요. 대상 메서드가 이 folding 로직과 관련 없는 인자를 더 받아야 한다면, 그 인자들은 모두 앞쪽에 와야 해요.

메서드 핸들을 만들고 어떻게 조합해야 하는지 볼게요.


MethodHandle targetMethodHandle = lookup.findStatic(Example.class, "target",
        MethodType.methodType(void.class, int.class, int.class, int.class, int.class));
MethodHandle combinerMethodHandle = lookup.findStatic(Example.class, "sum",
        MethodType.methodType(int.class, int.class, int.class));
MethodHandle preProcessedArguments = MethodHandles.foldArguments(targetMethodHandle, 1, combinerMethodHandle);

foldArguments 메서드는 다음을 받아요.

  • MethodHandle target: 대상 메서드 핸들 (여기서는 target 메서드를 가리키는 것)
  • int pos: folding과 관련된 인자의 시작 위치를 지정하는 정수. 우리 예시에서는 sum 인자가 위치 1에 있으므로 1을 전달했어요. 이 인자를 생략하면 pos는 기본값 0이 돼요.
  • MethodHandle combiner: 컴바이너 메서드 핸들 (여기서는 sum 메서드를 가리키는 것)

마지막으로 결과 메서드 핸들을 호출하고, 미리 계산될 sum을 제외한 모든 인자를 전달해요.


preProcessedArguments.invokeExact(10000, 1, 2); // outputs: "1 + 2 equals 3 and 10000 is ignored"

컴바이너 메서드가 값을 처리하지만 아무것도 반환하지 않는 경우도 가능해요. 이 경우 대상 메서드 인자 목록에는 결과 자리 표시자가 필요 없어요.

반환 값 필터링(filter return value)

인자와 비슷하게, 반환 값에 변환을 적용하는 어댑터도 사용할 수 있어요.

String을 반환하는 메서드가 있고, 이 메서드에서 반환된 어떤 값이든 다른 메서드(문자 dm으로 바꾸고 결과 값을 대문자로 만들기)로 보내고 싶은 상황을 상상해 볼게요.

항상 "dummy" 값을 반환하는 getSomeString 메서드에 대한 메서드 핸들이 여기 있어요.


MethodHandle getSomeString = lookup.findStatic(Example.class, "getSomeString", MethodType.methodType(String.class));

변환을 수행하는 resultTransform 메서드가 여기 있어요.


private static String resultTransform(String value) {
    return value.replace('d', 'm').toUpperCase();
}

변환 메서드에 대한 메서드 핸들이 여기 있어요.


MethodHandle resultTransform = lookup.findStatic(Example.class, "resultTransform", MethodType.methodType(String.class, String.class));

마지막으로, getSomeString 메서드가 반환한 결과가 resultTransform 메서드에 제공되어 그에 맞게 수정되도록 두 메서드 핸들을 조합한 것이 여기 있어요.


MethodHandle getSomeUppercaseString = MethodHandles.filterReturnValue(getSomeString, resultTransform);
IO.println(getSomeUppercaseString.invoke()); // outputs: "MUMMY"

메서드 핸들 vs Reflection API

메서드 핸들은 JDK 7에서 컴파일러와 언어 런타임 개발자를 돕는 도구로 도입됐어요. 결코 리플렉션을 대체하려던 것은 아니었어요.

Reflection API는 메서드 핸들이 할 수 없는 것을 제공해요. 바로 클래스 멤버를 나열하고 그 속성을 검사하는 일이에요. 반면 메서드 핸들은 Reflection API로는 불가능한 방식으로 변환되고 조작될 수 있어요.

메서드 호출에 관해서는 접근 검사와 보안 고려 사항에 차이가 있어요. Reflection API는 모든 호출자에 대해 매 호출마다 접근 검사를 수행하는 반면, 메서드 핸들의 접근은 생성 시에만 검사돼요. 그 덕분에 메서드 핸들을 통한 호출이 리플렉션보다 빨라요. 다만 메서드 핸들이 접근해서는 안 되는 코드로 전달되지 않도록 특정 예방 조치를 취해야 해요.

Reflection에 대해 더 알고 싶다면 이 튜토리얼에서 더 배울 수 있어요.

Reflection API와 메서드 핸들 간 변환

Lookup 객체를 사용하면 Reflection API 객체를 동작적으로 동등한(behaviorally equivalent) 메서드 핸들로 변환할 수 있어요. 이렇게 하면 밑에 있는 클래스 멤버에 더 직접적이고 효율적으로 접근할 수 있어요.

주어진 Method(lookup 클래스에 그렇게 할 권한이 있다면)를 가리키는 메서드 핸들을 만들려면 unreflect를 쓰면 돼요.

Example 클래스에 String 인자를 받아 String을 반환하는 test 메서드가 있다고 해 볼게요. Reflection API로 Method 객체를 얻을 수 있어요.


Method method = Example.class.getMethod("test", String.class);

lookup 객체의 도움으로 Method 객체를 unreflect해서 MethodHandle을 얻어요.


MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle methodHandle = lookup.unreflect(method);
String result = (String) methodHandle.invoke("something");

비슷하게 Field 객체가 주어지면 getter와 setter 메서드 핸들을 얻을 수 있어요.


Field field = Example.class.getField("magic");
MethodHandle setterMethodHandle = lookup.unreflectSetter(field);
MethodHandle getterMethodHandle = lookup.unreflectGetter(field);
setterMethodHandle.invoke("something");
String result = (String) getterMethodHandle.invoke(); // result == "something"

MethodHandle에서 Member로의 변환도 가능한데, 단 주어진 MethodHandle에 어떤 변환도 수행되지 않았다는 조건이 있어요.

메서드를 직접 가리키는 메서드 핸들이 있다고 해 볼게요. MethodHandles.reflectAs 메서드로 Method 객체를 얻을 수 있어요.


Method method = MethodHandles.reflectAs(Method.class, methodHandle);

Field 객체에 대해서도 비슷하게 동작해요.


Field field = MethodHandles.reflectAs(Field.class, getterMethodHandle); // same result is achieved by reflecting `setterMethodHandle`

결론

이 튜토리얼에서 메서드 핸들 메커니즘을 살펴보고 어떻게 효율적으로 사용하는지 배웠어요. 이제 메서드 핸들이 효율적인 메서드 호출 수단을 제공하지만, 이 메커니즘은 Reflection API를 대체하려는 것이 아니라는 것을 알게 됐어요.

메서드 핸들은 다르게 접근 검사를 수행하기 때문에 메서드 호출에 성능상 이점을 제공해요. 하지만 접근은 메서드 핸들이 생성될 때만 검사되므로, 메서드 핸들은 주의해서 전달해야 해요.

Reflection API와 달리 메서드 핸들은 클래스 멤버를 나열하고 그 속성을 검사하는 도구를 제공하지 않아요. 반면 Method Handle API는 메서드와 필드에 대한 직접 포인터를 인자·반환 값 조작 같은 더 복잡한 로직으로 감쌀 수 있게 해 줘요.

더 알아보기 (Learn more)

메서드 핸들과 리플렉션 중 무엇을 쓸지는, 클래스 멤버의 목록과 속성이 필요한지(Reflection API) 아니면 효율적인 호출과 인자·반환 값 조작이 필요한지(Method Handle API)에 따라 달라져요. VarHandle과 함께 쓰는 방식도 공식 API 문서에서 더 확인할 수 있어요.