Sitelet https://github.com/on-the-ground/effectivejava/issues/2
Skip to content

Context Parameters 고도화 #2

Description

@joohyung-park

현재 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 호환성 비용이 매우 큽니다. 이 작은 라이브러리에
는 당장 권장하지 않습니다.

권장 로드맵

현재 구현을 버리지 않고 다음 순서로 발전시키는 것이 가장 현실적입니다.

  1. override/interface의 effect 계약 검사
  2. 생성자 @context와 new 표현식 검사
  3. 누락된 effect set 계산 및 Error Prone 자동 수정
  4. @ContextBoundary 도입으로 애플리케이션 루트의 미해소 효과 차단
  5. 진단에 요구사항의 호출 경로 표시
  6. 검증 가능한 동기 callback만 화이트리스트 방식으로 지원
  7. 필요성이 충분히 확인된 후에만 별도 타입 시스템 검토

핵심 방향은 이것입니다.

@context를 Kotlin context parameter처럼 보이게 만들려 하기보다, Java
에서 checked exception 수준으로 엄격하고 명시적인 effect contract로
완성한다.

이렇게 하면 Java 언어와 싸우지 않으면서도 “호출 체인의 최상위에서 효과
가 실제로 제공되지 않으면 컴파일 오류”라는 목표에 상당히 가까워질 수 있
습니다.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions