728x90

C#에서 TextBox에 log4net을 띄우기 위한 방법.

TextBox가 위치한 Form의 생성자 안에서 TextBoxAppender.SetupTextBoxAppend 를 호출하면 된다.
인자값으로는 Textbox 인스턴스와 PatternLayout에 넣는 PatternFormat 문자열만 넣으면 된다.


TextBoxAppender.SetupTextBoxAppend(txtLogs, "%date{HH:mm:ss,fff} %-5level %-33logger - %message%newline");
namespace Logger
{
    public class TextBoxAppender : AppenderSkeleton
    {
        static public void SetupTextBoxAppend(TextBox textbox, string sLayerFormat)
        {
            TextBoxAppender textBoxAppender = new TextBoxAppender();
            textBoxAppender.AppenderTextBox = textbox;
            textBoxAppender.Threshold = log4net.Core.Level.All;
            ILayout layout = null;
            if (string.IsNullOrEmpty(sLayerFormat))
            {
                layout = new log4net.Layout.SimpleLayout();
            }
            else
            {
                PatternLayout layoutPattern = new PatternLayout(sLayerFormat);
                layout = layoutPattern;                                
            }            
            textBoxAppender.Layout = layout;
            textBoxAppender.Name = string.Format("TextBoxAppender_{0}", textbox.Name);            
            textBoxAppender.ActivateOptions();
            Hierarchy h = (Hierarchy)log4net.LogManager.GetRepository();
            h.Root.AddAppender(textBoxAppender);
        }


        private TextBox _textBox;
        public TextBox AppenderTextBox
        {
            get
            {
                return _textBox;
            }
            set
            {
                _textBox = value;
            }
        }

        override protected bool RequiresLayout
        {
            get { return true; }
        }

        protected override void Append(log4net.Core.LoggingEvent loggingEvent)
        {
            if (_textBox == null)
                return;
            try
            {             
                string sMessage  = base.RenderLoggingEvent(loggingEvent);
                _textBox.BeginInvoke(new WriteMessageHandler(WriteMessage), sMessage);
            }
            catch
            {

            }
        }

        private delegate void WriteMessageHandler(string sMessage);

        private void WriteMessage(string sMessage)
        {
                _textBox.AppendText(sMessage);                
                _textBox.Select(_textBox.TextLength - 1, 0);
                _textBox.ScrollToCaret();
        }
    }
}


728x90
728x90

프로젝트를 하다 보니, 자연스럽게 log4net을 사용하게 되었다.
Visual Studio의 IDE 기반으로 된 디버그 기능은 매우 훌륭해서, 브레이크 포인트를 잡고 하는 디버깅을 사용하면 보다 쉽고 편하게 할 수 있지만, 간혹 백그라운드로 동작하는 기능이나, 데이터 흐름의 끊김 없이 쭉 보고자 할 때는 역시 로그 기능은 필수이다.
대개 실행 프로그램 프로젝트( WinForm 프로젝트, 콘솔 프로그램 프로젝트 )의 경우 app.config 라는 XML 기반 설정 파일을 잘 구성해서 넣어주면 log4net을 바로 쓸 수 있다. 설정도 바로 app.config 내에서 수정하면 즉시 즉시 발동되니 편하다.
그런데, 이번 프로젝트는 DLL 기반의 프로젝트로, 해당 DLL은 외부 프로그램에서 Run을 해주게 된다. 이러다 보니, 외부 프로그램에서 log4net을 활성화 시켜주지 않는 이상 내가 만드는 c# dll 안에서는 도무지 로그를 쌓을 수 없었다.

고민 고민하다가, 이전에 만들어 놓았던, log4net용 관리 클래스를 꺼내봤는데, 생각보다 그다지 썩 좋지 않은 방법을 이곳 저곳을 써서 그대로 쓸 수 없었다. 게다가, 설정 기능을 이리저리 끊어놔서 생각보다 쉽지는 않았다.

그래서 아예 Log 기능을 활성화시키는데 주안점을 두고 정리해봤다.

아래의 코드가 log4net을 활성화 시키는 코드들이다.

// log4net의 기본 구조를 꺼낸다.
// log4net의 기본 구조를 꺼낸다.
log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
// log4net의 설정 기능을 활성화 시킨다.
hierarchy.Configured = true;

// log4net을 이용하여 로깅을 할 때, 어떤 방식으로 로그를 남길지를 설정한다.
// 파일, DB, 이벤트로그, 등등 다양한 위치에 로그를 쌓을 수 있는데,
// 아래의 예제는 RollingFile 기반 ( 일정 사이즈가 되거나, 날짜가 변경되면 자동으로 새로운 파일을 생성해서 로그를 
// 남기는 방식 )으로 구현했다.
// 만약 다른 방식으로 변경하려면, rollingAppender의 클래스와 설정을 변경하면 된다.
log4net.Appender.RollingFileAppender rollingAppender = new log4net.Appender.RollingFileAppender();
rollingAppender.File = @"C:\DEV\Log\cupps.log";
rollingAppender.AppendToFile = true;
// RollingFile 방식을 설정한다.
rollingAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Composite;
// 로그 파일 최대 크기를 설정한다.
rollingAppender.MaxFileSize = 1024 * 1024 * 2;
// 로그 파일 작성시, 동시 Write 방지 Locking Model 방식을 설정한다.
rollingAppender.LockingModel = new log4net.Appender.FileAppender.MinimalLock();
// 날짜 변경시 기존 로그 파일이름을 어떤식으로 변경할지에 대해서 설정한다.
rollingAppender.DatePattern = "_yyyyMMdd\".log\"";
// 로그 파일 내에 로그(한 줄)를 어떤식으로 기록할지에 대한 Format을 설정한다.
log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout("%date %-5level %logger - %message%newline");
rollingAppender.Layout = layout;
// 위에서 설정한 Appender 섲렁을 활성화시킨다.
rollingAppender.ActivateOptions();
// Logger에다가 위에서 설정한 Appender를 추가한다.
hierarchy.Root.AddAppender(rollingAppender);
// 여기까지가 Appender 추가 방법.

// 로그를 남기는 레벨 설정 ( INFO, DEBUG, WARN, ERROR, ALL 등이 있다 )
hierarchy.Root.Level = log4net.Core.Level.All;

이 활성화는 프로그램 맨 처음에 한번만 해주면 된다.

로그를 남기려면 클래스 맨 앞에 static 형태로 변수 하나 설정한 뒤, 그 변수를 이용하여 로그를 남기면 된다.
다음 예제 클래스가 그 용도다.

class Class1
{
    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    public Class1()
    {
        log.Debug("Enter Constructer~");
    }
}

