WEB/Django

[Django] Django-waffle을 이용하여 Feature Flag 사용하기

김쿸후 2021. 10. 27. 18:30

1. Feature Flag 란

Feature Flags are software switches that turn on or off a feature - usually in real-time, without needing to release a new version of the software. For instance, if you are shipping a new feature and you want to be able to control who gets to see your new feature and who shouldn’t, then you use Feature Flags to accomplish that.

 

   Feature toggle/ Feature switch 라고도 불리는 Feature Flag는 소프트웨어에서 새로운 버전의 배포없이, 원하는 기능을 실험해보고 배포할 수 있는 기능이다. 예를 들어 모든 api가 JWT토큰이 있어야만 응답을 하는 방식에서 토큰이 없어도 guest authorization을 사용할 수 있도록 바꿀 때, 많은 api가 한번에 변경되기 때문에, 내가 원하는 코드가 정상적으로 돌아가는지 확인하기는 쉽지 않다. 또한 test 서버를 따로 만들어 배포를 하여 확인한다해도, 이렇게 커다란 변화는 오류가 생겼을 때 rollback 하기도 쉽지 않을 수 있다. 

 

   이때 Feature flag 를 사용하면, 문제가 생겼을 때 지정해둔 Flag 를 False로 바꾸기만 하면 새로운 배포없이 간단하게 이전 버전으로 rollback 할 수 있다. 

 

 

2. Feature Flag 의 사용 방식

Feature Flag 는 다양하게 사용이 가능하다. 

  • 쉬운 A/B Test
  • 특정 사용자에게 특정 기능을 사용해보게 하는 것 ( 권한 설정 등) 
  • 새로운 기능이 문제가 생겼을 때 쉬운 비활성화  
  • 이전 기능의 점진적인 비활성화
  • 자동화 test가 구축되어 있는 경우, dev branch 등의 feature 브랜치 사용 없이 바로 main branch로 푸쉬한 뒤 테스트해보는 것이 가능하다. 
  • 최종 사용자나 product manager 가 해당 피처를 직접 사용해볼 수 있다. 

 

 

3. Feature Flag 의 장단점

Feature Flag 의 장점 

  • 배포없이 기능을 키고 끌 수 있다.
    • 문제가 생겼을 때도, 이전 release 버전으로 새로운 배포 없이 flag를 inactive 시키기만 하면 된다.
  • micro-service 구축에 도움을 준다. 
    • 마이크로 서비스는 프로젝트를 관리하기 쉽게 쪼개는데 이때, 서로 의존적인 마이크로 서비스들의 경우, 하나의 서비스를 수정할 때, 다른 서비스도 잠시 작동을 멈출 필요가 있다. 
    • 이때 Feature flag는 간편하게 해당 기능을 inactive 시킬 수 있어 종속성/ 의존성 관리에 도움을 준다. 
    • 즉, Flag가 꺼진 상태로 서로 종속적인 마이크로 서비스들을 배치한 후 flag 를 켜서 작동을 시킬 수 있는 것이다. 
  • 제품 관리자가 손쉽게 기능을 관리할 수 있다.
    • feature flag가 없을 경우, 특정 user 에게 어떠한 기능이나 권한을 제공하기 위해선 개발의 공수가 필요하다. 
    • 그러나 feature flag를 사용하면 제품 관리자도 admin을 통해 flag에 유저를 할당하고 해당 유저의 기능이 잘 작동하는지 확인할 수 있다. 

 

Feature Flag 의 단점

  • Flag가 많아질 경우  관리가 어렵다. 
  • Flag끼리 의존적일 경우 새로운 문제가 발생할 수 있다. 
  • 코드가 복잡해진다. 
    • Feature flag를 사용하기 위해선 flag 가 active 한 상태일 때와 flag가 inactive 한 상태일 때 모두 코드가 필요하다. 

-> 따라서 Feature Flag를 사용하기 위해서는 일정한 주기로 flag를 없앤 후 코드를 합치는 과정이 필요하다. 

Feature flag 의 간단한 도식화

 

 

4. Django에서 Feature Flag 사용하기

장고에서 사용 가능한 Feature Flag Tools

https://featureflags.io/python-feature-flags/

 

Python Feature Flags

Python Feature Flag Resources/Solutions LaunchDarkly Python Feature Flag SDK – LaunchDarkly An installable feature flag software development kit for Python apps.  This SDK harnesses the Launc…

featureflags.io

장고에는 다양한 feature flags를 위한 모듈이 있는데 이중에서 나는 오픈소스인 waffle을 사용해보겠다. 

waffle 공식 document : https://waffle.readthedocs.io/en/stable/index.html

 

Waffle 사용법

 

1. set up

1. waffle 설치하기

pip install django-waffle

 

