Revit 애드인을 개발하던 중, 생성한 PathOfTravel 라인 중에 내 애드인에서 만든 라인만 제거하는 기능을 추가하게 되었다. 여러 방식으로 구현할 수 있겠지만, 싱글톤 패턴을 이용해서 내가 만든 라인들의 리스트를 관리하는 방식을 선택하게 되었다. 일종의 애드인 내 전역변수로의 활용인 셈이다. 이 과정에서 개념적으로만 알고 있던 싱글톤 패턴을 직접 구현하는 과정을 되짚어본다.

 

싱글톤 패턴이란?

싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스를 하나만 생성하도록 보장하는 디자인 패턴이다. 즉, 프로그램이 실행되는 동안 해당 클래스의 객체는 단 하나만 존재하며, 이 인스턴스는 어디서든지 동일하게 접근할 수 있다. 그럼 싱글톤 패턴은 언제 사용할까? 주로 전역적으로 접근 가능한 객체가 필요할 때(이번에 내가 사용한 경우이다.), 여러 객체가 공통의 상태를 공유해야할 때 등이 있겠다. 그럼 바로 구현 방법을 살펴보자.

 

싱글톤 패턴의 구현 (C# 예제)

기본 싱글톤 패턴 구현

public class Singleton
{
    // 정적 변수로 클래스의 유일한 인스턴스를 저장합니다.
    private static Singleton _instance;

    // 외부에서 인스턴스를 생성하지 못하도록 생성자를 private으로 설정합니다.
    private Singleton() 
    {
        // 생성자 내부에는 초기화 작업이 들어갈 수 있습니다.
    }

    // Singleton 인스턴스에 접근할 수 있는 정적 메서드를 제공합니다.
    public static Singleton Instance
    {
        get
        {
            // 인스턴스가 없으면 생성하고, 있으면 기존 인스턴스를 반환합니다.
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }

    // 예제 메서드: 싱글톤 객체가 하는 작업
    public void DoSomething()
    {
        Console.WriteLine("싱글톤 인스턴스에서 메서드 실행");
    }
}

사용 방법

class Program
{
    static void Main(string[] args)
    {
        // 싱글톤 인스턴스를 얻고 메서드 호출
        Singleton singleton1 = Singleton.Instance;
        singleton1.DoSomething();

        // 다른 곳에서 다시 싱글톤 인스턴스를 얻어도 같은 인스턴스가 반환됩니다.
        Singleton singleton2 = Singleton.Instance;

        // 두 인스턴스가 동일한지 확인
        Console.WriteLine(singleton1 == singleton2);  // True 출력 (같은 인스턴스)
    }
}

 

Eager Initialization vs Lazy Initialization

위에서 구현한 기본 예제는 멀티스레딩 환경에서 안전하지 않다. 여러 스레드가 동시에 싱글톤 인스턴스를 요청할 때, 두 개의 인스턴스가 생성될 위험이 있다. 이를 예방하기 위해 초기화 시기를 조절할 수 있는데, 초기화 시기에 따라 Eager Initialization과 Lazy Initialization으로 나눌 수 있다. 직역하면 빠른 초기화와 느린 초기화 정도가 되겠다. 멀티스레드 환경에서 안전한 싱글톤을 구현하기 위해서는 Eager Initialization 방식을 채택하여 클래스가 로드될 때 미리 인스턴스를 생성하여 멀티스레드 환경에서 안전하도록 한다. Lazy Initialization 방식은 메모리 자원 낭비를 막기 위해 필요할 때까지 생성을 최대한 늦추는 초기화 방법이다. 아래는 Eager Initialization 방식의 예시이다.

public class Singleton
{
    // 정적 변수에 미리 인스턴스를 생성해 둡니다.
    private static readonly Singleton _instance = new Singleton();

    // 생성자를 private으로 설정하여 외부에서 인스턴스 생성을 막습니다.
    private Singleton() 
    {
    }

    // 미리 생성된 인스턴스를 반환하는 정적 프로퍼티
    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("스레드 안전한 싱글톤에서 작업 수행");
    }
}

 

애드인에서 실전 사용 예시

내가 제작한 애드인에서 사용된 예시를 살펴보면, DrawCircle() 메서드로 특정 원을 그리고, 해당 원의 ElementId를 리턴으로 받아온다. 그렇게 생성한 원을 나중에 제거 기능을 위해 AddinStorage라고 하는 싱글톤 인스턴스에 리스트로 저장해 둔다.

원을 그리고 싱글톤 인스턴스에 저장하는 부분

ElementId circleId = DrawCircle(doorPoint); // 문 위치에 원을 그려 강조 표시하고 ElementId 저장
if (circleId != null)
{
    AddinStorage.Instance.AddCircleId(circleId); // 그린 원의 ElementId를 AddinStorage 싱글톤 패턴에 저장
}

private ElementId DrawCircle(XYZ center)
{
    double radius = 2.5; // 반지름 설정
    Plane plane = Plane.CreateByNormalAndOrigin(XYZ.BasisZ, center);
    Arc circle = Arc.Create(plane, radius, 0, 2 * Math.PI);
    SketchPlane sketchPlane = SketchPlane.Create(_doc, plane);
    ModelCurve modelCurve = _doc.Create.NewModelCurve(circle, sketchPlane); // 원을 ModelCurve로 그리기

    // **가시성 및 순서 조정** - OverrideGraphicSettings 적용
    OverrideGraphicSettings ogs = new OverrideGraphicSettings();
    Color red = new Color(255, 0, 0);
    ogs.SetProjectionLineColor(red); // 색상 설정
    ogs.SetProjectionLineWeight(9); // 선 두께 설정
    _doc.ActiveView.SetElementOverrides(modelCurve.Id, ogs); // 활성 뷰에서 모델 라인의 그래픽 설정을 덮어쓰기

    return modelCurve?.Id; // 그린 원의 ElementId 반환
}

싱글톤 패턴 클래스 AddinStorage.cs

public class AddinStorage
{
    private static AddinStorage _instance;
    private List<ElementId> _createdCircleIds;

    private AddinStorage()
    {
        _createdCircleIds = new List<ElementId>();
    }

    public static AddinStorage Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new AddinStorage();
            }
            return _instance;
        }
    }

    public List<ElementId> CreatedCircleIds
    {
        get { return _createdCircleIds; }
    }

    public void AddCircleId(ElementId id)
    {
        _createdCircleIds.Add(id);
    }

    public void ClearCircleIds()
    {
        _createdCircleIds.Clear();
    }
}