static을 이용해 log 라는 변수를 생성한 뒤, 로그를 쌓고 싶은 곳에서 log. 이라는 것으로 시작하여 로그를 기록하면 된다. 매 클래스에서 저 static 변수를 모두 넣는 것이 좋다. 그 이유는 로그를 쌓을 때, 어느 클래스에서 발생된 로그인지를 바로 알 수 있기 때문이다.

앞서 로그를 설정할 때, PatternLayout 이라는 값을 설정했다.

 %date %-5level %logger - %message%newline 

이 값들 중, %logger 부분이 로그를 발생시킨 대상을 표시하는 영역이 되는데, %logger 에 들어갈 문장을 "System.Reflection.MethodBase.GetCurrentMethod().DeclaringType"를 통해 넣게 되는 것이다.
그러면 아래와 같은 로그가 쌓이게 된다.

2014-10-23 17:23:11 DEBUG MyApps.Class1 - Enter Constructor ~

"MyApps.Class1" 이라는 부분이 그 부분이다.

귀찮더라도, 매 클래스 마다 아래의 줄을 추가하면 최소한 그 클래스 내부에서 로그를 쌓을 때 편하게 로그를 작성해서 구성할 수 있게 된다.

private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); 
728x90
728x90

Visual Studio 안에 있는 Setup Project로 간단한 설치 프로그램을 만들곤 한다.
(복잡한 설치 구성이 필요 없는 경우 이 설치기능은 참 편하다)

그런데, 프로그램을 일부 수정해서 다시 말아서 배포하는 경우가 있다. 
이때  새로 만든 설치 본으로 설치하려면 다음과 같은 메시지가 떠서 상당히 곤혹스럽게 만들 때가 있다.
img20140115121930001

즉, 이미 다른 버전이 설치되어 있어서 설치가 불가능하므로, 이전에 설치된 버전을 삭제한 후, 다시 설치하라는 의미이다. 업그레이드와는 다른, 즉 이전 버전인지 신규 버전인지를 판단하지 못하는 경우를 의미한다.

그럼 이 문제는 어떻게 해결 해야 하는 걸까?
고민 중에 다음과 같은 문서가 있는 링크를 찾았다.

What are Upgrade, Product and Package Codes used for?

MSI를 구성할 때, 제품만의 고유한 버전을 위해 3가지 값을 사용한다.

Update Code, Product Code, Package Code.

이 세가지 값을 이용하여, 설치된 프로그램의 종류, 버전 등을 구분하여 제공하게 된다. 즉 프로그램 설치/삭제/수정 등의 모든 작업을 운영체제에서 처리할 때, 사용자들이 알아보는 문자열 대신 일종의 일련 번호로써 역할을 하게 되는 것이다. 여기서는 해당 하는 값들과, 그 역할들을 위의 링크에 있는 페이지 내용을 기반을 그대로 옮겨 적어 기록한다.

각 코드 값은 GUID라는 고유 값을 문자열 형태로 나타내도록 되어 있다.
( 예를 들면 {FB23FA93-3A96-4C91-B441-042F22C88BA4} 과 같은 값 )

 

MSI에서의 UpgradeCode란?

Upgrade Code란, 만든 Application을 대표하는 값을 의미한다. 같은 프로그램은 아니지만, 하나의 계열을 이루고 있을 때, 이 코드가 사용된다. 예를 들면 MS Office 제품군 과 같은 계열로 구성되었을 때 같은 제품이라는 의미를 갖게 해주는 것이다. Office 2010 을 예로 들었을 때, Office 2010을 설치하면 Word 2010, Excel 2010 이런 제품들이 설치되게 된다. 이것을 하나로 아우르는 표현을 할 때, Word 2010도, Excel 2010도 같은 Upgrade Code를 갖게된다.

만일 Upgrade Code가 달라지는 경우, 아예 프로그램이 다르다! 라고 인식하게 된다. 그러므로, 완전히 틀려지는 제품이 아닌 경우에는 이 코드의 값을 변경하지 말아야 한다.

MSI에서의 Product Code란?

Product Code란, 같은 프로그램인데, 언어나, 버전이 틀린 경우 그 구분을 위해 제공되는 값이다. 즉 프로그램이 업그레이드를 했거나, 언어가 다른 경우 이 코드 값을 달리해서 적용해야 한다. 이 코드 값을 이용해 실질적인 업그레이드나 추가 작업을 수행하게 된다.

MSI에서의 Pakage Code 란?

앞서 설명한 Upgrade Code 및 Product Code와는 다르게, MSI 파일 자체에 대한 구분을 위한 버전이다. 작은 설치용 프로그램인 경우에는 크게 문제가 없지만, 만일 여러가지 구성요소들을 구분지어 설치파일을 만든 경우 MSI 구성요소들이 달라지게 된다. 즉 각각의 설치 구성요소 별로 구분을 짓기 위해서 이 Package Code를 구성하게 된다.
이 부분은 프로그램의 업그레이드 보다, 패치와 같은 단위 구성요소의 업데이트를 할 때 이 값을 이용해서 설치파일을 구성하게 된다.

 

각 코드 값을 이용한 프로그램 추가/삭제 동작 정리

