GDG Songdo 오픈소스 스터디 2기 회고 : Polly — PR부터 Merge까지

10월 1일에 GDG Songdo에서 진행된 오픈소스 스터디 2기에 참여하였고 스터디를 통해서 App-vNext/Polly: Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. From version 6.0.1, Polly targets .NET Standard 1.1 and 2.0+. (github.com)에 PR을 올리고 제가 올린 기능이 Merge 되었습니다.

PR이후 Merge까지는 여러가지 난관이 있었지만, 재밌는 경험이었고, 오픈소스 컨트리뷰팅이라는 하나의 벽을 깨게 된 계기가 되었습니다.

이 포스팅을 통해서 여러분들도 오픈소스에 참여해보고자 하는 생각이 드셨으면 좋겠습니다.

저는

이전까지는 오픈소스 PR을 한번도 해보지 않았고, 사실 오픈소스를 가져다 쓰는 정도였습니다. 보통 오픈소스를 사용하더라도 다른 사람들이 많이 사용하는 라이브러리를 사용하였고, 그냥 코드를 다 확인하지 않고 잘 돌겠지 하고 사용했습니다.

참여 계기

올해 여름에 GDG Songdo I/O Extended 2023(I/O Extended 2023 Incheon | Festa!) 행사에 참여하고 백엔드 세션에서 김인제님의 발표를 듣게 되었는데, 거기서 NETTY와 ARMERIA에 대해서 들었고, 올해 초 진행된 오픈소스 스터디 1기에 대해서도 듣게 되었습니다. 1기에서는 Redis에 대한 분석 및 발표를 진행했다고 들었습니다.

이후에 오픈소스의 코드를 분석하는데에 약간의 관심을 갖게 되었고 오픈 소스 코드를 분석하려고 시도해봤습니다. 하지만 소스가 공개되어 있다고 다 읽을 수는 없었습니다. 유명한 오픈소스는 이미 그만큼 시간이 지나서 양 자체도 방대해지고, 여러가지를 수용하게 되어 추상화 단계도 매우 깊게 구성되어 있습니다. 이런 소스들을 명확한 목적 없이 그냥 분석하는 것은 어려운 일이었습니다.

그래서 저는 이런 부분을 물어봐야겠다는 생각으로 2기에 참여하게 됐습니다.

스터디 내용과 스터디 목표

2기 스터디 또한 김인제님이 리드하였고, 스터디의 최종목표는 스터디 참여자 모두 각자 자유롭게 오픈소스를 선정하여 1개의 PR을 해보는 것 이었습니다.

사실 4주안에 PR을 올린다는 내용이 부담이 되긴 했지만, 준비하신 자료에 이런 내용이 있었습니다.

image

스터디 방식은 주 1회 온라인 미팅(10~30분) 동안 인제 님이 가이드를 제시해주고, 토요일에 모여서 한번 더 얘기 나누면서 진행하는 방식이었습니다.

Polly

제가 선정한 오픈소스는 Polly(App-vNext/Polly: Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. From version 6.0.1, Polly targets .NET Standard 1.1 and 2.0+. (github.com)) 입니다.

Polly는 Resilience 기능을 하는 라이브러리 입니다. .NET Foundation 프로젝트 소개(5): Polly - 📤 정보 공유 - 닷넷데브 (dotnetdev.kr)에 전반적인 소개가 나와있습니다. 요약하면 재시도, 회로(실행) 차단, 시간초과, 격벽 격리 및 대체와 같은 정책을 설정하고 특정 로직 실행중 문제 발생시 지정된 정책에 따라 동작 되도록 하는 라이브러리 입니다.

방대한 오픈소스는 어떻게 읽어야 할까요?

제가 가장 궁금했던 내용을 첫 온라인 미팅때 물어봤습니다. 이슈를 처리 하다보면 소스가 어떻게 구성되어있는지 자연스럽게 알게 됩니다. 이렇게 듣고 나니 이해가 됐습니다. 업무 적으로 새로운 소스를 받더라도 소스의 세세한 부분 하나하나 보기보단 업무를 처리해야 할 부분부터 보는 것이 일반적일 것입니다. 업무를 처리하다 보면 전체적인 흐름도 결국 알게 되죠. 오픈소스도 마찬가지였습니다. 전체적으로 보려면 지루해서 보다가 덮기 쉽지만, 이슈를 처리하는 목적을 갖고 이슈 중심으로 보게 되면 이슈와 연관된 내용부터 전체적인 흐름을 보게 됩니다.

이슈 선정

이슈가 6개가 있었는데 저는 사실 어떤 것을 시작 해야 할지 고르기가 어려웠는데, 인제님의 도움으로 CircuitBreaker의 중단시간을 동적으로 할당하는 내용에 대한 이슈를 선정하였습니다.

해당이슈는 한창 논의되다가 결국 처리가 되지않고 V8버전으로 넘어와서 올해 7월에 메인테이너중 한명이 설정옵션에 Func<Args,ValueTask> BreakDurationGenerator {get; set; } 이런식으로 넣으면 될 것 같다는 코멘트를 마지막으로 진행되지 않았습니다.

여기에 마지막으로 제가 이 이슈를 작업하겠다는 코멘트를 달았습니다.

처음엔 굳이 코멘트까지 달아야 하나 생각했지만, 나중에 다른분들의 케이스를 보니 같은 이슈를 다른 사람이 먼저 처리하거나 할 수도 있고 반드시 코멘트를 달아야 합니다.

분석

Polly의 소스 구조는 체계적이고 깔끔합니다.

다만 닷넷 통합이전의 문제 때문에 Polly와 Polly.Core로 나뉘어져 운영되는 것을 확인했는데 현재는 Polly.Core가 메인으로 사용되는 것으로 보입니다. 이슈에서 제시된 코드도 Polly.Core에 위치해있었고 저도 Polly.Core를 대상으로 작업을 시작했습니다.

우선 코드에서 기존의 고정된 BreakDuration이 어디서 할당되어 사용되는지 보는 것이 우선이었습니다.

public class CircuitBreakerStrategyOptions<TResult> : ResilienceStrategyOptions
{
//생략
	public TimeSpan BreakDuration { get; set; } = CircuitBreakerConstants.DefaultBreakDuration;
//생략
}

이런식으로 구성된 CircuitBreakerStrategyOptions을 확인할수 있었고, BreakDuration은 CircuitBreakerResiliencePipelineBuilderExtensions를 통해서 CircuitStateController로 주입된다는 것을 확인 했습니다.

이름만 가지고도 어느 클래스가 어떤 역할을 할지 대략적으로 보여서 한번 확인 한 이후에는 찾기가 정말 수월했습니다.

이름 그대로 CircuitStateController에서 서킷브레이커의 상태를 관리하고 있었고 현재 HealthInfo 에 따라서 어떤 동작을 할지에 대한 내용은 AdvancedCircuitBehavior에서 체크가 가능했습니다.

다음은 상태가 변경되는 CircuitStateController.cs의 코드 중 Action 실패이후 진행되는 메서드입니다.

public ValueTask OnActionFailureAsync(Outcome<T> outcome, ResilienceContext context)
{
    EnsureNotDisposed();

    Task? task = null;

    lock (_lock)
    {
        SetLastHandledOutcome_NeedsLock(outcome);

        _behavior.OnActionFailure(_circuitState, out var shouldBreak);

        if (_circuitState == CircuitState.HalfOpen)
        {
            OpenCircuit_NeedsLock(outcome, manual: false, context, out task);
        }
        else if (_circuitState == CircuitState.Closed && shouldBreak)
        {
            OpenCircuit_NeedsLock(outcome, manual: false, context, out task);
        }
    }

    return ExecuteScheduledTaskAsync(task, context);
}

private void OpenCircuit_NeedsLock(Outcome<T> outcome, bool manual, ResilienceContext context, out Task? scheduledTask)
{
    OpenCircuitFor_NeedsLock(outcome, _breakDuration, manual, context, out scheduledTask);
}

private void OpenCircuitFor_NeedsLock(Outcome<T> outcome, TimeSpan breakDuration, bool manual, ResilienceContext context, out Task? scheduledTask)
{
    scheduledTask = null;
    var utcNow = _timeProvider.GetUtcNow();

    _blockedUntil = IsDateTimeOverflow(utcNow, breakDuration) ? DateTimeOffset.MaxValue : utcNow + breakDuration;

    var transitionedState = _circuitState;
    _circuitState = CircuitState.Open;

    var args = new OnCircuitOpenedArguments<T>(context, outcome, breakDuration, manual);
    _telemetry.Report<OnCircuitOpenedArguments<T>, T>(new(ResilienceEventSeverity.Error, CircuitBreakerConstants.OnCircuitOpened), args);

    if (_onOpened is not null)
    {
        _executor.ScheduleTask(() => _onOpened(args).AsTask(), context, out scheduledTask);
    }
}

해석하자면 회로차단기 Closed(초기)상태에서 ActionFailure 발생 시 _behavior를 통해서 회로차단을 진행할지 shourdBreak를 통해서 전달 받고 현재시간에 _breakDuation 만큼 더해서 _blockedUntil에 할당하고 회로 차단(Open)을 진행하게 됩니다.

Open 이후에는 설정된 _blockedUntil 이 되지 않았다면 Action을 수행하지 않고 Exception을 발생 시킵니다.

코드 수정 \& 첫 PR 개시

수정 내용은 단순합니다. _blockUntil이 변경되는 메서드에서 DurationGenerator가 null이 아니면 _breakDuration 대신 함수를 실행시키면 됩니다.

이슈에서 제시된 Func<BreakDurationGeneratorArguments,ValueTask\> 대신 Func로 만들었습니다.보다 단순하게 첫 PR으, ValueTask는 위 메서드의동기식 내부에서 사용될 것이라 불필요해보였습니다. Func의 int값은 메서드 실행시 인수로 제공되는 FailureCount 값입니다. FailureCount도 stateController에는 없고, _behavior의 HealthMetric을 통해서 프로퍼티를 통해서 가져오도록 설정했습니다. 추가적으로 UnitTest 작성하고 첫 PR등록을 진행했습니다.

Calculated break duration for Circuit breaker by atawLee · Pull Request #1715 · App-vNext/Polly (github.com)

Review~Merge

  • Func에서 전달하는 상태를 BreakDurationGeneratorArguments형태로 바꿔달라는 피드백이 있었는데 Args에 대한 부분은 추후에 확장성에서 문제가 된다는 내용이어서 동의하고 해당 부분을 int에서 Args로 변경했습니다.

    가장 고민을 많이 한 Review

  • BreakDurationGenerator Func의 반환타입을 ValueTask으로 해달라는 요청이 있었습니다.
  • OnActionFailureAsync는 반환형식이 ValueTask로 이미 비동기 진행되고 있었고, 이 메서드 내부에서 실행하는 OpenCircuit_NeedsLock 메서드는 동기식 메서드 였습니다. 실제 Duration값을 할당하는 OpenCircuit_NeedsLock 메서드가 동기식 메서드이기때문에 여기에 ValueTask를 비동기로 사용하려면 이 메서드를 사용하는 메서드까지 async 지정이 반드시 필요하기 때문에 여기서 고민을 많이 했습니다.
  • 결론은 단순했습니다. 그냥 이렇게 해달라고하는 메인테이너에게 예시코드를 요청하면 된다는 가이드를 인제님에게 들었고, 저도 혼자서 고민하는걸 멈추고 그냥 해당리뷰의 메인테이너에게 직접적으로 예시코드를 달라고 요청했습니다.
    #pragma warning disable CA2012
    #pragma warning disable S1226
              breakDuration = _breakDurationGenerator(new(_behavior.FailureRate, _behavior.FailureCount, context)).GetAwaiter().GetResult();
    #pragma warning restore S1226
    #pragma warning restore CA2012
    
    .GetAwait().GetResult();로 동기식으로 사용하는 내용을 답변으로 받았습니다. 고민한것에 비해서 답변이 꽤 허무했는데 GetAwait().GetResult() 형태로 받아서 사용하는 것은 지향되는 코드스타일은 아닙니다.

    문서화

    메인테이너중 한명은 안티패턴 문서를 넣어달라고 다음과 같은 코멘트를 달았습니다. Please also update the related anti-pattern under the circuit breaker docs.

이런것도 해줘야 하는구나 싶었지만 안티패턴에 BreakDuration을 조정하여 사용하지 말라는 내용이 있었고, 저는 거기에 "BreakDuration을 조정하려면 BreakDurationGenerator를 사용하세요" 라는 내용을 추가 작성했습니다.

그러나 해당 코멘트를 단 메인테이너는 해당 안티패턴 내용자체가 필요없을 것 같고, 직접 .md파일을 수정하면 안된다고 알려줬습니다.

Please do not edit code snippets in the markdown files directly.
  • They are copied over from a compile-able cs file.
  • In case of circuit-breaker.md the related code file is the /src/Snippets/Docs/CircuitBreaker.cs.

For more information please checkout the [Snippets readme](GitHub - App-vNext/Polly: Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. From version 6.0.1, Polly targets .NET Standard 1.1 and 2.0+.blob/main/src/Snippets/README.md).

저 Readme.md 파일을 읽어보면

dotnet mdsnippets

cli를 통해서 코드베이스에서 문서를 자동생성하는 도구를 사용하는 내용이 있었습니다. 처음 사용해봤는데 이렇게하면 문서상에서 컴파일오류가 발생할수 있는 코드는 제외되기때문에 문서를 어느정도 보장하는 도구로 사용할수 있을거 같아 좋은거 같습니다.

유닛테스트(DotCover)

대부분의 이슈를 처리하고 GitHub Actions에서 마지막 검증절차를 진행하였는데 여기서 Polly 프로젝트의 테스트 커버리지가 Minimum 100%인 것을 확인했습니다. 저도 평소에 유닛테스트는 지향하는 편이고(이걸로 닷넷데브에서 발표도 했습니다.) 유닛테스트가 가능한 코드를 만들도록 노력하지만 CI/CD에서 커버리지 미니멈 100 이라는 메세지를 보고 당황했습니다.

이전에 테스트 커버리지를 측정하는 도구가 있다는 것은 알고 있었지만 사용은 해보지 않았습니다. 필요한 부분에 대해서 커버리지 제한 없이 테스트를 만들어 활용하고 있었습니다. VisualStuio 자체적으로도 있을 것 같지만 저는 Rider Ultimate를 구독했고, DotCover를 사용할 수 있어서 사용했습니다. ReShaper 메뉴에서 UnitTest - Cover All Tests from Solutions 를 선택하면 전체 테스트를 진행하고 커버리지 보고서를 확인할 수 있습니다.


위 사진과 같이 원본 소스에 테스트가 진행되는지를 확인 가능합니다.
보고서를 토대로 부족한 부분의 테스트 코드를 작성했는데 DotCover에서는 HealthInfo 같은 Record init에 대한 테스트 커버리지 체크가 안되는 문제가 있었지만 실제로는 커버하고 있습니다.

병합 실수로 새 PR로 등록

  • 메인테이너가 문서내용을 추가했고, 그 과정에서 gitHub내에서 Sync 하다가 실수로 제 Fork Branch의 커밋들이 Discard 되었습니다. 그 과정에서 PR이 자동으로 Closed 되었고, 저는 로컬에 있는 소스를 기준으로 메인테이너가 수정한 문서의 내용을 병합해서 Conflict를 처리하고 다시 PR을 올렸습니다.

Calculated break duration for Circuit breaker by atawLee · Pull Request #1776 · App-vNext/Polly (github.com)

Merged

모든 유닛테스트 작성하고 Warning 처리를 모두하고 난 뒤 GitHub Actions 통과를 확인하였고 이후 두명의 메인테이너가 승인하여 Merge까지 완료되게 되었습니다.

martintmk left a comment Looks good. Great work and thanks for contributing :) ❤️3

소감

PR올린 이후 3주 정도 걸렸는데, Polly는 메인테이너 분들의 피드백이 빠르고 관리가 잘되어있는 오픈소스입니다. 저도 퇴근 후에 보는 것이라 피곤하기도 했지만, Starred가 12400개, Used By는 20900명 이상이 기록되어 있는 오픈소스이고 이렇게 파급력이 큰 소스에 내가 만든 코드를 기여할수 있다는 점이 상당한 매리트로 느껴졌고, 즐겁게 작업 할 수 있었습니다.

이번 PR경험으로 얻은 것들

  1. 우선 다른 오픈소스 기여자에게 제 생각과 코드를 공유할 수 있게 된 점이 가장 큰 것 같습니다. 오픈소스 메인테이너나 기여자들은 어마어마하게 실력이 높은 사람들 그리고 어려운 코드들 이라고 생각했습니다. 하지만 그들도 사람이고, 오픈소스를 통해서 돈을 버는 경우는 대부분 없기 때문에 메인테이너들도 완벽하게 소스를 운영하진 못합니다. 그러니 편하게 접근하는게 좋습니다. 아마 제가 막혔던 ValueTask문제에서 그냥 조금 덜 고민하고, 일찍 물어봤다면 더 빨리 Merge에 성공할수 있었을 것 같습니다.
  2. 두번째는 프로젝트를 구성하는 지식들을 많이 얻었습니다. Polly의 경우 라이브러리 프로젝트를 구성함에 있어서 어떻게 해야 사용자들이 더 편하게 보고 사용할수 있도록, 구조가 잘 잡혀져있는 소스이기 때문에 소스의 구조자체에서 배울수 있는 것들이 많았습니다.
  3. 사용경험 없는 도구들을 사용 업무 환경에서 써보지 않은 도구들 몇가지를 경험했습니다. 코드베이스 문서 자동화 툴이나 Moq외에 모킹용으로 사용할수 있는 NSubstitute 라이브러리, 이전에 업무환 경에서 사용해본적 없는 DotCover나 GitHub Actions 등에 대한 내용도 좋은 경험이 되었습니다.

오픈소스 기여를 추천 하는 이유

  1. 업무에 사용하는 코드에서 벗어나서 다른 사람의 코드와 오픈소스에서 사용된 툴과 라이브러리들을 접해볼수 있습니다.
  2. 오픈소스에 기여하는 개발자가 상대적으로 적기 때문에 개인 이력에 기여 경험에 대해서 추가할수 있는 점도 큰 매리트 입니다.
  3. 주변 환경에 따라서 개개인마다 개발 환경의 차이가 있을 수 있지만, 오픈소스에서는 누구나 좋은 소스에 기여 할수 있고, 좋은 개발자들과 커뮤니케이션 할 수 있습니다.

앞으로

이번에 진행하면서 깃허브의 소스를 볼때 이슈를 한번씩 훑어보는 습관이 생겼습니다. 저는 앞으로도 저 자신을 위해서 오픈소스에 기여하는 일을 틈이 나는대로 해볼 것 같습니다.

GDG Songdo 오픈소스 스터디 홍보

제가 참여한 스터디는 각자 1PR을 하는 것이 목표였고, 20명이 넘는 대다수의 분들이 해당 목표에 성공하셨습니다. 혼자서하면 진행하고 유지하기 힘든것들도 다른 사람들과 공유해서 한다면, 좀 더 흥미를 갖고 진행할 수 있는 것 같아서 혼자해보기 어렵다면 스터디에 참여하는 것도 추천합니다. 약 한달간 인제님은 20명이 넘는 인원들에게 가이드를 제시해주셨고, 그 가이드가 없었다면 저도 Merge까지 오는데 더 힘들었을 것 같습니다. 3기도 진행 예정인데 소식이 올라오면 공유하도록 하겠습니다. (선착순인 경우 올리기전에 끝날수도 있어요.)

GDG 송도 소개

GDG Incheon & Songdo with Flutter Songdo (notion.site) GDG 송도에서는 오픈소스 스터디 외에도 다양한 이벤트 및 행사들이 진행되고 있습니다.

오픈소스 스터디 리더 소개

13개의 좋아요