2. settings.py 고치기

INSTALLED_APPS = (
    # ...
    'waffle',
    # ...
)

MIDDLEWARE = (
    # ...
    'waffle.middleware.WaffleMiddleware',
    # ...
)​

 

3. migrate하기 

4. Django-admin 가서 새로운 flag 만들기 

  • Name: flag 이름
  • Everyone:
    • True = flag 무조건 키기
    • False = flag를 사용할 사용자 범위 설정 가능 (0% 이면 flag 끄는 것)
    • Unknown = 특정 사용자가 flag 사용하기
  • Percent: 해당 flag로 영향을 받을 사람들의 비율
  • Superusers: superuser면 flag 활성화
  • Staff: staff 면 flag 활성화
  • Authenticated: 권한이 확인된 사용자면 flag 활성화
  • Groups: 이 flag를 사용할 특정 group
  • Users: 이 flag를 사용할 특정 user

 

2.  flag 사용

1. View 에서 사용하기 

 

1. waffle 에서 flag_is_active를 불러온 뒤, admin에서 만든 flag를 불러온다

from waffle import flag_is_active
 
def signup(request):
    send_verification_code_enabled = flag_is_active('sms_verification_code')​

2. flag 로 감싸지기 원하는 부분을 if 절로 감싸준다.

  if send_verification_code_enabled:
                phone_number = form.cleaned_data.get('phone')
                send_verification_code(phone_number)
                return render(request, 'signup.html', {'form': form})​

 

 

이렇게 설정해준 flag 는 django-admin에서 쉽게 사용자 비율을 조절하며 on-off 할 수 있다. 

 

 

5. 프론트엔드 파트에서 Feature Flag 활용하기

Firebase Remote Config

위에서 언급한 Django waffle 말고도 다양한 Feature flag가 존재한다.

나는 flutter를 이용하여 어플리케이션을 만들었기 때문에 연동이 간단한 firebae를 사용하였다. 

Firebase remote config는 어플리케이션이나 react 환경에서 손쉽게 feature flag 를 구축할 수 있다. 

 

1. Firebase Remote config 사용법

다음과 같이 콘솔에 들어오면 remote config 를 만들수 있다. 

매개 변수를 입력하여 제작한 뒤, Flutter로 들어와 원하는 기능에 feature flag를 감싸주면 된다. 

 

우선, Flutter 에서 firebase remote config를 사용하기 위해 pub.dev 에서 firebase_remote_config를 다운받아 주었다. 

flutter pub add firebase_remote_config

이후, dependency 를 설정해준다. 

dependencies:
  firebase_remote_config: ^0.11.0+2

이후 main function으로 들어가 원하는 부분에 remote config 로 감싸준다.

// ignore_for_file: require_trailing_commas
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MaterialApp(
      title: 'Remote Config Example',
      home: FutureBuilder<RemoteConfig>(
        future: setupRemoteConfig(),
        builder: (BuildContext context, AsyncSnapshot<RemoteConfig> snapshot) {
          return snapshot.hasData
              ? WelcomeWidget(remoteConfig: snapshot.requireData)
              : Container();
        },
      )));
}

class WelcomeWidget extends AnimatedWidget {
  WelcomeWidget({
    required this.remoteConfig,
  }) : super(listenable: remoteConfig);

  final RemoteConfig remoteConfig;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Remote Config Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Welcome ${remoteConfig.getString('welcome')}'),
            const SizedBox(
              height: 20,
            ),
            Text('(${remoteConfig.getValue('welcome').source})'),
            Text('(${remoteConfig.lastFetchTime})'),
            Text('(${remoteConfig.lastFetchStatus})'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          try {
            // Using zero duration to force fetching from remote server.
            await remoteConfig.setConfigSettings(RemoteConfigSettings(
              fetchTimeout: const Duration(seconds: 10),
              minimumFetchInterval: Duration.zero,
            ));
            await remoteConfig.fetchAndActivate();
          } on PlatformException catch (exception) {
            // Fetch exception.
            print(exception);
          } catch (exception) {
            print(
                'Unable to fetch remote config. Cached or default values will be '
                'used');
            print(exception);
          }
        },
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

Future<RemoteConfig> setupRemoteConfig() async {
  await Firebase.initializeApp();
  final RemoteConfig remoteConfig = RemoteConfig.instance;
  await remoteConfig.setConfigSettings(RemoteConfigSettings(
    fetchTimeout: const Duration(seconds: 10),
    minimumFetchInterval: const Duration(hours: 1),
  ));
  await remoteConfig.setDefaults(<String, dynamic>{
    'welcome': 'default welcome',
    'hello': 'default hello',
  });
  RemoteConfigValue(null, ValueSource.valueStatic);
  return remoteConfig;
}

 

References

더보기