사전 컨텍스트
용어함정

컨텍스트

gabury1고친 사람 github-actions[bot]

컨텍스트는 맨 위에서 정한 값을 여러 겹의 함수 호출 아래에서 바로 꺼내 쓰게 해 줍니다. 서버에서는 요청을 받을 때 정한 로그인한 사용자 정보를 맨 아래 함수가 인자 없이 꺼내 씁니다. 화면 코드에서는 위쪽 화면 조각이 놓아 둔 테마를, 데이터베이스에서는 접속 하나에 붙여 둔 값을 같은 방식으로 꺼냅니다. 운영체제·언어 모델·도메인 주도 설계에서는 같은 낱말이 전혀 다른 것을 가리킵니다.

쉽고 빠른 이해

무슨 일을 하는 말인가 — 어떤 범위에 값을 붙여 두고 그 범위 안 어디서든 꺼내 쓰게 합니다. 요청 하나를 처리하는 동안 로그인한 사용자 정보를 맨 아래 함수까지 인자로 일일이 넘기지 않아도 됩니다.

왜 쓰나 — 값을 쓰는 곳이 맨 아래에 있으면 중간 함수들이 쓰지도 않는 값을 받아 넘겨야 합니다. 넘길 값이 하나 늘 때마다 중간 함수를 전부 고쳐야 합니다.

어떻게 도나

  1. 범위를 하나 정합니다. 화면 조각 하나 아래, 요청 하나, 데이터베이스 접속 하나가 범위가 됩니다
  2. 범위의 맨 위에서 값을 넣어 둡니다
  3. 범위 안의 코드는 필요할 때 그 값을 꺼내 씁니다

헷갈리는 점 — 운영체제에서는 멈춘 작업을 다시 돌리려고 저장해 둔 계산 상태가 컨텍스트입니다. 언어 모델에서는 모델이 한 번에 읽는 글이 컨텍스트입니다. 도메인 주도 설계에서는 한 용어가 같은 뜻으로 통하는 업무 범위가 컨텍스트입니다. 셋 다 값을 건네는 장치와는 상관이 없습니다.

대가 — 함수 인자에 드러나지 않아서 그 값이 어디서 들어왔는지 코드만 보고 알기 어렵습니다. 테스트할 때도 그 값을 미리 채워 둬야 합니다. 그래서 요청 번호·로그인한 사용자처럼 범위 전체가 함께 쓰는 값만 넣고, 함수 하나만 쓰는 값은 인자로 넘깁니다.

상세

먼저 「컨텍스트」가 가리키는 뜻들을 표 하나로 가릅니다. 그다음 백엔드 개발자가 만나는 세 뜻을 차례로 봅니다. 화면 코드, 서버의 요청 처리, 데이터베이스 세션입니다.

세 뜻은 한 줄기라서 같은 문제를 같은 방식으로 풉니다. 그 문제부터 짧은 함수 사슬로 보고 각 뜻으로 들어갑니다. 끝에서는 이름만 같은 뜻 셋과, 어느 뜻인지 가르는 단서를 모읍니다.

컨텍스트가 가리키는 뜻들

맥락마다 컨텍스트가 무엇을 가리키는지 아래 표에 모았습니다. 표의 아래쪽 세 줄은 뒤 소절에서 하나씩 풉니다. 표에 처음 나오는 낱말 둘만 먼저 풉니다.

첫 줄의 컴포넌트는 화면의 한 조각을 그리는 코드 단위입니다. 버튼 하나, 머리글 하나가 각각 컴포넌트입니다.

셋째 줄의 세션은 애플리케이션이 데이터베이스에 접속해서 끊을 때까지의 연결 하나입니다. 데이터베이스 서버는 접속마다 세션을 하나씩 따로 둡니다.

