티스토리 뷰
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 가 발생하지 않는다.
'Trouble Shooting' 카테고리의 다른 글
Spring Cloud Gateway Redirect 시 호스트 변경됨 (0) | 2020.11.29 |
---|---|
갑자기 AWS 인스턴스 재부팅 됨 (Suddenly AWS instance was rebooted) (0) | 2020.04.18 |
docker: Error response from daemon: service endpoint with name {randomName} already exists. (0) | 2019.05.29 |
Java DateTimeFormatter 저장 오류 (0) | 2019.05.29 |
Hibernate toString stackOverflowError (0) | 2019.05.29 |
- Total
- Today
- Yesterday
- getBoolean
- ConcurrentHashMap
- referencedColumnName
- notifyAll()
- rate limit
- reative
- dynamodb
- Flux
- AbstractMethodError
- HashMap
- mariadb-connector-j
- Lazy
- DyanomoDB
- RoutePredication
- msyql-connector-java
- circurit breaker
- custom config data convertion
- aurora
- Seperate Chaining
- spring cloud gateway
- wait()
- ResultSet
- router
- mariada-connector
- N+1
- notify()
- RouteDefinition
- GlobalFilter
- MariaDB
- reactor
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |