Spring/스프링으로 시작하는 리액티브 프로그래밍

[Reactor] Context

꾸준함. 2024. 7. 30. 18:07

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