티스토리 뷰

프로그래밍

VB 닷넷 서비스 만들기

야라바 2016. 3. 29. 16:56


리눅스와 윈도우, 맥과 같은 요즘의 운영 체제는 대부분 한번에 여러개의 프로그램이나 프로세스를 실행시키는 멀티태스킹(Multi-tasking) 운영체제입니다. 멀티태스킹 운영체제에서 화면을 통해 사용자와 대화식으로 동작하는 콘솔 응용, 윈도우 폼 응용도 있지만 사용자 눈에 보이지는 않지만 백그라운드에서 동작하는 데몬(Daemon) 프로세스도 있습니다. 장치에 대한 처리를 수행하거나 일정한 예약시간에 프로그램 실행시키거나 하는 프로그램으로 윈도우에서는 이러한 데몬 프로세스를 서비스(Service)라 부르고 제어판>관리도구>서비스를 통해 관리할 수 있습니다.

위의 그림은 서비스 관리자의 목록 화면으로 일반적인 응용과 달리 시스템 구도 시점에 자동으로 시작시키는지 수동으로 시작시킬 지를 "시작 유형"을 통해서 지정할 수 있고, 서비스에 대한 시작, 중단과 함께 잠시 멈춤과 재가동 명령도 내릴 수 있습니다. 기존 프로그램과의 큰 차이점은 콘솔이든 창이든 사용자와 대화할 수 있는 인터페이스가 없다는 점입니다. 백그라운드에서 네트워크나 파일 시스템, 공유 자원에 대한 처리를 수행하고 그 과정은 통상 로그 파일로 남겨 놓습니다.

윈도우 폼 응용이나 콘솔 응용처럼 서비스 프로그램도 프로젝트 템플릿에서 선택하여 편리하게 만들수도 있지만 Visual Basic Express 버전에서는 "Windows service project" 템플릿을 지원하지 않으므로 본 글에서는 템플릿을 사용하지 않고 일단 콘솔 프로젝트로 시작하여 필요한 코드를 추가하여 윈도우 서비스 프로젝트로 전환시키는 과정을 밟겠습니다. 예제를 위한 서비스 프로젝트는 주기적으로 시스템의 CPU 로드를 기록하는 단순한 프로그램을 작성하는 것으로 합니다. 

새 프로젝트에서 일단 "콘솔 응용 프로그램" 템플릿으로 프로젝트를 생성합니다.

프로젝트 속성 창의 참조에서 [추가(A)...]버튼을 눌러 위의 그림과 같이 .NET의 System.ServiceProcess와 System.Configuration.Install 클래스에 대한 참조를 추가합니다.

Module Module1

    Sub Main()

    End Sub

End Module

이제 본격적으로 코드를 작성해야 하는데 위와 같은 기본 소스 파일인 Module1.vb를 새로운 프로젝트의 이름으로 변경하고 Module ...End Module 대신 System.ServiceProcess.ServiceBase를 상속하는 클래스 블럭으로 수정합니다. 필자의 경우에는 MyRestSvc.vb로 파일명을 바꾸고 아래와 같이 클래스를 작성하기 시작했습니다.

Public Class MyRestSvc
    Inherits System.ServiceProcess.ServiceBase

    Public Sub New()
        Me.ServiceName = "MyRestSvc"
        Me.CanStop = True
        Me.CanPauseAndContinue = True
        Me.AutoLog = True
    End Sub

End Class

서비스 클래스를 만들기 위해서는 위의 New() 처럼 꼭 작성해야 하는 몇가지 메소드가 있습니다. New() 메소드가 서비스 클래스의 기본 설정을 다룬다면 Main()를 작성해서 해당 서비스의 클래스를 등록해야 합니다.

    Shared Sub Main()
        System.ServiceProcess.ServiceBase.Run(New MyRestSvc)
    End Sub

위와 같은 Main() 메소드를 서비스 클래스 내에 작성합니다. 

    Protected Overrides Sub OnStart(ByVal args() As String)
        'process service start
    End Sub

    Protected Overrides Sub OnStop()
        'process service stop
    End Sub

    Protected Overrides Sub OnPause()
        'process pause
    End Sub

    Protected Overrides Sub OnContinue()
        'process pause and continue
    End Sub

    Protected Overrides Sub OnShutdown()
        'process system shutdown
    End Sub

