5강. Model/View 구조로 파일 처리 코드 분리하기

0. 학습 목표
→ 파일 처리 코드를 FileModel로 분리하고, 화면 코드는 FileEditorView에 남기는 구조를 학습합니다.
이번 글에서 다룰 내용
이번 글에서는 Model모델과 View뷰 구조로 파일 처리 코드를 분리합니다.
4강에서는 하나의 GUI 클래스 안에 파일 쓰기, 읽기, 복사 기능을 모두 넣었습니다.
이번 5강에서는 파일 처리 로직은 FileModel로 옮기고, 화면과 버튼 처리는 FileEditorView에 남깁니다.
이렇게 나누면 나중에 기능이 많아져도 코드를 관리하기 쉬워집니다.
| 구분 | 내용 |
| 핵심 개념 | 파일 기능은 Model이 담당하고, 화면과 버튼은 View가 담당하도록 역할을 나눕니다. |
| 실습 준비 | 4강에서 만든 GUI 파일 처리 코드를 기준으로 구조를 개선합니다. |
| 최종 목표 | FileModel, FileEditorView, main.py로 나누어진 파일 처리 GUI 프로그램을 완성합니다. |
이번 단계의 핵심: Model은 파일 처리 로직을 담당하고, View는 사용자가 보는 화면과 버튼 동작을 담당합니다.
1. 버튼 함수 안에 모든 코드가 있으면 왜 불편할까?
→ 4강 코드의 문제점을 확인하고, 역할 분리가 필요한 이유를 이해합니다.
1.1 4강 코드의 현재 구조
4강에서는 하나의 FileEditorWindow 클래스 안에 화면 코드와 파일 처리 코드를 모두 넣었습니다.
처음 배울 때는 이 방식이 이해하기 쉽습니다.
하지만 기능이 많아질수록 클래스 안의 코드가 길어지고 복잡해집니다.
FileEditorWindow
├─ QTextEdit 화면 생성
├─ 버튼 생성
├─ QFileDialog 실행
├─ QFile로 파일 쓰기
├─ QFile로 파일 읽기
├─ QFile.copy()로 파일 복사
└─ QMessageBox로 결과 안내
이 구조에서는 화면 코드와 파일 기능 코드가 섞여 있습니다.
그래서 파일 저장 방식만 바꾸고 싶어도 화면 클래스 안의 코드를 함께 살펴봐야 합니다.
| 문제 | 설명 |
| 코드가 길어짐 | 버튼 함수 안에 파일 처리 코드가 계속 늘어납니다. |
| 역할이 섞임 | 화면 코드와 파일 처리 코드가 같은 클래스 안에 있습니다. |
| 재사용이 어려움 | 파일 저장 기능만 다른 화면에서 다시 쓰기 어렵습니다. |
| 테스트가 어려움 | 파일 기능을 확인하려면 GUI 창을 함께 실행해야 합니다. |
1.2 로그인 구조로 비유해 보기
Model/View 구조는 로그인 화면으로 생각하면 이해하기 쉽습니다.
로그인 화면에서 View는 아이디 입력창, 비밀번호 입력창, 로그인 버튼을 담당합니다.
Model은 아이디와 비밀번호가 맞는지 검사하는 로직을 담당합니다.
| 구조 | 역할 |
| LoginView | 아이디 입력창, 비밀번호 입력창, 로그인 버튼을 보여줍니다. |
| LoginModel | 아이디와 비밀번호를 검사하고 로그인 성공 여부를 판단합니다. |
파일 처리 예제도 같은 방식으로 나눌 수 있습니다.
| 구조 | 역할 |
| FileEditorView | 텍스트 편집기, 버튼, 파일 선택 창, 메시지 박스를 담당합니다. |
| FileModel | 파일 쓰기, 파일 읽기, 파일 복사 기능을 담당합니다. |
1.3 이번 강의에서 바꿀 구조
이번 강의에서는 파일 처리 기능을 FileModel 클래스로 분리합니다.
View는 파일 처리 방법을 직접 알 필요가 없습니다.
View는 Model의 메서드를 호출하고, 결과에 따라 메시지만 보여줍니다.
# 기존 구조
FileEditorWindow
├─ 화면 코드
└─ 파일 처리 코드
# 새 구조
FileModel
└─ 파일 처리 코드
FileEditorView
└─ 화면 코드 + Model 호출
문제의 핵심: 지금 필요한 개선은 기능을 더 추가하는 것이 아니라, 이미 만든 기능의 역할을 나누어 관리하기 쉽게 만드는 것입니다.
2. FileModel과 FileEditorView로 코드 분리하기
→ 파일 처리 로직은 FileModel로 옮기고, 화면 코드는 FileEditorView에 남깁니다.
2.1 프로젝트 구조 만들기
먼저 프로젝트 파일을 아래처럼 나눕니다.
QFileModelViewDemo/
├─ main.py
├─ file_model.py
└─ file_editor_view.py
main.py는 프로그램 실행 진입점입니다.
file_model.py는 파일 쓰기, 읽기, 복사를 담당합니다.
file_editor_view.py는 화면과 버튼 동작을 담당합니다.
2.2 FileModel 작성하기
먼저 파일 처리 기능만 담당하는 FileModel을 만듭니다.
이 클래스에는 QTextEdit, QPushButton, QMessageBox 같은 화면 코드가 들어가지 않습니다.
# file_model.py
from PySide6.QtCore import QFile, QIODevice, QTextStream
class FileModel:
"""파일 쓰기, 읽기, 복사를 담당하는 모델 클래스"""
def write_text(self, file_path, text):
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
return False, file.errorString()
stream = QTextStream(file)
stream << text
file.close()
return True, ""
def read_text(self, file_path):
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
return False, "", file.errorString()
stream = QTextStream(file)
content = stream.readAll()
file.close()
return True, content, ""
def copy_file(self, src_path, dst_path, overwrite=False):
if not QFile.exists(src_path):
return False, "원본 파일이 존재하지 않습니다."
if QFile.exists(dst_path):
if not overwrite:
return False, "대상 파일이 이미 존재합니다."
if not QFile.remove(dst_path):
return False, "기존 대상 파일을 삭제할 수 없습니다."
src_file = QFile(src_path)
if not src_file.copy(dst_path):
return False, src_file.errorString()
return True, ""
FileModel은 파일 처리 결과를 True 또는 False로 반환합니다.
실패했을 때는 오류 메시지도 함께 반환합니다.
| 메서드 | 역할 |
| write_text() | 지정한 경로에 텍스트를 저장합니다. |
| read_text() | 지정한 경로의 텍스트 파일을 읽습니다. |
| copy_file() | 원본 파일을 대상 경로로 복사합니다. |
2.3 FileEditorView 작성하기
이제 화면을 담당하는 FileEditorView를 만듭니다.
View는 직접 파일을 열고 쓰지 않습니다.
대신 self.model.write_text(), self.model.read_text(), self.model.copy_file()을 호출합니다.
# file_editor_view.py
from PySide6.QtWidgets import (
QFileDialog,
QHBoxLayout,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)
from file_model import FileModel
class FileEditorView(QMainWindow):
"""화면과 버튼 동작을 담당하는 View 클래스"""
def __init__(self):
super().__init__()
self.model = FileModel()
self.setWindowTitle("QFile Model/View 파일 도구")
self.resize(700, 500)
self._setup_ui()
self._connect_signals()
def _setup_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
self.text_edit = QTextEdit()
self.text_edit.setPlaceholderText("파일에 저장할 내용을 입력하거나, 파일에서 읽은 내용이 표시됩니다.")
main_layout.addWidget(self.text_edit)
file_button_layout = QHBoxLayout()
self.btn_write = QPushButton("파일에 쓰기")
self.btn_read = QPushButton("파일에서 읽기")
file_button_layout.addWidget(self.btn_write)
file_button_layout.addWidget(self.btn_read)
main_layout.addLayout(file_button_layout)
src_layout = QHBoxLayout()
src_label = QLabel("원본 파일:")
self.line_src = QLineEdit()
self.line_src.setPlaceholderText("복사할 원본 파일 경로")
self.btn_find_src = QPushButton("원본 찾기")
src_layout.addWidget(src_label)
src_layout.addWidget(self.line_src)
src_layout.addWidget(self.btn_find_src)
main_layout.addLayout(src_layout)
dst_layout = QHBoxLayout()
dst_label = QLabel("복사 대상:")
self.line_dst = QLineEdit()
self.line_dst.setPlaceholderText("복사될 파일 경로")
self.btn_find_dst = QPushButton("대상 선택")
dst_layout.addWidget(dst_label)
dst_layout.addWidget(self.line_dst)
dst_layout.addWidget(self.btn_find_dst)
main_layout.addLayout(dst_layout)
copy_layout = QHBoxLayout()
self.btn_copy = QPushButton("파일 복사")
copy_layout.addStretch()
copy_layout.addWidget(self.btn_copy)
main_layout.addLayout(copy_layout)
def _connect_signals(self):
self.btn_write.clicked.connect(self.on_write_clicked)
self.btn_read.clicked.connect(self.on_read_clicked)
self.btn_find_src.clicked.connect(self.on_find_src_clicked)
self.btn_find_dst.clicked.connect(self.on_find_dst_clicked)
self.btn_copy.clicked.connect(self.on_copy_clicked)
def on_write_clicked(self):
text = self.text_edit.toPlainText()
if not text:
QMessageBox.information(self, "알림", "저장할 텍스트가 없습니다.")
return
file_path, _ = QFileDialog.getSaveFileName(
self,
"텍스트 파일 저장",
"",
"텍스트 파일 (*.txt);;모든 파일 (*.*)",
)
if not file_path:
return
success, error_message = self.model.write_text(file_path, text)
if success:
QMessageBox.information(self, "저장 완료", f"파일이 저장되었습니다.\\n경로: {file_path}")
else:
QMessageBox.critical(self, "오류", f"파일을 저장할 수 없습니다.\\n오류: {error_message}")
def on_read_clicked(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"텍스트 파일 열기",
"",
"텍스트 파일 (*.txt);;모든 파일 (*.*)",
)
if not file_path:
return
success, content, error_message = self.model.read_text(file_path)
if not success:
QMessageBox.critical(self, "오류", f"파일을 읽을 수 없습니다.\\n오류: {error_message}")
return
self.text_edit.setPlainText(content)
QMessageBox.information(self, "읽기 완료", f"파일 내용을 불러왔습니다.\\n경로: {file_path}")
def on_find_src_clicked(self):
file_path, _ = QFileDialog.getOpenFileName(
self,
"원본 파일 선택",
"",
"모든 파일 (*.*)",
)
if file_path:
self.line_src.setText(file_path)
def on_find_dst_clicked(self):
file_path, _ = QFileDialog.getSaveFileName(
self,
"복사 대상 파일 선택",
"",
"모든 파일 (*.*)",
)
if file_path:
self.line_dst.setText(file_path)
def on_copy_clicked(self):
src_path = self.line_src.text().strip()
dst_path = self.line_dst.text().strip()
if not src_path:
QMessageBox.warning(self, "경고", "원본 파일 경로를 입력하거나 선택하세요.")
return
if not dst_path:
QMessageBox.warning(self, "경고", "복사 대상 파일 경로를 입력하거나 선택하세요.")
return
overwrite = False
if self.model.file_exists(dst_path):
result = QMessageBox.question(
self,
"파일 덮어쓰기 확인",
f"대상 파일이 이미 존재합니다.\\n덮어쓰시겠습니까?\\n\\n경로: {dst_path}",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result == QMessageBox.StandardButton.No:
return
overwrite = True
success, error_message = self.model.copy_file(src_path, dst_path, overwrite=overwrite)
if success:
QMessageBox.information(
self,
"복사 완료",
f"파일 복사가 완료되었습니다.\\n원본: {src_path}\\n대상: {dst_path}",
)
else:
QMessageBox.critical(self, "오류", f"파일 복사에 실패했습니다.\\n오류: {error_message}")
위 코드에는 아직 한 가지 문제가 있습니다.
View에서 self.model.file_exists(dst_path)를 호출하고 있지만, FileModel에는 아직 file_exists() 메서드가 없습니다.
그래서 FileModel에 파일 존재 여부 확인 메서드를 추가해야 합니다.
2.4 FileModel에 file_exists() 추가하기
FileModel에 아래 메서드를 추가합니다.
def file_exists(self, file_path):
return QFile.exists(file_path)
그러면 FileModel 전체 코드는 다음처럼 정리됩니다.
# file_model.py
from PySide6.QtCore import QFile, QIODevice, QTextStream
class FileModel:
"""파일 쓰기, 읽기, 복사를 담당하는 모델 클래스"""
def file_exists(self, file_path):
return QFile.exists(file_path)
def write_text(self, file_path, text):
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.WriteOnly | QIODevice.OpenModeFlag.Text):
return False, file.errorString()
stream = QTextStream(file)
stream << text
file.close()
return True, ""
def read_text(self, file_path):
file = QFile(file_path)
if not file.open(QIODevice.OpenModeFlag.ReadOnly | QIODevice.OpenModeFlag.Text):
return False, "", file.errorString()
stream = QTextStream(file)
content = stream.readAll()
file.close()
return True, content, ""
def copy_file(self, src_path, dst_path, overwrite=False):
if not QFile.exists(src_path):
return False, "원본 파일이 존재하지 않습니다."
if QFile.exists(dst_path):
if not overwrite:
return False, "대상 파일이 이미 존재합니다."
if not QFile.remove(dst_path):
return False, "기존 대상 파일을 삭제할 수 없습니다."
src_file = QFile(src_path)
if not src_file.copy(dst_path):
return False, src_file.errorString()
return True, ""
2.5 main.py 작성하기
마지막으로 프로그램을 실행하는 main.py를 작성합니다.
# main.py
import sys
from PySide6.QtWidgets import QApplication
from file_editor_view import FileEditorView
def main():
app = QApplication(sys.argv)
window = FileEditorView()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
이제 main.py를 실행하면 Model/View 구조로 분리된 파일 처리 GUI 프로그램이 실행됩니다.
핵심 확인: FileModel은 파일 기능만 담당하고, FileEditorView는 화면과 사용자 상호작용만 담당합니다.
3. Model/View 코드 흐름 자세히 살펴보기
→ 버튼 클릭부터 Model 호출, 결과 표시까지의 흐름을 분석합니다.
3.1 파일 저장 흐름
파일 저장 버튼을 클릭하면 View와 Model이 함께 동작합니다.
[파일에 쓰기] 버튼 클릭
↓
View가 QTextEdit에서 텍스트 가져오기
↓
View가 QFileDialog로 저장 경로 받기
↓
View가 model.write_text(file_path, text) 호출
↓
Model이 QFile과 QTextStream으로 파일 저장
↓
Model이 성공 여부 반환
↓
View가 QMessageBox로 결과 표시
View는 파일 저장 방법을 직접 처리하지 않습니다.
파일 저장은 Model에게 맡기고, View는 결과를 사용자에게 보여줍니다.
| 담당 | 하는 일 |
| View | 텍스트 입력값과 저장 경로를 가져옵니다. |
| Model | 파일을 열고 텍스트를 저장합니다. |
| View | 저장 성공 또는 실패 메시지를 보여줍니다. |
3.2 파일 읽기 흐름
파일 읽기 흐름도 비슷합니다.
[파일에서 읽기] 버튼 클릭
↓
View가 QFileDialog로 파일 경로 받기
↓
View가 model.read_text(file_path) 호출
↓
Model이 QFile과 QTextStream으로 파일 읽기
↓
Model이 성공 여부와 파일 내용 반환
↓
View가 QTextEdit에 내용 표시
↓
View가 QMessageBox로 결과 표시
Model은 읽은 텍스트를 View에게 돌려줍니다.
View는 그 텍스트를 QTextEdit에 표시합니다.
| 코드 | 의미 |
| self.model.read_text(file_path) | Model에게 파일 읽기를 요청합니다. |
| success | 파일 읽기 성공 여부입니다. |
| content | Model이 읽어서 돌려준 파일 내용입니다. |
| self.text_edit.setPlainText(content) | 읽은 내용을 화면에 표시합니다. |
3.3 파일 복사 흐름
파일 복사에서는 View와 Model의 역할 분리가 더 잘 보입니다.
View는 사용자에게 덮어쓰기 여부를 묻습니다.
Model은 실제 파일 존재 여부 확인, 삭제, 복사를 처리합니다.
[파일 복사] 버튼 클릭
↓
View가 원본 경로와 대상 경로 가져오기
↓
View가 대상 파일 존재 여부를 Model에 확인
↓
필요하면 View가 사용자에게 덮어쓰기 질문
↓
View가 model.copy_file(src, dst, overwrite=True/False) 호출
↓
Model이 QFile.exists(), QFile.remove(), QFile.copy() 처리
↓
Model이 성공 여부 반환
↓
View가 QMessageBox로 결과 표시
| 담당 | 하는 일 |
| View | 입력값 확인, 사용자 질문, 메시지 표시를 담당합니다. |
| Model | 파일 존재 여부 확인, 파일 삭제, 파일 복사를 담당합니다. |
주의할 점: View가 Model을 호출할 수는 있지만, Model이 QMessageBox나 QTextEdit 같은 화면 요소를 직접 다루지는 않도록 유지하는 것이 좋습니다.
4. 한 클래스 방식과 Model/View 방식 비교하며 정리하기
→ 4강의 한 클래스 방식과 5강의 역할 분리 방식을 비교합니다.
4.1 기존 방식과 새 방식 비교
4강에서는 하나의 클래스 안에 모든 코드를 넣었습니다.
5강에서는 파일 처리 로직을 FileModel로 분리했습니다.
| 구분 | 한 클래스 방식 | Model/View 방식 |
| 구조 | FileEditorWindow 안에 화면 코드와 파일 처리 코드가 함께 있습니다. | FileModel은 파일 처리, FileEditorView는 화면 처리를 담당합니다. |
| 장점 | 처음 만들 때는 단순하고 빠르게 구현할 수 있습니다. | 역할이 나뉘어 유지보수와 재사용이 쉬워집니다. |
| 단점 | 기능이 늘어나면 클래스가 길고 복잡해집니다. | 처음에는 파일이 나뉘어 조금 복잡해 보일 수 있습니다. |
| 추천 상황 | 작은 테스트나 빠른 실습에 적합합니다. | 기능이 늘어나는 실제 앱 구조에 적합합니다. |
4.2 이번 강의에서 만든 구조 정리
이번 강의에서 만든 구조는 다음과 같습니다.
main.py
↓
FileEditorView 생성
↓
FileEditorView가 FileModel 생성
↓
사용자가 버튼 클릭
↓
View가 Model 메서드 호출
↓
Model이 파일 처리 후 결과 반환
↓
View가 화면에 결과 표시
이 구조에서는 파일 처리 로직을 다른 화면에서도 다시 사용할 수 있습니다.
예를 들어 나중에 파일 저장 버튼이 있는 다른 창을 만들어도 FileModel은 그대로 재사용할 수 있습니다.
| 파일 | 역할 |
| main.py | 앱을 실행하고 메인 창을 띄웁니다. |
| file_model.py | 파일 쓰기, 읽기, 복사 기능을 담당합니다. |
| file_editor_view.py | 화면 구성, 버튼 연결, 메시지 표시를 담당합니다. |
4.3 최종 정리
이번 글에서는 GUI 파일 처리 코드를 Model/View 구조로 분리했습니다.
| 핵심 내용 | 정리 |
| 문제 파악 | 하나의 GUI 클래스 안에 화면 코드와 파일 처리 코드가 섞이면 관리가 어려워질 수 있음을 확인했습니다. |
| 문제 해결 | 파일 처리 로직을 FileModel로 분리하고, View에서는 Model을 호출하도록 수정했습니다. |
| 코드 분석 | 버튼 클릭 후 View가 Model을 호출하고, Model이 결과를 반환하는 흐름을 살펴봤습니다. |
| 비교 정리 | 한 클래스 방식은 빠른 실습에 좋고, Model/View 방식은 기능이 커지는 앱에 더 적합합니다. |
기억할 문장: View는 사용자가 보는 화면을 담당하고, Model은 실제 데이터를 처리하는 로직을 담당합니다.
참고. 공식 문서로 확인하기
→ Model/View 개념과 QFile 관련 클래스는 공식 문서에서 더 자세히 확인할 수 있습니다.
참고 문서
이번 글에서 사용한 QFile, QFileDialog, QMessageBox, Qt Model/View 개념은 아래 공식 문서에서 확인할 수 있습니다.
- Qt for Python Model/View Programming 공식 문서
- PySide6 QFile 공식 문서
- PySide6 QTextStream 공식 문서
- PySide6 QFileDialog 공식 문서
- PySide6 QMessageBox 공식 문서
참고: 이번 강의의 FileModel과 FileEditorView는 초보자용으로 단순화한 구조입니다. Qt의 공식 Model/View 프레임워크는 QAbstractItemModel과 View 클래스를 사용하는 더 확장된 구조입니다.