[JVM] 10. 프런트엔드 컴파일과 최적화

keymu·2025년 12월 7일

컴파일타임? 아래와 같이 3가지로 설명될 수 있음:

  1. 프런트엔드 컴파일러: JDK의 javac(.java -> .class로 변경), 이클립스 JDT의 증분 컴파일러(변경된 파일만 다시 컴파일하도록. 일반 컴파일은 500개 파일 중 하나만 고쳐도 의존성 따라 싹 다 다시 컴파일해야 하는데 그러지 않도록하는 것이 증분 컴파일임)
  2. JIT 컴파일러: 핫스팟 가상 머신의 C1, C2 컴파일러
  3. AOT 컴파일러

이번 장은 그 중 1번.
이전 6, 7, 8, 9장에서 설명되었던 런타임단계의 클래스 로딩, 링킹, 초기화, 스프링 컨테이너 빈 생성, 의존관계 주입 등 class파일을 JVM 메모리에 올리고 객체를 Container 등록하는 과정 전에 일어나는 빌드 타임 작업에 대한 설명을 할 거란 말. 톰캣 올리기 전 끝나있는 class 파일 만드는 "컴파일"에 대해 설명하는 장임.

최적화 또한 다음과 같이 정의한다:
javac와 같은 프런트엔드 컴파일러는 코드 실행 효율 측면의 최적화는 거의 하지 않으며, JDK 1.3 이후로는 성능 최적화를 런타임 컴파일러에 집중시켰다.
(그루비 클래스 파일 등도 최적화 효과를 공평하게 누리기 위해)

개발단계까지 최적화를 넓혀 부른다면,
런타임: 실행 효율을 높이는 최적화를 JIT 컴파일러에서 지속해서 수행
컴파일타임: 개발자 코딩 효율 높이는 최적화를 프런트엔드 컴파일러가 수행

10.2 javac 컴파일러

  • 순수 자바로 작성
  • JDK 6부터 javac 표준 자바 SE API에 포함. 원래는 구현 코드도 다 tools.jar에 별도로 담겨있었음. tools.jar 라이브러리가 클래스패스에 존재해야 javac 쓸 수 있었음 -> 표준 자바 클래스 라이브러리로 지금은 옮겨짐

Java소스파일을 클래스 파일로 컴파일 하는 방법은 아주 느슨하게 설명되어있어 자유도가 높음
장점: 자바 프런트엔드 컴파일러의 자유도 vs 단점:컴파일 과정이 특정 JDK 또는 컴파일러 구현에 따라 달라지는 경우도 생김

javac 코드의 전체 구조 확인을 통한 컴파일 처리 단계: 1단계 준비 + 3단계 처리
0: 플러그인 annotation 처리기 초기화
1: 구문 분석 및 심벌 테이블 채우기(소스 코드 토큰화하여 추상 구문 트리 구성/심벌 주소와 심벌 정보 생성)
2: 플러그인 annotation 처리기들로 annotation 처리
3: 의미분석 및 바이트코드 생성(특성검사를 통한 문법의 정적 정보 확인/데이터 흐름 및 제어 흐름 분석을 통한 프로그램의 동적 실행 과정 확인/편의문법 제거하여 원래 형식으로 복원/바이트 코드 생성)

compile 도중에 플러그인 annotation 실행 시, 새로운 심벌 생성될 수 있음.
그럼 이 새 심벌 다시 처리하려고 심벌 테이블 채우던 때로 돌아가야 함
그 상호작용순서는 아래와 같음:

javac의 컴파일 과정 맡은 클래스: com.sun.tools.javac.main.JavaCompiler
앞선 3단계 처리는 compile() method에 집중되어있음

javac 소스코드 기준으로 3단계 처리 과정 항목별 설명:

1: 구문분석 및 심벌 테이블 채우기
구문분석:

  • parseFiles() method.
  • 소스코드, 문자스트림 -> 토큰 집합으로(int a = b -> int, a, =, b)
  • com.sun.tools.javac.parser.Scanner 클래스가 해당 나눔을 담당 -> 해당 나눔은 곧 "추상 구문 트리"를 만드는 것과 같음

