본문 바로가기
JAVA

The Java, 코드를 조작하는 다양한 방법

by 공부 안하고 싶은 사람 2021. 9. 1.
반응형

클래스 로더

  • .class 에서 바이트코드를 읽고 메모리에 저장
  • 로딩 : 클래스를 읽어오기
  • 링크 : 레퍼런스를 연결하는 과정
  • 초기화 : static 값들 초기화 및 변수에 할당

메모리

  • 메소드 영역 : 클래스 수준의 정보 (클래스 이름, 부모 클래스 이름, 메소드, 변수) 저장. 공유자원
  • 힙 영역 : 객체를 저장. 공유자원
  • 스택영역 : 쓰레드 마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택 프레임이라 부르는 블럭으로 쌓는다. (Exception에서 나오는 함수 스택)
  • PC : 쓰레드 마다 쓰레드 내 현재 실행할 instruction의 위치를 가리키는 포인터
  • 네이티브 메소드 스택 (JNI 사용시)

실행엔진

  • 인터프리터 : 바이트 코드 한줄 씩 실행
  • JIT 컴파일러 : 인터프리터 효율을 높히기 위해, 반복되는 코드 발견시 모두 네이티브 코드로 바꿔둔다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다.
  • GC : 더 이상 참조되지 않는 객체를 모아서 정리

JNI(Java Native Interface)

  • 자바에서 C,C++,어셈블리로 작성된 함수(라이브러리)를 사용할 수 있는 방법을 제공
  • Native 키워드를 사용한 메소드 호출

네이티브 메소드 라이브러리

  • C,C++로 작성 된 라이브러리

 

 

클래스 로더

  • 로딩, 링크, 초기화 순으로 진행된다.
  • 로딩
    • .class파일을 읽고 그 내용에 따라 적절한 바이너리 데이터를 만들고 메소드 영역에 저장
    • 이때 메소드 영역에 저장하는 데이터
      • FQCN
      • 클래스, 인터페이스, 이늄
      • 메소드, 변수
    • 로딩이 끝나면 해당 클래스 타입의 class객체를 생성하여 힙영역에 저장
    • 클래스 로더는 계층 구조이며, 기본적으로 세가지 클래스 로더가 제공
      • 부트 스트랩 클래스 로더 - JAVA_HOME\lib에 있는 코어 자바 API를 제공한다. 최상위 우선순위를 가진 클래스 로더
      • 플랫폼 클래스로더 - JAVA_HOME\lib\ext 폴더 또는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다.
      • 애플리케이션 클래스로더 - 애플리케이션 클래스패스(애플리케이션 실행할 때 주는 -classpath 옵션 또는 java.class.path 환경 변수의 값에 해당하는 위치)에서 클래스를 읽는다.
  • 링크
    • verify, prepare, resolve(optional) 세 단계로
    • verify : .class 파일 형식이 유효한지 체크
    • preparation : 클래스 변수와 기본값에 필요한 메모리
    • Reslove : 심볼릭 메모리 레퍼런스(참조된 클래스)를 메소드 영역에 있는 실제 레퍼런스로 교체
  • 초기화
    • static변수의 값을 할당 (static 블럭이 있다면 이시점에 실행)

 

 

바이트코드 조작

예시) 코드 커버리지 측정 ( 테스트 코드가 확인한 소스 코드의 %) (바이트 코드가 실행됐는지 확인)

플러그인 추가

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.4</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

빌드 실행

mvn clean verify

커버리지 만족 못할시 빌드 실패하도록

 <execution>
   <id>jacoco-check</id>
   <goals>
     <goal>check</goal>
   </goals>
   <configuration>
     <rules>
       <rule>
         <element>PACKAGE</element>
         <limits>
           <limit>
             <counter>LINE</counter>
             <value>COVEREDRATIO</value>
             <minimum>0.50</minimum>
           </limit>
         </limits>
       </rule>
     </rules>
   </configuration>
</execution>

 

 

ASM / Javassist / ByteBuddy / CGlib 등 으로 바이트코드 조작 가능

-> 소스와 상관없이 바이트코드를 조작하여 변경할 수 있다. (사용이 필요할 경우에, ByteBuddy를 학습해보도록 하자)

public class Moja {
    public String pullOut() {
        return "";
    }
}
public class Masulsa {
    public static void main(String[] args) {
        System.out.println(new Moja().pullOut());
    }
}

위 내용에서 바이트 코드를 조작하여 콘솔에 문자가 찍히도록 조작할 수 있다.

바이트 코드 조작을 활용한 기능

  • 분석
    • 버그 찾기
    • 복잡도 계산
  • 클래스 생성
    • 프록시
    • API 제한
  • 코드를 건드리지 않고 변경
    • 프로파일러
    • 최적화
    • 로깅
  • 스프링의 컴포넌트 스캔(ASM)
    • ClassPathScanningCandidateComponentProvider -> SImpleMetadataReader
    • ClassReader와 Visitor 사용하여 클래스의 메타 정보를 읽어온다

 

 

리플렉션

DI가 동작하는 법