내 애드인에서 제작된 circle만 제거하는 remove 기능 부분

// 생성한 원 삭제
List<ElementId> circleIds = AddinStorage.Instance.CreatedCircleIds;
foreach (ElementId circleId in circleIds)
{
    doc.Delete(circleId);
}
AddinStorage.Instance.ClearCircleIds();

 

발견된 추가적인 문제점

이렇게 remove 기능으로 제거한 원을 Undo(Ctrl+Z) 기능으로 Revit에서 살려내면 제거된 원이 다시 살아난다. 그런데 문제점은 되살아난 해당 Element가 싱글톤 인스턴스의 리스트에 다시 복원되지 않아서 remove 기능을 다시 사용하고자 할 때 제거되지 않는 문제가 존재한다. 결국 AddinStorage 클래스의 ClearCircleIds() 메서드를 없애고, remove 부분에서도 AddinStorage.Instance.ClearCircleIds()를 실행하지 않는 것으로 해결하였다. 즉, 생성된 원들의 ElementId List는 삭제 뒤에도 계속 싱글톤 인스턴스에 남아있게 되는 것이다. 그렇다면 어디에선가는 Clear를 해 주어야 하는데, Clear는 새로운 원들을 그릴 때 Clear하는 것으로 수정하였다.

728x90

.msi 파일을 이용해서 revit 애드인 배포를 계획하고 있지만, 다른 애드인의 경우 .exe 파일 형식으로 배포하는 경우도 종종 존재한다. 그럼 .exe 파일과 .msi 파일은 어떻게 다르기에 다른 포맷으로 배포하는 것일까? 우선, .exe 파일과 .msi 파일은 모두 소프트웨어 설치를 위한 형식이지만, 설치 프로세스 관리와 구성 방식에서 중요한 차이점이 있다.

 

.exe 파일 (Executable File)

