서두

System.Data.DataRow

데이터베이스 기반의 CS나 Web 관련 개발을 하면 DataTable을 많이 활용하는데, 이 때 데이터들은 DataRow를 통해서 전달 받게 된다. 그런데, 이 DataRow에 담긴 값들은 모두 Object 형식으로 담겨 있는데, 이 때 데이터를 끄집어 내는데, 그냥 ASP 시절의 웹 프로그래밍 방식을 따르게 되면 오류 발생율이 높다. 물론 의도적으로 try ~ catch 문에서 끄집어 내기 위해서 만들기도 하지만, 단순히 데이터를 끄집어 오는데, try ~ catch로 잡아야 될 정도면 개인적으로는 아니다 싶을 때가 많다.

 

본문

예를 들면, Person 이라는 테이블이 있는데, Name - varchar(50), Age – int, Address – varchar(100) 의 형태의 테이블이 있다고 하자.

Name Age Address
Goshe Mack 12 Gangnam
Park Sung-ook 23 [NULL]
Sonia Aka 22 Mapo

데이터테이블이 dtResult 라는 변수에 담겨 있다고 한다면,

   
string sName = dtResult.Rows[0]["Name"].ToSting();

하면 그 해당하는 값인 “Goshe Mack” 이라는 데이터가 나온다.

여기서 발생할 수 있는 오류의 형태는 2가지.

첫번째로는 데이터가 없을 때다. 즉 dtResult 안에 DataRow가 하나도 없으면 저기서 에러가 난다. [0] 부분은 0번째 Row를 가져오겠다는 말인데, 그 안에 앖이 없으면 Null 이고 Null에 대한 [Name]은 없으므로 에러다.

두번째로는 Name 부분이 DbNull 인 경우다. 이 경우 [0][Name] 결과가 null 이 되므로, null의 ToString 에서 오류가 발생하게 된다.

그렇다면 이제 Null을 피해보도록 하자.

첫번째의 경우.

foreach(DataRow row in dtResult.Rows)
{
       string sName = row["Name"].ToString();
}

라고 하면 일단 Data가 없는 경우에 발생되는 문제는 피할 수 있다. 왜냐면 foreach를 할 때, dtResult 안에 Row가 없으면 더 이상 안의 내용을 처리하지 않기 때문이다.

두번째의 경우는 어떻게 할까?

위의 경우 처럼 직관적으로 표현하자면 이렇게 할 수 있을 것이다.

foreach(DataRow row in dtResult.Rows)
{
    if(row["Name"] != null || row["Name"] != DBNull.Value)
    {
        string sName = row["Name"].ToString();
    }
}

조금 더 다듬어 보자.

row["Name"]
이 너무 자주 나온다!  이것을 조금 더 개선 시키는 방향은 아예 값을 밖으로 빼서 하나의 변수에 넣고, 그 값을 돌려 쓰는 방식을 취한다. 이러면 딱 한번만 row[“Name”] 을 부를 뿐 실제 값은 변수에서 직접 가져와 쓰게 된다.
foreach(DataRow row in dtResult.Rows)
{
    object value = row["Name"];
    if(value != null || value != DBNull.Value)
    {
        string sName = value.ToString();
    }
}

만일 Null 일 때 Default 값을 설정하고 싶다면? 더 간단하다. 이번에는 sName 부분을 위로 끄집어 올린다.

foreach(DataRow row in dtResult.Rows)
{
    string sName = "DEFAULT";
    object value = row["Name"];
    if(value != null || value != DBNull.Value)
    {
        sName = value.ToString();
    }
}

여기까지는 좋다고 생각된다. 그런데, 만일, 여러 개의 필드일 때는 어떻게 할까? 더욱이 String도 아닌 Int 같은 필드 인 경우 ToString 으로 해결도 안된다. 한번 위의 방식대로 코드를 짜보자