Class에 접근하는 법

  • 모든 클래스를 로딩한 다음 Class의 인스턴스가 생긴다. 타입.class 로 접근가능
  • 모든 인스턴스는 getClass() 메소드를 가지고 있따. 인스턴스.getClass() 로 접근가능
  • 클래스를 문자열로 읽어오는 방법
    • Class.forName("FQCN")
    • 클래스패스("FQCN")이 없다면 ClassNotfoundException

 

접근하여 할 수 있는 것 -> 클래스의 모든 정보 접근

  • 필드 (목록) 가져오기
  • 메소드 (목록) 가져오기
  • 상위 클래스 가져오기
  • 인터페이스 (목록) 가져오기
  • 애노테이션 가져오기
  • 생성자 가져오기
  • ...

 

애노테이션

  • @Retention : 언제까지 유지 ? 소스 / 클래스 / 런타임
  • @Target : 어디에 ?
  • @Inherited : 하위에 전달할 것인가 ?
  • 필드 값을 가질 수도 있다
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ELementType.FIELD})
@Inherited
pubic class Book {
      String value() default "TEST";
      int number() default 10;
}

 

 

API (클래스 정보 수정 또는 실행)

Class<T> bookClass = Class.forName("me.whiteshtip.Book");
Constructor<T> constructor = bookClass.getConstructor(String.class);
// Class 인스턴스 만들기
Book book = (Book) constructor.newInstance("myBook");

// 필드 값에 접근
Field a = Book.class.getDeclaredField("A");
a.setAccessible(true);
System.out.println(a.get(book));
a.set(book, "AA");
System.out.println(a.get(book));
// Static 필드를 가져올 때는 object가 없어도 되니까 null을 넘기면 된다.

// 메소드 실행
Method c = Book.class.getDeclaredMethod("d", int.class, int.class);
int invoke = (int) c.invoke(book, 1, 2);
System.out.println(invoke);

 

 

나만의 DI 프레임워크 만들기

@Retention(RetentionPolich.RUNTIME)
public @interface Inject {
}
public class ContainerService {
        public static <T> T getObject(Class<T> classType) {
          T instance = createInstnace(classType);
          Arrays.stream(classType.getDeclaredFields()).forEach(f -> {
          if (f.getAnnotation(Inject.class) != null) {
              Object fieldInstance = createInstance(f.getType());
              f.setAccessible(true);
              try {
                  f.set(instance, fieldInstance);
              } catch (Exception e) {
                  throw new Exception(e);
              }
            }
          });
            return instance;
    }

    private static <T> T createInstance(Class<T> classType) {
      try {
        return classType.getConstructor(null).newInstance();
      } catch (Exception e) {
        throw new Exception(e);
      }
    }
}
public class BookService {
        @Inject
    BookRepository bookRepository;
}

주의점

  • 지나친 사용은 성능이슈 야기할 수 있다.
  • 런타임시에만 문제 확인이 가능하다
  • 접근 지시자(캡슐성) 무시 가능하다

사용

  • 스프링
    • 의존성 주입
    • 뷰에 넘어돈 데이터를 객체에 바인딩
  • 하이버네이트
    • @Entity에 setter사용 없이
  • JUnit

 

 

리플렉션 중 다이나믹 프록시

  • Spring-Data-JPA는 어떻게 동작하는가? 인터페이스 타입의 인스턴스는 누가 만들어주는가?
    • Spring AOP를 기반으로 동작하며 RepositoryFactorySupport에서 프록시를 생성한다.
  • 사용처
    • Spring data JPA
    • Spring AOP
    • Mockito
    • 하이버네이트 lazy initialization
    • ...

 

 

프록시 패턴

  • 프록시와 리얼 서브젝트가 공유하는 인터페이가 있고, 클라이언트는 해당 인터페이스 타입으로 프록시를 사용한다. (프록시는 리얼 서브젝트를 가지고 있다)
  • 클라이언트는 프록시를 거쳐서 리얼 서브젝트를 사용하기 때문에 프록시는 리얼 서브젝트에 대한 접근을 관리하거나 부가기능을 제공하거나 리턴값을 변경할 수 있다.
  • 리얼 서브젝트는 자신이 해야 할 일만 하면서 프록시를 사용해서 부가적인 기능(접근 제한, 로깅, 트랜잭션 등)을 제공할 때 이런 패턴을 사용한다.

// 클라이언트 소스
BookService bookService = new BookServiceProxy(new DefaultBookService());
// 같은 인터페이스 BookService를 구현하여 같은 기능이 있다.
// 한 기능을 사용할 때, Proxy에선 Proxy기능을 실행 후 Default기능을 실행하게 할 수 있다.

 

 

 

다이나믹 프록시 (인터페이스)

  • 런타임에 특정 인터페이스들을 구현하는 클래스 또는 인스턴스를 만드는 기술
  • 유연한 구조가 아니다 -> Spring AOP 등장
