앞선 작업에서는 load_excel() 안에 한꺼번에 들어 있던 상태 초기화와 UI 초기화를 메서드로 분리했다. 코드를 나누고 나니 각각 무엇을 하는지는 이전보다 잘 보였다. 하지만 파일 하나를 불러오는 전체 흐름은 여전히 한 메서드 안에서 여러 단계가 섞여 있었고, 분리한 메서드를 잘못 호출하는 문제도 생겼다.
이번에는 메서드 수를 늘리는 것 자체보다, 파일 읽기 흐름을 위에서 아래로 읽을 수 있게 만드는 데 집중했다.
2편 이후의 load_excel()
파일 업로드 버튼을 누르면 프로그램은 다음 일을 처리해야 한다.
- 파일 선택 창을 연다.
- 선택한 파일의 시트 목록을 읽는다.
- 사용할 시트를 사용자에게 묻는다.
- 헤더가 없는 원본 데이터를 읽는다.
- 이전 파일의 상태와 화면을 초기화한다.
- 새 원본 데이터를 미리보기에 표시한다.
각 작업은 어렵지 않지만, 하나의 메서드에 모이면 다이얼로그 처리와 데이터 읽기, 상태 변경, UI 갱신을 동시에 따라가야 한다. 2편에서 상태와 UI 갱신 일부를 메서드로 떼어냈지만 호출부에는 이런 코드가 남아 있었다.
self._update_loaded_file_labels(self, file_path, sheet_name=selected_sheet)
self._set_file_loaded_buttons_enabled(self)
인스턴스 메서드를 self.method(...) 형태로 호출하면 파이썬이 self를 자동으로 전달한다. 따라서 위 코드는 self를 두 번 전달하며, 파일을 읽은 뒤 TypeError를 발생시킨다.
이 문제는 단순히 인자를 지우면 해결된다. 다만 이번에는 여기서 멈추지 않고, load_excel()이 전체 흐름만 설명하도록 다시 정리했다.
load_excel()은 흐름만 조정한다
리팩토링한 load_excel()은 다음처럼 읽힌다.
def load_excel(self):
file_path = self._select_source_file()
if not file_path:
return
try:
selected_sheet = self._select_source_sheet(file_path)
if not selected_sheet:
return
raw_df = ExcelService.read_excel_raw(file_path, sheet_name=selected_sheet)
self._apply_loaded_file(file_path, selected_sheet, raw_df)
except Exception as e:
QMessageBox.critical(self, "오류", f"엑셀 파일을 불러오지 못했습니다.\n\n{e}")
이제 메서드 본문만 읽어도 “파일 선택 → 시트 선택 → 원본 읽기 → 화면 반영” 순서가 보인다. 파일 형식 검증과 실제 데이터 읽기는 기존처럼 ExcelService가 담당하고, 다이얼로그와 화면 반영은 MainWindow가 담당한다.
시트 목록을 읽은 뒤 파일 닫기
리팩토링 후 흐름을 검증하면서 ExcelService.get_sheet_names()가 pd.ExcelFile을 열고 닫지 않는 문제도 발견했다. Windows에서는 열린 파일 핸들이 남아 있으면 사용자가 원본 Excel 파일을 삭제하거나 덮어쓰지 못할 수 있다.
with pd.ExcelFile(file_path) as excel_file:
return excel_file.sheet_names
ExcelFile을 컨텍스트 매니저로 사용하면 시트 목록을 읽은 직후 파일이 닫힌다. 서비스가 외부 파일을 여는 책임을 맡았다면, 사용을 마친 파일을 닫는 책임도 함께 맡아야 한다.
파일 선택과 시트 선택 분리
파일 선택은 _select_source_file()로 옮겼다. 사용자가 취소하면 None을 반환하고, 파일을 골랐다면 마지막으로 연 폴더를 기억한 뒤 경로를 반환한다.
def _select_source_file(self) -> str | None:
file_path, _ = QFileDialog.getOpenFileName(...)
if not file_path:
return None
self._remember_open_dir(file_path)
return file_path
시트 선택도 _select_source_sheet()로 분리했다. 이 메서드는 시트 목록 조회와 선택 다이얼로그만 책임진다. 시트가 없는 파일은 예외로 처리하고, 사용자가 선택을 취소하면 None을 반환한다.
이렇게 나누면 load_excel()에서는 각 다이얼로그의 세부 설정을 읽지 않아도 된다. 나중에 CSV 파일은 시트 선택 창을 생략하거나, 최근 사용한 시트를 기본값으로 보여주는 기능을 추가할 때도 수정 위치가 명확해진다.
읽은 파일을 적용하는 한 지점 만들기
파일을 정상적으로 읽은 뒤에는 상태와 UI를 함께 바꿔야 한다. 둘 중 하나만 갱신되면 화면에 보이는 파일과 내부 데이터가 달라질 수 있다. 그래서 성공한 읽기 결과를 적용하는 진입점을 _apply_loaded_file() 하나로 모았다.
def _apply_loaded_file(self, file_path, sheet_name, raw_df):
self._set_loaded_file_state(file_path, sheet_name, raw_df)
self._reset_loaded_file_ui(file_path, sheet_name, raw_df)
_set_loaded_file_state()는 데이터프레임, 파일 경로, 시트 이름처럼 프로그램 내부 상태를 바꾼다. _reset_loaded_file_ui()는 라벨, 원본 미리보기, 매핑 영역, 결과 테이블, 버튼 상태처럼 화면에 보이는 부분을 초기화한다.
상태 변경과 UI 변경을 완전히 독립적으로 만들 수는 없다. 이 둘은 파일 로드 성공이라는 같은 사건에 반응하기 때문이다. 대신 각 책임은 별도 메서드에 두고, 반드시 함께 호출되어야 한다는 규칙을 _apply_loaded_file()에 표현했다.
메서드를 나누는 기준
이번 작업에서 배운 점은 메서드를 많이 만드는 것과 흐름을 잘 분리하는 것은 다르다는 것이다. 이름이 모호하거나 호출 관계가 복잡하면 메서드가 늘어도 코드는 더 읽기 어려워질 수 있다.
이번에는 다음 기준을 사용했다.
- 상위 메서드는 사용자의 작업 흐름을 보여준다.
- 다이얼로그 하나는 선택 결과 하나를 반환한다.
- 파일 입출력은
ExcelService에 맡긴다. - 로드 성공 후 상태와 UI 갱신은 하나의 진입점으로 묶는다.
- 분리한 메서드에는 입력과 반환 타입을 적어 호출 방법을 분명히 한다.
특히 _apply_loaded_file()처럼 작은 메서드도 의미가 있다. 코드 몇 줄을 줄이기 위한 것이 아니라, “파일을 읽은 결과는 이 메서드를 통해 적용한다”는 규칙을 이름으로 남기기 때문이다.
다음 리팩토링 후보
파일을 처음 읽는 흐름은 이전보다 짧고 명확해졌다. 하지만 헤더 행을 적용하는 apply_selected_header()에는 아직 다음 작업이 함께 들어 있다.
- 선택한 행을 헤더로 사용해 파일 다시 읽기
- 비어 있거나 이름 없는 컬럼명 정리
- 헤더 적용 미리보기 갱신
- 매핑 UI 생성
- 버튼 상태 변경
- 완료 메시지 표시
다음 단계에서는 컬럼명 정리처럼 UI와 무관한 로직을 서비스나 별도 함수로 옮길 수 있다. 파일 읽기 흐름에서 했던 것처럼, 먼저 사용자 흐름과 데이터 처리 책임을 구분하면 어디부터 떼어낼지 보일 것이다.
이번 리팩토링의 결과는 거대한 구조 변경이 아니다. 대신 파일 업로드가 어떤 순서로 진행되고, 각 단계가 어디에서 처리되는지 코드가 직접 설명하게 되었다. 작은 프로그램일수록 이런 흐름의 선명함이 다음 기능을 고칠 때 큰 차이를 만든다.
'Data Engineering & Automation > GBIF Darwin Core' 카테고리의 다른 글
| PySide6 리팩토링 4편 : 헤더 적용과 컬럼명 정리 분리하기 (0) | 2026.06.04 |
|---|---|
| PySide6 리팩토링 2편: Excel 파일 읽기 흐름 분리하기 (0) | 2026.05.28 |
| PySide6 리팩토링 1편: 프로젝트 구조 다시 읽기 (0) | 2026.05.27 |
댓글