맥락 컨텍스트가 가리키는 것 예
화면 컴포넌트 트리 위쪽 컴포넌트가 놓아 두고 아래쪽이 꺼내 쓰는 값 화면 테마 · 로그인한 사용자
서버의 요청 처리 요청 하나를 따라다니는 값과 마감 시각 요청 번호 · 처리를 멈추라는 신호
데이터베이스 세션 접속 하나에 붙여 두는 이름과 값 접속한 사용자의 부서
운영체제 멈춘 작업을 다시 돌리려고 저장해 두는 계산 상태 멈춘 지점 · 계산 중이던 값
언어 모델 모델이 답을 만들며 한 번에 읽는 글 앞선 대화 · 지금 질문
도메인 주도 설계 한 용어가 같은 뜻으로 통하는 범위 주문 업무 · 배송 업무

위의 세 줄은 한 줄기입니다. 셋 다 범위를 하나 정합니다. 그 범위에 값을 붙여 둡니다. 범위 안 어디서든 그 값을 꺼내 씁니다.

아래 세 줄은 이름만 같습니다. 위의 뜻을 알아도 아래 뜻은 짐작이 안 됩니다. 그래서 문서에서 컨텍스트를 만나면 어느 분야 이야기인지부터 봐야 합니다.

일일이 넘기는 수고

요청을 처리하는 코드는 여러 함수가 차례로 서로를 부르며 내려갑니다. 컨트롤러가 서비스를 부릅니다. 서비스는 저장소를 부릅니다.

맨 아래 저장소 함수가 로그에 요청 번호를 남기려 한다고 해 봅시다. 그 값은 맨 위에서 요청을 받을 때 정해집니다. 그러니 값이 저장소까지 내려와야 합니다.

가장 단순한 방법은 함수마다 인자로 받아 다음 함수에 넘기는 것입니다. 그러면 서비스는 요청 번호를 쓰지 않는데도 인자로 받아야 합니다. 넘길 값이 하나 늘면 사슬 위의 함수를 전부 고쳐야 합니다.

컨텍스트는 이런 값들을 한 묶음으로 범위에 붙여 둡니다. 중간 함수는 그 묶음을 모르거나, 묶음 하나만 받아 넘깁니다. 안을 들여다보는 것은 값을 꺼내 쓸 함수뿐입니다.

화면 컴포넌트 트리의 컨텍스트

화면 코드에서 이 문제가 어떻게 나타나는지, 컨텍스트가 그것을 어떻게 푸는지 React 코드로 봅니다.

React 같은 화면 라이브러리는 화면을 컴포넌트로 쪼갭니다. 컴포넌트 안에 컴포넌트를 넣어 트리로 쌓습니다. 부모 컴포넌트가 자식에게 값을 건네는 기본 통로가 props입니다. props 는 함수의 인자처럼 부모가 자식을 그릴 때 넘겨주는 값입니다.

트리가 깊어지면 앞에서 본 수고가 그대로 생깁니다. 맨 위에서 정한 화면 테마를 맨 아래 버튼이 써야 한다면, 사이의 컴포넌트들이 쓰지도 않는 테마를 props 로 받아 넘겨야 합니다. 이 수고가 흔히 말하는 prop drilling 입니다.

컨텍스트는 사이를 건너뜁니다. 위쪽 컴포넌트가 값을 놓아 두면, 그 아래 어느 깊이의 컴포넌트든 사이를 거치지 않고 꺼내 씁니다. 값을 놓는 쪽을 Provider 라고 합니다.

flowchart TD
    A["App · Provider 로 테마를 놓는다"] --> P["Page · 테마를 모른다"]
    P --> H["Header · 테마를 모른다"]
    H --> B["Button · 테마를 꺼내 쓴다"]
    A -. 컨텍스트로 건너뛴다 .-> B

그림의 실선은 컴포넌트가 자식을 그리는 관계입니다. 점선은 테마가 실제로 지나가는 길입니다. Page 와 Header 는 테마를 받지도 넘기지도 않습니다.

이것을 코드로 옮기면 아래와 같습니다. createContext 가 컨텍스트를 만듭니다. useContext 는 그 값을 꺼냅니다.

