Feature flags 적용 (1)

Gravatar for kjw0323@gmail.com
김정우, 양정윤, 이낙원재밌게 개발하자🍅
2022. 10. 26.

목차

들어가며

최근 수십명 규모로 빠르게 늘어난 팜모닝 조직에서는 개발 생산성에 문제가 생기고 있었습니다.

  • 여러가지 기능이 동시에 개발되면서 의존성이 생겼습니다. 이로 인해 브랜치 사이에서 의존성이 증가하고 코드 충돌이 길어졌습니다.
  • 큰 기능을 개발하면서 수명이 긴 브랜치들이 생기기 시작했습니다. 이는 코드 리뷰의 어려움을 가져왔고 코드 병합시 알 수 없는 두려움을 가지게 했습니다.

추가로 아래와 운영적 문제점도 있었습니다.

  • 신규 기능을 출시할 때 코드 배포와 출시가 동시에 진행되다보니 업무 내 시간에 배포하고 업무 외 시간에 출시를 하는게 힘들었습니다.
  • 신규 기능 출시에 문제가 있을 때 이를 되돌리려면 재배포를 해야해서 약 10분정도의 시간이 소요되었습니다.

개발자의 생산성 문제를 해결하기 위해서 최근에 Trunk Based Developement(이하 TBD)를 도입하였습니다. TBD는 트렁크(메인) 브랜치를 단일 소스로 하여 완벽하지 않은 기능을 부담없이 지속적으로 병합을 가능하게 합니다. 다만, 이 완벽하지 않은 기능을 비활성화하기 위해서 Feature flags(기능 플래그)의 필요합니다. 그래서 팜모닝팀에서는 이를 연구하고 적용하기로 했습니다.

소개

기능 플래그(혹은 기능 토글)는 코드의 변경 없이 설정만으로 시스템을 바꿀 수 있는 강력한 도구입니다. 아주 다양한 사용방법이 있고 이는 팀이 풀고자 하는 문제에 맞게 다양하게 분류할 수 있고 구현할 수 있습니다.

예시를 들어보겠습니다.

  • 팀에서 신규 기능A를 개발하고 출시를 했습니다.
  • 이를 출시 했는데 레이턴시가 너무 높고 치명적인 버그가 발견되었습니다.
  • 이 때, 서버를 롤백하는 것이 아닌 플래그(출시 토글)를 이용해서 기능A를 바로 비활성화 시킬 수 있습니다.

또 다른 예시입니다.

  • 팀에서 신규 기능B를 베타 서비스로 출시를 하려고 하는데, 실험적으로 베타 유저에게만 노출하고 싶습니다.
  • 이 때도 플래그(승인 토글)를 이용하면 베타 유저에게만 먼저 베타 기능B를 노출시킬 수 있습니다.

어떤가요? 현재의 팜모닝팀에 알맞은 필요성이라 생각을 했습니다. 그럼 기능 플래그에 대하여 조금 더 자세히 알아보겠습니다.

분류

대략 기능 플래그가 무엇인지는 알겠습니다. 여러 시나리오에 맞게 다양한 방식으로 플래그들을 구현할 수 있는데요. 마틴 파울러의 블로그에서는 피처 플래그를 4가지로 분류하였습니다. 그의 분류법에 따라서 각각에 대하여 간단히 살펴보겠습니다.

출시 토글(Release Toggles)

Continuous Delivery를 실행하고자 하는 팀을 위해 TBD를 가능하게 해주는 기능 플래그입니다. main 브랜치에 작업 중인 피처를 가질 수 있도록 해주고 언제든지 운영 환경에 배포할 수 있도록 도와줍니다. 출시 토글은 완전하지 않고, 테스트되지 않은 코드를 켜지지 않은 상태로 프로덕션 코드에 가지고 있을 수 있게 해줍니다.

실험 토글(Experiment Toggles)

실험 토글은 여러가지 변수 혹은 A/B testing 등에 활용될 수 있습니다. 시스템(응용 프로그램)의 사용자는 어떠한 집단에 위치되고, toggle router는 해당 유저가 속한 특정 코드에 속하게 됩니다. 각기 다르게 적용된 코드에 따른 유저 행동을 추적하여 팀에게 데이터에 기반한 의사결정을 할 수 있도록 합니다.

운영 토글(Ops Toggles)

운영 토글은 시스템 동작의 운영적 측면을 제어하는데 사용됩니다. 시스템 운영자가 필요에 따라 해당 기능을 비활성화하고 싶을 때 사용할 수 있고 새로운 기능을 출시할 때에도 사용하 할 수 있습니다. 대부분의 운영 토글은 수명이 짧고, 운영에 확신이 생기면 플래그는 폐기되는게 좋습니다.

승인 토글(Permission Toggles)

특정 사용자가 받는 기능이나 제품 경험을 변경하는데 사용되는 플래그입니다. 예를 들어 승인 토글을 이용하여 프리미엄 고객에게만 제공되는 프리미엄 기능 혹은 알파 기능, 베타 기능에 대하여 내부 유저나 베타 유저에게만 노출되도록 할 수 있습니다. 이는 몇가지 측면에서 Canary Release와 비슷하지만, 카나리 배포는 무작위 유저에게 노출되는 반면에 승인 토글은 특정 유저에게 노출이 됩니다.

적용

그린랩스의 팜모닝팀에서 당장 필요성이 있는 출시 토글부터 먼저 적용해보고자 했습니다. 이를 통해 배포와 출시를 분리하고 운영에 도움을 주고자 합니다.

설계