심벌 테이블 채우기:

  • enterTrees() method.
  • 심벌테이블 = 심벌 주소 + 심벌 정보
  • 의미 분석 과정 중 의미 확인(이름을 원래 선언과 일치하게 사용하는지 확인), 중간 코드 생성 시 참조, 목적 코드 생성 시에도 주소 할당에 심벌 테이블 활용
  • com. sun. tools.javac.comp.Enter 클래스가 담당 -> 이 단계의 결과로 컴파일 단위 각각에 대한 추상 구문 트리의 최상위 노드와 package-info.java의 최상위 노드 목록 만들어짐(전체 프로젝트의 구조 지도를 만든다는 뜻 -> 다음 단계에서 그래야 타입 검사, 흐름 분석, 바이트 코드를 생성할 수 있으므로)

2: 애너테이션 처리

  • 애너테이션 처리 중에 구문 트리를 수정하면 컴파일러는 '구문 분석 및 심벌 테이블 채우기' 단계로 돌아가야 함 -> 이걸 모든 플러그인 애너테이션 처리기가 구문 트리 수정 안할 때까지 반복함
  • 컴파일러의 애너테이션 처리 API 사용: 개발자 코드가 컴파일러 동작에 영향 줄 수 있음. 예시로, Lombok의 Getter/Setter 생성, null 확인, 검사 예외 테이블 생성, equals(), hashCode() method 생성 등을 자동으로 작성하게 할 수 있음
  • 해당 자동화 애너테이션 처리기는 0단계인 initProcessAnnotations()에서 초기화하여 실행은 processAnnotation()이 담당.
  • processAnnotation()은 com.sun.tools.javac.processing.JavacProcessingEnvironment 클래스의 doProcessing()을 실행 할 새 애너테이션 처리기가 있을 시 호출 -> 해당 메서드가 새 JavaCompiler 객체 생성하여 컴파일 이후 단계 처리.

3: 의미 분석과 바이트코드 생성

  • 의미분석: 구조적으로 올바른 소스가 '맥락상으로도 올바른지' 확인(예: 타입 검사, 제어 흐름 검사, 데이터 흐름 검사)
    그 중

1) 특성 검사: 변수를 사용하기 앞서 선언이 되어 있는지, 변수와 할당될 데이터의 타입이 일치하는지 등을 확인 / 접기(constant folding)라는 최적화도 수행
2) 데이터와 제어 흐름 분석: 프로그램이 맥락상 논리적으로 올바른지 확인. 기본적으로 클래스 로딩 시 수행하는 데이터 및 제어 흐름 분석의 목적과 같음 BUT 검증 범위가 다르기 때문에 항목에 따라 컴파일 타임에만 수행되거나 런타임에만 수행되기도 함
(ex. 지역변수는 상수 풀에 대한 심벌 참조가 없어 접근 플래그 정보 저장 못되므로, final이 붙어도 런타임에는 아무 영향이 없음. so, 오직 컴파일타임에 javac 컴파일러에서만 보장, 검사되는 대표적 예시)

  • 편의 문법 제거: desugar() method 촉발. 람다 등 편의 문법을 원래 기본 구문으로 복원
  • 바이트코드 생성: com.sun.tools.javac.jvm.Gen 클래스 담당 / 심벌 테이블 단계에서 이름만 등록되고 실제 코드 구현은 바이트코드 생성 시 완성되는 자동생성작업도 여기서 진행(ex. <init())

JAVA 편의문법:
Generic, 오토박싱/언박싱, 개선된 for문, 가변 길이 매개 변수, 조건부 컴파일 등(C#과 비교하여 코드 트렌드를 따르는 다양한 편의 문법의 예시와 역사, 사용법)

실전: 플러그인 애너테이션 처리기 제작

profile
Junior Backend Developer

0개의 댓글