C# :: Lecture & TIPs

[C#] [TIP] Windows Forms 앱에서 개발 환경과 배포 환경의 기능 범위 구분하기

앱을 개발하다 보면 개발 과정에서는 가능한 모든 설정 값을 확인해야 하지만, 실제 사용자에게 배포하는 버전에서는 지나치게 크거나 실용성이 낮은 값을 제한해야 하는 경우가 있습니다.

이번에 개발 중인 이미지 및 신호 처리 앱에서도 이와 비슷한 상황이 있었는데요.

필터에 사용되는 커널 반경을 개발 과정에서는 1 부터 255 까지 자유롭게 선택할 수 있도록 구성했지만, 실제 배포 버전에서는 최대 반경을 64 로 제한할 필요가 있었습니다.

커널 반경이 64 라면 커널의 전체 너비는 다음과 같이 계산됩니다.

Kernel Width = (Radius × 2) + 1
= (64 × 2) + 1
= 129

따라서 반경 64 는 129 × 129 크기의 2 차원 커널에 해당합니다.

이 정도 크기라면 고해상도 과학 영상에서 배경 또는 조명 성분을 추정하거나, 영상에 포함된 저주파 경향을 확인하는 일반적인 실험 범위를 충분히 포함할 수 있습니다.

반면 반경 255 는 511 × 511 크기의 커널에 해당하므로, 2 차원 커널을 직접 처리하는 구현에서는 반경 64 와 비교해 커널에 포함되는 요소 수가 약 15.7 배 증가합니다. 필터의 구현 방식과 영상 해상도에 따라 실제 처리 시간과 메모리 사용량도 크게 늘어날 수 있습니다.

그렇다고 개발 중인 버전에서 최대값을 64로 완전히 고정하면 더 큰 커널을 시험하거나 경계 조건을 검증하기 위해 코드 또는 디자이너 파일을 반복해서 수정해야 하는 문제가 발생합니다.

따라서 다음과 같은 기준으로 선택 가능한 범위를 구분했습니다.

  • DEBUG 빌드는 항상 1 부터 255 까지 제공
  • RELEASE 빌드라도 Visual Studio에서 실행했다면 1 부터 255 까지 제공
  • Visual Studio 외부에서 독립적으로 실행한 배포용 .exe 는 1 부터 64 까지만 제공

이와 같은 구조를 사용하면 개발 및 검증 과정에서는 전체 범위를 자유롭게 사용할 수 있고, 실제 사용자에게 전달되는 실행 파일에서는 실용적인 범위만 한정하여 배포할 수 있습니다.

 

DEBUG 와 RELEASE 만으로 구분하기 어려운 까닭

개발용 기능과 배포용 기능을 구분할 때 가장 먼저 떠올릴 수 있는 것은 DEBUG 전처리 기호입니다.

#if DEBUG
    return 255;
#else
    return 64;
#endif

위와 같이 작성하면 Debug 빌드에서는 최대 반경 255 를, Release 빌드에서는 최대 반경 64 를 사용할 수 있습니다.

하지만 실제 개발 과정에서는 Release 빌드를 Visual Studio 에서 직접 실행하여 테스트해야 하는 경우도 비일비재합니다.

컴파일러 단에서 제공하는 코드 최적화 기능이 활성화된 상태에서 프로그램의 처리 속도를 확인해야 하거나, 실제 배포 설정과 동일한 조건에서 동작 여부를 검증하려면 Release 빌드로 전환한 후 F5 또는 Ctrl + F5 키를 눌러 실행하여야 정확한 검증이 가능합니다.

이때 빌드 구성만으로 기능을 구분하면 Visual Studio 에서 Release 빌드를 테스트하는 경우에도 배포용 제한이 적용되어 제대로 된 구분이 어렵습니다.

ComboBox 항목이나 최대값을 직접 임시로 수정할 수도 있지만, 테스트 전후로 매번 되돌려야 하고 잘못된 설정이 그대로 배포될 위험도 있어 불안했습니다. 그래서 Debug 와 Release 구분만으로 판단하지 않고, 다음 두 가지를 차례로 확인하기로 했습니다.

  1. 현재 빌드가 Debug 빌드인지
  2. Release 빌드라면 Visual Studio 에서 시작된 실행인지

 

실행 환경에 따라 최대 반경 결정하기

커널 반경의 최대값은 KernelRadiusMax 속성에서 결정하도록 구성했습니다.

private static int KernelRadiusMax
{
    get
    {
#if DEBUG
        return 255;
#else
        return IsDevelopmentExecution() ? 255 : 64;
#endif
    }
}

Debug 빌드에서는 별도의 런타임 검사를 수행하지 않고 항상 255 를 반환합니다.

Release 빌드에서는 현재 프로세스가 Visual Studio 를 통해 실행되었는지 확인합니다.

Visual Studio에서 실행된 것으로 판단되면 개발 및 검증 환경으로 간주하여 255 를 반환하고, 그렇지 않다면 배포된 실행 파일로 간주하여 64 를 반환합니다.

전처리 조건을 사용하였으므로, Debug 빌드에는 Visual Studio 실행 여부를 확인하는 추가 코드 자체가 포함되지 않습니다.

 

Visual Studio 에서 실행되었는지 확인하기

Release 빌드가 Visual Studio 에서 실행되었는지 여부는 두 가지 방법을 함께 사용하여 확인할 수 있었습니다.

#if !DEBUG
private static bool IsDevelopmentExecution()
{
    if (Debugger.IsAttached)
        return true;

    try
    {
        return
            Environment.GetEnvironmentVariable(
                "VisualStudioVersion") != null
            ||
            Environment.GetEnvironmentVariable(
                "VSAPPIDNAME") != null;
    }
    catch
    {
        return false;
    }
}
#endif

먼저 Debugger.IsAttached 속성을 사용하여 현재 프로세스에 디버거가 연결되어 있는지 확인합니다.

Debugger.IsAttached는 디버거가 현재 프로세스에 연결되어 있으면 true, 그렇지 않으면 false 를 반환합니다. 따라서 Visual Studio에서 F5 키를 눌러 디버깅과 함께 실행한 경우를 확인하는 데 사용할 수 있습니다.

if (Debugger.IsAttached)
    return true;

하지만 Visual Studio에서 Ctrl + F5를 눌러 디버거 없이 실행한 경우에는 프로세스가 Visual Studio에서 시작되었더라도 디버거가 연결되지 않는 경우가 발생하는데 이 때, Debugger.IsAttached 는 false 가 되므로 해당 단일 플래그만으로는 Visual Studio에서 실행한 Release 빌드와 외부에서 직접 실행한 배포 파일을 구분할 수 없습니다.

이를 보완하기 위해 프로세스 환경에서 다음 값을 추가로 확인하도록 구현했습니다.

Environment.GetEnvironmentVariable("VisualStudioVersion")
Environment.GetEnvironmentVariable("VSAPPIDNAME")

두 값 중 하나가 존재한다면 Visual Studio에서 시작된 개발 실행일 가능성이 있는 것으로 판단하여 개발용 범위를 유지합니다.

다만 환경 변수는 프로세스 시작 환경과 Visual Studio 버전 또는 실행 구성에 영향을 받을 수 있어, 검증은 보안 강화 용도나 라이선스 제한을 구현하는 용도로 사용하기보다, 개발 편의를 위한 실행 환경 판별 수단으로만 사용하는 것이 적절합니다.

해당 기능은 사용자가 같은 이름의 환경 변수를 직접 정의할 수도 있고, 실행 방식에 따라 예상한 변수가 전달되지 않을 가능성도 있기에, 별도의 코드 수정이나 불필요한 추가 작업 없이 개발 환경에서는 넓은 테스트 범위를 제공하고, 일반 배포 환경에서는 UI를 실용적인 범위로 정리함으로써 개발 편의성과 사용자 경험을 함께 확보하려는 취지와 목적을 가지고 구현되었습니다.

 

ComboBox 의 항목을 실행 환경에 맞게 제한하기

커널 반경을 선택하는 ComboBox에는 디자이너에서 미리 1 부터 255 까지의 항목을 입력해두었습니다.

 

 

배포 버전만을 위해 디자이너에서 항목 수를 줄이거나, 빌드할 때마다 디자이너 파일을 수정하는 방법은 비효율적인 방법이므로 사용하지 않았습니다. 대신 InitializeComponent() 가 완료된 직후 현재 실행 환경에서 허용되지 않는 값만 자동으로 제거되도록 했습니다.

private void ApplyKernelRadiusLimit()
{
    if (cbxKernelRadius == null)
        return;

    int max = KernelRadiusMax;

    for (int i = cbxKernelRadius.Items.Count - 1;
         i >= 0;
         i--)
    {
        string itemText = Convert.ToString(
            cbxKernelRadius.Items[i],
            CultureInfo.InvariantCulture);

        if (int.TryParse(
                itemText,
                NumberStyles.Integer,
                CultureInfo.InvariantCulture,
                out var radius)
            &&
            (radius < 1 || radius > max))
        {
            cbxKernelRadius.Items.RemoveAt(i);
        }
    }
}

현재 최대값이 255 라면 기존 항목이 모두 유지됩니다

반면 배포된 실행 파일에서 최대값이 64 로 결정되면 65 부터 255 까지의 항목이 ComboBox에서 제거됩니다.

항목을 제거하는 경우, 마지막 인덱스부터 앞으로 이동하며 제거를 수행하게 됩니다.

for (int i = cbxKernelRadius.Items.Count - 1;
     i >= 0;
     i--)

앞에서부터 순회하면서 항목을 제거하면 뒤에 있던 항목의 인덱스가 앞으로 이동하므로 컬렉션 내부의 일부 항목을 건너뛸 수 있습니다.

예를 들어 현재 인덱스 64 의 항목을 제거하면 기존 인덱스 65 의 항목이 64 로 이동하여 인덱스가 당겨집니다. 하지만 반복문의 인덱스가 다음 값으로 증가하면 새롭게 이동한 항목을 검사하지 않고 지나칠 수 있습니다.

마지막 항목부터 역순으로 제거하면 아직 검사하지 않은 앞쪽 항목의 인덱스가 변경되지 않으므로 안전하게 처리할 수 있습니다.

 

제한 적용 시점

ApplyKernelRadiusLimit() 메서드는 InitializeComponent() 호출 직후, ComboBox의 선택값을 설정하거나 저장된 설정을 불러오기 전에 실행하도록 했습니다.

public MainForm()
{
    InitializeComponent();

    ApplyKernelRadiusLimit();

    LoadSavedSettings();
}

호출 순서가 중요한 이유는 기존 설정에 최대 반경보다 큰 값이 저장되어 있을 수 있기 때문입니다.

예를 들어 개발 중 반경 128 을 선택한 상태에서 설정이 저장되었고, 같은 설정 파일을 사용하는 배포 버전을 실행했다고 가정할 수 있습니다.

배포 버전에서는 최대값이 64 이므로, ComboBox 항목을 먼저 제한하지 않고 저장된 값 128 을 적용하면 UI의 선택 상태와 실제 허용 범위가 일치하지 않아 ArgumentOutOfRangeException 이 발생할 위험이 있는데요, 보통 ComboBox 의 SelectedIndex 가 -1 (선택 없음) 로 풀려버리거나, 아무것도 표시되지 않는 공란 상태가 되어 사용자가 보기에 버그처럼 보이게 됩니다.

따라서 처리 순서를 다음과 같이 구성하는 편이 좋습니다.

  1. InitializeComponent()를 호출하여 디자이너 항목 생성
  2. 현재 실행 환경의 최대 반경 결정
  3. 허용 범위를 벗어난 ComboBox 항목 제거
  4. 저장된 설정값을 읽음
  5. 설정값을 현재 허용 범위로 제한
  6. ComboBox 선택값 적용

저장된 설정값에도 별도의 범위 검사가 필요합니다.

int radius = Math.Max(
    1,
    Math.Min(savedRadius, KernelRadiusMax));

ComboBox 에서 항목을 제거하는 것은 단순히 사용자가 UI 를 통해 범위를 벗어난 값을 선택하지 못하도록 하는 처리로, 설정 파일, 명령줄 인수 또는 다른 내부 경로에서 값이 들어올 수 있다면 실제 앱 내 기능을 실행하기 직전에도 범위를 다시 확인해야 하며, UI 제한만으로 데이터 유효성 검사를 대신해서는 안 됩니다.

 

디자이너 항목을 그대로 유지한 까닭

ComboBox 항목을 실행 시점에 새로 생성하지 않고 디자이너에서 1 부터 255 까지 미리 입력해둔 이유는 디자이너 화면에서도 전체 항목과 기본 선택값을 쉽게 확인하기 위함으로, 개발 환경과 배포 환경을 위해 서로 다른 디자이너 파일을 유지하면 디자이너 파일을 변경하거나, 항목을 추가하거나 기본 선택값을 변경하는 경우 두 가지 파일을 함께 수정해야 합니다.

반면 디자이너에는 가능한 전체 범위를 유지하고, 앱이 시작될 때 현재 환경에서 허용되지 않는 값만 제거하면 단일 파일의 디자이너 구성을 계속 사용할 수 있으며, 불필요한 수정 작업도 필요하지 않습니다.

정리하면,

  • 디자이너는 전체 기능 범위인 1 - 255 를 정의
  • 개발 환경에서는 전체 항목을 그대로 사용
  • 배포 환경에서는 시작 시 65 - 255 제거
  • 디자이너 파일은 빌드 구성에 따라 수정하지 않음

위와 같은 방식의 경우, 항목 구성이 단순하고 고정되어 있을 때 사용하기 편리합니다.

다만 선택 범위가 자주 변경되거나 항목 수가 상당히 많아진다면, 디자이너에 모든 항목을 저장하기보다 실행 시점에 허용 범위만큼 동적으로 생성하는 편이 구현 측면에서 보다 단순해질 수 있습니다.

 

DEBUG 빌드와 개발자 모드는 같은 의미가 아닙니다.

이번 구현 방식에서 유념하여야 하는 부분은 DEBUG 빌드와 개발자 모드를 같은 개념으로 사용하지 않았다는 것입니다.

DEBUG 는 컴파일 시점의 빌드 구성을 나타냅니다.

반면 개발자 모드는 다음 두 가지 실행 상태 모두를 의미합니다.

  • Debug 구성으로 빌드한 실행 파일
  • Release 구성이지만 Visual Studio에서 개발 및 검증 목적으로 실행한 프로세스

따라서 Release 구성이라고 해서 항상 배포 모드로 동작하는 것은 아닙니다.

반대로 Release 실행 파일을 Visual Studio 외부에서 직접 실행하면 일반 배포 환경으로 판단하여 최대 반경을 64 로 제한합니다.

이를 정리하면 다음과 같습니다.

빌드 구성실행 방식최대 반경
DebugVisual Studio F5255
DebugVisual Studio Ctrl + F5255
Debug.exe 직접 실행255
ReleaseVisual Studio F5255
ReleaseVisual Studio Ctrl + F5255 를 의도
Release.exe 직접 실행64
Release설치 후 실행64

Debug 실행 파일은 전처리 조건에 의해 무조건 전체 범위를 제공합니다.

따라서 Debug .exe 를 Visual Studio 외부에서 직접 실행하더라도 최대 반경은 255 입니다.

실제 사용자에게는 Release 빌드만 배포한다는 전제를 사용하는 구조로 이루어져 있습니다.

 

마치며

처음에는 Debug 빌드에서 최대 반경을 255 로, Release 빌드에서는 64 로 제한하는 단순한 전처리 조건만으로 충분할 것으로 생각되어 구현을 시작했으나, Release 빌드로도 실제 배포 이전에 Visual Studio에서 반복적으로 실행하고 검증해야 했기 때문에, 빌드 구성만으로 개발 환경과 일반 사용자 환경을 구분하는 것은 불편했습니다.

따라서 Debug 빌드는 항상 전체 범위를 제공하고, Release 빌드에서는 디버거 연결 상태와 Visual Studio에서 전달된 환경을 확인하여 개발 실행인지 판단하도록 구성했습니다.

그 결과 Visual Studio 에서 F5 또는 Ctrl + F5 키를 눌러 실행하는 경우에는 1 부터 255 까지의 전체 커널 반경을 사용할 수 있고, 외부에 배포하거나 설치한 Release 실행 파일에서는 1 부터 64 까지의 실용적인 범위만 표시할 수 있었습니다.

ComboBox의 전체 항목은 디자이너에 그대로 유지하면서 시작 시점에 허용되지 않는 항목만 제거하는 구조로, 빌드 구성을 변경할 때마다 디자이너 파일을 수정할 필요도 없어 디버깅 및 배포시에 효율적인 프로세스를 보장합니다.

다만, 본 게시글에서 소개한 기능은 라이선스나 보안 기능이 아니라 개발 편의를 위한 실행 환경 판별입니다. 앱 내에서 중요한 기능을 보호하거나 라이선스화 해야 하는 경우라면, 코드 서명, 라이선스 검증 또는 서버 측 권한 확인과 같은 별도의 방법이 필요합니다. 이번 사례처럼 개발 과정에서는 넓은 실험 범위를 유지하고, 배포 버전에서는 사용자가 실수로 비실용적인 값을 선택하지 않도록 UI 범위를 정리하는 용도로 사용한다면 효율적으로 배포 및 테스팅이 가능하여 적합한 기능입니다.

고맙습니다.

Leave a Reply

Discover more from Dream big, Achieve more.

Subscribe now to keep reading and get access to the full archive.

Continue reading