wanfile_thumb[2]

Azure에서 NAS (SMB, CIFS) 사용하기

Azure의 초창기부터 제공되어오던 핵심 서비스 중에서 저장소가 있습니다. 저장소는 Amazon Web Service 등과 마찬가지로 BLOB 저장소, 큐 저장소, 테이블 저장소로 구성되어있습니다. Azure 가상 컴퓨터는 VHD 파일을 BLOB 저장소 상에 만들어 가상 컴퓨터를 부팅하고 실행 중 일어나는 데이터 읽기와 쓰기 작업을 BLOB 저장소 상의 VHD 파일과 동기화합니다.

높은 수준의 가용성을 보장받고, CDN 과의 연동 등을 생각하면 BLOB 저장소를 이용하여 파일을 관리하는 것은 여러모로 이상적입니다. 하지만 모든 경우에 대해 사용 가능한 옵션이 아니고, 기존에 SMB (CIFS) 형태로 개발해서 사용해왔던 온 프레미스 애플리케이션들은 이런 방법을 사용할 경우 완전히 애플리케이션을 새로 개발해야 하기 때문에 적절한 선택지가 되기 어렵습니다.

지금 소개해드릴 방법은 SMB (CIFS) 형태의 저장소를 사용하는 기존 애플리케이션을 손쉽게 Azure로 마이그레이션할 수 있는 방법입니다.

Azure 파일 저장소의 이해

Azure 파일 저장소는 BLOB 저장소와 다른 개념을 가지고 있습니다. BLOB 저장소는 “컨테이너”라고 불리는 파티션 안에 많은 갯수의 파일을 저장할 수 있으며, 파일의 이름에 경로 구분 문자인 ‘/’를 지정할 수 있어 Prefix Match 방식으로 계층적인 접근을 다룰 수 있었습니다. 그리고 모든 입출력은 HTTP 프로토콜만을 사용한다는 것이 특징이었습니다.

반면 파일 저장소는 “공유”라고 불리는 파티션을 할당하며, 이 파티션 내에 전통적인 파일 시스템의 디렉터리와 파일을 저장할 수 있습니다. 하나의 공유를 만들게 되면 최대 크기를 지정하게 되며, Azure 저장소 이름을 사용자 ID, 기본 액세스 키를 비밀 번호로 하는 계정이 자동으로 부여됩니다.

files-concepts
Azure 파일 저장소 컨셉

Azure 파일 저장소가 도입되기 이전에 이와 같은 컨셉을 구현하기 위해서는 Azure VM을 만들고, 추가 디스크를 부착한 다음, VM 간의 가상 네트워크를 만드는 등의 추가 절차를 거쳐 SMB (CIFS) 공유를 형성할 수 있었습니다. 하지만 설명에서도 대강 열거되어있듯이 많은 리소스를 투여해서 복잡하게 구성해야 했기 때문에 시간, 확장성, 경제적인 면에서 모두 비효율적이었습니다.

하지만 Azure 파일 저장소가 정식으로 릴리스됨에 따라 별도의 추가 VM이나 가상 네트워크 구성 없이 더 확실하고 견고하면서도 직관적인 SMB (CIFS) 서비스를 사용할 수 있게 되었습니다.

무엇이 더 좋아지는가?

Azure 파일 저장소를 사용하면 다음의 이점이 있습니다.

  • 적어도 LRS (로컬 중복 저장)와 GRS (지역 중복 저장) 복제 정책 중 하나의 혜택을 볼 수 있어 장애 대응에 탁월한 선택지가 될 수 있습니다. 최소 수준으로 LRS만을 택하더라도 세 벌 이상의 복제본이 동시에 관리되며, GRS는 LRS를 기반으로 지역 내 데이터센터간 비동기 복제도 같이 겸하게 됩니다.
    • 이 부분에 대한 자세한 내용은 https://azure.microsoft.com/ko-kr/documentation/articles/storage-redundancy/ 페이지를 참고하시면 좋습니다.
  • 관리 도구와 웹 API를 활용하여 파일 저장소 상에 파일을 다운로드하거나 업로드하거나 관리할 수 있습니다.
    • Cloudberry Azure Storage Explorer 등의 서드 파티 도구를 사용하면 VM 밖에서도 편리하게 웹 API를 사용하여 파일을 업로드하거나 다운로드할 수 있습니다.
    • 다만 파일 공유의 특성 상 VM 내에서 파일을 열고 있어 잠기는 경우 웹 API에도 동일한 상황이 나타나게 됩니다. 동시성에 민감하다면 이 부분은 주의를 요합니다.
  • ISP가 TCP/445 아웃바운드 연결을 차단하지 않는다는 전제 하에 (혹은 Azure 데이터센터로 VPN 연결을 만든 다음에) 클라우드 외부에서 SMB (CIFS) 방식으로 직접 접근하는 것도 가능합니다.
  • 리눅스 VM에서도 파일 저장소를 사용할 수 있습니다.
    • 이 부분에 대한 자세한 내용은 https://azure.microsoft.com/ko-kr/documentation/articles/storage-how-to-use-files-linux/ 페이지를 참고하시면 좋습니다.