위의 메소드들은 서비스의 상태 변화에 따른 처리 로직을 작성할 수 있는 메소드로 필요에 따라 적절하게 오버라이딩 방법으로 사용할 수 있습니다. 일단 이 상태로 프로젝트를 빌드해서 오류가 없는지 확인합니다.

현재의 코드를 디자인 모드로 보면 윈도우 폼 응용이 아니기 때문에 위의 그림과 같이 표시할 창은 존재하지 않습니다. 이상태에서 서비스 수행에 필요한 컨트롤을 추가합니다.

서비스 프로그램과 사용자 간의 가장 일반적인 소통 채널인 이벤트 로그 컨트롤을 추가하고 Log 타입은 Application, Source에는 프로젝트 이름을 기술합니다. 서비스의 수행 과정을 이벤트 로그에 남기는 구조입니다.

Imports System.Threading
Imports System.ServiceProcess

Public Class MyRestSvc
    Inherits System.ServiceProcess.ServiceBase

    Shared Sub Main()
        Dim ServicesToRun() As ServiceBase
        ServicesToRun = New ServiceBase() {New MyRestSvc()}
        System.ServiceProcess.ServiceBase.Run(ServicesToRun)
    End Sub

    Public Sub New()
        MyBase.New()
        InitializeComponent()
    End Sub
    Friend WithEvents EventLog1 As System.Diagnostics.EventLog
    Private components As System.ComponentModel.IContainer


    Private Sub InitializeComponent()
        Me.EventLog1 = New System.Diagnostics.EventLog()
        CType(Me.EventLog1, System.ComponentModel.ISupportInitialize).BeginInit()
        '
        'EventLog1
        '
        Me.EventLog1.Log = "Application"
        Me.EventLog1.Source = "MyRestSvc"
        '
        'MyRestSvc
        '
        Me.ServiceName = "MyRestSvc"
        CType(Me.EventLog1, System.ComponentModel.ISupportInitialize).EndInit()

    End Sub

    Private ServiceThread As Thread
    Private StopThread As Boolean = False

    Protected Overrides Sub OnStart(ByVal args() As String)
        EventLog1.WriteEntry("Starting ...", EventLogEntryType.Information)
        ServiceThread = New Thread(AddressOf DoWork)
        ServiceThread.Start()
    End Sub

    Protected Overrides Sub OnStop()
        EventLog1.WriteEntry("Stopping ...", EventLogEntryType.Information)

        StopThread = True
        ServiceThread.Join(TimeSpan.FromSeconds(20))

        If ServiceThread.ThreadState = Threading.ThreadState.Running Then
            ServiceThread.Abort()
        End If
    End Sub

    Private Sub DoWork()
        Dim cpuCounter As New System.Diagnostics.PerformanceCounter("Processor", "% Processor Time", "_Total")

        EventLog1.WriteEntry("Processing...", EventLogEntryType.Information)
        Do Until StopThread
            EventLog1.WriteEntry("CPU Load : " & cpuCounter.NextValue() & "%", EventLogEntryType.Information)
            Thread.Sleep(TimeSpan.FromSeconds(10))
        Loop
        EventLog1.WriteEntry("Stopped", EventLogEntryType.Information)
    End Sub

End Class

서비스를 시작하면 수행 로직은 별도 쓰레드로 가동시키고 주기적으로(10초) CPU 로드를 로그에 남기고 서비스를 중단시키면 쓰레드도 중단하는 로직은 위와 같습니다. 컨트롤을 디자인뷰에서 추가하면 해당 컨트롤에 관련한 코드는 InitializeComponent()에 자동으로 담기게 되므로 서비스가 생성되는 시점에 호출되는 New() 메소드에서는 InitializeComponent() 메소드를 호출하도록 코드를 추가해주면 됩니다.

지금까지의 코드가 서비스의 수행 과정을 다루었다면 남은 한가지는 서비스 설치와 연관된 코드를 추가해 주어야 합니다. 서비스 프로그램은 비주얼스튜디오에서 Run이나 Debug로 수행시킬 수 없고 서비스 관리자에 의해서 시스템에서 수행시키므로 서비스 등록하는 과정에서 필요한 정보를 제공하는 클래스를 추가해 주어야 합니다. 이 과정 또한 VS Express 버전이 아니면 디자이너 화면에서 컨텍스트 메뉴>Add Installer 메뉴로 간단하게 추가할 수 있지만 VS Express 버전인 경우에는 코드를 직접 추가해주는 과정이 필요합니다.