jsx
const Theme = createContext("light");

function App() {
  return (
    <Theme.Provider value="dark">
      <Page />
    </Theme.Provider>
  );
}

function Page() {
  return <Header />;
}

function Header() {
  return <Button />;
}

function Button() {
  const t = useContext(Theme); // "dark"
  return <button>{t}</button>;
}

Button 은 자기 위에서 가장 가까운 Provider 의 값을 읽습니다. 그래서 "dark" 를 얻습니다. 위쪽에 Provider 가 하나도 없으면 createContext 에 준 기본값 "light" 를 읽습니다.

서버 요청 처리의 컨텍스트

서버에서는 요청 하나가 범위입니다. 요청이 들어와 응답이 나갈 때까지 따라다녀야 할 값이 있습니다. 요청 번호, 로그인한 사용자, 그리고 언제까지 끝내야 하는지를 정한 마감 시각입니다.

이 값을 나르는 방식은 크게 둘입니다. 컨텍스트를 객체 하나로 만들어 함수마다 첫 인자로 넘기는 방식과, 요청을 맡은 스레드에 붙여 두는 방식입니다.

첫 방식의 대표가 Go 의 context 패키지입니다. 묶음 하나만 넘기므로 넘길 값이 늘어도 함수의 인자는 그대로입니다. 이 묶음에는 값뿐 아니라 마감 시각과 처리를 멈추라는 신호도 실립니다.

Go
func handle(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := context.WithTimeout(
		r.Context(), 2*time.Second)
	defer cancel()
	findOrder(ctx, "A-17")
}

func findOrder(ctx context.Context, id string) {
	db.QueryRowContext(ctx, query, id)
}

r.Context() 는 요청마다 하나씩 생기는 컨텍스트입니다. WithTimeout 은 거기에 2초 마감을 덧붙인 새 컨텍스트를 만듭니다. findOrder 는 그것을 첫 인자로 받아 데이터베이스 쿼리에 넘깁니다.

마감이 지나거나 클라이언트가 연결을 끊으면 컨텍스트에 멈추라는 신호가 켜집니다. 그 컨텍스트를 받은 쿼리도 신호를 보고 멈춥니다. 응답을 기다리는 사람이 없는데 쿼리만 계속 도는 일을 막는 장치입니다.

cancel 은 이 신호를 직접 켜는 함수입니다. defer cancel() 은 handle 이 끝날 때 cancel 을 부르라고 미리 적어 둡니다. 응답을 다 만들었으니 이 요청으로 시작한 일은 모두 멈추라고 알리는 것입니다.

둘째 방식은 자바 서버에서 흔합니다. 이런 서버는 요청 하나를 스레드 하나가 처음부터 끝까지 맡습니다. 그러니 요청의 값을 그 스레드에 붙여 두면 요청 내내 따라다닙니다.

스레드마다 따로 갖는 저장 칸을 스레드 로컬이라고 합니다. 같은 이름으로 꺼내도 스레드마다 제 값이 나옵니다. 그래서 동시에 도는 요청끼리 값이 섞이지 않습니다.

Spring Security 가 이 방식을 씁니다. 로그인한 사용자를 SecurityContext 에 담아 스레드 로컬에 둡니다. 그래서 컨트롤러 아래 어느 함수에서든 인자 없이 지금 사용자를 꺼낼 수 있습니다.

요청이 다른 서버로 넘어가면 컨텍스트도 함께 보내야 합니다. 요청을 추적하는 번호 같은 값을 요청 헤더에 실어 다음 서버로 넘기는 일을 컨텍스트 전파라고 합니다.

데이터베이스 세션의 컨텍스트

데이터베이스에서는 세션 하나가 범위입니다. 세션에 이름과 값을 붙여 두면, 그 세션에서 도는 쿼리와 함수가 언제든 꺼내 씁니다. 예는 Oracle 로 듭니다.