Azure 파일 저장소를 실제로 사용해보기

Windows 가상 컴퓨터를 기준으로 Azure 파일 저장소를 실제로 사용해보도록 하겠습니다.

1단계: 저장소 계정과 공유 만들기

portal.azure.com으로 이동하여 저장소 계정을 새로 만들거나 기존 저장소 계정에 대한 블레이드를 다음과 같이 열고, 액세스 키 메뉴를 클릭합니다.

001

그 다음 저장소 계정 이름과 KEY 1의 값을 잘 기록해둡니다. 이이서 서비스 섹션의 파일 버튼을 클릭합니다.

002

도구 모음의 공유 추가 버튼을 누르면 새 공유 볼륨을 만들 수 있습니다. 적절한 이름과 크기를 지정하면 새로운 공유 볼륨이 만들어집니다.

003

2단계: VM에서 공유 폴더 접근 테스트하기

1단계에서 메모한 정보를 참고하여 접속을 테스트합니다.

  • 파일 저장소의 경로는 \\<저장소 계정 이름>.file.core.windows.net\<공유 이름> 으로 바꿉니다.
  • 접속 시 사용자 ID는 <저장소 계정 이름>을 사용합니다.
  • 접속 시 사용자 비밀 번호는 <KEY 1의 값>을 사용합니다.

그러면 VM 내에서 다음과 같이 폴더 창을 열고 파일 입출력을 테스트해볼 수 있습니다.

004

3단계: 사용자 계정을 추가하고 서비스 등에 연결하기

파일 저장소에 접근이 잘 되는 것을 확인하였다면 Windows 사용자 계정을 만들어 서비스 등의 시작 계정으로 지정하면 편리하게 사용할 수 있습니다.

 

정리

운영 중인 레거시 애플리케이션을 Azure 기반으로 마이그레이션하게 된다면, 여러 가지 고려 사항이 있을 수 있겠지만, 그 중에서 스토리지에 관한 부분은 다음과 같이 정리할 수 있습니다.

  • 파일 저장소의 경로는 \\<저장소 계정 이름>.file.core.windows.net\<공유 이름> 으로 바꿉니다.
  • 접속 시 사용자 ID는 <저장소 계정 이름>을 사용합니다.
  • 접속 시 사용자 비밀 번호는 <KEY 1의 값>을 사용합니다.
  • IIS나 NT 서비스 등에서 사용하기 위한 Windows 계정을 만들고 새로 지정합니다.

그리고 다음의 제약 사항은 마이그레이션 이전에 검토해야 할 수 있습니다.

  • 공유 볼륨을 열거하는 기능은 2016년 4월 현재 제공되지 않습니다. 즉, \\<저장소 계정 이름>.file.core.windows.net 으로 접근해서는 경로가 잘못되었다는 오류 메시지만 보게 됩니다.
    • 이와 관련하여, IIS에서 Azure 저장소 공유 볼륨에 대해 가상 디렉터리를 만들고 권한 체크를 하려고 시도하면 경로가 존재하지 않는다는 오류 메시지가 나타나게 되지만, 무시하셔도 됩니다.
disposable-cups-250x250

IDisposable 패턴의 올바른 구현 방법

.NET Framework에서 새로운 클래스를 만들 때 여러가지 메모리 관리 디자인 패턴과 기법을 적용할 수 있지만, 한시적으로 사용해야 할 필요가 있는 자원들을 묶어서 관리할 때에는 IDisposable 패턴을 적극적으로 활용하는 것이 매우 유용합니다.

하지만 생각보다 IDisposable 패턴을 제대로 구현해서 사용하는 것은 쉽지 않으며 잘못 구현하기 쉽습니다.