foreach(DataRow row in dtResult.Rows)
{
    string sName = "DEFAULT";
    int nAge = 0;
    string sAddress = "DEFAULT";

    object value = row["Name"];
    if(value != null || value != DBNull.Value)
    {
        sName = value.ToString();
    }

    object value = row["Address"];
    if(value != null || value != DBNull.Value)
    {
        sAddress = value.ToString();
    }

    object value = row["Age"];
    if(value != null || value != DBNull.Value)
    {
        string sTemp = value.ToString();
        try
        {
            nAge = int.Parse(sTemp);
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }

}

갑자기 길어지는 코드!!!!

지금은 값이 세 개니까, 저렇지 만일 필드수가 10개 이상된다고 하면, 값 한번 조회하는 코드만 100라인 넘게 된다. 까잇거 Copy&Paste 신공이 있는데! 라고 외치며 무식하게 짜시는 분이 계신다면, 나중에 그 코드를 유지보수해야 할 때 참여하는 자신 혹은 남을 위해 자제해주셨으면 한다. 제발 ㅋ

그래서 아예 값을 별도로 처리하기 위한 Util 형태의 Static 클래스를 만들어 사용하는 방법을 추천한다.

/// 
/// System.Data.DataRow의 데이터를 쉽게 접근하기 위해서 사용되는 함수 모음
/// 
static public class DBUtils
{

    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 문자열을 읽어온다.
    /// 별도 기본값을 제공하여 읽어오는데 실패한 경우 입력받은 기본값으로 돌려준다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름
    /// 읽어오는 것을 실패한 경우 돌려줄 기본값
    /// 읽어온 문자열 값
    public static string ReadString(DataRow row, string sColumnName, string sDefaultValue)
    {
        string sResult = sDefaultValue;
        if (row.Table.Columns.Contains(sColumnName))
        {
            object value = row[sColumnName];
            if ((value is DBNull) == false && value != null)
            {
                sResult = value.ToString();
                    
            }
        }
        sResult = sResult.Trim();
        return sResult;
    }

    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 문자열을 읽어온다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름    
    /// 읽어온 문자열 값
    public static string ReadString(DataRow row, string sColumnName)
    {
        return ReadString(row, sColumnName, string.Empty);
    }


    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 숫자값을 읽어온다.
    /// 별도 기본값을 제공하여 읽어오는데 실패한 경우 입력받은 기본값으로 돌려준다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름
    /// 읽어오는 것을 실패한 경우 돌려줄 기본값
    /// 읽어온 숫자 값
    public static int ReadInteger(DataRow row, string sColumnName, int nDefaultValue)
    {
        int nResult = nDefaultValue;
        if (row.Table.Columns.Contains(sColumnName))
        {
            object value = row[sColumnName];
            if ((value is DBNull) == false && value != null)
            {
                string sValue = value.ToString();
                if (int.TryParse(sValue, out nResult) == false)
                    nResult = nDefaultValue;
            }
        }
        return nResult;
    }

    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 숫자값을 읽어온다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름
    /// 읽어온 숫자 값
    public static int ReadInteger(DataRow row, string sColumnName)
    {
        return ReadInteger(row, sColumnName, 0);
    }

    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 DateTime값을 읽어온다.
    /// 별도 기본값을 제공하여 읽어오는데 실패한 경우 입력받은 기본값으로 돌려준다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름
    /// 읽어오는 것을 실패한 경우 돌려줄 기본값
    /// 읽어온 DateTime 값
    public static DateTime ReadDateTime(DataRow row, string sColumnName, DateTime defaultTime)
    {
        DateTime result = defaultTime;
            
        if (row.Table.Columns.Contains(sColumnName))
        {
            object value = row[sColumnName];
            if ((value is DBNull) == false && value != null)
            {
                try
                {
                    result = (DateTime)value;
                }
                catch (Exception ex)
                {
                    Core.LogWriter.Current.Write(ex);
                }
                //try
                //{
                //    result = DateTime.Parse(sValue);
                //}
                //catch
                //{
                //}
            }
        }
        return result;
    }


    /// 
    /// DataRow 상에서 특정 Coloumn 이름에 해당하는 DateTime값을 읽어온다.
    /// 
    /// 읽어올 데이터가 담긴 DataRow
    /// 읽어올 데이터의 Column 이름
    /// 읽어온 DateTime 값
    public static DateTime ReadDateTime(DataRow row, string sColumnName)
    {
        return ReadDateTime(row, sColumnName, DateTime.MinValue);
    }

    public static System.Data.SqlTypes.SqlBinary ReadSQLBinary(DataRow row, string sColumnName)
    {
        System.Data.SqlTypes.SqlBinary result = System.Data.SqlTypes.SqlBinary.Null;

        if (row.Table.Columns.Contains(sColumnName))
        {
            object value = row[sColumnName];
            if ((value is DBNull) == false && value != null)
            {
                try
                {
                    result = new System.Data.SqlTypes.SqlBinary((byte[])value);                        
                }
                catch (Exception ex)
                {
                    Core.LogWriter.Current.Write(ex);
                }
                //try
                //{
                //    result = DateTime.Parse(sValue);
                //}
                //catch
                //{
                //}
            }
        }
        return result;

    }
}

저런 클래스를 만들면 위에서 제시한 예제 코드는 다음 형태로 변하게 된다.

foreach(DataRow row in dtResult.Rows)
{
    string sName = DBUtils.ReadString(row,"Name","DEFAULT");
    int nAge = DBUtils.ReadInteger(row,"Age",0);
    string sAddress = DBUtils.ReadString(row,"Address","DEFAULT");
}

 

정리

반복적인 작업을 보다 효율적으로 빠르게 처리하라고 있는 컴퓨터에서 노가다하고 있는 우리네 현실을 보면 조금 답답한 것 같다. 사실 C#에는 프로그래머에게 보다 더 편리하고 간단하게 짜라는 장치가 많다. 하지만, 100% 현실에 맞는 것은 아닌 것 같다.

하나씩 자신만의 Util을 만들어 자신의 코드 내에 있는 노가다의 요소들을 정리해보는 것은 어떨까?

728x90

+ Recent posts