반응형
클래스 로더
- .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
반응형
댓글