학원 과제로 C# 콘솔 앱에서 엑셀 파일을 읽는 프로세스를 공부하는 중이다.
학원에서는 Microsoft.Office.Interop.Excel 라이브러리를 통해 엑셀 파일을 제어하는 방법을 알려주고, 간단한 엑셀 파일을 읽어 콘솔 앱에 출력하는 것을 과제로 내줬다.
그런데 과제를 하는데에 문제가 하나 있다 우리집엔 엑셀이 깔려있지 않아 Microsoft.Office.Interop.Excel 라이브러리를 사용할 수 없다는 것이다.
포토샵 대신 김프를, 엑셀 대신 구글 스프레드시트를 사용하는 나로서는, 이 과제를 위해 엑셀을 구매할 생각은 전혀 없다.
그래서 Microsoft.Office.Interop.Excel 라이브러리 없이 엑셀 파일을 제어하는 방법을 찾아보게 되었고,
EPPlus 패키지 기반으로 하는 OfficeOpenXml 라는 라이브러리를 알게 되었고
학원에서 제공한 소스와 챗GPT가 알려준 예제 소스를 참고하여 OfficeOpenXml 라이브러리를 통해 엑셀 파일을 읽어와 Dictionary 자료형으로 저장하는 코드를 짰다.
using System;
using System.Collections.Generic;
using OfficeOpenXml; //EPPlus pakage
using System.IO;namespace LearnCS_Excel
{
internal class ExcelReader
{
///
/// Dictionary<시트이름, Dictionary<셀이름, 셀값>>
///
protected Dictionary<string, Dictionary<string, string>> DataSet;/// <summary> /// 시트 이름에 따른 시트의 열 개수 /// </summary> public Dictionary<string, int> ColCount = new Dictionary<string, int>(); /// <summary> /// 시트 이름에 따른 시트의 행 개수 /// </summary> public Dictionary<string, int> RowCount = new Dictionary<string, int>(); protected string FilePath; protected ExcelPackage Excel; /// <summary> /// 지정된 시트 이름에 따른 Dictionary 값 반환 /// </summary> /// <param name="sheetName">시트 이름</param> /// <returns>Dictionary<셀이름, 셀값></returns> public Dictionary<string, string> this[string sheetName] { get { return DataSet[sheetName]; } } /// <summary> /// 지정된 셀이름에 따른 값 반환 /// </summary> /// <param name="sheetName">셀 이름 (A1, B2, AA1)</param> /// <param name="cellName">셀값 (문자열)</param> /// <returns></returns> public string this[string sheetName, string cellName] { get { return DataSet[sheetName][cellName]; } } #region [생성자] public ExcelReader() { // 비상업적 용도 표기. 안쓰면 오류나게 되어있음 ExcelPackage.LicenseContext = LicenseContext.NonCommercial; DataSet = new Dictionary<string, Dictionary<string, string>>(); } public ExcelReader(string filePath) { // 비상업적 용도 표기. 안쓰면 오류나게 되어있음 ExcelPackage.LicenseContext = LicenseContext.NonCommercial; DataSet = new Dictionary<string, Dictionary<string, string>>(); FilePath = filePath; if (ReadExcelFile(FilePath) == false) { throw new Exception("파일 읽기에 실패했습니다."); } } #endregion [생성자] /// <summary> /// 파일 읽고 DataSet에 시트명과 셀이름에 따라 값 세팅 /// </summary> /// <param name="FilePath">엑셀 파일 경로</param> /// <returns>읽기 성공 시 true, 아닐 시 false</returns> public bool ReadExcelFile(string FilePath) { try { Excel = new ExcelPackage(new FileInfo(FilePath)); foreach (ExcelWorksheet worksheet in Excel.Workbook.Worksheets) { Dictionary<string, string> sheetDataSet = new Dictionary<string, string>(); string sheetName = worksheet.Name; int colCount = ReconfirmColRowCount(worksheet, worksheet.Dimension.Columns); int rowCount = ReconfirmColRowCount(worksheet, worksheet.Dimension.Rows, false); ColCount.Add(sheetName, colCount); RowCount.Add(sheetName, rowCount); for (int col = 1; col <= ColCount[sheetName]; col++) { for (int row = 1; row <= RowCount[sheetName]; row++) { string cellName = CellName(col, row); string cellValue = worksheet.Cells[row, col].Value?.ToString(); if (cellValue != null) { sheetDataSet.Add(cellName, cellValue); } } } DataSet.Add(worksheet.Name, sheetDataSet); } } catch(Exception) { return false; } return true; } /// <summary> /// 열,행 번호에 따라 셀 이름을 반환함 /// </summary> /// <param name="col">열 번호, 1부터 시작</param> /// <param name="row">행 번호, 1부터 시작</param> /// <returns>셀 이름 (예: A1, B2, CA12)</returns> public static string CellName(int col, int row) { col--; const int baseNum = 26; string colName = string.Empty; if (col < baseNum) { colName = Convert.ToString((char)(65 + col)); } else { colName = Convert.ToString((char)(64 + (col / baseNum))); colName += Convert.ToString((char)(64 + (col % baseNum))); } string cellName = colName + row; return cellName; } /// <summary> /// 엑셀 파일이 가진 전체 시트의 이름을 List로 반환함 /// </summary> /// <returns>시트 이름 배열</returns> public List<string> GetSheetNames() { if(DataSet == default(Dictionary<string, Dictionary<string, string>>)) { return default(List<string>); } if(Excel == default(ExcelPackage)) { return default(List<string>); } if (Excel.Workbook == default(ExcelWorkbook)) { Console.WriteLine("워크북 오류"); return default(List<string>); } if (Excel.Workbook.Worksheets == default(ExcelWorksheets)) { Console.WriteLine("시트 오류"); return default(List<string>); } var sheetNames = new List<string>(); foreach (ExcelWorksheet worksheet in Excel.Workbook.Worksheets) { sheetNames.Add(worksheet.Name); } return sheetNames; } protected int ReconfirmColRowCount(ExcelWorksheet worksheet, int count, bool isCol = true) { for (int i = 1; i <= count; i++) { int row = isCol ? 1 : i; int col = isCol ? i : 1; string cell = worksheet.Cells[row, col].Value?.ToString(); if (cell == null || cell == string.Empty) { return isCol ? i : i - 1; } } return count; } }}
그런데 학원에서 준 코드와 알 수 없는 차이점이 눈에 밟혔다. 학원에서 준 코드에서는 Marshal.ReleaseComObject(), GC.Collect()를 이용해 메모리 최적화를 해왔는데,
내 소스에는 그런 최적화 과정이 없다.
이 부분에 대해 챗GPT를 통해 알아보았는데,
Microsoft.Office.Interop.Excel은 COM(Component Object Model) 기반의 인터페이스를 사용하여 Excel을 제어하는 라이브러리이고,
반면에 OfficeOpenXml은 EPPlus 라이브러리를 기반으로 하며, .NET 프레임워크를 통해 Excel 파일을 처리한다고 하기 때문에 OfficeOpenXml은 별도의 최적화는 필요 없을 것이라고 한다.
COM(Component Object Model)이 무엇인가 하면 마이크로소프트에서 개발한 소프트웨어 컴포넌트를 작성하고 실행하는 데 사용되는 기술 및 아키텍처인데,
COM을 사용하는 라이브러리는 직접 .NET 프레임워크를 쓰지 않고 COM을 거치는 과정에서 리소스 낭비가 발생해 최적화를 위해 Marshal.ReleaseComObject() 등을 이용한다는 것이다.
반면 내가 짠 코드는 OfficeOpenXml 라이브러리를 사용하므로 이런 최적화 과정이 필요없으며,
만일 엑셀 파일이 매우 크거나해서 추가적인 최적화가 정말로 필요하다면
1. using(var a = new lib){ ... } 과 같은 구문을 사용하기
2. 파일을 전부 읽어오지 말고 조금씩 나누어 읽어오기
3. 셀 단위가 아닌 Range 단위로 읽어오기
세 가지 방법을 통해 최적화를 시도해 볼 것을 권유 받았다.
당장은 엑셀 파일이 그리 크지 않기에 추가적인 최적화는 필요 없을 것 같다.
인공지능이 일자리를 상당 수 빼았을 것이라는 우려가 있긴 하지만 동시에 일을 하는데에 있어 상당히 유용한 도구가 될 수도 있다는 점을 다시 한 번 느낀 케이스였다.
모 게임사에서는 이미 일러스트 작업에 AI를 사용하고 있는 것으로 의심되는 데, 확실히 매우 유용한 도구라는 것은 부정할 수 없을 것이다.