현재 Error Prone 기반 구현을 발전시키는 방향과 Kotlin
의 context parameter를 그대로 흉내 내는 방향은 구분해야 합니다.
현재 @context는 본질적으로 “컴파일러가 이해하는 타입”이 아니라 Error
Prone이 별도로 해석하는 효과 선언입니다.
@context(Logger.class)
void work() { ... }
Java 타입 시스템에서 work()의 타입은 여전히 그냥 void work()입니다. 그
래서 오버로드 선택, 메서드 참조 타입, 제네릭 추론, 함수형 인터페이스 적
합성에는 Logger 요구사항이 포함되지 않습니다.
현실적인 목표
현재 구현은 Kotlin context parameter보다는 다음 모델로 발전시키는 것이
적합합니다.
checked exception과 비슷한, 명시적으로 선언되고 호출자를 따라 전파되
는 정적 effect system
예를 들어:
@context(Logger.class)
void leaf() {}
@context(Logger.class)
void middle() {
leaf();
}
void root() throws Exception {
HandlerScope.open()
.bind(Logger.class, LoggerImpl::new)
.run(this::middle);
}
여기서 달성해야 할 보장은:
-
leaf()가 Logger를 요구한다.
-
middle()은 이를 다시 선언하거나 직접 제공해야 한다.
-
최종 호출 경계에서는 실제 bind(Logger.class, ...)가 요구사항을 해소한
다.
-
어느 호출 체인에서도 요구사항이 조용히 사라지지 않는다.
현재 checker도 이 모델의 초기 형태이지만 아직 선언과 Java 언어 요소 전
반을 충분히 다루지 못합니다.
1단계: 현재 명시적 모델을 완성
가장 먼저 할 일은 추론보다 soundness를 높이는 것입니다.
Override 계약 검사
다음 코드는 위험합니다.
interface Service {
void execute();
}
class ServiceImpl implements Service {
@OverRide
@context(Logger.class)
public void execute() {}
}
호출자가 Service 타입만 보면 Logger 요구사항을 알 수 없습니다.
Service service = new ServiceImpl();
service.execute(); // 정적 타입에는 @context가 없음
규칙은 함수 타입의 효과 계약처럼 잡아야 합니다.
- 구현 메서드는 상위 메서드에 없는 효과를 새로 요구할 수 없음
- 상위 메서드가 요구하는 효과를 구현체가 덜 요구하는 것은 허용
- 동일 효과 집합을 반복 선언하게 할지 상속된 것으로 간주할지 결정
권장 규칙:
implementation requirements ⊆ overridden declaration requirements
효과가 필요한 인터페이스라면 인터페이스 선언부에 붙여야 합니다.
interface Service {
@context(Logger.class)
void execute();
}
이 검사는 다음 우선순위의 soundness 작업입니다.
생성자 지원
현재는 생성자에 @context를 선언할 수 없습니다.
@context(Logger.class) // 현재 불가능
Service() {
HandlerScope.find(Logger.class);
}
@target에 CONSTRUCTOR를 추가하고 생성자 호출도 검사해야 합니다.
@context(Logger.class)
Service() {}
@context(Logger.class)
void create() {
new Service();
}
new Service()도 일반 메서드 호출과 같은 효과 요구로 다뤄야 합니다.
Initializer block은 선언할 시그니처가 없으므로 효과 사용을 금지하는 편
이 좋습니다.
메서드 참조 및 람다 규칙 완성
메서드 참조는 이번에 차단했지만 장기적으로는 무조건 금지보다 효과 요구
사항을 가진 함수 타입이 필요합니다.
Java의 표준 Runnable, Supplier에는 effect set을 표현할 자리가 없습니
다.
Runnable task = this::effectfulMethod;
따라서 현재 단계에서는 차단하는 것이 맞습니다.
향후에는 라이브러리 전용 함수형 인터페이스를 고려할 수 있습니다.
@FunctionalInterface
interface ContextRunnable {
@context(/* 문제: 타입 인자로 표현하기 어려움 */)
void run();
}
하지만 annotation 값은 제네릭 매개변수가 될 수 없기 때문에 임의 effect
set을 표현하기 어렵습니다. 효과 조합별 인터페이스를 만드는 것은 확장성
이 없습니다.
그러므로 일반 고차 함수까지 정적으로 지원하려면 별도의 타입 시스템이 필
요합니다.
2단계: 선언 누락을 추론하고 자동 수정
현재는 개발자가 직접 @context를 붙여야 합니다. 이를 Error Prone 안에서
개선할 수 있습니다.
예를 들어:
void greet() {
HandlerScope.find(Logger.class).log(...);
}
현재는 오류만 내지만, checker가 다음 수정안을 제공할 수 있습니다.
@context(Logger.class)
void greet() {
...
}
호출된 메서드의 요구사항도 합칠 수 있습니다.
@context(Logger.class)
void logSomething() {}
@context(Metrics.class)
void recordMetric() {}
void process() {
logSomething();
recordMetric();
}
추론 결과:
@context({Logger.class, Metrics.class})
void process() {
...
}
여기서 중요한 정책은 추론 결과를 암묵적으로 인정하지 않는 것입니다.
- checker는 필요한 effect set을 계산
- 선언이 없거나 부족하면 오류
- 자동 수정으로 정확한 @context({...}) 제안
- 소스에는 최종 계약이 명시적으로 남음
이렇게 하면 리팩터링 편의성과 API 가시성을 모두 얻습니다.
불필요한 선언 검사
반대 방향도 가능합니다.
@context({Logger.class, Metrics.class})
void work() {
HandlerScope.find(Logger.class);
}
Metrics가 정말 불필요하다면 경고할 수 있습니다. 다만 공개 API가 미래 구
현이나 대체 구현을 위해 더 넓은 계약을 선언할 수 있으므로 기본 오류보다
는 선택적 경고가 적절합니다.
3단계: requires와 provides를 분리
현재 checker는 두 개념을 암묵적으로 처리합니다.
- @context(Logger.class): 호출자가 제공해야 함
- bind(Logger.class).run(...): 이 영역에서 제공함
이것을 checker 내부 모델에서 명시적으로 분리하면 설계가 훨씬 선명해집니
다.
Required effects
Provided effects
Effective environment = inherited requirements + lexical providers
각 호출 지점에서:
missing = callee.required - current.provided - enclosing.required
를 계산합니다.
장기적으로는 annotation 이름도 더 명확하게 할 수 있습니다.
@RequiresContext(Logger.class)
void work() {}
다만 외부 API 이름을 바꿀 필요까지는 없습니다. checker 내부 모델만
requires/provides로 나누어도 충분합니다.
4단계: 루트에서 미해소 효과 검증
현재 모델에서는 다음과 같이 선언만 계속 위로 올릴 수 있습니다.
@context(Logger.class)
public void applicationEntryPoint() {
work();
}
이 메서드의 호출자가 checker가 분석하는 코드에 없다면, 실제로 누가
Logger를 제공하는지 증명하지 못합니다.
이는 checked exception에서도 비슷합니다. 라이브러리의 public 메서드가
throws IOException을 선언하는 것은 문제가 아니지만, 애플리케이션 진입점
은 결국 처리해야 합니다.
이를 구분할 루트 정책이 필요합니다.
예:
@contextroot
public static void main(String[] args) {
// 모든 effect requirement가 이 메서드 안에서 bind되어야 함
}
또는 규칙 기반으로:
- main
- JUnit @test
- 프레임워크 entrypoint
- 사용자가 지정한 @ContextBoundary
에서는 @context로 다시 미루는 것을 금지하고 실제 bind().run()으로 닫도
록 할 수 있습니다.
@ContextBoundary
void handleRequest() throws Exception {
HandlerScope.open()
.bind(Logger.class, ...)
.run(this::process);
}
이것이 “최상위에 실제 바인딩이 없으면 오류”에 가장 가까운 기능입니다.
단, 라이브러리 API의 public @context 메서드는 호출자에게 요구사항을 노
출하는 것이 목적이므로 오류로 보면 안 됩니다. 애플리케이션 root와 라이
브러리 API를 구분해야 합니다.
5단계: 별도 분석 패스로 전이 효과 계산
더 발전시키려면 메서드별 effect set을 계산할 수 있습니다.
directEffects(method)
= method 안의 HandlerScope.find(X)
transitiveEffects(method)
= directEffects(method)
∪ 호출하는 각 메서드의 transitiveEffects
- method 내부에서 bind한 effects
호출 그래프에 재귀가 있을 수 있으므로 고정점 계산이 필요합니다.
A → B → C
↑ ↓
└───────┘
이 분석으로 다음을 할 수 있습니다.
- 빠진 @context 자동 제안
- 선언과 실제 전이 효과 비교
- 최상위 boundary에서 미해소 효과 보고
- 어떤 호출 경로로 효과가 필요해졌는지 진단
진단도 훨씬 좋아집니다.
processOrder requires Logger
processOrder
→ validateOrder
→ auditFailure
→ HandlerScope.find(Logger.class)
다만 Error Prone은 기본적으로 로컬 AST 검사에 적합하므로, 전체 프로젝트
호출 그래프 분석은 점점 복잡해집니다. 외부 라이브러리나 동적 디스패치는
선언된 @context를 신뢰해야 합니다.
따라서 현실적으로는:
- 같은 컴파일 단위: 가능한 범위에서 추론
- 외부 artifact: CLASS retention의 @context 계약 사용
- reflection: 분석 대상 밖
- dynamic dispatch: 선언된 상위 메서드 계약 사용
으로 제한해야 합니다.
6단계: Kotlin에 가까운 타입 시스템이 필요하다면
정말 다음과 같은 수준을 원한다면 Error Prone만으로는 한계가 있습니다.
함수 타입 자체가 요구하는 context를 가짐
제네릭 함수가 callback의 effect set에 대해 다형적임
오버로드 및 타입 추론이 context를 고려함
예를 들어 개념적으로:
EffectfulFunction<Requires, Input, Output>
같은 타입을 만들 수 있지만 Java 제네릭으로 effect set의 합집합과 차집합
을 표현하기 어렵고 API가 매우 무거워집니다.
가능한 기술적 선택지는:
- Checker Framework 기반 커스텀 타입 시스템
- javac 플러그인으로 attribution/type-check 단계 확장
- 별도 Java 언어 플러그인 또는 소스 변환기
- 명시적 capability parameter를 생성하는 코드 생성
입니다.
하지만 유지보수성과 IDE 호환성 비용이 매우 큽니다. 이 작은 라이브러리에
는 당장 권장하지 않습니다.
권장 로드맵
현재 구현을 버리지 않고 다음 순서로 발전시키는 것이 가장 현실적입니다.
- override/interface의 effect 계약 검사
- 생성자 @context와 new 표현식 검사
- 누락된 effect set 계산 및 Error Prone 자동 수정
- @ContextBoundary 도입으로 애플리케이션 루트의 미해소 효과 차단
- 진단에 요구사항의 호출 경로 표시
- 검증 가능한 동기 callback만 화이트리스트 방식으로 지원
- 필요성이 충분히 확인된 후에만 별도 타입 시스템 검토
핵심 방향은 이것입니다.
@context를 Kotlin context parameter처럼 보이게 만들려 하기보다, Java
에서 checked exception 수준으로 엄격하고 명시적인 effect contract로
완성한다.
이렇게 하면 Java 언어와 싸우지 않으면서도 “호출 체인의 최상위에서 효과
가 실제로 제공되지 않으면 컴파일 오류”라는 목표에 상당히 가까워질 수 있
습니다.
현재 Error Prone 기반 구현을 발전시키는 방향과 Kotlin
의 context parameter를 그대로 흉내 내는 방향은 구분해야 합니다.
현재 @context는 본질적으로 “컴파일러가 이해하는 타입”이 아니라 Error
Prone이 별도로 해석하는 효과 선언입니다.
@context(Logger.class)
void work() { ... }
Java 타입 시스템에서 work()의 타입은 여전히 그냥 void work()입니다. 그
래서 오버로드 선택, 메서드 참조 타입, 제네릭 추론, 함수형 인터페이스 적
합성에는 Logger 요구사항이 포함되지 않습니다.
현실적인 목표
현재 구현은 Kotlin context parameter보다는 다음 모델로 발전시키는 것이
적합합니다.
예를 들어:
@context(Logger.class)
void leaf() {}
@context(Logger.class)
void middle() {
leaf();
}
void root() throws Exception {
HandlerScope.open()
.bind(Logger.class, LoggerImpl::new)
.run(this::middle);
}
여기서 달성해야 할 보장은:
leaf()가 Logger를 요구한다.
middle()은 이를 다시 선언하거나 직접 제공해야 한다.
최종 호출 경계에서는 실제 bind(Logger.class, ...)가 요구사항을 해소한
다.
어느 호출 체인에서도 요구사항이 조용히 사라지지 않는다.
현재 checker도 이 모델의 초기 형태이지만 아직 선언과 Java 언어 요소 전
반을 충분히 다루지 못합니다.
1단계: 현재 명시적 모델을 완성
가장 먼저 할 일은 추론보다 soundness를 높이는 것입니다.
Override 계약 검사
다음 코드는 위험합니다.
interface Service {
void execute();
}
class ServiceImpl implements Service {
@OverRide
@context(Logger.class)
public void execute() {}
}
호출자가 Service 타입만 보면 Logger 요구사항을 알 수 없습니다.
Service service = new ServiceImpl();
service.execute(); // 정적 타입에는 @context가 없음
규칙은 함수 타입의 효과 계약처럼 잡아야 합니다.
권장 규칙:
implementation requirements ⊆ overridden declaration requirements
효과가 필요한 인터페이스라면 인터페이스 선언부에 붙여야 합니다.
interface Service {
@context(Logger.class)
void execute();
}
이 검사는 다음 우선순위의 soundness 작업입니다.
생성자 지원
현재는 생성자에 @context를 선언할 수 없습니다.
@context(Logger.class) // 현재 불가능
Service() {
HandlerScope.find(Logger.class);
}
@target에 CONSTRUCTOR를 추가하고 생성자 호출도 검사해야 합니다.
@context(Logger.class)
Service() {}
@context(Logger.class)
void create() {
new Service();
}
new Service()도 일반 메서드 호출과 같은 효과 요구로 다뤄야 합니다.
Initializer block은 선언할 시그니처가 없으므로 효과 사용을 금지하는 편
이 좋습니다.
메서드 참조 및 람다 규칙 완성
메서드 참조는 이번에 차단했지만 장기적으로는 무조건 금지보다 효과 요구
사항을 가진 함수 타입이 필요합니다.
Java의 표준 Runnable, Supplier에는 effect set을 표현할 자리가 없습니
다.
Runnable task = this::effectfulMethod;
따라서 현재 단계에서는 차단하는 것이 맞습니다.
향후에는 라이브러리 전용 함수형 인터페이스를 고려할 수 있습니다.
@FunctionalInterface
interface ContextRunnable {
@context(/* 문제: 타입 인자로 표현하기 어려움 */)
void run();
}
하지만 annotation 값은 제네릭 매개변수가 될 수 없기 때문에 임의 effect
set을 표현하기 어렵습니다. 효과 조합별 인터페이스를 만드는 것은 확장성
이 없습니다.
그러므로 일반 고차 함수까지 정적으로 지원하려면 별도의 타입 시스템이 필
요합니다.
2단계: 선언 누락을 추론하고 자동 수정
현재는 개발자가 직접 @context를 붙여야 합니다. 이를 Error Prone 안에서
개선할 수 있습니다.
예를 들어:
void greet() {
HandlerScope.find(Logger.class).log(...);
}
현재는 오류만 내지만, checker가 다음 수정안을 제공할 수 있습니다.
@context(Logger.class)
void greet() {
...
}
호출된 메서드의 요구사항도 합칠 수 있습니다.
@context(Logger.class)
void logSomething() {}
@context(Metrics.class)
void recordMetric() {}
void process() {
logSomething();
recordMetric();
}
추론 결과:
@context({Logger.class, Metrics.class})
void process() {
...
}
여기서 중요한 정책은 추론 결과를 암묵적으로 인정하지 않는 것입니다.
이렇게 하면 리팩터링 편의성과 API 가시성을 모두 얻습니다.
불필요한 선언 검사
반대 방향도 가능합니다.
@context({Logger.class, Metrics.class})
void work() {
HandlerScope.find(Logger.class);
}
Metrics가 정말 불필요하다면 경고할 수 있습니다. 다만 공개 API가 미래 구
현이나 대체 구현을 위해 더 넓은 계약을 선언할 수 있으므로 기본 오류보다
는 선택적 경고가 적절합니다.
3단계: requires와 provides를 분리
현재 checker는 두 개념을 암묵적으로 처리합니다.
이것을 checker 내부 모델에서 명시적으로 분리하면 설계가 훨씬 선명해집니
다.
Required effects
Provided effects
Effective environment = inherited requirements + lexical providers
각 호출 지점에서:
missing = callee.required - current.provided - enclosing.required
를 계산합니다.
장기적으로는 annotation 이름도 더 명확하게 할 수 있습니다.
@RequiresContext(Logger.class)
void work() {}
다만 외부 API 이름을 바꿀 필요까지는 없습니다. checker 내부 모델만
requires/provides로 나누어도 충분합니다.
4단계: 루트에서 미해소 효과 검증
현재 모델에서는 다음과 같이 선언만 계속 위로 올릴 수 있습니다.
@context(Logger.class)
public void applicationEntryPoint() {
work();
}
이 메서드의 호출자가 checker가 분석하는 코드에 없다면, 실제로 누가
Logger를 제공하는지 증명하지 못합니다.
이는 checked exception에서도 비슷합니다. 라이브러리의 public 메서드가
throws IOException을 선언하는 것은 문제가 아니지만, 애플리케이션 진입점
은 결국 처리해야 합니다.
이를 구분할 루트 정책이 필요합니다.
예:
@contextroot
public static void main(String[] args) {
// 모든 effect requirement가 이 메서드 안에서 bind되어야 함
}
또는 규칙 기반으로:
에서는 @context로 다시 미루는 것을 금지하고 실제 bind().run()으로 닫도
록 할 수 있습니다.
@ContextBoundary
void handleRequest() throws Exception {
HandlerScope.open()
.bind(Logger.class, ...)
.run(this::process);
}
이것이 “최상위에 실제 바인딩이 없으면 오류”에 가장 가까운 기능입니다.
단, 라이브러리 API의 public @context 메서드는 호출자에게 요구사항을 노
출하는 것이 목적이므로 오류로 보면 안 됩니다. 애플리케이션 root와 라이
브러리 API를 구분해야 합니다.
5단계: 별도 분석 패스로 전이 효과 계산
더 발전시키려면 메서드별 effect set을 계산할 수 있습니다.
directEffects(method)
= method 안의 HandlerScope.find(X)
transitiveEffects(method)
= directEffects(method)
∪ 호출하는 각 메서드의 transitiveEffects
- method 내부에서 bind한 effects
호출 그래프에 재귀가 있을 수 있으므로 고정점 계산이 필요합니다.
A → B → C
↑ ↓
└───────┘
이 분석으로 다음을 할 수 있습니다.
진단도 훨씬 좋아집니다.
processOrder requires Logger
processOrder
→ validateOrder
→ auditFailure
→ HandlerScope.find(Logger.class)
다만 Error Prone은 기본적으로 로컬 AST 검사에 적합하므로, 전체 프로젝트
호출 그래프 분석은 점점 복잡해집니다. 외부 라이브러리나 동적 디스패치는
선언된 @context를 신뢰해야 합니다.
따라서 현실적으로는:
으로 제한해야 합니다.
6단계: Kotlin에 가까운 타입 시스템이 필요하다면
정말 다음과 같은 수준을 원한다면 Error Prone만으로는 한계가 있습니다.
함수 타입 자체가 요구하는 context를 가짐
제네릭 함수가 callback의 effect set에 대해 다형적임
오버로드 및 타입 추론이 context를 고려함
예를 들어 개념적으로:
EffectfulFunction<Requires, Input, Output>
같은 타입을 만들 수 있지만 Java 제네릭으로 effect set의 합집합과 차집합
을 표현하기 어렵고 API가 매우 무거워집니다.
가능한 기술적 선택지는:
입니다.
하지만 유지보수성과 IDE 호환성 비용이 매우 큽니다. 이 작은 라이브러리에
는 당장 권장하지 않습니다.
권장 로드맵
현재 구현을 버리지 않고 다음 순서로 발전시키는 것이 가장 현실적입니다.
핵심 방향은 이것입니다.
이렇게 하면 Java 언어와 싸우지 않으면서도 “호출 체인의 최상위에서 효과
가 실제로 제공되지 않으면 컴파일 오류”라는 목표에 상당히 가까워질 수 있
습니다.