이 아티클에서는 IDisposable 패턴을 구현하는 몇 가지 일반적인 전략들을 소개합니다. 잘못 설명된 부분이 있거나 보충이 필요한 부분은 댓글로 피드백을 자세히 남겨주시면 적극 반영하겠습니다.

IDisposable 인터페이스에 대한 이해

IDisposable 인터페이스가 제공하는 Dispose 메서드는 명시적이고 코드 작성자가 직접 호출할 수 있는 finalizer로, 이 메서드가 불리면 가비지 컬렉터에 의하여 나중에 호출되는 finalizer의 역할을 대체하도록 되어있습니다. 물론, 메모리 상에 할당된 메모리 블록의 해제까지 건너뛴다는 의미는 아닙니다.

그리고 무엇을 Dispose 메서드에서 제거해야 하는지 기준을 세운다면 객체에 대한 소유 권한을 정의하는 것이 필요합니다. 적어도 다음의 경우에는 확실히 Dispose 메서드 내에서 정리가 되어야 합니다.

  • 해당 객체를 Dispose 하게 되면 외부에서 더 이상 사용하는 것이 의미가 없는 객체 (예를 들어 클래스 내부에서 사용하던 파일 입출력 관련 객체, 비동기 작업을 위하여 만들어 놓은 스레드 관련 객체)
  • 외부에서 전달받은 객체이지만 객체의 생명 주기를 위탁하여 관리하도록 지정한 객체 (예를 들어 StreamReader나 StreamWriter가 객체 생성 시 인자로 Stream을 받는 사례)

가장 기본이 되는 IDisposable 구현 패턴

.NET은 finalizer에 해당되는 멤버를 재정의할 수 있습니다. 하지만 finalizer가 언제 호출이 될 것인지 기약할 수 없으므로 이 finalizer를 대신하여 좀 더 이른 시기에 명시적으로 소멸자와 동등한 효과를 낼 수 있도록 만든 것이 바로 IDisposable.Dispose 메서드가 되겠습니다.

IDisposable 패턴을 처음 구현할 때에는 다음의 사항들이 핵심이 됩니다.

  • protected virtual void Dispose(bool disposing) 메서드를 추가합니다. sealed 클래스에 대해서는 private void Dispose(bool disposing)으로 바꾸어 정의합니다.
  • 객체가 dispose 처리가 이루어진 상태인지를 관리할 수 있는 boolean 필드를 하나 추가하고 이 필드는 기본값을 false로 설정합니다.
  • Dispose(bool disposing) 호출 시 다음의 로직을 구현합니다.
    • 만약 객체가 dispose 상태인 경우에는 함수를 종료합니다.
    • 객체가 dispose 상태임을 필드에 지정합니다. (true로 설정)
    • disposing 매개 변수의 상태와 관계없이 P/Invoke 등을 활용하여 메모리 할당을 받은 나머지 모든 리소스들에 대해 할당을 해제하는 코드를 추가합니다.
    • disposing 매개 변수가 true로 전달되는 경우는 명시적으로 Dispose 메서드를 호출한 경우이며, 이 때에 다른 모든 IDisposable을 구현하는 객체들을 Dispose 처리합니다.
  • IDisposable.Dispose 메서드 구현 시 Dispose(true)를 호출하고 finalizer 호출을 건너뛰기 위하여 GC.SuppressFinalize(this)를 호출합니다.
  • 소멸자에서는 Dispose(false)를 호출합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
	public DisposableSample()
	{ }
 
	~DisposableSample()
	{
		this.Dispose(false);
	}
 
	private bool disposed;
 
	public void Dispose()
	{
		this.Dispose(true);
		GC.SuppressFinalize(this);
	}
 
	protected virtual void Dispose(bool disposing)
	{
		if (this.disposed) return;
		if (disposing)
		{
			// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
		}
		// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
		this.disposed = true;
	}
}

IDisposable 객체의 컬렉션에 대한 소거