위의 그림과 같이 프로젝트에 클래스를 추가합니다.

Imports System.ComponentModel
Imports System.Configuration.Install

Public Class PrjInstall
    Inherits System.Configuration.Install.Installer

    Friend WithEvents ServiceProcessInstaller1 As System.ServiceProcess.ServiceProcessInstaller
    Friend WithEvents ServiceInstaller1 As System.ServiceProcess.ServiceInstaller
    Private components As System.ComponentModel.IContainer

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    Private Sub InitializeComponent()
        Me.ServiceProcessInstaller1 = New System.ServiceProcess.ServiceProcessInstaller
        Me.ServiceInstaller1 = New System.ServiceProcess.ServiceInstaller
        '
        'ServiceInstaller1
        '
        Me.ServiceInstaller1.ServiceName = "MyRestSvc"
        Me.ServiceInstaller1.StartType = ServiceProcess.ServiceStartMode.Automatic

        '
        'ServiceProcessInstaller1
        '
        Me.ServiceProcessInstaller1.Account = ServiceProcess.ServiceAccount.LocalSystem
        Me.ServiceProcessInstaller1.Password = Nothing
        Me.ServiceProcessInstaller1.Username = Nothing

        Me.Installers.AddRange(New System.Configuration.Install.Installer() {Me.ServiceProcessInstaller1, Me.ServiceInstaller1})
    End Sub


    Public Sub New()
        MyBase.New()
        InitializeComponent()
    End Sub
End Class

추가한 클래스는 위의 코드처럼 System.Configuration.Install.Installer를 상속하는 클래스로 작성합니다. 클래스 작성후에는 프로젝트를 빌드하여 오류 여부를 확인하여 정상적으로 빌드가 되었으면 추가한 서비스 설치 클래스를 디자인 뷰로 엽니다.

위의 서비스 설치 클래스 코드를 작성하면 위의 그림과 같이 디자인뷰에서 두개의 오브젝트를 확인할 수 있습니다. ServiceInstaller1의 경우 서비스명과 시작 방식등을 지정합니다. 시작 방식은(StartType) 서비스 관리자의 속성을 통해서도 수정할 수 있으며 자동(ServiceProcess.ServiceStartMode.Automatic), 수동(.Manual), 사용안함(.Disabled)으로 설정할 수 있습니다. 자동으로 설정하면 시스템을 부팅할때 자동으로 서비스를 시작하고, 수동으로 설정하면 서비스 관리자에서 수동으로 시작 지시를 내려야 합니다.

프로젝트가 정상적으로 빌드되었으면 시스템에 해당 서비스를 등록해야 합니다. 서비스 등록 및 해제는 닷넷 프레임워크 폴더인 C:\Windows\Microsoft.NET\Framework\v4.0.30319나 C:\Windows\Microsoft.NET\Framework\v2.0.50727에 있는 InstallUtil.exe라는 도구를 사용합니다. 

위의 그림은 빌드후 프로젝트의 bin\Release 디렉토리에서 InstallUtil.exe 도구를 활용하여 서비스를 등록하는 과정입니다.

위의 그림은 서비스가 정상 등록된 이후 서비스 관리자 화면에서 추가한 서비스를 확인하고 있는 화면으로 바로 시작 지시할 수도 있고 시스템을 재가동시켜 자동으로 서비스가 가동되는지 확인할 수도 있습니다. 서비스 관리자를 통해서 새롭게 등록한 서비스의 정보를 확인하면 InstallUtil.exe 도구가 별도 클래스로 생성했던 서비스 설치 코드를 통해서 시작 유형이라던가 하는 정보들을 자동으로 설정했음을 알 수 있습니다. 서비스 제거는 InstallUtil.exe 서비스프로그램명 /u 로 수행할 수 있습니다.

서비스 가동 상황은 서비스 관리자에서 확인할 수 있지만 자세한 이벤트 로그는 제어판>관리 도구>이벤트 뷰어에서 확인할 수 있습니다.

좌측메뉴 에서 이벤트 뷰어(로컬)>Windows 로그>응용 프로그램을 선택하면 위의 그림과 같이 서비스 프로그램에서 남긴 로그를 확인할 수 있습니다. 프로그램에서 10초마다 남긴 "CPU Load : ...%" 메시지를 확인할 수 있습니다. 새로운 로그는 우측의 "새로 고침"을 클릭하여 확인할 수 있습니다.


댓글
댓글쓰기 폼