Intro
입사한 지 한 달이 되지 않아 아직까지는 버그 티켓을 처리하고 있습니다. 그렇다 보니 코드 몇 줄을 추가하는 작업이 대부분입니다.
그럼에도 개선할 부분이 없는지 고민하고 유지 보수성이나 단위 테스트를 추가하려고 노력하고 있습니다.
빌더 패턴을 사용하여 코드를 개선한 경험을 공유하겠습니다.
빌더 패턴
개선전 예시 코드
from datetime import datetime
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Pt
def create_meeting_minute(meeting_details, participants):
document = Document()
# 제목
p = document.add_paragraph()
run = p.add_run("회의록")
run.bold = True
run.font.size = Pt(16)
p.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
p.paragraph_format.space_after = Pt(24)
# 일시 및 장소
p = document.add_paragraph()
date_time = meeting_details["date"].strftime("%Y년 %m월 %d일 %H:%M")
location = meeting_details.get("location", "온라인 회의")
p.add_run(f"1. 일시: {date_time}\\n")
p.add_run(f"2. 장소: {location}\\n")
p.paragraph_format.space_after = Pt(18)
# 참석자
p = document.add_paragraph()
p.add_run("3. 참석자:\\n")
for participant in participants:
p.add_run(f" {participant['role']} : {participant['name']}\\n")
p.paragraph_format.space_after = Pt(18)
# 안건
p = document.add_paragraph()
for idx, agenda in enumerate(meeting_details["agendas"]):
p.add_run(f"안건 {idx + 1}: {agenda['title']}\\n").bold = True
p.add_run(f"{agenda['description']}\\n")
result = "승인" if agenda["approved"] else "부결"
p.add_run(f"결과: {result}\\n")
p.paragraph_format.space_after = Pt(18)
# 종료 선언
p = document.add_paragraph()
p.add_run("이상으로 모든 안건을 마치며 회의를 종료합니다.")
p.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
return document
상황
의결이 종료되면 의사록 문서를 생성하는 기능이 있습니다. 개선전 코드는 회의 관련 정보와 문서 작성 로직을 단일 함수 안에 포함하고 있습니다. 문서의 각 단락을 절차적으로 추가하고 있으며, 각 단락은 순서대로 document.add_paragraph() 및 p.add_run() 메서드를 호출하여 생성하고 있습니다.
개선점 정의
이 코드에서의 개선점을 아래와 같이 정의해봤습니다.
- 유연성 부족
- 문서의 각 항목이 단일 함수에 하드코딩되어 있어 일부 항목을 재사용하거나 순서를 바꾸는 것이 어렵다고 판단했습니다.
- 코드의 유지보수성
- create_meeting_minute 함수 내부에 긴 로직이 포함되어 있어, 코드의 이해와 유지보수가 어렵다고 느꼈습니다.
제가 느꼈다면 다른 개발자도 동일하게 느낄 것이라 생각했습니다. 또한 기능별로 메서드가 분리되지 않아 새로운 요구사항이 생기면 전체 함수를 수정해야 합니다.
- create_meeting_minute 함수 내부에 긴 로직이 포함되어 있어, 코드의 이해와 유지보수가 어렵다고 느꼈습니다.
- 중복 및 가독성 저하
- 각 항목을 추가할 때마다 동일한 add_paragraph와 add_run 호출이 반복되고 있어 단락이 추가되는 지점을 파악하기 어려웠습니다.
해결
- 각 paragraph 별로 메서드를 분리하여 각 단락이 하나의 책임만을 수행한다.
- 문서 생성 책임을 지는 클래스는 각 문단 구성 요소나 문단 내부의 내용이 바뀌어도 생성 로직에는 영향을 받지 않는다.
생성과 각 문단을 분리하자.
위와 같은 근거로 빌더 패턴을 선택했습니다. 빌더 패턴은 생성 알고리즘과 요소 객체들의 표현이나 조립을 독립적으로 분리할 수 있기 때문에, 문서 생성 로직과 단락별 생성 로직을 명확히 구분하는 데 적합하다고 판단했습니다.
개선 후 예시 코드
from datetime import datetime
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import Pt
class MeetingMinuteBuilder:
def __init__(self):
self.document = Document()
self.p = None
def add_title(self, title):
self.p = self.document.add_paragraph()
run = self.p.add_run(title)
run.bold = True
run.font.size = Pt(16)
self.p.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
self.p.paragraph_format.space_after = Pt(24)
return self
def add_meeting_info(self, date, location):
self.p = self.document.add_paragraph()
self.p.add_run(f"1. 일시: {date.strftime('%Y년 %m월 %d일 %H:%M')}\\n")
self.p.add_run(f"2. 장소: {location or '온라인 회의'}\\n")
self.p.paragraph_format.space_after = Pt(18)
return self
def add_attendees(self, attendees):
self.p = self.document.add_paragraph()
self.p.add_run("3. 참석자:\\n")
for attendee in attendees:
self.p.add_run(f" {attendee['role']} : {attendee['name']}\\n")
self.p.paragraph_format.space_after = Pt(18)
return self
def add_agenda(self, agendas):
for idx, agenda in enumerate(agendas):
self.p = self.document.add_paragraph()
self.p.add_run(f"안건 {idx + 1}: {agenda['title']}\\n").bold = True
self.p.add_run(f"{agenda['description']}\\n")
result = "승인" if agenda["approved"] else "부결"
self.p.add_run(f"결과: {result}\\n")
self.p.paragraph_format.space_after = Pt(18)
return self
def add_closing_statement(self):
self.p = self.document.add_paragraph()
self.p.add_run("이상으로 모든 안건을 마치며 회의를 종료합니다.")
self.p.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
return self
def build(self):
return self.document
# 예시 사용
def create_meeting_minute_with_builder(meeting_details, participants):
builder = MeetingMinuteBuilder()
document = (
builder
.add_title("회의록")
.add_meeting_info(meeting_details["date"], meeting_details.get("location"))
.add_attendees(participants)
.add_agenda(meeting_details["agendas"])
.add_closing_statement()
.build()
)
return document
개선 후 코드는 문서의 각 단락 로직은 문서 생성 로직에 영향을 주지 않고 필요한 부분을 자유롭게 수정하거나 변경할 수 있게 되었습니다.
단위 테스트 관점에서도 개선 전 코드에서는 단일 함수에 생성과 모든 단락 생성 로직이 함께 있어 특정 단락만 테스트하기 어려웠지만,
개선된 코드에서는 단락별로 단위 테스트가 가능해졌습니다.
import unittest
from datetime import datetime
from docx import Document
from meeting_minute_builder import MeetingMinuteBuilder
class TestMeetingMinuteBuilder(unittest.TestCase):
def setUp(self):
self.builder = MeetingMinuteBuilder()
self.sample_date = datetime(2024, 10, 20, 14, 30)
self.sample_location = "서울 강남구"
self.attendees = [
{"role": "의장", "name": "홍길동"},
{"role": "이사", "name": "김철수"}
]
self.agendas = [
{"title": "안건 1", "description": "첫 번째 안건 설명", "approved": True},
{"title": "안건 2", "description": "두 번째 안건 설명", "approved": False}
]
def test_add_title(self):
document = self.builder.add_title("회의록").build()
paragraph = document.paragraphs[0]
self.assertEqual(paragraph.text, "회의록")
self.assertTrue(paragraph.runs[0].bold)
self.assertEqual(paragraph.runs[0].font.size.pt, 16)
self.assertEqual(paragraph.paragraph_format.alignment, 1) # Center alignment
def test_add_meeting_info(self):
document = self.builder.add_meeting_info(self.sample_date, self.sample_location).build()
paragraph = document.paragraphs[0]
expected_date_text = "1. 일시: 2024년 10월 20일 14:30\\n"
expected_location_text = f"2. 장소: {self.sample_location}\\n"
self.assertIn(expected_date_text, paragraph.text)
self.assertIn(expected_location_text, paragraph.text)
def test_add_attendees(self):
document = self.builder.add_attendees(self.attendees).build()
paragraph = document.paragraphs[0]
expected_text = (
"3. 참석자:\\n"
" 의장 : 홍길동\\n"
" 이사 : 김철수\\n"
)
self.assertEqual(paragraph.text, expected_text)
def test_add_agenda(self):
document = self.builder.add_agenda(self.agendas).build()
paragraphs = document.paragraphs
agenda_1_text = "안건 1: 첫 번째 안건 설명\\n결과: 승인\\n"
agenda_2_text = "안건 2: 두 번째 안건 설명\\n결과: 부결\\n"
self.assertEqual(paragraphs[0].text, agenda_1_text)
self.assertEqual(paragraphs[1].text, agenda_2_text)
def test_add_closing_statement(self):
document = self.builder.add_closing_statement().build()
paragraph = document.paragraphs[0]
self.assertEqual(paragraph.text, "이상으로 모든 안건을 마치며 회의를 종료합니다.")
self.assertEqual(paragraph.paragraph_format.alignment, 1) # Center alignment
def test_full_document_structure(self):
# 전체 빌더를 사용하여 문서가 예상한 순서대로 작성되는지 확인
document = (
self.builder
.add_title("회의록")
.add_meeting_info(self.sample_date, self.sample_location)
.add_attendees(self.attendees)
.add_agenda(self.agendas)
.add_closing_statement()
.build()
)
# 검증: 전체 문서 구조가 예상대로 생성되었는지 확인
self.assertEqual(document.paragraphs[0].text, "회의록")
self.assertIn("1. 일시: 2024년 10월 20일 14:30\\n", document.paragraphs[1].text)
self.assertIn("2. 장소: 서울 강남구\\n", document.paragraphs[1].text)
self.assertEqual(document.paragraphs[2].text, "3. 참석자:\\n 의장 : 홍길동\\n 이사 : 김철수\\n")
self.assertEqual(document.paragraphs[3].text, "안건 1: 첫 번째 안건 설명\\n결과: 승인\\n")
self.assertEqual
마무리
요즘은 코드 개선을할때 성급한 추상화와 기존 코드의 일관성을 지나치게 깨지 않는지를 우선적으로 고려하고 있습니다.
이러한 부분은 동료에게 코드 리뷰를 적극적으로 요청하는게 중요하다고 생각이 됩니다.
이 코드도 아직 리뷰를 받기 전이라 반영될 수 있을지는 모르겠습니다!!!
부족하지만 긴 글을 읽어주신 분들께 감사를 드리며, 혹시 개선할 부분이나 잘못 이해한 개념이 있다면 피드백 주시면 정말 감사하겠습니다

'개발' 카테고리의 다른 글
[서평] 출판사: 길벗, 책: 코드 작성 가이드, 저자: 이시가와 무네토시 (0) | 2024.12.08 |
---|