Context
- 프로그래밍 세계에서의 Context는 어떠한 상황을 처리하거나 해결하기 위해 필요한 정보를 제공하는 어떤 것
- ex) Spring Security에서 SecurityContextHolder는 SecurityContext를 관리하는 주체, 여기서의 SecurityContext는 애플리케이션 사용자의 인증 정보를 제공하는 인터페이스
- ex) Spring Framework에서 ApplicationContext는 애플리케이션의 정보를 제공하는 인터페이스로 ApplicationContext에서 제공하는 대표적인 정보가 Spring Bean
- Reactor에서 Context는 각 리액티브 시퀀스의 실행 시점에 관련된 데이터를 보관할 수 있는 불변 데이터 구조
- Map과 유사한 key-value 쌍의 형태로 데이터를 저장
- Spring MVC 같이 thread-per-request 모델의 ThreadLocal과 유사하지만
- ThreadLocal은 각각의 실행 쓰레드와 매핑되는 반면
- Context는 Subscriber와 매핑되기 때문에 구독이 발생할 때마다 해당 구독과 연결된 하나의 Context가 생김
코드 부연 설명
- deferContextual은 Context를 지연 평가하는 메서드로 Context에서 데이터를 가져와 Mono를 생성
- ctx.get("firstName")으로 Context에서 firstName 키의 값을 가져와 "Hello"와 결합한 문자열을 생성
- doOnNext는 데이터가 생성될 때 부가적인 작업을 수행
- subscribeOn은 구독이 발생할 쓰레드를 지정
- boundedElastic 스케줄러는 I/O 작업에 적합한 쓰레드를 제공
- publishOn은 이후 연산이 실행될 쓰레드를 지정
- parallel 스케줄러는 CPU 집약적인 작업에 적합
- transformDeferredContextual은 컨텍스트를 변환하기 위한 메서드이며 여기서는 mono의 데이터를 변환하여 ctx.get("lastName")을 추가
- contextWrite는 Context에 데이터를 추가
- Context에 데이터를 쓸 때는 Context를 사용하지만 Context에 저장된 데이터를 읽을 때는 ContextView를 사용
전체적인 흐름
- Context에 firstName과 lastName을 추가하는데 contextWrite는 스택처럼 동작하여, 나중에 추가된 것이 먼저 적용
- Mono.deferContextual에서 컨텍스트를 가져와 "Hello Jaime" 문자열을 생성
- doOnNext에서 "Hello Jaime"를 로그로 출력
- subscribeOn과 publishOn으로 스케줄링을 처리하며 구독은 boundedElastic 쓰레드에서, 이후 작업은 parallel 쓰레드에서 실행
- transformDeferredContextual에서 컨텍스트를 가져와 "Hello Jaime Min" 문자열을 생성
- 최종적으로 구독하여 "Hello Jaime Min"을 로그로 출력
자주 사용되는 Context API
- Context에 데이터를 쓸 때는 Context를 사용
Context API | 설명 |
put(key, value) | key-value 형태로 Context에 값을 넣음 |
of(key1, value1, key2, value2, ...) | key-value 형태로 Context에 여러 개의 값을 넣음 |
putAll(ContextView) | 현재 Context와 파라미터로 입력된 ContextView를 merge |
delete(key) | Context에서 key에 해당하는 value 제거 |
전체적인 흐름
- contextWrite(context -> context.put(key1, "SW Developer"))이 먼저 실행되어 key1에 "SW Developer" 값을 넣음
- 다음으로 contextWrite(context -> context.putAll(Context.of(key2, "Jaime", key3, "Min").readOnly()))이 실행되어 key2에 "Jaime", key3에 "Min" 값을 넣음
- Mono.deferContextual에서 Context를 가져와 key1("SW Developer"), key2("Jaime"), key3("Min") 값을 결합한 문자열인 "SW Developer, Jaime Min" 생성
- 구독 시점에 데이터를 소비하고, 최종 결과 문자열 "SW Developer, Jaime Min"을 로그로 출력하며 publishOn(Schedulers.parallel())에 의해 이후 작업이 병렬 스케줄러에서 실행되기 때문에 parallel-1 쓰레드에서 출력
자주 사용되는 ContextView API
- Context에 저장된 데이터를 읽을 때는 ContextView를 사용
ContextView API | 설명 |
get(key) | ContextView에서 key에 해당하는 value 반환 |
getOrEmpty(key) | ContextView에서 key에 해당하는 value를 Optional로 래핑해서 반환 |
getOrDefault(key, default value) | ContextView에서 key에 해당하는 value를 가져오되 해당하는 value가 없을 경우 default value를 가져옴 |
hasKey(key) | ContextView에서 특정 key가 존재하는지 확인 |
isEmpty() | Context가 비어있는지 확인 |
size() | Context 내에 있는 key-value 개수 반환 |
전체적인 흐름
- contextWrite(context -> context.put(key1, "no job"))가 실행되어 컨텍스트에 key1에 "no job" 값을 넣음
- Mono.deferContextual에서 Context를 가져와 key1("no job"), key2("no firstName"), key3("no lastName") 값을 결합한 문자열을 생성
- Context가 없으므로 모두 default 값을 반환
- 구독 시점에 데이터를 소비하고, 최종 결과 문자열 "no job, no firstName no lastName"을 로그로 출력하며 publishOn(Schedulers.parallel())에 의해 이후 작업이 병렬 스케줄러에서 실행되기 때문에 parallel-1 쓰레드에서 출력
Context의 특징
1. Context는 구독별로 연결되는 특징이 있기 때문에 구독이 발생할 때마다 해당하는 하나의 Context가 하나의 구독에 연결됨
부연 설명
- 첫 번째 구독에서는 'Apple'이 출력되었고, 두 번째 구독에서는 'Microsoft'가 출력된 것을 확인할 수 있음
- Context는 Operator 체인의 아래에서 위로 전파
- 동일한 키에 대한 값을 중복해서 저장할 경우 Operator 체인에서 가장 위쪽에 위치한 contextWrite()이 저장한 값으로 덮어씀
- 두 번째 구독에서 동일한 key1을 덮어썼기 때문에 'Microsoft'가 나온 것이 자명하다고 반박할 수 있음
- 이에 따라 두 번째 구독에서는 contextWrite문은 없애고 구독만 진행
- Context is empty 에러 발생
2. Context의 경우 Operator 체인상의 아래에서 위로 전파되는 특징 존재
부연 설명
- publishOn 직후 contextWrite에서 key2에 `Gudetama`를 넣어줬으므로 `SW Developer, Gudetama` 로그가 출력될 것으로 예상했지만 결과는 예상과 달랐음
- 이유는 Context의 경우 Operator 체인상의 아래에서 위로 전파되는 특징이 있기 때문
- transformDeferredContextual 문 내 ContextView를 통해 `name`이라는 키로 값을 읽어 오고 있지만 해당 시점에는 `name` 키에 해당하는 값이 Context에 존재하지 않기 때문에 getOrDefault() API의 디폴트 값으로 지정한 `Jaime`가 Subscriber에게 전달됨
- 만약 getOrDefault() API가 아니라 get() API를 사용했다면 NoSuchElementException 예외가 발생했을 것
- 따라서 일반적으로 모든 Operator에서 Context에 저장된 데이터를 읽을 수 있도록 contextWrite()를 Operator 체인의 맨 마지막에 위치
3. Operator 체인에서는 가장 상위의 contextWrite() 값이 우선되며, Inner Sequence는 외부 Context를 읽을 수 있지만 외부는 Inner Sequence의 Context를 읽을 수 없음
부연 설명
- Context에 데이터를 두 번 쓰는 것이 특징
- 한 번은 Operator 체인의 제일 마지막에 Context에 값을 쓰고 있고
- 또 다른 한 번은 flatMap()이라는 Operator 내부에 존재하는 Operator 체인에서 값을 쓰고 있음
- flatMap() Operator 내부에 있는 Sequence를 Inner Sequence라고 함
- Inner Sequence에서는 Inner Sequence 바깥쪽 Sequence에 연결된 Context의 값을 읽을 수 있음
- 만약 주석된 코드를 해제하면 NoSuchElementException 예외 발생
- Inner Sequence에 저장한 `role` 키를 Inner Sequence 외부에서 조회할 수 없기 때문
4. Context는 인증 정보 같은 직교성(독립성)을 가지는 정보를 전송하는 데 적합함
전체적인 흐름
- 도서 정보인 Book을 전송하기 위해 Mono<Book> 객체를 postBook() 메서드의 파라미터로 전달
- zip() Operator를 사용해 Mono<Book> 객체와 인증 토큰 정보를 의미하는 Mono<String> 객체를 하나의 Mono로 합치고 이때 합쳐진 Mono는 Mono<Tuple2>의 객체가 됨
- flatMap() Operator 내부에서 도서 정보를 전송
부연 설명
- 예제 코드의 핵심은 Context에 저장한 인증 토큰을 두 개의 Mono를 합치는 과정에서 다시 Context로부터 읽어 와서 사용한다는 것
- Mono가 어떤 과정을 거치든 상관없이 가장 마지막에 반환된 Mono를 구독하기 직전에 contextWrite()으로 데이터를 저장하기 때문에 Operator 체인의 위쪽으로 전파되고, Operator 체인 어느 위치에서든 Context에 접근할 수 있음
참고
- 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
반응형
'Spring > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[Reactor] Reactor 테스트 (0) | 2024.07.31 |
---|---|
[Reactor] Reactor Sequence 디버깅 방법 (0) | 2024.07.31 |
[Reactor] Scheduler (0) | 2024.07.24 |
[Reactor] Sinks (0) | 2024.07.23 |
[Reactor] Backpressure (0) | 2024.07.18 |