본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 5. Backward compatibility(역호환

목차

    해당 파트는 직접적인 개발은 아니지만, Legacy version의 호환 가능성과 규칙에 대해 정의한다.

    5. Backward compatibility

    5.1. 서론

     
    과거에 클래스 파일 포맷에 새로운 요소가 도입되었고, 미래에도 새로운 요소들이 추가될 것이다(예: 모듈성, 자바 타입에 대한 애너테이션 등). ASM 3.x까지, 이러한 변화는 ASM API에 역호환성이 없는 변경을 초래했는데, 이는 바람직하지 않다.
     
    이러한 문제를 해결하기 위해 ASM 4.0에서 새로운 메커니즘이 도입되었다. 그 목적은 클래스 파일 포맷에 새로운 기능이 도입되더라도, 모든 미래의 ASM 버전이 ASM 4.0 이후의 모든 이전 버전과 역호환성을 유지하도록 하는 것이다. 이는 클래스 생성기, 클래스 분석기 또는 클래스 어댑터가 ASM 버전 4.0부터 작성되었다면, 어떤 미래의 ASM 버전과도 여전히 사용 가능함을 의미한다. 그러나, 이 속성은 ASM만으로 보장될 수 없다. 코드를 작성할 때 몇 가지 간단한 지침을 따라야 한다. 이 장의 목적은 이러한 지침을 소개하고, ASM 코어 API에서 역호환성을 보장하기 위해 사용되는 내부 메커니즘에 대한 아이디어를 제공하는 것이다.
     
    참고사항.
    ASM 4.0에서 도입된 역호환성 메커니즘은 ClassVisitor, FieldVisitor, MethodVisitor 등을 인터페이스에서 추상 클래스로 변경해야 했으며, 이는 ASM 버전을 인자로 받는 생성자가 필요하다. ASM 3.x에 대해 구현된 코드가 있다면, 코드 분석기와 어댑터에서 implements를 extends로 바꾸고, 생성자에서 ASM 버전을 지정함으로써 ASM 4.0으로 업그레이드할 수 있다. 또한, ClassAdapter와 MethodAdapter는 ClassVisitor와 MethodVisitor로 합쳐졌다. 코드를 변환하기 위해서는 단순히 ClassAdapter를 ClassVisitor로, MethodAdapter를 MethodVisitor로 교체하면 된다. 또한, custom FieldAdapter 또는 AnnotationAdapter 클래스를 정의한 경우, 이제 FieldVisitor와 AnnotationVisitor로 교체할 수 있다.
     

    5.1.1. 역호환성 계약

    역호환성을 보장하기 위한 사용자 지침을 소개하기 전에, "역호환성"이 무엇을 의미하는지 좀 더 정확하게 정의한다.
     
    무엇보다도, 새로운 클래스 파일 기능이 코드 생성기, 분석기 및 어댑터에 어떤 영향을 미치는지 연구하는 것이 중요하다. 즉, 어떠한 구현 및 바이너리 호환성 문제와 독립적으로, 새로운 기능이 도입되기 전에 설계된 클래스 생성기, 분석기 또는 어댑터가 이러한 수정 후에도 여전히 유효한가? 다르게 말하면, 새로운 기능이 도입되기 전에 설계된 변환 체인을 통해 새 기능이 단순히 무시되고 그대로 전달된다고 가정하면, 이 체인은 여전히 유효한가? 실제로 영향은 클래스 생성기, 분석기 및 어댑터에 따라 다르다:
     

    • 클래스 생성기는 영향을 받지 않는다: 고정된 클래스 버전으로 코드를 생성하며, 이 생성된 클래스는 JVM이 역바이너리 호환성을 보장하기 때문에 미래의 JVM 버전에서도 여전히 유효하다.
    • 클래스 분석기는 영향을 받을 수도 있고 받지 않을 수도 있다. 예를 들어, 자바 4용으로 작성된 바이트코드 명령어를 분석하는 코드는 애너테이션이 도입되었음에도 불구하고 자바 5 클래스와 함께 여전히 작동할 수 있다. 그러나 이와 동일한 코드는 새로운 invokedynamic 명령어를 무시할 수 없기 때문에 자바 7 클래스와 함께는 더 이상 작동하지 않을 수 있다.
    • 클래스 어댑터는 영향을 받을 수도 있고 받지 않을 수도 있다. 죽은 코드 제거 도구는 애너테이션의 도입이나 심지어 새로운 invokedynamic 명령어에 의해 영향을 받지 않는다. 반면, 클래스 이름 변경 도구는 둘 다에 의해 영향을 받는다.

     
    이는 새로운 클래스 파일 기능이 기존의 클래스 분석기나 어댑터에 예측할 수 없는 영향을 미칠 수 있음을 보여준다.
    새 기능이 단순히 무시되고 분석 또는 변환 체인을 통해 변경 없이 전달되면, 때때로 이 체인은 오류 없이 실행되고 유효한 결과를 생성할 것이고, 때때로는 오류 없이 실행되지만 유효하지 않은 결과를 생성할 것이며, 때때로는 실행 중에 실패할 것이다.
    두 번째 경우는 특히 문제가 되는데, 이는 사용자가 이를 인지하지 못한 채로 분석 또는 변환 체인의 의미를 깨뜨리기 때문이다. 이는 찾기 어려운 버그로 이어질 수 있다. 이를 해결하기 위해, 새 기능을 무시하는 대신, 분석 또는 변환 체인에서 알 수 없는 기능이 발견되는 즉시 오류를 발생시키는 것이 더 바람직하다고 생각한다. 오류는 이 체인이 새 클래스 포맷과 함께 작동할 수도 있고 작동하지 않을 수도 있으며, 필요한 경우 그것의 저자가 상황을 분석하여 업데이트해야 한다는 것을 알린다.
     
    이 모든 것은 다음과 같은 역호환성 계약의 정의로 이어진다:
     

    • ASM 버전 X는 x보다 작거나 같은 버전의 자바 클래스를 위해 작성되었다. y > x인 버전의 클래스를 생성할 수 없으며, x보다 큰 버전의 클래스를 입력으로 주어진 경우 ClassReader.accept에서 실패해야 한다.
    • 아래에 제시된 지침을 따라 ASM X에 대해 작성된 코드는 x 버전까지의 입력 클래스와 함께 어떤 미래의 버전 Y > X의 ASM에서도 수정 없이 계속 작동해야 한다.
    • 아래에 제시된 지침을 따라 ASM X에 대해 작성된 코드는 선언된 버전이 y이지만 x 이하의 버전에서 정의된 기능만을 사용하는 입력 클래스와 함께 ASM Y나 그 이후의 어떤 버전에서도 수정 없이 계속 작동해야 한다.
    • 아래에 제시된 지침을 따라 ASM X에 대해 작성된 코드는 x > y인 클래스 버전에서 도입된 기능을 사용하는 클래스를 입력으로 받는 경우 ASM X나 그 이후의 어떤 버전에서도 실패해야 한다.

    마지막 세 가지 포인트는 클래스 입력을 갖지 않는 클래스 생성기에는 해당되지 않는다.
     

    5.1.2. 예시

    역호환성을 보장하기 위한 사용자 지침과 내부 ASM 메커니즘을 설명하기 위해, 이 장에서는 자바 8 클래스에 클래스 작성자(들)를 저장하기 위한 두 가지 새로운 가상 속성과 라이선스를 저장하기 위한 속성이 추가될 것이라고 가정한다. 또한 이러한 새 속성이 ASM 5.0에서 ClassVisitor의 두 가지 새 메소드를 통해 노출될 것이라고 가정한다:
     

    void visitSource(String author, String source, String debug);

     
    구 visitSource 메소드는 유효하지만, ASM 5.0에서는 사용되지 않는 것으로 선언된다:

     @Deprecated void visitSource(String source, String debug);

     
    작성자와 라이선스 속성은 선택 사항이며, visitLicense를 호출하는 것은 필수가 아니며, visitSource 호출에서 author는 null일 수 있다.
     

    5.2. 지침

    이 섹션에서는 앞서 언급한 계약의 의미에서 어떤 미래의 ASM 버전과도 코드가 유효하게 유지될 수 있도록 코어 ASM API를 사용할 때 따라야 할 지침을 제시한다.
     
    먼저, 클래스 생성기를 작성하는 경우 따를 지침이 없다. 예를 들어, ASM 4.0용 클래스 생성기를 작성한다면, visitSource(mySource, myDebug)와 같은 호출을 포함할 것이며, 물론 visitLicense 호출은 없을 것이다. ASM 5.0으로 변경 없이 실행하면, 이는 사용되지 않는 visitSource 메소드를 호출할 것이지만, ASM 5.0 ClassWriter는 내부적으로 이를 visitSource(null, mySource, myDebug)로 리디렉션하여 예상된 결과를 생성할 것이다(단, 코드를 새 메소드로 직접 호출하도록 업그레이드하는 것보다는 약간 덜 효율적이다).
     
    마찬가지로, visitLicense 호출이 없어도 문제가 되지 않는다(생성된 클래스 버전도 변경되지 않았으며, 이 버전의 클래스에는 라이선스 속성이 없을 것으로 예상된다). 반면에, 클래스 분석기 또는 클래스 어댑터를 작성하는 경우, 즉 ClassVisitor 클래스(또는 FieldVisitor 또는 MethodVisitor와 같은 다른 유사한 클래스)를 오버라이드하는 경우, 아래에 제시된 몇 가지 지침을 따라야 한다.
     

    5.2.1. 기본 규칙

    여기서는 ClassVisitor를 직접 확장하는 클래스의 단순한 경우를 고려한다(다른 방문자 클래스에 대한 논의와 지침은 동일하며, 간접 하위 클래스의 경우는 다음 섹션에서 논의된다). 이 경우 하나의 지침만 있다: 지침 1: ASM 버전 X에 대한 ClassVisitor 하위 클래스를 작성하려면, 이 정확한 버전을 인자로 받는 ClassVisitor 생성자를 호출하고, 이 버전의 ClassVisitor 클래스에서 사용되지 않는 메소드를 오버라이드하거나 호출하지 않는다(또는 나중 버전에서 도입된 메소드도 마찬가지). 그게 전부다. 예시 시나리오에서(섹션 5.1.2 참조), ASM 4.0용으로 작성된 클래스 어댑터는 다음과 같아야 한다:
     

    class MyClassAdapter extends ClassVisitor {
        public MyClassAdapter(ClassVisitor cv) {
            super(ASM4, cv);
        }
    ...
        public void visitSource(String source, String debug) { // optional
    ...
            super.visitSource(source, debug); // optional
        }
    }
    

     
    ASM 5.0으로 업데이트되면, visitSource(String, String)는 제거되어야 하며, 클래스는 다음과 같아야 한다:
     

    class MyClassAdapter extends ClassVisitor {
        public MyClassAdapter(ClassVisitor cv) {
            super(ASM5, cv);
        }
    ...
        public void visitSource(String author,
                                String source, String debug) { // optional
    ...
            super.visitSource(author, source, debug); // optional
        }
        public void visitLicense(String license) { // optional
    ...
            super.visitLicense(license); // optional
        }
    }

     
    이것이 어떻게 작동하는가? 내부적으로, ClassVisitor는 ASM 4.0에서 다음과 같이 구현된다:
     

    public abstract class ClassVisitor {
        int api;
        ClassVisitor cv;
        public ClassVisitor(int api, ClassVisitor cv) {
            this.api = api;
            this.cv = cv;
        }
    ...
        public void visitSource(String source, String debug) {
            if (cv != null) cv.visitSource(source, debug);
        }
    }
    

     
    ASM 5.0에서, 이 코드는 다음과 같이 변한다:
     

    public abstract class ClassVisitor {
    ...
        public void visitSource(String source, String debug) {
            if (api < ASM5) {
                if (cv != null) cv.visitSource(source, debug);
            } else {
                visitSource(null, source, debug);
            }
        }
        public void visitSource(Sring author, String source, String debug) {
            if (api < ASM5) {
                if (author == null) {
                    visitSource(source, debug);
                } else {
                    throw new RuntimeException();
                }
            } else {
                if (cv != null) cv.visitSource(author, source, debug);
            }
        }
        public void visitLicense(String license) {
            if (api < ASM5) throw new RuntimeException();
            if (cv != null) cv.visitSource(source, debug);
        }
    }
    

     
    MyClassAdapter 4.0이 ClassVisitor 4.0을 확장한다면, 모든 것이 예상대로 작동한다. ASM 5.0으로 코드를 변경하지 않고 업그레이드하면, MyClassAdapter 4.0은 이제 ClassVisitor 5.0을 확장하게 된다.
    그러나 api 필드는 여전히 ASM4 < ASM5이며, 이 경우 ClassVisitor 5.0이 visitSource(String, String)를 호출할 때 ClassVisitor 4.0처럼 동작하는 것을 쉽게 볼 수 있다. 또한, 새 visitSource 메소드가 null 작성자와 함께 호출되면, 호출이 이전 버전으로 리디렉션된다.
    마지막으로, 입력 클래스에서 null이 아닌 작성자 또는 라이선스가 발견되면, 계약에서 정의한 대로 실행이 실패한다(새 visitSource 메소드나 visitLicense에서).
     
    ASM 5.0으로 업그레이드하고 동시에 코드를 업데이트하면, 이제 MyClassAdapter 5.0이 ClassVisitor 5.0을 확장한다. api 필드는 이제 ASM5이며, visitLicense와 새 visitSource 메소드는 단순히 다음 방문자 cv에 호출을 위임한다. 또한, 이전 visitSource 메소드는 이제 호출을 새 visitSource 메소드로 리디렉션한다. 이는 변환 체인에서 우리 자신의 앞에 오래된 클래스 어댑터가 사용되는 경우에도 MyClassAdapter 5.0이 이 방문 이벤트를 놓치지 않도록 보장한다.
     
    ClassReader는 항상 각 방문 메소드의 최신 버전을 호출한다.
    따라서, MyClassAdapter 4.0을 ASM 4.0과 함께 사용하거나 MyClassAdapter 5.0을 ASM 5.0과 함께 사용하는 경우 어떠한 간접 호출도 발생하지 않는다.
    오직 MyClassAdapter 4.0을 ASM 5.0과 함께 사용하는 경우에만 ClassVisitor에서 간접 호출이 발생한다(새 visitSource 메소드의 3번째 줄에서). 따라서, 오래된 코드는 새 ASM 버전과 함께 여전히 작동하지만, 약간 느리게 실행될 것이다. 새 API를 사용하도록 업그레이드하면 성능이 복원된다.
     

    5.2.2. 상속 규칙

    위의 지침은 ClassVisitor 또는 다른 유사한 클래스의 직접 하위 클래스에 충분하다. 간접 하위 클래스의 경우, 즉 ClassVisitor를 확장하는 하위 클래스 A1을 정의하고, 이것이 A2에 의해 확장되고, ... An에 의해 확장되는 경우에는, 이러한 모든 하위 클래스가 동일한 ASM 버전에 대해 작성되어야 한다.
     
    실제로, 상속 체인에서 다른 버전을 혼합하는 것은 visitSource(String,String)와 visitSource(String,String,String)와 같은 동일한 메소드의 여러 버전이 동시에 오버라이드되어, 잘못되거나 예측할 수 없는 결과를 초래할 수 있다. 이 클래스들이 독립적으로 업데이트되고 별도로 출시되는 다른 소스에서 온 경우, 이 속성을 보장하는 것은 거의 불가능하다. 이는 두 번째 지침으로 이어진다:
     
    지침 2: 방문자의 상속을 사용하지 말고, 대신 대리(delegation)를 사용하라(즉, 방문자 체인을 사용하라). 방문자 클래스를 기본적으로 final로 만드는 것이 좋은 관행이다. 실제로 이 지침에는 두 가지 예외가 있다:
     

    • 상속 체인 전체를 직접 제어할 수 있고, 계층의 모든 클래스를 동시에 출시할 수 있는 경우, 방문자의 상속을 사용할 수 있다. 그런 다음 계층의 모든 클래스가 동일한 ASM 버전에 대해 작성되었는지 확인해야 한다. 여전히, 계층의 리프 클래스를 final로 만든다.
    • 방문자 메소드를 오버라이드하는 클래스가 리프 클래스를 제외하고 없는 경우 "방문자"의 상속을 사용할 수 있다(예를 들어, ClassVisitor와 구체적인 방문자 클래스 사이에 편의 메소드를 도입하기 위해 중간 클래스를 사용하는 경우). 여전히, 계층의 리프 클래스를 final로 만든다(리프 클래스도 어떤 방문 메소드도 오버라이드하지 않는 경우에는 제외; 이 경우 서브클래스가 작성된 ASM 버전을 지정할 수 있도록 인자로 ASM 버전을 받는 생성자를 제공한다).

     

    Reference

     
    https://asm.ow2.io/asm4-guide.pdf
     
    ASM USER GUIDE
     
    Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.
     
    THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.