.exe 파일은 실행 파일로, 설치 프로그램뿐만 아니라 다양한 프로그램 실행 파일로 사용할 수 있다. .exe 파일의 특징은 다음과 같다.

  • 유연성: .exe 파일은 단순히 소프트웨어 설치를 넘어서 다양한 용도로 사용할 수 있다. 소프트웨어 설치를 위한 인스톨러를 실행하거나, 특정 명령을 수행하는 스크립트 또는 애플리케이션도 .exe 형식으로 제공된다.
  • 외부 스크립트 포함 가능: 설치 파일 내에서 외부 라이브러리, 설치 스크립트, 설정 파일 등을 포함하여 복잡한 설치 절차를 수행할 수 있다.
  • 복잡한 설치 과정 지원: .exe는 사용자 정의 스크립트 외에도 사용자로부터 여러 가지 입력을 받고, 다양한 소프트웨어 의존성을 확인하거나, 외부 서버에서 파일을 다운로드하는 등의 작업이 가능하다.
  • 인터페이스: 설치 프로그램의 사용자 인터페이스(UI)를 사용자 정의할 수 있다. 이로 인해 설치 과정에서 세부적인 제어와 더 많은 사용자 상호작용이 가능하다.
  • 동적 설치: .exe 인스톨러는 설치 중 추가 파일을 다운로드하거나, 실행 시 필요한 파일을 추출하는 동적 설치 방식을 사용한다.

 

.msi 파일 (Microsoft Installer File)

.msi 파일은 Windows Installer 패키지 파일로, 주로 Microsoft Windows에서 소프트웨어를 설치, 수정, 제거하기 위해 사용된다. .msi 파일의 특징을 정리하면 다음과 같다.

  • 구조화된 설치: .msi 파일은 Windows Installer 시스템을 사용하여 설치를 자동으로 처리한다. 이 시스템은 설치, 수정, 제거 등 설치 프로세스의 여러 단계를 자동화하고, 안정적으로 관리한다.
  • 표준화된 설치 방식: .msi 파일은 매우 표준화된 방식으로 소프트웨어를 설치한다. 설치 중 오류가 발생했을 때, 롤백이나 복구 같은 기능을 통해 안정성이 뛰어나다.
  • 의존성 관리: .msi 파일은 다른 설치 파일이 필요할 경우 의존성을 관리하거나, 소프트웨어 업데이트를 쉽게 처리할 수 있다.
  • 제어판 통합: .msi로 설치된 프로그램은 제어판의 프로그램 추가/제거 섹션에서 쉽게 찾을 수 있으며, 제거 또는 수정 기능을 간단하게 처리할 수 있다.
  • 기업 환경에서 관리: 많은 기업들은 .msi 파일을 사용하여 소프트웨어 배포 및 설치를 표준화하고, 자동화된 스크립트로 대규모 배포를 진행한다.

 

결론: 그래서 어느 상황에 어떤 파일 형식이 나은가

간단한 설치나 소규모 소프트웨어라면 표준화된 설치 방식과 안정적인 설치 및 제거 프로세스를 제공하기 때문에 .msi 파일을 사용하는 것이 좋다.

복잡한 설치 과정(ex. 여러 파일을 다운로드, 사용자 정의 스크립트 실행 등)이 필요하거나, 설치 UI를 커스텀해야 한다면 .exe 파일이 더 적합하다.

 

 

728x90

 

관련글