소켓, 스레드 풀, 혹은 커넥션 풀 같은 컬렉션을 객체 내부에 보관해야 하는 경우도 있습니다. 이러한 경우에도 컬렉션 내의 모든 IDisposable 객체에 대해서 정리를 하는 것이 필요합니다.

  • 컬렉션 내의 모든 요소가 IDisposable 인터페이스를 구현하고 있다면 Dispose(bool disposing) 메서드에서 disposing이 true일 때 정리를 하면 됩니다. 그렇지 않은 경우, disposing 매개 변수의 상태에 무관하게 정리합니다.
  • Dispose 작업 도중 새로운 항목이 추가되는 것을 방지하기 위하여 disposed 필드를 확인하도록 하는 코드를 추가해야 할 수 있습니다.
  • 컬렉션 내의 모든 요소를 배열에 복사하여 Immutable Collection으로 변환한 다음 하나씩 방문하여 Dispose를 진행하고, 최종적으로 컬렉션의 요소들을 모두 제거합니다.

다음은 코드 예시입니다.

public class DisposableSample : IDisposable
{
	public DisposableSample()
	{
		this.items = new List&lt;IDisposable&gt;();
	}
 
	~DisposableSample()
	{
		this.Dispose(false);
	}
 
	private bool disposed;
	private List&lt;IDipsosable&gt; items;
 
	public void Dispose()
	{
		this.Dispose(true);
		GC.SuppressFinalize(this);
	}
 
	protected virtual void Dispose(bool disposing)
	{
		if (this.disposed) return;
		if (disposing)
		{
			// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
			IDisposable[] targetList = new IDisposable[this.items.Count];
			this.items.CopyTo(targetList);
			foreach (IDisposable eachItem in targetList)
			{
				eachItem.Dispose();
			}
			this.items.Clear();
		}
		// .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다.
		this.disposed = true;
	}
}

Dispose 메서드 내의 예외 처리

만약 Dispose 메서드를 실행하는 도중에 예외가 발생한다면, CA1065의 지침에 따라 명시적인 Dispose 호출이었든 아니었든 예외를 전파하지 않도록 처리하는 것이 필요합니다. 다만 명시적으로 Dispose를 호출하면서 예외가 발생했다면 예외를 전파하지 않는 대신 적절한 예외 처리는 필요합니다.

이 부분에 대한 자세한 내용은 https://msdn.microsoft.com/ko-kr/library/bb386039.aspx 페이지의 내용을 참고하시면 도움이 될 것입니다.

컬렉션 내의 모든 요소들을 Dispose 하는 코드를 조금 더 보강하면 다음과 같이 고쳐쓸 수 있겠습니다.

public class DisposableSample : IDisposable
{
	public DisposableSample()
	{
		this.items = new List&lt;IDisposable&gt;();
	}
 
	~DisposableSample()
	{
		this.Dispose(false);
	}
 
	private bool disposed;
	private List&lt;IDisposable&gt; items;
 
	public void Dispose()
	{
		this.Dispose(true);
		GC.SuppressFinalize(this);
	}
 
	protected virtual void Dispose(bool disposing)
	{
		if (this.disposed) return;
		try
		{
			if (disposing)
			{
				// IDisposable 인터페이스를 구현하는 멤버들을 여기서 정리합니다.
				IDisposable[] targetList = new IDisposable[this.items.Count];
				this.items.CopyTo(targetList);
				foreach (IDisposable eachItem in targetList)
				{
					try { eachItem.Dispose(); }
					catch (Exception ex) { /* 예외 처리를 수행합니다. */ }
					finally { /* 정리 작업을 수행합니다. */ }
				}
				this.items.Clear();
			}
			try { /* .NET Framework에 의하여 관리되지 않는 외부 리소스들을 여기서 정리합니다. */ }
			catch { /* 예외 처리를 수행합니다. */ }
			finally
			{
				/* 정리 작업을 수행합니다.  */
				this.disposed = true;
			}
		}
		finally { /* 정리 작업을 수행합니다. */ }
	}
}

소유하고 있는 객체들에 대한 Dispose 또는 정리 작업들을 각각 try, catch, finally 블록안에 두어 예외가 발생하면 적절한 예외 처리를 할 수 있게 하고, Dispose 메서드 전체에 대해서 try, finally 블록 안에 두어 예외가 전파되지 않도록 하였습니다.

결론

.NET Framework 기반의 응용프로그램이 안정적으로 장시간 실행될 수 있게 만들어야 할 때 고려해야 할 요소들 가운데에서 가장 비중있게 다루어야 할 부분이 바로 메모리 관리입니다. IDisposable 인터페이스를 통한 명시적인 finalizer 호출은 적절하게 활용하면 응용프로그램의 메모리 관리를 단순하게 만드는데 큰 도움을 줍니다.