클라이언트에서 서버로 기능 플래그의 상태를 요청하고 활성화 상태에 따라해당 서비스를 노출할지 결정합니다. 그린랩스에서는 GraphQL을 사용하며 아래와 같이 스키마를 구성합니다.

1directive @feature(type: FeatureType!) on FIELD_DEFINITION
2
3enum FeatureType {
4 CHATTING
5 ...
6}
7
8type Feature {
9 type: FeatureType!
10 description: String!
11 active: Boolean!
12}
13
14type Query {
15 features: [Feature!]!
16}

설정값은 데이터베이스에 key-value 값으로 저장할 수 있으며 JSON 형식으로 아래처럼 값을 저장하고 GraphQL features 요청이 들어왔을 때 이를 그대로 응답합니다.

1[
2 {
3 "type": "CHATTING",
4 "active": false,
5 "description": "채팅"
6 },
7 ...
8]

구현

프론트엔드

1query Features {
2 features {
3 type
4 description
5 active
6 }
7}

프론트엔드에서는 Provider 를 작성하여 query 한 features field를 context에 담아주고, hook을 사용하여 원하는 기능 플래그의 Boolean값을 호출할 수 있도록 구현합니다.

1useFeatureFlag : RelaySchemaAssets_graphql.enum_FeatureTypebool
Provider 구현 예시
1type features = FeatureHelper_Query_graphql.Types.response_features
2
3type t = {features: array<features>}
4
5module Query = %relay(`
6 query FeatureHelper_Query {
7 features {
8 featureType: type
9 active
10 description
11 }
12 }
13`)
14
15let context = React.createContext(None)
16let useContext = () => React.useContext(context)->Option.getExn
17
18module Provider = {
19 let provider = React.Context.provider(context)
20
21 module CreateElement = {
22 @react.component
23 let make = (~children, ~queryRef) => {
24 let {features} = Query.usePreloaded(~queryRef, ())
25
26 let contextValue = React.useMemo0(() => {
27 features: features,
28 })
29
30 React.createElement(provider, {"value": Some(contextValue), "children": children})
31 }
32 }
33
34 @react.component
35 let make = (~children) => {
36 let (queryRef, loadQuery, _) = Query.useLoader()
37
38 React.useEffect0(() => {
39 loadQuery(~variables=(), ())
40 None
41 })
42
43 {
44 queryRef->Option.mapWithDefault(React.null, queryRef' =>
45 <CreateElement queryRef=queryRef'> {children} </CreateElement>
46 )
47 }
48 }
49}
50
51let useFeatureFlag = flagType => {
52 let commonContext = useContext()
53 commonContext.features
54 ->Array.getBy(feature => feature.featureType == flagType)
55 ->Option.mapWithDefault(false, feature' => feature'.active)
56}

기능 플래그가 달려 있는 기능의 경우, hook을 호출한 후 분기 처리를 통해 해당 기능이 노출될지 말지를 결정할 수 있습니다.

1@react.component
2let make = () => {
3 let chatFeatureFlag = FeatureHelper.useFeatureFlag(#CHATTING)
4
5 chatFeatureFlag
6 ? <Container/>
7 : React.null
8}

백엔드

프론트엔드에서 화면을 노출하는 것과 별개로 악의적 공격이나 타이밍 이슈에 의한 API 호출에 대비하여 플래그가 비활성화 상태일 때 잘못된 요청임을 알려줘야합니다. 어플리케이션 레벨에서 선언적으로 필터링 로직을 추가하기 위해 GraphQL의 커스텀 directives를 이용하여 이를 정의하고자 했고 위의 스키마는 아래처럼 디렉티브를 달아주었습니다.

1type Query {
2 "@feature type이 active인 경우에만 정상 동작하고 아닌 경우 에러 응답"
3 chatChannels(postId: ID!, first: Int, after: String): ChatChannelConnection @feature(type: FeatureType.CHATTING)
4 ...
5}

GraphQL 필드에 @feature 디렉티브가 있으면 서버에서 플래그 상태를 검사하고 에러를 반환하도록 하였습니다.

1{
2 "data": null,
3 "errors": [
4 {
5 "message": "The FeatureType.CHATTING flag was inasctivated",
6 "locations": [
7 {
8 "line": 2,
9 "column": 3
10 }
11 ],
12 "path": ["chatChannels"]
13 }
14 ]
15}

효과

팜모닝팀에서는 빠르게 기능 플래그를 도입하였고 세가지의 효과를 가졌습니다.

  1. 배포와 출시를 분리하였습니다. 코드 배포는 되었지만 출시는 원하는 날짜와 시간에 할 수 있게 되었습니다.
  2. 버그 대응이 빨라졌습니다. 플래그가 달린 기능에 대하여 버그가 있을 때 1초 이내에 해당 기능을 비활성화 시킬 수 있게 되었습니다.
  3. 버전 관리가 더 간편해졌습니다. 기존에는 큰 기능을 개발할 때 git에서 수명이 긴 브랜치가 생길 때도 있었는데, 현재는 빠르게 병합할 수 있는 환경이 되었습니다.

팜모닝의 사용자수는 점점 늘어나고 있어 유저 유형의 세분화가 필요해지고 있습니다. 그에 따라 사용자의 행동 패턴에 따른 맞춤 서비스를 제공할 예정입니다. 그래서 팜모닝팀의 다음 계획은 승인 토글을 도입하는 것입니다. 이를 위한 방법 중 하나로 알파, 베타, 혹은 프리미엄 서비스들을 선택적으로 특정 집단에만 제공하고 선택적으로 운영이 가능하게 하려고 합니다. 이에 대한 내용은 Feature flags 2부에서 뵙겠습니다 :)

참조


추천 콘텐츠