2024.09.24 - [🔨 개발/🏢 C# + Revit API] - Windows 운영체제 환경에서의 소프트웨어 배포: .exe와 .msi 파일의 차이

 

C# + Revit API 카테고리의 첫 글이 배포라는 점이 이상하긴 하지만, 회사에서 개발하던 애드인이 드디어 세상의 빛을 볼 때가 되었다. 비주얼 스튜디오에서 처음으로 Release를 선택해 빌드를 하니 기분이 이상하다. 이제 애드인 사용자들이 쉽게 애드인을 설치할 수 있도록 .msi 설치파일을 만들어 볼까 한다. WiX Toolset이라는 툴을 사용하였다.

 

WiX Toolset 설치 및 Visual Studio 프로젝트 생성

https://wixtoolset.org/

 

WiX Toolset

The most powerful set of tools available to create your Windows installation experience.

wixtoolset.org

상당히 올드해 보이는 홈페이지다. Download 메뉴에 들어가면 설치 방법이 나오는데, 그냥 cmd 창에서 커맨드를 입력해서 설치하고, 잘 설치되었는지 버전을 확인하자.

dotnet tool install --gloabl wix
wix --version

버전을 확인했더니 5.0.1+2f00cbe6 이라는 괴랄한 버전이 나온다. 아마 뒤에 달린건 커밋해시가 아닐까 감히 추측해본다. 그리고나서 비주얼스튜디오로 돌아가서 WiX 프로젝트를 만들면 된다고 하는데... 안뜨네? (이 방법으로도 Visual Studio에서 WiX 프로젝트를 생성할 수 있는 방법을 나중에 찾아봐야겠다.)

https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2022Extension

적당히 구글링해서 Visual Studio Marketplace에 있는 WiX v3 - Visual Studio 2022 Extension 이란걸 설치했다. 다시 새로운 프로젝트를 생성해보니 Setup Project for WiX v3 라는 템플릿이 뜬다ㅎㅎㅎ. 추후에 설명하겠지만 위의 Extension은 Visual Studio에서 WiX 프로젝트를 생성하기 위한 템플릿 파일만 제공하는 듯 하고, 진짜 알맹이는 나중에 다시 설치하는 과정이 나온다.

 

Product.wxs 파일 수정 및 빌드

프로젝트에는 Product.wxs라는 기본 설치 스크립트 파일이 덩그러니 존재한다. XML 기반의 문법을 갖고 있고 뭔가 주저리주저리 쓰여 있다. 대략적으로 다음과 같은 구조로 파일이 구성되어 있다.

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="*" Name="SAMOO_evacuationDistanceAnalysis" Language="1033" Version="1.0.0.1" Manufacturer="회사명" UpgradeCode="버전별GUID">

		<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />

		<Media Id="1" Cabinet="media1.cab" EmbedCab="yes" />

		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="CommonAppDataFolder">
				<Directory Id="Autodesk" Name="Autodesk">
					<Directory Id="Revit" Name="Revit">
						<Directory Id="Addins2023" Name="Addins">
							<Directory Id="Year2023" Name="2023">
								<Component Id="컴포넌트ID" Guid="컴포넌트GUID">
									<File Id="애드인 DLL ID" Source="빌드 완료된 .dll 파일이 존재하는 경로" KeyPath="yes" />
									<File Id="애드인 매니페스트 ID" Source="매니페스트 .addin 파일이 존재하는 경로"/>
								</Component>
							</Directory>
						</Directory>
					</Directory>
				</Directory>
			</Directory>
		</Directory>

		<Feature Id="ProductFeature" Title="제목" Level="1">
			<ComponentRef Id="컴포넌트ID를 한번 더" />
		</Feature>
	</Product>
</Wix>

 

몇 가지 설명을 추가하자면, Product 태그 내의 UpgradeCode와 Component 태그 내에 Guid가 있는데, Visual Studio의 도구 메뉴에서 GUID 생성을 통해 값을 집어넣으면 된다. 이 때, 두 GUID 값을 별개의 값으로 해주자. UpgradeCode는 프로그램 버전 간 업그레이드를 식별하기 위한 고유값이고, Component의 GUID는 설치된 파일을 고유하게 식별하기 위한 코드라고 한다.

Directory 태그를 통해 애드인 .dll 파일과 .addin 파일이 저장될 위치를 지정할 수 있는데, 태그를 여러 번 nested 해서 사용하기 싫다고 "CommonAppDataFolder\Autodesk\Revit\Addins\2023\" 이렇게 붙여 쓰면 빌드에 실패한다. 귀찮아도 한 스텝씩 내려가주자.

KeyPath는 WiX에서 컴포넌트의 핵심 파일 또는 리소스를 지정하는 데 사용된다. 설치된 구성 요소(컴포넌트)가 올바르게 설치되었는지, 수정, 복구할 때 중요한 기준이 되는 파일이나 리소스를 가리킨다. KeyPath는 .dll 파일 하나에만 설정해주자.

이런저런 설정을 마치고 Visual Studio에서 프로젝트를 빌드하면, 오류가 또 뜬다... WiX Toolset을 깔라는 내용이 또 뜨는데(위에서 설치한 Extension은 Visual Studio 템플릿만 설치해주나보다)

https://github.com/wixtoolset/wix3/releases/tag/wix3112rtm

 

Release WiX Toolset v3.11.2 · wixtoolset/wix3

WiX v3.11.2 is the latest recommended maintenance release of WiX v3.11; it contains mitigations in WiX v3.11 for a vulnerability affecting Microsoft.Deployment.Compression.Cab.dll and Microsoft.Dep...

github.com

나는 여기에서 .exe파일을 통해 설치했다. 그랬더니 빌드 잘 된다...

이제 프로젝트/bin/Release/ 에 생성된 .msi 파일을 더블클릭해서 설치를 테스트하면 끝.

728x90

+ Recent posts