6 분 소요

dash_icon

(6) 콜백(Callbacks) 간 데이터 공유

상태 공유란?


4장에서 콜백 함수에 대한 내용을 다루면서 콜백을 통해 전역 상태를 변경하는 것은 지양해야 한다고 기술했습니다. 그 이유는 Dash 앱의 세션이 여러개가 열렸을 경우 전역 상태(Global state) 변경이 이루어지면, 데이터 공유에 오류가 발생할 수 있기 때문인데요, 아래 예시를 통해 보겠습니다.

df = pd.DataFrame({
  'student_id' : range(1, 11),
  'score' : [1, 5, 2, 5, 2, 3, 1, 5, 1, 5]
})

app.layout = html.Div([
	dcc.Dropdown(list(range(1, 6)), 1, id='score'),
	'was scored by this many students:',
	html.Div(id='output'),
])

@app.callback(Output('output', 'children'), Input('score', 'value'))
def update_output(value):
    global df
    df = df[df['score'] == value]
    return len(df)


위 콜백은 처음 호출 될때는 올바른 데이터 프레임 df 을 반환하지만, 전역 변수로 선언된 df 가 수정되면, 해당 데이터 프레임을 사용하는 후속 콜백은 더이상 원본 데이터프레임을 사용할 수 없게 됩니다.

df = pd.DataFrame({
  'student_id' : range(1, 11),
  'score' : [1, 5, 2, 5, 2, 3, 1, 5, 1, 5]
})

app.layout = html.Div([
    dcc.Dropdown(list(range(1, 6)), 1, id='score'),
	'was scored by this many students:',
	html.Div(id='output'),
])

@app.callback(Output('output', 'children'), Input('score', 'value'))
def update_output(value):
    filtered_df = df[df['score'] == value]
    return len(filtered_df)


따라서 위와 같이 콜백 내부에서 전역 변수 변경이 이루어지지 않도록, 새로운 변수에 할당해서 사용해야합니다.


만약, 전역 변수나 상태를 콜백을 통해 꼭 변경하고 싶은 상황이라면 어떻게 해야 할까요? 이번 장에서 이 문제를 해결하기 위한 ‘콜백 간 데이터 공유 방안’ 을 다루겠습니다.


일반적으로 대용량의 데이터와 연동된 어플리케이션을 개발하게 되면, 데이터베이스 쿼리 작성, 데이터 다운로드, 시뮬레이션 실행과 같이 무거운 작업이 이루어지는 콜백이 있을 수 있습니다.


각 콜백 실행 때 마다 이 작업들을 실행하는 건 매우 비효율적일 수 밖에 없습니다. 그래서 Dash 에서는 이 무거운 작업을 하나의 콜백이 실행하고 결과를 나머지 콜백들과 공유할 수 있게 합니다. 여기에는 크게 두가지 방법이 있을 수 있습니다.

  1. 여러개의 구성 요소 Output 을 한번에 반환하는 하나의 콜백을 생성한다.
  2. 무거운 작업을 전담하는 콜백을 만들어서 다른 콜백 간 변수와 상태를 공유하도록 한다.


예를 들어, 사용자가 선택한 옵션 (날짜와 온도 표시 방식 - ℃/℉) 에 따라 데이터베이스에서 데이터를 쿼리한 다음 그래프와 표에 모두 표시하고 싶은 상황을 가정하겠습니다. 첫번째 방법은 데이터를 한번만 쿼리한 다음, 콜백의 Output 을 여러개로 두어서 구성 요소를 한번에 업데이트 하는 방법입니다.


가장 쉽게 생각할 수 있는 방법입니다. 하지만, 첫번째 방법은 만약 사용자가 옵션을 변경할 때마다 (ex. 온도 표시 방식을 바꾸면) 쿼리가 실행되는 상황이기 때문에, 완전히 효율적인 방식은 아닙니다.


