서두
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을 만들어 자신의 코드 내에 있는 노가다의 요소들을 정리해보는 것은 어떨까?