Oracle 에서는 이것을 애플리케이션 컨텍스트로 부릅니다. 먼저 이름 공간 하나를 만듭니다. 이름 공간은 여러 값의 이름을 한데 모아 부르는 이름입니다. 그 안에 속성 이름과 값을 짝지어 넣습니다.

값은 아무 코드나 넣지 못합니다. 이름 공간을 만들 때 정해 둔 코드만 값을 넣을 수 있습니다. 사용자가 자기 부서를 마음대로 바꿔 넣지 못하게 막는 장치입니다.

그 코드는 패키지 하나로 정합니다. 패키지는 데이터베이스 안에 저장해 두고 이름으로 부르는 저장 프로시저 여러 개를 한 이름으로 엮은 것입니다.

SQL
CREATE CONTEXT app USING app_pkg;

-- app_pkg 안에서만 부를 수 있다
DBMS_SESSION.SET_CONTEXT('app', 'dept', '20');

SELECT name FROM employees  -- 20번 부서만
 WHERE dept_id = SYS_CONTEXT('app', 'dept');

첫 줄이 app 이라는 이름 공간을 만들고, 값을 넣을 수 있는 패키지를 app_pkg 로 정합니다. 애플리케이션은 접속한 뒤 app_pkg 를 불러 이 세션의 부서를 '20' 으로 넣어 둡니다. 그 뒤 SYS_CONTEXT 로 그 값을 꺼낸 쿼리는 20번 부서 직원만 돌려줍니다.

이 값은 주로 접속한 사용자마다 볼 수 있는 행을 가르는 데 쓰입니다. 테이블마다 정책을 걸어 두면, 데이터베이스가 쿼리에 컨텍스트 값을 읽는 조건을 알아서 덧붙입니다. 이 장치가 행 수준 보안입니다.

PostgreSQL 에서는 세션 설정값에 이름을 지어 넣어 두고 current_setting 함수로 꺼내 읽는 방식으로 같은 일을 합니다.

한 줄기의 세 뜻이 치르는 대가

세 뜻 모두 값을 함수 인자 밖으로 뺍니다. 그래서 함수 머리만 봐서는 그 함수가 어떤 값에 기대는지 보이지 않습니다. 값이 틀렸을 때 어디서 넣었는지 찾으려면 범위의 맨 위까지 거슬러 올라가야 합니다.

테스트도 번거로워집니다. 함수 하나만 떼어 시험하려 해도 그 함수가 꺼내 쓸 컨텍스트를 먼저 채워 둬야 합니다.

무엇이든 넣을 수 있다는 점도 함정입니다. 아무 값이나 넣기 시작하면 컨텍스트는 범위 안 어디서나 읽고 쓰는 전역 변수와 비슷해집니다.

그래서 컨텍스트에는 범위 전체가 함께 쓰는 값만 넣습니다. 요청 번호, 로그인한 사용자, 마감 시각이 그런 값입니다. 함수 하나만 쓰는 값은 인자로 넘겨야 함수 머리에 드러납니다.

뜻마다 따로 치르는 대가도 있습니다. 화면 코드에서는 컨텍스트 값이 바뀌면 그 값을 꺼내 쓰는 컴포넌트가 모두 다시 그려집니다. 값 하나가 자주 바뀌면 화면 여러 곳이 덩달아 다시 그려집니다.

스레드 로컬에도 따로 치르는 대가가 있습니다. 서버는 흔히 스레드 풀을 씁니다. 요청마다 스레드를 새로 만들지 않고, 다 쓴 스레드를 다음 요청에 다시 맡기는 방식입니다. 그래서 요청이 끝날 때 스레드 로컬 값을 지우지 않으면 다음 요청이 앞 요청의 값을 읽습니다.

운영체제의 컨텍스트

여기부터는 줄기가 다른 뜻입니다. 운영체제는 CPU(Central Processing Unit, 중앙 처리 장치) 하나로 여러 작업을 번갈아 돌립니다.