두번째 방법처럼 데이터를 쿼리하는 콜백을 따로 두어서, 사용자의 옵션에 따라 나머지 콜백이 재실행되어도 무거운 쿼리 작업은 앱 실행에서 1회만 시행될 수 있게하는 것이 효율적인 방법입니다.


공유 데이터 저장


하나의 콜백이 실행한 결과 데이터를 다른 콜백들과 데이터를 안전하게 저장하고 공유하는 방법은 다음과 같이 세가지가 있습니다.

  1. dcc.Store 를 통한 사용자 브라우저 세션에 저장
  2. 디스크에 저장 (파일 또는 데이터베이스)
  3. 서버와 프로세스 간에 메모리 공유가 가능한 데이터베이스 사용 (ex. Redis 데이터베이스)


이 중에서 두번째와 세번째 방법은 간단하게 파일을 덮어 쓰는 방식이거나, Flask-Cache 통해 Redis 를 사용하는 특수한 방식이기 때문에, 여기서 다루지는 않겠습니다. 자세한 내용은 Flash-Cache 공식 문서 를 참고해 주세요.


사용자 브라우저 세션에 데이터 저장


데이터가 브라우저 세션에 저장되기 위해서는 모든 데이터는 JSON 이나 base64 로 인코딩 된 이진 문자열로 변환되어야 합니다.

이렇게 변환되어 브라우저 세션에 활용되는 데이터를 캐시(Cache) 라고 하는데, 캐시는 Dash 앱이 실행 중인 ‘현재’ 세션에서만 활용이 가능합니다. 만약 새 브라우저를 열거나, 다른 Dash 앱이 실행되면 이전에 저장했던 캐시는 사용할 수 없습니다.

아래는 Dash 에서 제공하는 dcc.Graph 컴포넌트를 사용해서 캐시를 중간 값(intermediate) 형태로 저장하고, 여러 콜백에서 입력 값으로 사용하는 예제입니다. 이렇게 하면 비용이 많이 드는 작업 (clean_data) 은 한번만 실행할 수 있습니다.

app.layout = html.Div([
    dcc.Graph(id='graph'),
    html.Table(id='table'),
    dcc.Dropdown(id='dropdown'),

    # dcc.Store 에 저장할 'intermediate-value'
    dcc.Store(id='intermediate-value')
])


@app.callback(Output('intermediate-value', 'data'), Input('dropdown', 'value'))
def clean_data(value):
     # 비용이 높은 작업을 정의 (slow_process_step)
     cleaned_df = slow_processing_step(value)
     # 데이터 프레임 -> json 포멧 변환
     return cleaned_df.to_json(date_format='iso', orient='split')


# dcc.Store 의 중간 값을 콜백 Input 으로 사용
@app.callback(Output('graph', 'figure'), Input('intermediate-value', 'data'))
def update_graph(jsonified_cleaned_data):
    dff = pd.read_json(jsonified_cleaned_data, orient='split')
    figure = create_figure(dff)
    return figure


# dcc.Store 의 중간 값을 콜백 Input 으로 사용
@app.callback(Output('table', 'children'), Input('intermediate-value', 'data'))
def update_table(jsonified_cleaned_data):
    dff = pd.read_json(jsonified_cleaned_data, orient='split')
    table = create_table(dff)
    return table


Dash의 무상태(Stateless) 속성


기본적으로 Dash 는 상태를 저장하지 않는 ‘무상태(Stateless)’ 속성으로 설계된 프레임워크 입니다. 클라이언트와 서버 관계에서 서버가 클라이언트의 상태를 보존하지 않는 것을 의미합니다. 최근 대부분의 웹사이트는 서버의 확장성이 높아, 대량의 트래픽 처리가 수월하다는 장점으로 Stateless 특성을 사용합니다.


Stateless 속성 때문에 만약 Dash 앱이 여러 Docker 컨테이너 환경에서 실행되는 상황이라면, Kubernetes 가 제공하는 로드 밸런싱 기능을 활용하는 것도 가능합니다.



본 포스트는 Dash 공식 가이드 를 참고하였습니다.

댓글남기기