사실 단순하게 코드 값을 변경해서 이런 저런 설치 동작을 하면 쉽게 이해가 되긴 하지만, 의외로 Setup 프로그램을 만들어 매번 등록 삭제 하려면 정리가 쉽진 않다. 그래서 각 코드의 변경에 따라 어떻게 동작하는지를 정리했다.
(이 내용은 앞서 언급한 링크의 내용을 참고로 구성했다

  1. 같은 Product Code에 같은 Package Code로 된 MSI를 설치하려는 경우 “수정” 혹은 “삭제” 동작을 수행하게 된다.
  2. 같은 Product Code에 다른 Package Code로 된 MSI를 설치하려는 경우 실행되지 않는다. 이 경우 맨 처음 필자가 겪은 메시지창이 뜨면서 설치가 되지 않는다.
  3. 다른 Product Code에 같은 Package Code로 설치하는 경우 “수정” 혹은 “삭제” 동작을 수행하게 된다. 만일 RemovePreviosVersion이 True인 경우 기존 버전을 삭제하고 재설치를 수행하게 된다.
  4. 다른 Product Code에, 다른 Package Code로 설치하는 경우, 인스톨러는 완전히 다른 제품으로 인식해서 새로 설치하게 된다. (중복 설치)

즉 필자 처럼 단순 업그레이드로 구성하려면, Setup Project 옵션의 RemovePreviosVersions를 True로 한 뒤, 새로운 Product Code를 구성해야 한다.

일단, 자동 빌드 구성에 위의 내용을 기반으로 해보니, 별다른 오류 없이 제대로 설치가 되었다.
나중에 Patch 판을 만드는 방법도 고민해봐야 겠다.

728x90
  1. 방문자 2014.07.07 17:44

    좋은 글 감사합니다.

728x90

앞의 포스트에서 log4net의 동적 설정에 대해서 이런저런 이야기를 펼쳤다.

이 기준을 가지고 몇 가지 수정하면서 간단한 설정용 클래스를 만들었다.
물론 Rolling File에 대한 설정만 되는 간단한 구조이기 때문에 추후 지속적인 업데이트가 필요하다.
다만, 프로젝트들을 하면서 매번 코드 덩어리를 붙여넣기에 한계가 있어, 아예 클래스로 구성해봤다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace net.knoie
{
    public class LoggerSettings
    {
        private string m_sLogFullFilename = string.Empty;

        public  static LoggerSettings Current
        {
            get;
            private set;
        }

        public static void CreateInstance(string sLogFullFilename)
        {
            Current = new LoggerSettings(sLogFullFilename);
        }

        private LoggerSettings(string sLogFullFilename)
        {
            m_sLogFullFilename = sLogFullFilename;
            createLoggerSettings();
        }

        public int LogLevel
        {
            get
            {
                if (CurLogger.Level == log4net.Core.Level.Alert)
                    return 1;
                if (CurLogger.Level == log4net.Core.Level.All)
                    return 2;
                if (CurLogger.Level == log4net.Core.Level.Critical)
                    return 3;
                if (CurLogger.Level == log4net.Core.Level.Debug)
                    return 4;
                if (CurLogger.Level == log4net.Core.Level.Emergency)
                    return 5;
                if (CurLogger.Level == log4net.Core.Level.Error)
                    return 6;
                if (CurLogger.Level == log4net.Core.Level.Fatal    )
                    return 7;
                if (CurLogger.Level == log4net.Core.Level.Fine    )
                    return 8;
                if (CurLogger.Level == log4net.Core.Level.Finer    )
                    return 9;
                if (CurLogger.Level == log4net.Core.Level.Finest    )
                    return 10;
                if (CurLogger.Level == log4net.Core.Level.Info    )
                    return 11;
                if (CurLogger.Level == log4net.Core.Level.Log4Net_Debug        )
                    return 12;
                if (CurLogger.Level == log4net.Core.Level.Notice        )
                    return 13;
                if (CurLogger.Level == log4net.Core.Level.Off        )
                    return 14;
                if (CurLogger.Level == log4net.Core.Level.Severe        )
                    return 15;
                if (CurLogger.Level == log4net.Core.Level.Trace            )
                    return 16;
                 if (CurLogger.Level == log4net.Core.Level.Verbose)
                    return 17;
                if (CurLogger.Level == log4net.Core.Level.Warn)
                    return 18;
                return 14;
            }
            set
            {
                switch (value)
                {
                    case 0:
                        this.Level = log4net.Core.Level.Off;
                        break;
                    case 1:
                        this.Level = log4net.Core.Level.Alert;
                        break;
                    case 2:
                        this.Level = log4net.Core.Level.All;
                        break;
                    case 3:
                        this.Level = log4net.Core.Level.Critical;
                        break;
                    case 4:
                        this.Level = log4net.Core.Level.Debug;
                        break;
                    case 5:
                        this.Level = log4net.Core.Level.Emergency;
                        break;
                    case 6:
                        this.Level = log4net.Core.Level.Error;
                        break;
                    case 7:
                        this.Level = log4net.Core.Level.Fatal;
                        break;
                    case 8:
                        this.Level = log4net.Core.Level.Fine;
                        break;
                    case 9:
                        this.Level = log4net.Core.Level.Finer;
                        break;
                    case 10:
                        this.Level = log4net.Core.Level.Finest;
                        break;
                    case 11:
                        this.Level = log4net.Core.Level.Info;
                        break;
                    case 12:
                        this.Level = log4net.Core.Level.Log4Net_Debug;
                        break;
                    case 13:
                        this.Level = log4net.Core.Level.Notice;
                        break;
                    case 14:
                        this.Level = log4net.Core.Level.Off;                        
                        break;
                    case 15:
                        this.Level = log4net.Core.Level.Severe;
                        break;
                    case 16:
                        this.Level = log4net.Core.Level.Trace;
                        break;
                    case 17:
                        this.Level = log4net.Core.Level.Verbose;
                        break;
                    case 18:
                        this.Level = log4net.Core.Level.Warn;
                        break;
                }
            }
        }

        public log4net.Core.Level Level
        {
            get
            {
                return CurLogger.Level;
            }
            set
            {
                CurLogger.Level = value;
            }
        }

        public bool EnableRollingAppender
        {
            get
            {
                foreach (log4net.Appender.AppenderSkeleton appender in CurLogger.Appenders)
                {
                    if (appender is log4net.Appender.RollingFileAppender)
                        return true;
                }
                return false;
            }
            set
            {
                if (value)
                {
                    if (this.EnableRollingAppender == false)
                    {
                        string sLogPath = m_sLogFullFilename;
                        log4net.Appender.RollingFileAppender rollingAppender = CreateRollingFileAppender(sLogPath);
                        CurLogger.AddAppender(rollingAppender);
                        rollingAppender.ActivateOptions();
                    }
                }
                else
                {
                    if (this.EnableRollingAppender)
                    {
                        log4net.Appender.RollingFileAppender rollingAppender = null;
                        foreach (log4net.Appender.IAppender appender in CurLogger.Appenders)
                        {
                            if (appender is log4net.Appender.RollingFileAppender)
                            {
                                rollingAppender = (log4net.Appender.RollingFileAppender)appender;
                                break;
                            }
                        }
                        if (rollingAppender != null)
                            CurLogger.RemoveAppender(rollingAppender);
                    }
                }
            }
        }

        private log4net.Repository.Hierarchy.Logger CurLogger
        {
            get
            {
                log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
                return hierarchy.Root;
            }
        }

        private void createLoggerSettings()
        {
            try
            {
                log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
                hierarchy.Configured = true;
            }
            catch 
            {
            }
        }

        private log4net.Appender.RollingFileAppender CreateRollingFileAppender(string sLogPath)
        {
            log4net.Appender.RollingFileAppender rollingAppender = new log4net.Appender.RollingFileAppender();
            rollingAppender.File = sLogPath;
            rollingAppender.AppendToFile = true;
            rollingAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
            rollingAppender.LockingModel = new log4net.Appender.FileAppender.MinimalLock();
            rollingAppender.DatePattern = "_yyyyMMdd\".log\"";
            log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout("%date %-5level %logger - %message%newline");
            rollingAppender.Layout = layout;
            return rollingAppender;
        }
    }
}

이 코드를 사용하려면, log를 사용하기 전에 실행되는 위치에 다음과 같은 코드를 넣어주어야 한다.

string sLogPath = "C:\Temp\this.log";
net.knoie.LoggerSettings.CreateInstance(sLogPath);
net.knoie.LoggerSettings.Current.EnableRollingAppender = true;
net.knoie.LoggerSettings.Current.Level = log4net.Core.Level.All;

sLogPath 라는 부분이 있는데, 이 안에 로그파일이 생성되어야 할 위치를 설정해 주면 된다.

이 클래스와 테스트 프로그램은 첨부파일로 같이 첨부한다.
( 첨부된 예제는 Visual Studio 2010 으로 작성된 WinForm Application 이다.)


MyTestProject.zip


728x90
728x90

log4j 라는 Apache 프로젝트가 있다. ( http://logging.apache.org/log4j )
java 내에 동작하는 각종 Log 내용을 쌓기 위한 모듈인데, 내부적으로 어떠한 설계로 구성되었는지는 살펴보지 않아, 잘은 모르지만, 성능적 저하 없이 많은 양의 Log 관리에 훌륭하게 대처 할 수 있다. 또, Log로 남겨지는 형태를 단순 Text 파일 형식에서 부터 DB 파일까지 그 대응적 능력이 우수하다.

.NET 프로그램에서도 이 강력한 log4j를 사용할 수 있는데, 그 도구가 바로 log4net 이다. ( http://logging.apache.org/log4net/ )

여기에 쓸 내용은 지금까지 Application 내에 적용하면서 진행되었던 사항들을 적용 순서 대로 나열할 예정이다. 그래서 그 순서가 log4net에 대한 기초와는 다르다. 그러다 보니, 독자가 현재 적용 중인 Project나 구성과는 사뭇 다를 수 있으며, 스스로 잘못 이해한 부분으로 인한 오류도 있다.
더욱이 Web-Base가 아닌 WinForm-Base로 제작하면서 익힌 기능이다 보니, Web 기반의 구성과 일부 차이가 발생할 수 도 있다. 이점을 이해하면서 살펴보면 좋을 것 같다. 또, 이 글 외에도 많은 log4net에 대한 예제, 구성 이야기들을 살펴볼 수 있으므로, 사뭇 달라도, 자신의 적용과 유사한 다른 글들을 찾아보기도 쉬울 것이다.

 

log4net 접근 시작

최초에는 독자적으로 log 파일을 쌓고 구성했다. 그런데, 문제는 Single Thread 기반의 Application이다 보니, 로그 쌓는데, 의외로 많은 Process Time을 잡아 먹는 문제점이 발생했다. 로그는 좀 더 Detail 하게 쌓고 싶었지만, 그 성능적 문제로 인해 log를 쌓기에 두려움까지 얻게 되었다.

그러다가, java 관련 프로젝트를 수행하면서 log4j 에 대한 접근을 좀 하고 나니, log 쌓는 작업에 대한 대치성에 대해서 고민하게 되었고, 잠시 짬을 두어 log4net을 살펴보았다.

오픈 소스다 보니, 소스 자체를 공개하고 있으며, 다양한 Platform에 대한 컴파일이 이미 되어 있어, 사실 자신이 원하는 프로젝트에 대한 Platform에 맞추어 미리 컴파일된 버전을 바로 연결해서 사용가능 하다.
http://logging.apache.org/log4net/download_log4net.cgi 에서 다운로드 페이지 내에, Binaries 중,
log4net-1.2.13-bin-newkey.zip 을 다운 받았다. 그 안을 열면 여러가지 폴더가 있는데, bin 폴더에 들어가, 자신의 .NET Framework 버전에 맞추어 들어가면, log4net.dll 이 있는데, 이 파일만 꺼내오면 된다.

이제 자신의 Project 내에 저 파일을 복사( 개인적으로는 Solution 폴더 바로 아래에 Assembly 폴더를 만들어서 그 안에 복사) 해준다.

이제 자신의 프로젝트에 참조를 건다.

참조 건 뒤에 빌드 할 때 마다 파일이 복사되서 전달 될 수 있도록 Copy Local 이 True인지도 살펴본다.
(간혹 이 부분을 누락해서, 배포할 때, DLL이 빠져서 실제 이 배포본을 받는 사람은 실행이 안되는 문제가 발생하기도 한다.)

그리고 로그를 쌓는 로직을 작성한다.

그리고 이제 소스 상에서 다음과 같이 추가한다.

여기서는 제일 만만한 Form1.cs 파일을 열고 수정한다.

최초로 열면 아래와 같은 소스이다.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace MyTestProject
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

이제 이 소스를 다음과 같이 수정한다.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace MyTestProject
{
    public partial class Form1 : Form
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        public Form1()
        {
            log.Info("Form Init Start");    
            InitializeComponent();
            log.Info("Form Init End");
        }
    }
}

이제 프로젝트를 실행해보자.

분명 로그를 쌓으라고는 했으나, 로그가 쌓이지는 않는다. 당연하다.
로그를 쌓기 위한 log 개체는 만들었다.

private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

하지만, 위의 개체에다 아무리 로그를 쌓으라고 해도 어디다가 어떻게 쌓아야 될지에 대한 설정이 없으므로 쌓일리 없다.
즉 로그가 모두 무시되는 상황. 이를 위해서 설정을 해주어야 한다.

 

이 설정 방법은 크게 두 가지가 있는데, 하나는 정적인 방법이고, 다른 하나는 동적인 방법이다.

그 방법이 아래와 같다.

 

로그 쌓기 설정 그 방법 1.

구글을 통해 이런 저런 설정 관련 예제를 받으면 대부분은 .config 파일에다 그 설정을 넣는 방법을 제공한다.
즉 응용 프로그램 프로젝트의 경우 App.config 파일이 그 해당 파일인데 응용 프로그램 프로젝트를 최초로 만들 때는 없는 경우가 많다. 그 경우 새 항목을 추가해서 새로 만들어 주도록 한다.

이제 App.config 파일을 열어보면, 설정을 위한 XML 파일이 열린다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration?> 
</configuration?>

이 안을 다음과 같이 설정한다.

<?xml version="1.0" encoding="utf-8" ?>
<configuration?> 
  <configsections?>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /?>
  </configsections?>
  <log4net?>
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender"?>
      <file value="example.log" /?>
      <appendtofile value="true" /?>
      <maximumfilesize value="100KB" /?>
      <maxsizerollbackups value="2" /?>
      <layout type="log4net.Layout.PatternLayout"?>
        <conversionpattern value="%level %thread %logger - %message%newline" /?>
      </layout?>
    </appender?>
    <!-- Set root logger level to DEBUG and its only appender to A1 --?>
    <root?>
      <level value="INFO" /?>
      <appender-ref ref="RollingFile" /?>
    </root?>
  </log4net?>
</configuration?>

그리고 Properties 폴더를 열고, AssemblyInfo.cs 파일을 연다.

이제 맨 아래 줄에다가, 다음 줄을 삽입한다.

[assembly: log4net.Config.XmlConfigurator(Watch = true)]

앞에서 로그 쌓는 로직이 정상적으로 작성되어 있다면, 이제 다시 프로젝트를 실행해보자.

실행 한 뒤에 Build 결과물 폴더를 열어보면 example.log 라는 파일이 생성되는데 그 안에 보면,

Form Init Start와 Form Init End가 보일 것이다.

설정이야 어쨌던 로그가 쌓인다.

 

로그 쌓기 그 방법 2

대부분의 경우는 위의 경우에 다 처리 될 수 있다. (상세 설정은 추가적인 검색을 하면 상세한 설정이 가능하다.)
그런데, 이런 저런 프로젝트에 적용하다가 보니, 이 로그 설정을 동적으로 설정해야 할 경우가 발생했다.

즉 app.config를 이용해서 설정하는 방식으로는 그 설정 내용을 마음대로 주무르기가 무척 힘들다는 사실이다.
예를 들면 로그 파일의 이름이나, 경로 같은 경우다. 경로가 딱 정해진 위치라면 상관 없지만, 프로그램이 실행된 뒤, 동적으로 변경되어야 하는 경우라면 이야기가 많이 달라진다.

예를 들면 Application 관련 설정이 다른 위치에 저장되고, 설정 창을 통해 로그 파일의 위치가 변경되는 경우라면 어떻게 할 것인가? 그럼 매번 app.config를 변경해서 재실행해야 할까?

이런 저런 생각에 동적(코드상)으로 변경하는 방식을 찾아보았고, 그 방법을 정리해 보았다.

 

먼저 동적인 방법으로 설정을 하기 위해서는 log 개체에 대한 구성체계를 가져와야 한다.

	log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
	hierarchy.Configured = true;

 

이제 모든 설정은 hierarchy를 통해서 진행되게 된다.

먼저 제일 중요한 로직이 어디다가 어떻게 쌓을지에 대해서다.
log4net 에서 제공되는 쌓는 방식은 19가지 정도 되지만, 위의 예제도 그렇듯이, 파일로 제공하는 형태로 구성할 예정이다. 방법 1과 동일한 방식으로 쌓으려면 아래와 같은 로직을 통해 쌓는 방식에 대한 클래스를 생성한다.

	log4net.Appender.RollingFileAppender rollingAppender = new log4net.Appender.RollingFileAppender();
	rollingAppender.File = "C:\Temp\Test.log"; // 전체 경로에 생성할 메인 로그 파일 이름
	rollingAppender.AppendToFile = true;
	rollingAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
	rollingAppender.LockingModel = new log4net.Appender.FileAppender.MinimalLock();
	rollingAppender.DatePattern = "_yyyyMMdd\".log\""; // 날짜가 지나간 경우 이전 로그에 붙을 이름 구성
	log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout("%date [%property{buildversion}] %-5level %logger - %message%newline");
	rollingAppender.Layout = layout;

여기에 있는 RollongFileAppender는 log 내용을 파일로 쌓는 역할을 제공하는 모듈이다.
이 모듈에 대한 개체를 생성했으면, 이제는 hierarchy에 붙이도록 한다.

	hierarchy.Root.AddAppender(rollingAppender);
	rollingAppender.ActivateOptions();

이제 로그를 쌓는 레벨을 설정한다.

hierarchy.Root.Level = log4net.Core.Level.All;

이 로직을 로그가 쌓이기 전에 한번 실행 할 수 있도록 구성하면 된다. 다음 코드는 위의 부분 부분을 모두 합친 부분이다.

	log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
	hierarchy.Configured = true;

	log4net.Appender.RollingFileAppender rollingAppender = new log4net.Appender.RollingFileAppender();
	rollingAppender.File = "C:\Temp\Test.log"; // 전체 경로에 생성할 메인 로그 파일 이름
	rollingAppender.AppendToFile = true;
	rollingAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
	rollingAppender.LockingModel = new log4net.Appender.FileAppender.MinimalLock();
	rollingAppender.DatePattern = "_yyyyMMdd\".log\""; // 날짜가 지나간 경우 이전 로그에 붙을 이름 구성
	log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout("%date [%property{buildversion}] %-5level %logger - %message%newline");
	rollingAppender.Layout = layout;

	hierarchy.Root.AddAppender(rollingAppender);
	rollingAppender.ActivateOptions();
	
	hierarchy.Root.Level = log4net.Core.Level.All;

프로그램 제일 먼저 시작하는 로직에 추가하면 된다. 대개의 경우에는 program.cs 파일내 추가해주면 된다.

        static void Main()
        {
            #region 로그 설정
            log4net.Repository.Hierarchy.Hierarchy hierarchy = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository();
            hierarchy.Configured = true;

            log4net.Appender.RollingFileAppender rollingAppender = new log4net.Appender.RollingFileAppender();
            rollingAppender.File = "C:\Temp\Test.log"; // 전체 경로에 생성할 메인 로그 파일 이름
            rollingAppender.AppendToFile = true;
            rollingAppender.RollingStyle = log4net.Appender.RollingFileAppender.RollingMode.Date;
            rollingAppender.LockingModel = new log4net.Appender.FileAppender.MinimalLock();
            rollingAppender.DatePattern = "_yyyyMMdd\".log\""; // 날짜가 지나간 경우 이전 로그에 붙을 이름 구성
            log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout("%date [%property{buildversion}] %-5level %logger - %message%newline");
            rollingAppender.Layout = layout;

            hierarchy.Root.AddAppender(rollingAppender);
            rollingAppender.ActivateOptions();
	
            hierarchy.Root.Level = log4net.Core.Level.All;
            #endregion


            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

이제 app.config 없이도 log가 쌓이게 된다.

 

정리

이 포스트의 목적은 바로 이 2번째 방법에 따른 방식을 소개하기 위한 글이다.

log4net의 기능의 강력함은 동작 중에 무시할 만큼의 리소스를 소모하면서 강력하게 로그를 쌓는 방식에 있을 것이다. 더욱이 자동으로 파일로 떨구기도 하고, 데이터베이스에 쌓을 수도 있기 때문에, 활용도에서도 우수하다.

개인적으로 현재 프로젝트를 모두 이 log4net 으로 전환하고, 쌓이는 로그를 WinTail(http://www.baremetalsoft.com/wintail/)  이라는 프로그램을 통해서 살펴보고 있다. 동작 중에 발생되는 각종 값에 대한 감시 및 데이터 체크는 모두 이를 통해서 하고 있다. 동적으로 프로그램을 실행하면서 디버깅을 할 때 의외로 편하다.

닷넷 프로그램으로 로깅을 남기는 작업을 해보고 있다면 한번 시도해보심이?

728x90
  1. good 2019.03.12 20:27

    선생님의 설명 덕분에 큰 깨달음을 얻고 갑니다.
    자세한 설명에 무릎 탁 치고 갑니다.

728x90

요즘 C#을 통해서 Application을 만드는 중인데, 데이터 처리해야 될 내용이 많아, Excel과 Access를 주로 사용하고 있다. 특히 Excel의 경우에는 직접 COM 개체를 활성화 시켜서 Cell 단위까지 찾아 데이터를 가져오는 방법보다,
마치 DB Access 한 것 처럼 OLEDB로 연결해서 가져오는 방법이 가장 효율적이였던 것 같다.

그런데, 문제는 설치 대상 PC안에 오피스 제품이 무엇이 깔렸는지에 따라 이 OLEDB를 사용할 수 있는 것이 전혀 다르다. 특히 2013 이후에는 x64 전용이 있어, 응용 프로그램이 x64에서 동작하게 되면 OLEDB 32bit 버전은 접근이 불가능했다. 그래서 매번 어떤 오피스가 깔렸는지, 32bit, 64bit 구분을 하기에는 너무 로직이 복잡해졌다.

그래서 구글링으로 찾아 다양한 테스트 후 적용했는데 지금은 큰 문제가 없어 보여서 블로깅을 시작한다.

 

문제

오피스 데이터를 DB 처럼 접근하기 위해서는 그에 맞는 Connection String을 구성해야 한다.

MS Access 같은 경우에는,

"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\mydatabase.mdb;User Id=admin; Password=;"

라고 쓰고

MS Excel 같은 경우에는

"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\MyExcel.xls; Extended Properties="Excel 8.0;HDR=Yes;IMEX=1";"

라고 쓴다.

그런데, 문제는 저 Provider다.
현재 설치되어 있는 오피스 버전에 따라 Microsoft.Jet.OLEDB.4.0 을 쓸수도 있고,
때론 Microsoft.ACE.OLEDB.12.0 라고 쓰기도 하고,
x64 Office 2013의 경우에는 Microsoft.ACE.OLEDB.15.0 라고 해야 동작한다.
그 이유는 설치한 Office 마다 설치되는 OLEDB Provider가 달라서 벌어지는 일이다.

일반적으로 Office 2000, XP, 2003 까지는 Microsoft.Jet.OLEDB.4.0 를,
Office 2007, 2010의 경우에는 Microsoft.ACE.OLEDB.12.0 를
Office 2013의 경우에는 Microsoft.ACE.OLEDB.15.0를 사용하게 된다.

그렇다면 이 Provider가 어떻게 설치되어 있는지 알 수 있을까?
맨 처음, 적용했던 방식은 Registry를 통한 방식이였다.
Registry 내에 설치되어 있는 Provider가 있는지 검색하는 방법인데,
이 방법으로 했을때는 그럭저럭 잘 동작 했다.

그런데, 왠걸... x64로 넘어가자 문제가 발생했다.
분명 해당 Provider는 설치되어 있었지만,
내 프로그램에서 해당 Provider를 찾지 못하는 문제가 발생했다.
즉 x86일 때와 x64일 때 제공되는 OLE DB Provider가 다르다는 사실을 그 때 처음 알았다.

결국 원점으로 돌아왔고, 근본적인 방법을 찾아야 겠다는 생각이 들어 수정하기 시작했다.

 

해결

구글링을 통해서 다양한 웹페이지들을 들락달락해 본 결과 다음과 같은 예제 소스를 볼 수 있었다.
대게의 경우 앞서 언급한 특정 레지스트릐의 값을 통해 등록되어 있는지 여부를 찾는 것이였는데,
애석하게도 실패를 맞봤기 대문에, 실질적인 방법이 필요했다.그러다가 단서를 하나 발견했다.

http://stackoverflow.com/questions/6570066/c-net-get-the-oledb-provider-version

내용을 대충 훝어 본 후, 적절한 예제로 담긴 MSDN 링크를 따라 들어갔다.

http://msdn.microsoft.com/en-us/library/system.data.oledb.oledbenumerator.getelements.aspx

OleDbEnumerator.GetElements 라는 메소드를 사용해보라는 것이였다.
예제의 내용은 아래와 같다.

using System;
using System.Data;
using System.Data.OleDb;

class Program
{
    static void Main()
    {
        OleDbEnumerator enumerator = new OleDbEnumerator();
        DataTable table = enumerator.GetElements();
        DisplayData(table);
        Console.WriteLine("Press any key to continue.");
        Console.ReadKey();
    }

    static void DisplayData(DataTable table)
    {
        foreach (DataRow row in table.Rows)
        {
            foreach (DataColumn col in table.Columns)
            {
                Console.WriteLine(" = ", col.ColumnName, row[col]);
            }
            Console.WriteLine("==================================");
        }
    }
}

즉 OleDbEnumerator 라는 개체를 만들고 그 메소드 중, GetElements 라는 메소드를 호출하면 현재 프로그램 안에서
사용가능한 모든 Provider를 담은 정보를 DataTable 형식으로 돌려주게 된다.

여기서 착안하여, 실제 Provider들 중, 최신 Provider 이름을 추출하도록 했다.

OleDbEnumerator enumerator = new OleDbEnumerator();
DataTable table = enumerator.GetElements();
List aryProviders = new List();
foreach (DataRow row in table.Rows)
{
    aryProviders.Add(row[0].ToString());
}
table.Dispose();
aryProviders.Sort((p, n) => n.CompareTo(p));
string sFoundString = aryProviders.Find(m => m.StartsWith("Microsoft.ACE.OLEDB"));
if (string.IsNullOrEmpty(sFoundString))
    sFoundString = aryProviders.Find(m => m.StartsWith("Microsoft.Jet.OLEDB"));
g_sProviderName = sFoundString;

 

최종적으로 사용되는 값은 g_sProviderName 이라는 값으로, 이 값을 Connection String에 대입하기만 하면 된다.

string sConnectionString = string.Format("Provider=;Data Source=C:\MyExcel.xls; Extended Properties="Excel 8.0; HDR = Yes; IMEX = 1";", g_sProvider);

라고 하면 끝. 그러면 해당 Provider로 연결하게 된다.

Provider 검색하는 코드를 간단하게 설명하자면,  아래와 같다.

  1. 현재 가져올 수 있는 모든 Provider 정보를 DataTable로 가져온다.
  2. 가져온 Provider에서 Provider 이름만 모두 가져와 string array로 만든다.
  3. string array를 이름 별로 Sort 한다. - 최신 버전이 위로 갈 수 있도록 -
  4. 돌면서 최신 버전에 해당하는 것이 있는지 체크하면서 찾으면 해당 이름을 저장한다.
  5. Connection String을 만들 때, 저장한 Provider 이름을 사용한다.

간단하면서도 잘 알지 못하면, 참 어렵게 프로그램을 짤 번했던 기억이 든다.

참고로 Excel의 경우 "xsl" 파일은 Microsoft.Jet.OLEDB.4.0 만으로도 읽을 수 있지만, "xslx" 파일은 반드시 "Microsoft.ACE.OLEDB.12.0" 이상 버전의 Provider를 사용해야만 읽을 수 있다.

즉 위에서 필터링하는 방식에 따라, xlsx 파일을 읽을 수 있는지 없는지를 체크하는 방편이 될 수 있다.

728x90
  1. 희동 2013.11.13 10:36

    귀하의 노력에 감동받습니다~

  2. 손뎅 2014.08.12 11:08

    아 찾던건데 정말 감사합니다

  3. 천잰데? 2014.09.30 15:00

    엄벌리버블

  4. 호기심 소년 2015.08.20 10:15

    잘보고 갑니다.
    그런데 xlsx의 경우에 꼭 그런 것만은 아닙니다.
    오피으 버전마다 다르기도 하고요.
    확인해보니 열려 있는 상태에서는 Jet4.0;Excel 8.0으로 잘읽어옵니다.

  5. vba왕초보 2019.07.06 06:27

    동일worksheet 내에서도 잘 돌아가던게 안되는 경우가 있어서 도무지 원인을 찾지 못했는데,
    문제점을 찾아낸다는 것 만으로도 대단하십니다.
    저도 이 소스를 가지고 해결점을 찾아 보겠습니다.

  6. test 2021.02.18 13:46

    감사합니다 정말 감사합니다ㅜㅜ

  7. 대단하시네요 2021.06.11 17:34

    이 문제로 골치가 아팠는데 덕분에 한방에 해결했습니다
    정말 대단하십니다. (ㅡ.ㅡ)b

728x90

요 근래 C#으로 데이터 베이스를 다루는 작업이 늘어가고 있다.

그런데, 단순히 Select / Insert / Update / Delete의 문제는 단순 쿼리 작성으로 해결은 할 수 있었는데,
문제는 변경된 사항만 적용하는 점이다.

 

Select를 해서 가져온 DataTable을 Grid 같은 곳에 DataSource로 붙인 것 까지는 좋은데,
이 Grid에서 데이터를 변경해서 나온 값을 가지고 뭔가 하려고 하니, 도데체 무엇이 변경되었는지를
찾는게 하나의 일이 되버렸다.

 

그렇다고, 전수 검사하는 것도 웃기기도 하고...

 

하지만, 태생이 DB 출신인 DataTable이 변경점 하나 없이 동작한다는 것은 말이 안될 것 같고 해서 이리 저리 사이트 돌아다면서 확인해보니, 역시 있었다.

 

DataTable.GetChanges()

 

이 메소드를 사용하면, DataTable 내의 데이터 변경 점을 모두 가져올 수 있었다.

 

사용방법은 아래와 같다.

 

준비

 

먼저 GetChanges를 호출하는 방법부터 확인하자.

 

이를 위해서 데이터를 준비한다. (여기서의 모든 예제는 SQLite를 사용한다. )

 

 

 

SQLiteConnectionStringBuilder sbConnectionString = new SQLiteConnectionStringBuilder();
sbConnectionString.DataSource = "test.db";
string sConnectionString = sbConnectionString.ToString();
SQLiteConnection conn = new SQLiteConnection(sConnectionString);
conn.Open();

// DB에 테이블을 생성
SQLiteCommand cmd = new SQLiteCommand(conn);
cmd.CommandType = CommandType.Text;
cmd.CommandText = "CREATE TABLE  IF NOT EXISTS T_CATEGORIES ( ID INTEGER PRIMARY KEY NOT NULL, NAME TEXT )";
cmd.ExecuteNonQuery();

// 테이블에 테스트 데이터 추가.
cmd = new SQLiteCommand(conn);
cmd.Transaction = transaction;
for (int i = 0; i < 100; i++)
{
	cmd.CommandType = CommandType.Text;
	cmd.CommandText = "INSERT INTO T_CATEGORIES(NAME) VALUES ( 'Category" + i.ToString() + "');";
	cmd.ExecuteNonQuery();
}

// 테이블의 내용을 Select
DataTable dtResult = new DataTable();
dtResult.AcceptChanges();
cmd = new SQLiteCommand(conn);
cmd.CommandType = CommandType.Text;
cmd.CommandText = "SELECT * FROM T_CATEGORIES";

// DataTable에 Select한 내용을 채우기.
SQLiteDataAdapter atapter = new SQLiteDataAdapter();
atapter.SelectCommand = cmd;
atapter.Fill(dtResult);

코드 상에서는 SQLite 개체를 쓰기는 했지만, OleDB가 되었던 MS SQL Server Client가 되었던,
요점은 ConnectionString을 구해와서 Connection을 맺고, Command를 만든 뒤, Adapter 개체를 생성해, DataTable에 Fill 하는 것이다.

 

일단 위의 코드를 사용하면, SQLite를 이용하여 test.db 안에 있는 T_CATEGORIES 라는 테이블의 모든 데이터를 dtResult 라는 곳에 채우는 것이다.

 

일단, dtResult 라는 DataTable을 만들었으면 조작을 시작해보자.

 

 

INSERT

 

데이터를 추가해보자.

DataRow newRow = dtResult.NewRow();
newRow["ID"] = dtResult.Rows.Count;
newRow["NAME"] = "New Category!";
dtResult.Rows.Add(newRow);

데이터 추가 로직은 간단하다. NewRow() 메소드로 DataTable의 Column에 맞게 구성된 DataRow를 확보한 뒤, 그 안에다 적절한 데이터를 넣는다. 그리고 난 뒤, RowCollection 에다 Add 를 한다.

 

이렇게 하면 dtResult라는 DataTable안에 새로운 데이터가 추가된다.

 

그렇다면 변경된 정보는 어떻게 확보할까?

그 때 GetChanges() 라는 메소드를 쓴다. GetChanges() 메소드를 사용하면, 전체 데이터 중에 변경점에 해당하는 값들을 모두 가져올 수 있다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Added);
foreach (DataRow row in dtChanged.Rows)
{
	if(row.RowState == DataRowState.Added)
	{
		foreach (DataColumn col in dtChanged.Columns)
		{
			Debug.Write(row[col].ToString());
			// 여기서 INSERT 구문을 작성한다.
	        }
		System.Diagnostics.Debug.WriteLine(" ");
	}
}

GetChanges()를 실행할 때, 파라미터를 하나 넣는데, 그 안에 넣는게 바로 DataRowStatus다.
만일 Insert 된 사항이 있으면 Added를 Update된 사항은 Modified, Delete된 사항이 있으면 Deleted를 아예 변경된적이 없는 경우에는 Unchanged를 넣으면 된다. 물론 복합적으로 사용도 된다. "|" 표시를 하면 복합적으로 검색해서 넣어준다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Added|DataRowState.Deleted| DataRowState.Modified);

변경 점들이 담긴 Table에서 Row들을 하나씩 꺼낸다. 이 정보를 이용해 INSERT 구문을 만들어 DataBase 상에 INSERT 해주면 된다.

 

 

UPDATE

 

INSERT와 매우 유사하다.

다만 틀린 것은 새로 추가된 사항이 아니고, 변경된 점 대비 이전 값이라는 것도 한번 더 체크해야 된다는 점이 다르다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Modified);
foreach (DataRow row in dtChanged.Rows)
{
    if (row.RowState == DataRowState.Modified)
    {
        object beforevalue = null;
        object aftervalue = null;
        foreach (DataColumn col in dtChanged.Columns)
        {
            beforevalue = row[col, DataRowVersion.Original];
            aftervalue = row[col, DataRowVersion.Current];

            // UPDATE 구문을 작성한다.
        }
        System.Diagnostics.Debug.WriteLine(" ");
    }                                
}

이전 값을 가져올 때는 DataRow의 배열에서 DataRowVersion 값까지 같이 넣어주면 된다. 
이 때, Original을 선택하면 변경 전의 값을 가져온다. 

단, 이 구문은 INSERT된 항목에서는 사용하면 안된다.
INSERT에서는 Original 값이라는 것이 없기 때문이다!

 

 

DELETE

 

이번에는 삭제된 값이다.
삭제된 값을 얻어오는 것도 위의 UPDATE 와 별다르지는 않다.
다만! 현재 값이 존재하지 않는다는게 문제다.

 

만일 아래와 같이 짜게 되면 Exception을 발견하게 된다.

DataTable dtChanged = dtResult.GetChanges(DataRowState.Deleted);
foreach (DataRow row in dtChanged.Rows)
{
    if (row.RowState == DataRowState.Deleted)
    {
        object beforevalue = null;
        object aftervalue = null;
        foreach (DataColumn col in dtChanged.Columns)
        {
            beforevalue = row[col, DataRowVersion.Original];
            aftervalue = row[col];    // 에러 발생!
            aftervalue = row[col, DataRowVersion.Current];    // 에러 발생!

            // DELETE 구문을 작성한다.
        }
        System.Diagnostics.Debug.WriteLine(" ");
    }                                
}

주석 중 "에러 발생!" 이라고 적은 부분이 있는데, row[col] 혹은 row[col, DataRowVersion.Current]를 하게 되면,

"Deleted row information cannot be accessed through the row" 라는 Exception이 발생한다.
이미 삭제했기 때문에, 현재 값이라는 것이 존재하지 않기 때문이다.

 

즉 aftervalue 라는 것 대신 before value(DataRowVersion.Original)을 이용해서 이전 값을 이용해 DELETE 문을 위한 구문을 작성하면 된다.

 

 

 

정리

 

.NET Framework에서는 DataSet(DataTable)을 이용하여 OldDB 개체를 통해 직접 수정하면 데이터베이스 상에서도 반영되게 만들어 줄 수 있다. 즉 DataTable 혹은 DataSet을 Select 등을 활용해 가져온 뒤, DataGrid에서 값을 변경하면, 자동적으로 데이터베이스와 연동해서 바뀐 값이 바로 바로 변경되게 구성할 수 있다는 점이다.

매우 DB 친화적인 것처럼 보일 수 있겠지만, 실제로는 거의 써먹질 못한다.

그 이유 중 하나가 Select를 할 때, 보통 Join을 해서 여러 개의 테이블을 복합적으로 사용하는 경우가 많은데,
이 경우 복합적으로 만들어지는 DataSet(DataTable)은 자동적으로 Update, Insert, Delete를 해줄 수 없다.
결국 개발자가 변경 점에 맞추어 SQL Query문을 생성해서 실행해야 된다는 것이다.

 

이런 변경 점을 즉시 즉시 찾을 수 있다면, 별도로 Dirty Flag를 만들어 변경 점이 있는지 없는지 늘 체크할 필요도 없고,
이전 데이터와 비교한다고 생쑈를 부릴 필요가 없어진다.

( 필자의 프로젝트 수행한 것 중, 무식하게 비교 점을 찾는 프로그램이 있었다. 틈 나면 그 부분도 갈아 엎어야 할 듯...)

728x90
  1. 나그네 2020.04.29 11:01

    삭제된 row 의 row[col, DataRowVersion.Original];
    유용했네요.
    감사합니다^^

728x90

넥서스(NEXUS) S 순정롬 업데이트 프로그램( http://www.hind.pe.kr/1166 )을 현재 제공하고 있습니다.

이 프로그램의 소스를 공개합니다.

공개 프로젝트는 네이버 개발자 센터에 위치해 있으며 URL은 http://dev.naver.com/projects/nexusupdattor 입니다.

해당 소스는 SVN을 통해 Up/Down이 가능합니다.

(물론 Commit은 저만 되어 있는데, 필요하신 분이 있으면 선택적으로 승인할 예정이니다.)

 

이 프로젝트는 MS Visual Studio 2010 으로 만들어졌습니다. (물론 Visual Studio 2010 Express C# 으로도 조작이 가능합니다. ). SVN 연동을 하기위해서는 AnkhSvn 이라는 프로그램이 설치되어 있으면 좋습니다.

코드 내용은 그렇게 어렵지는 않겠지만, 난독증을 유발한다면, 해당 부분을 캡쳐해서 저에게 전달 해주시면 답변 드리도록 하겠습니다.

image

728x90

+ Recent posts

728x90