작업을 멈출 때는 나중에 이어서 돌릴 수 있게 그 순간의 계산 상태를 저장해 둡니다. 이 상태가 운영체제에서 말하는 컨텍스트입니다. CPU 안의 작은 저장 칸인 레지스터의 값과, 다음에 실행할 명령의 위치가 여기 듭니다.

저장해 둔 상태를 바꿔 끼워 다른 작업으로 넘어가는 일이 컨텍스트 스위치입니다. 값을 건네는 장치와는 상관이 없습니다.

언어 모델의 컨텍스트

대규모 언어 모델은 앞의 글을 읽고 그 뒤를 이어 씁니다. 모델이 답을 만들 때 한 번에 읽는 글 전체를 컨텍스트라고 합니다. 앞선 대화, 지금 질문, 함께 붙여 준 문서가 여기 듭니다.

한 번에 읽을 수 있는 양에는 상한이 있습니다. 이 상한이 컨텍스트 창입니다. 양은 글을 잘게 쪼갠 단위인 토큰의 수로 셉니다.

도메인 주도 설계의 컨텍스트

도메인 주도 설계는 업무의 규칙과 용어를 코드 모델로 옮기는 설계 방법입니다. 여기서 컨텍스트는 한 용어가 같은 뜻으로 통하는 범위를 말합니다.

주문 업무의 「상품」은 가격과 재고를 담습니다. 배송 업무의 「상품」은 무게와 부피를 담습니다. 같은 낱말이라도 업무마다 담는 것이 다르므로 범위를 나눠 따로 모델을 둡니다. 이렇게 나눈 범위 하나를 경계 컨텍스트라고 합니다.

어느 뜻인지 가르는 단서

글에서 컨텍스트를 만나면 함께 나온 낱말을 보면 됩니다. 아래 표는 자주 같이 나오는 낱말과 그때의 뜻입니다.

함께 나오는 말 뜻
컴포넌트 · props · Provider 화면 컴포넌트 트리의 컨텍스트
요청 · 마감 시각 · 스레드 로컬 · 전파 서버 요청 처리의 컨텍스트
세션 · 이름 공간 · 행 수준 보안 데이터베이스 세션의 컨텍스트
CPU · 레지스터 · 컨텍스트 스위치 운영체제의 컨텍스트
언어 모델 · 토큰 · 컨텍스트 창 언어 모델의 컨텍스트
업무 · 용어 · 경계 컨텍스트 도메인 주도 설계의 컨텍스트

관련 항목

화면 컴포넌트 사이에서 컨텍스트와 함께 값을 나르는 수단

props · prop drilling · 상태 관리 · 전역 상태 · 컴포넌트 · React · Redux · 콜백

요청 하나를 따라 컨텍스트에 실리는 값과 그 저장소

요청 ID · 타임아웃 · 인증 · 스레드 로컬 · Spring Security · Go · 미들웨어

서버 사이로 컨텍스트를 넘기는 추적 도구

컨텍스트 전파 · 분산 추적 · 트레이스 · 스팬 · OpenTelemetry

데이터베이스 세션 컨텍스트를 담고 읽는 구조

세션 · 스키마 · 사용자 계정 · 롤 · 저장 프로시저 · 패키지 · 행 수준 보안 · Oracle · PostgreSQL

컨텍스트와 이름이 겹치는 다른 분야의 용어

문맥 교환 · 레지스터 · 프로세스 · 실행 컨텍스트 · 컨텍스트 창 · 토큰 · 대규모 언어 모델 · 경계 컨텍스트 · 컨텍스트 맵 · 도메인 주도 설계 · 영속성 컨텍스트 · 컨텍스트 손실 · 컨텍스트 매니저

컨텍스트를 함부로 쓸 때 생기는 문제

전역 변수 · 의존성 · 불필요한 리렌더링 · 스레드 풀 · 메모리 누수

다른 이름: context · 콘텍스트