Trouble Shooting

Java AbstarctMethodError

소농배 2020. 3. 9. 23:18

Spring Boot  설정 중에 Runtime 에 AbstractMethodError 가 발생하였다.

 

 

What is AbstractMethodError (출처 :  https://docs.oracle.com/javase/7/docs/api/java/lang/AbstractMethodError.html)

 

Thrown when an application tries to call an abstract method. Normally, this error is caught by the compiler; this error can only occur at run time if the definition of some class has incompatibly changed since the currently executing method was last compiled.

 

application 에서 abstracth method 를 호출할때 발생하였다. 보통 이 에러는 컴파일 단계에서 잡히지만 어떤 클래스의 정의가 잘못된 변경에 의해서 변경된 경우에 런타임에서도 발생할 수 있다.

 

의심가는 부분을 간단한 코드로 작성하여 재현해 보았다.

 

Code

Main.java

public class Main {
	public static void main(String[] args) {
    	Test t = new TestImpl();
        System.out.println(t.test("STRING"));
    }
}

 

Test Interface Version 1

 

Public interface Test {
	CharSequence test(String s);
}

Test Interface Version 2

 

Public interface Test {
	Object test(String s);
}

 

Test Impl Class

 

public class TestImpl implements Test {
	@Override
    public CharSequence test(String s) {
    	return s;
    }
}

 

재현 시나리오

1. Test 인터페이스의 test() 메서드가 CharSequence 를 리턴하도록 작성.

2. TestImpl 은 Test 인터페이스 (Version 1) 을 구현하여 컴파일. -> TestImpl.class 생성.

3. Test 인터페이스의 test() 메서드가 Object 를 리턴하도록 작성 (Version 2).

4. Test 인터페이스 컴파일

5. Main 실행

 

결과

AbstractMethodError 가 발생하였다.

 

Exception in thread "main" java.lang.AbstractMethodError: TestImpl.test(Ljava/lang/String;)Ljava/lang/Object;
        at main.main(main.java:5)

 

 

하지만 이상한 점이 발견되었다.

재현 시나리오에서 애초에 Obejct 를 리턴하는 인터페이스(Version 2)로 TestImpl.java 를 구현하고 Test.java 를 컴파일하면 AbstractMethodError 가 발생하지 않는다.

 

Version 1 을 구현한 TestImpl.class 를 만든후에 Version 2 를 컴파일한 Test.class 로 실행한 것과

Version 2 로 TestImpl.class 와 Test.class 를 컴파일하여 실행한 것의 차이는 무엇일까?? 결국 같은 코드일텐데!

 

원인

javap 명령어를 이용하여 dissemble 해보면 알 수 있다.

 

javap -verbose 명령어로 클래스 파일을 살펴본 결과이다.

 

TestImpl.class (실패 : TestImpl.java Version 1 으로 컴파일, Test.java Version 2 로 컴파일) 

{
  public TestImpl();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
 
  public java.lang.CharSequence test(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/CharSequence;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 5: 0
}
SourceFile: "TestImpl.java"

TestImpl.class (성공 : TestImpl.java & Test.java 모두 Version 2 로 컴파일

{
  public TestImpl();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
 
  public java.lang.CharSequence test(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/CharSequence;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 5: 0
 
  public java.lang.Object test(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokevirtual #2                  // Method test:(Ljava/lang/String;)Ljava/lang/CharSequence;
         5: areturn
      LineNumberTable:
        line 1: 0
}

 

성공 케이스

 1. CharSequence test(String)

 2. Object test(String)

실패 케이스

 1. CharSequence test(String)

 

런타임에 Test.java 의 Abstract 메서드는 Object 를 리턴하는 메서드이다.

실패 케이스는 CharSequence 를 리턴하는 메서드만 존재하기 때문에 Test 클래스를 제대로 구현했다고 할 수 없다 <= this error can only occur at run time if the definition of some class has incompatibly changed

반면 성공 케이스는 CharSequence 를 리턴하는 메서드와 Object 를 리턴하는 메서드 둘 다 존재하며 Object 를 리턴하는 메서드에서 CharSequence 를 리턴하는 메서드를 invoke 하는 걸 볼 수 있다.

 

결론

 - 자바 언어는 컴파일 타임에 abstract 메서드의 리턴 값에 따라 구현한 클래스의 컴파일 결과는 달라지며 런타임에 참조되는 abstract 메서드의 리턴값이 변경되면 AbstractMethodError 가 발생한다.

 - AbstractMethodError 가 발생되면 jar 형태로 만들어져있는 클래스가 바라보는 abstract class , interface 의 버전이 변경 되었는지 확인해보자!

 


Helper 예제를 들어 설명. 

 전제: jar 파일은 이미 컴파일 되었으므로 다시 컴파일 안된다.

1. DefaultValueHelper 는 jar 로 컴파일 될 당시에 Helper.java 의 apply() 메서드는 CharSequence 만 리턴했다.
2. DefaultValueHelper.class 의 메서드 정보는 오로지 CharSequence 만 리턴하도록 생성됨.
3. 4.0.6 handlerbas 버전이 임포트 되면서 Helper.java 가 변경되었고 abstract 메서드가 Object 를 변경하도록 변경됨.
4. DefaultValueHelper 는 Helper 를 구현하였으므로 Object 를 리턴하는 apply() 메서드를 JVM 이 찾아보았지만 DefaultValueHelper는 컴파일 당시에 CharSequence 만 리턴하고 있었으므로 Object 를 리턴하는 메서드 정보가 없다.
6. Abstract 메서드를 제대로 구현하지 않았다고 판단하여 AbstractMethodError 발생.


DefaultValueHelper 를 복붙하여 다시 빌드할 경우에 동작하는 이유.

1. DefaultValueHelper 가 복붙되어 프로젝트로 들어올 경우에 다시 컴파일이 진행된다.
2. 컴파일 타임에 Helper.java 는 현재 import  되어 있는 버전을 바라보게 된다.
3. DefaultValueHelper 의 apply() 메서드는 CharSequence 를 리턴하지만 Helper.java 의 apply() 메서드는 Object 를 리턴한다.

4. 클래스 파일이 생성될때 2가지 메서드 정보가 모두 생긴다. Object 를 리턴하는 apply(), CharSequence 를 리턴하는 apply()
5. 런타임에 Helper.java apply() 는 Object 를 리턴하기 떄문에 DefaultValueHelper 에서 Object 를 리턴하는 apply() 호출
6. Object 를 리턴하는 apply() 에서 CharSequence 를 리턴하는 apply() 메서드가 invoke 된다.
7. CharSequence 타입이 결과로 리턴된다.

클래스 파일이 생성될때 두가지 타입 모두 리턴할 수 있도록 컴파일 되었으므로 AbstractMethodError 가 발생하지 않는다.