BookService bookService = (BookService) Proxy.newProxyInstance(BookService.class.getClassLoader(), new Class[]{BookService.class}, new InvocationHandler() {
            BookService bookService = new DefaultBookService();
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                  // 특정 메소드에만 Proxy기능을 추가하고 싶다면
                if (method.getName().equals("rent")) {
                    System.out.println("aaaa");
                    Object invoke = method.invoke(bookService, args);
                    System.out.println("bbbb");
                    return invoke;
                }
                return method.invoke(bookService, args);
            }
        });

 

다이나믹 프록시 (클래스)

  • 상속이 불가능한 경우 프록시를 만들 수 없다.
  • 인터페이스 방식이 더 좋다

 

CGlib

  • 스프링, 하이버네이트가 사용하는 라이브러리 (버전 호환성이 좋지 않아 서로 다른 라이브러리 내부에 내장된 형태로 제공)
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
MethodInterceptor handler = new MethodInterceptor() {
    BookService bookService = new BookService();
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
          // 인터페이스와 동일하게 추가 작업 가능
        return method.invoke(bookService, objects);
    }
};

BookService bookService = (BookService) Enhancer.create(BookService.class, handler);

 

ByteBuddy

Class<? extends BooksService> proxyClass = new ByteBuddy().subclass(BookService.class)
  // 특정 메소드에
  .method(named("rend").intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
    BookService bookService = new BookService();
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 추가 기능 작업 가능
      return method.invoke(bookService, args);
    }
  }))
     .make().load(BookService.class.getClassLoader()).getLoaded();

BookService bookService = proxyClass.getConstructor(null).newInstance();

 

 

애노테이션 프로세서

장점

  • 런타임 비용이 제로(컴파일 시점에 해결)

단점

  • 기존 클래스 코드를 변경할 때는 약간의 해킹이 필요

 

롬복

  • @Getter @Setter @Builder 등 으로 표준적으로 작성해야할 코드를 개발자 대신 생성
  • 사용
    • 의존성 추가
    • org.projectlombok
    • IntelliJ lombok 플러그인 설치
    • IntelliJ Annotation processiong 옵션 활성화
  • 원리
    • 컴파일 시점에 애노테이션 프로세서를 사용하여 소스코드의 AST(abstract syntax tree)를 조작한다
  • 논란
    • 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여, 기존 소스 코드를 조작하여 생성한다.
    • 대안 (모든 편의성을 대체 하진 못한다)
      • AutoValue
      • Immutables

 

 

@Override

  • 애노테이션 프로세서의 예

 

 

애노테이션 프로세서

컴파일 시점에 소스코드를 조작

@Retention(RetentionPolicy.SOURCE)
public @interface Magic {
}
@AutoService(Processor.class) // Service Provider // 컴파일 시점에 애노테이션 프로세스를 사용하여 
  // META-INF/services/javax.annotation.processor.Processor 파일 자동으로 생성
public class MagicMojaProcessor extends AbstractProcessor {
  // 어떤 annotation을 처리할지
  @Override
  public Set<String> getSupportedAnnotationTypes() {
      return Set.of(Magic.class.getName());
    }


  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment rou ndEnv) {
    Set<? extends TypeElement> elements = reoundEnv.getElementsAnnotationWith(Magic.class);
    // 여러 라운드에 거쳐
    for (Element el : elements) {
      if (el.getKind() != ElementKind.INTERFACE)
        processingEnv.getMeassager().printMessage(Diagnostic.Kind.ERROR, "error message");
      else
        processingEnv.getMeassager().printMessage(Diagnostic.Kind.NOTE, "proccessing message");

      // 소스 및 컴파일 된 코드 처리
      TypeElement typeElement = (TypeElement)element;
      ClassName className = ClassName.get(typeElement);

      MethodSpec pullOut = MethodSpec.methodBuilder("pullOut")
        .addModifiers(Modifier.PUBLIC)
        .returns(String.class)
        .addStatement("return $S", "Rabbit!")
        .build();

      TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")
        .addModifiers(Modifier.PUBLIC)
        .addSuperinterface(className)
        .addMethod(pullOut)
        .build();

      Filer filer = processingEnv.getFiler();
      try {
        JavaFile.builder(className.packageName(), magicMoja)
          .build()
          .writeTo(filer);
      } catch (IOException e) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: " + e);
      }    
    }
    return true;
  }
}

 

위 프로젝트를 의존성 주입받으면 (annotation porccesor 옵션 활성화 필요)

@Magic // @Magic annotation을 인터페이스에 사용할시 MogicMoja 구현체를 주입
public interface Moja {
}

 

마무리

  • JVM 구조
  • 바이트 코드 조작 - ASM 또는 Javassist, ByteBuddy
  • 리플렉션 API - 클래스 정보 참조 (메소드, 필드, 생성자, ...)
    • 다이나믹 프록시 기법 - Proxy, CGlib, ByteBuddy
  • 애노테이션 프로세서 - AbstractProcessor, Filer, ..., AutoService, Javapoet
728x90
반응형

'JAVA' 카테고리의 다른 글

JVM  (0) 2021.06.28
String / StringBuffer / StringBuilder  (0) 2021.06.28
SOLID  (0) 2021.06.28
디자인 패턴  (0) 2021.06.28
인코딩 변환  (0) 2021.04.29

댓글