파이썬과 객체지향 프로그래밍 - . 5가지 클래스 설계의 원칙 (S.O.L.I.D) - 참고사항으로 이해합니다. (초심화)

12. 5가지 클래스 설계의 원칙 (S.O.L.I.D) - 참고사항으로 이해합니다. (초심화)

  • S - SRP(Single responsibility principle) 단일 책임 원칙
  • O - OCP(Open Closed Principle) 개방 - 폐쇄 원칙
  • L - LSP(Liskov Substitusion Principle) 리스코프 치환 법칙
  • I - ISP(Interface Segregation Principle) 인터페이스 분리 원칙
  • D - DIP(Dependency Inversion Principle) 의존성 역전 법칙

출처) https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design

객체지향과 디자인 패턴 관련 책 : http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=28301535 파이썬은 아니나, Object Oriendted design과 Design pattern에 관심있으신분은 해당 서적을 참고하시면 좋습니다.

클래스 설계 및 팁

  • 직접 많이 작성하며 이해하는 부분이지만, 처음에는 들어봤다가 중요합니다.
  • 원칙을 이해하면서, 각각의 클래스 설계 예를 들여다보고, 이해를 한다는 느낌으로 접근합니다.

실제 현업에서의 코드 작성시 주의하는 부분

중복코드 제거는 매우 좋음

  • 단 두번이라도 사용된다면, 코드 수정시 각 코드를 모두 수정해야 하기 때문
  • 여러 동일한 코드를 수정하다보면 시간도 많이 소요되고, 복잡도 높아지고, 버그 가능성이 많음
  • 중복 코드는 함수로 만들던지, 클래스 메서드/인터페이스로 만들던지 반드시 하나의 코드로 만드는 것이 필요함

최적화는 제일 나중에 할것: 굳이 잘돌아가면, bottleneck 이 존재할때만

  • 괜히 최적화한답시고, 중간마다 계속 바꾸면, 시간만 많이 들 수 있음
  • 완벽하게 상세한 레벨까지 최적화된 설계를 처음부터 해도 시간이 많이 들고, 결국 실제 구현시 바뀌는 경우가 많음

12.1. SRP(Single responsibility principle) 단일 책임 원칙

  • 클래스는 단 한개의 책임을 가져야 함 (클래스를 수정할 이유가 오직 하나이어야 함)
  • 예: 계산기 기능 구현시, 계산을 하는 책임과 GUI를 나타낸다는 책임을 서로 분리하여, 각각 클래스로 설계해야 함
  • 실제로는 애매한 부분이 많이 존재함, 가급적 설계시 이런 부분을 염두에 두자가 합리적일 수 있음
In [ ]:
# 나쁜 예
# 학생성적과 수강하는 코스를 한개의 class에서 다루는 예
# 한 클래스에서 두개의 책임을 갖기 때문에, 수정이 용이하지 않다.
class StudentScoreAndCourseManager(object):
    def __init__(self):
        scores = {}
        courses = {}
        
    def get_score(self, student_name, course):
        pass
    
    def get_courses(self, student_name):
        pass
    
# 변경 예
# 각각의 책임을 한개로 줄여서, 각각 수정이 다른 것에 영향을 미치지 않도록 함
class ScoreManager(object):
    def __init__(self):
        scores = {}
        
    def get_score(self, student_name, course):
        pass
    
    
class CourseManager(object):
    def __init__(self):
        courses = {}
    
    def get_courses(self, student_name):
        pass
초간단 연습1
1. SRP 원칙을 고려하여 다음 코드를 클래스로 만들어봅니다.
- https://www.seeko.co.kr/zboard4/zboard.php?id=mainnews 웹페이지에서 타이틀과, 댓글 수를 가져오기
- 엑셀 파일로 만들기

생각해보기
1. 데이터베이스가 있다고 생각해봅니다. 이 중에서도 MySQL 이라는 데이터베이스에 넣는 로직을 구현한다면?
- pymysql 과 MySQL 프로그램을 자신의 PC에 설치하고, 관련 프로그램 파이썬으로 구현 필요
2. 이렇게 만든 클래스를 다른 PC에서 실행하면서 엑셀 파일만 만들고 싶다
3. 다른 PC에서 실행시키려는데, xlsxwriter가 설치 안되어있다. 나는 해당 PC의 데이터베이스만 쓰고 싶다
4. https://www.seeko.co.kr/zboard4/zboard.php?id=mainnews 에서 특정 키워드가 있는 데이터만 추출하고 싶다.
5. 엇, 나는 csv 파일로 만들고 싶다.

웹크롤링 코드 예제 - 코드를 이해할 수 없습니다. 어떤 변수에 결과물이 담기는지만 집중하세요

In [1]:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

title_list = list()
hit_count_list = list()

for pageNum in range(1):
    html = urlopen("https://www.seeko.co.kr/zboard4/zboard.php?id=mainnews&page=" + str(pageNum + 1) + "&select_arrange=headnum&desc=asc&category=&sn=off&ss=on&sc=off&keyword=&sn1=&divpage=10")
    html_object = BeautifulSoup(html, "html.parser")
    title_data = html_object.findAll("td", {"class": "article_subject"})
    title_hit = html_object.findAll("td", {"class": "article_count"})
    for title_wrap in title_data:
        print(title_wrap.get_text())
        title_list.append(title_wrap.get_text())
    for title_hit in title_hit:
        hit_count_list.append(title_hit.get_text())
  KT, LG전자 ‘V30S씽큐’ 공시위반 ‘논란’  
  LG전자 조성진 친노동 뒤엔 ‘감금식 노동착취’ 논란  [2]
   LG전자, 스마트폰 수익성 확보 특단조치…‘V30S씽큐’, 공급 최소화  
  LG V30S씽큐 출시 판매가 104만원 논란  [6]
  [단독] "판매보다 출시에 의의?" LG전자, 구색갖추기 V30S ThinQ 출시 논란  
  구글, 사물 인식하는 인공지능 '구글 렌즈' 서비스 모든 안드로이드 스마트폰에 지원   
  온라인플레이도 없고 직접 구매도 없다?, 닌텐도 국내 e샵 런칭 예정 정보 공개,   
  과기정통부·이통사, 2G 서비스 종료 착수...2G폰→LTE폰 교체 지원  [2]
  英 다이슨, 유선 진공청소기 개발 중단 선언  
  美서 사상최대 1.7Tbps 규모 디도스 공격 발생	  
  [단독] 방통위 "최성준 前 방통위원장 수사의뢰 예정"  
  '무서운' 화웨이, MWC 어워즈서 8관왕…삼성전자 3관왕  
  삼성전자, TV 점유율 회복 위해 마이크로 LED TV 하반기 출시  [1]
  중국 '반도체 굴기' 낸드는 올해 말, D램은 내년 양산 돌입  
  “애플, 2Q 저가형 13인치 맥북에어 출시 전망”  
  中, 인텔과 손잡고 3D낸드 개발...'반도체 코리아' 위협  [2]
  중국, 반도체에 172조원 쏟아붓고도 모자라, 34조원 추가 투자.  [2]
  LG전자, MWC 2018에서 G7 (Neo) 비공개 전시?  [3]
  네이버, 통신사업 진출… AI '클로바'로 통화  
  갤S9 초반 성적 `기대 이하`… 갤S8 못미쳐  [3]
  중국산 메모리 반도체, 올 연말 저가 공세 예고.  [3]
  LG전자, ZKW 인수 무산? 가격차로 철회된 듯  
  소니 '엑스페리아XZ2' 써보니  
  삼성 VS LG 희비교차...삼성, 中업체에 OLED 공급  
  소니·구글·애플 기술 답습한 '갤S9', 모방했지만 완성도 높였다?  

데이터 저장 예제 (엑셀 파일로 만들기) - 코드를 다 이해할 수 없습니다. 어떤 변수에 있는 데이터가 엑셀 파일에 씌여지는지만 집중하세요!

In [25]:
import xlsxwriter

workbook = xlsxwriter.Workbook('ITArticleReport.xlsx')
worksheet = workbook.add_worksheet()

worksheet.set_column(0, 0, 5)
worksheet.set_column(1, 1, 80)

cell_format = workbook.add_format({'bold': True, 'align': 'center', 'fg_color': '#01579B', 'color': 'white', 'border': 1})
worksheet.write(1, 1, '타이틀', cell_format)
worksheet.write(1, 2, '클릭수', cell_format)

cell_format_gray = workbook.add_format({'fg_color': '#ECEFF1', 'border': 1})
cell_format_white = workbook.add_format({'fg_color': 'white', 'border': 1})

for num in range(len(title_list)):
    worksheet.write(num + 2, 1, title_list[num], cell_format_gray)
    worksheet.write(num + 2, 2, hit_count_list[num], cell_format_gray)

workbook.close()

12.2. OCP(Open Closed Principle) 개방 - 폐쇄 원칙

  • 확장에는 열려있어야 하고, 변경에는 닫혀있어야 함
  • 예: 캐릭터 클래스를 만들 때, 캐릭터마다 행동이 다르다면, 행동 구현은 캐릭터 클래스의 자식 클래스에서 재정의(Method Override)한다.
    • 이 경우, 캐릭터 클래스는 수정할 필요 없고(변경에 닫혀 있음)
    • 자식 클래스에서 재정의하면 됨(확장에 대해 개방됨)
In [ ]:
# 나쁜 예
class Rectangle(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        
class Circle:
    def __init__(self, radius):
        self.radius = radius

class AreaCalculator(object):
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.width * shape.height
        return total

shapes = [Rectangle(2, 3), Rectangle(1, 6)]
calculator = AreaCalculator(shapes)
print("The total area is: ", calculator.total_area())
In [8]:
# 좋은 예
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height
    
class Circle:
    def __init__(self, radius):
        self.radius = radius
        
    def area(self):
        return 3.14 * self.radius ** 2
    
    
'''다른 도형에 대해 확장하기 위해서,
AreaCalculator는 수정이 필요 없음 (변경에 닫혀 있음)
단지, Shape을 상속받은 다른 class를 정의하기만 하면 됨 (확장에 대해 개방됨)
'''
class AreaCalculator(object):
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.area()
        return total


shapes = [Rectangle(1, 6), Rectangle(2, 3), Circle(5), Circle(7)]
calculator = AreaCalculator(shapes)

print("The total area is: ", calculator.total_area())
The total area is:  244.36
초간단 연습2
게임 캐릭터 클래스 설계 예제 상기해봅니다. 다음 세 캐릭터의 다양한 메서드를 만드려면 어떻게 해야할지, 클래스 설계를 위 OCP 원칙을 생각하며 잡아봅니다.
게임 캐릭터는 다음과 같이 3명이 존재하고, 각각의 메서드는 다음과 같음
Warrior
 - 공격하면 칼로 찌른다를 출력
Elf
 - 공격하면 마법을 쓴다를 출력
Wizard
 - 공격하면 마법을 쓴다를 출력

12.3. LSP(Liskov Substitusion Principle) 리스코프 치환 법칙

자식 클래스는 언제나 자신의 부모클래스와 교체할 수 있다는 원칙

  • 갤럭시폰 is a kind of 스마트폰

    • 스마트폰은 다른 사람과 전화와 메시지가 가능하다.
    • 스마트폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
    • 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.
  • 위 설명을 갤럭시 폰으로 대체하면 아래와 같다.

    • 갤럭시 폰은 다른 사람과 전화와 메시지가 가능하다.
    • 갤럭시 폰은 데이터 또는 와이파이를 이용해 인터넷을 사용할 수 있다.
    • 스마트폰은 앱 마켓을 통해 앱을 다운 받을 수 있다.

초간단 연습3
다음 캐릭터의 메서드를 모두 담은 클래스를 만든다면?
어떻게 하면 OCP 원칙을 고려할 수 있을까요?

Warrior
 - attack: 상대방 객체를 입력받아서, '칼로 찌르다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
 - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력
 - use_shield: 1번 공격을 막는다.
Elf
 - attack: 상대방 객체를 입력받아서, '마법을 쓰다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
 - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력 
 - wear_manteau: 1번 공격을 막는다.
Wizard
 - attack: 상대방 객체를 입력받아서, '마법을 쓰다' 출력하고, 상대방의 receive 메서드를 호출해서, striking_power만큼 상대방의 health_point를 낮춰준다.
 - receive: 상대방의 striking_point를 입력으로 받아서, 자신의 health_point를 그만큼 낮추기, health_point가 0 이하이면 '죽었음' 출력 
 - use_wizard: 자신의 health_point를 3씩 올려준다.
In [7]:
# 추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    def __init__(self, name='yourname', health_point=100, striking_power=3, defensive_power=3):
        self.name = name
        self.health_point = health_point
        self.striking_power = striking_power
        self.defensive_power = defensive_power        
    
    def get_info(self):
        print (self.name, self.health_point, self.striking_power, self.defensive_power)
    
    @abstractmethod
    def attack(self, second):
        pass

    @abstractmethod
    def receive(self):
        pass

    @abstractmethod
    def special(self):
        pass

12.4. ISP(Interface Segregation Principle) 인터페이스 분리 원칙

  • 클래스에서 사용하지 않는(상관없는) 메서드는 분리해야 한다.
In [8]:
# 추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
    
    @abstractmethod
    def move(self):
        pass

    @abstractmethod
    def eat(self):
        pass

여기서 잠깐: metaclass 란?

  • 클래스를 만들기 위해 파이썬에서는 기본 metaclass가 사용됨
    • 즉, 클래스를 만들기 위해서 메타클래스 라는 것이 필요했던 것임
    • class 생성시, () 아무 것도 넣지 않으면, 기본 파이썬에서 클래스를 만들기 위한 메타클래스가 쓰인다고 보면 됨
    • 추상 클래스 만들시에는 기본 메타클래스로는 생성이 어려우니, 다음과 같이 작성
      • class Character(metaclass=ABCMeta)
    • 싱글톤을 위해 기본 메타클래스를 바꾸는 것임 (싱글톤은 다음에 나오는 디자인 패턴에서 설명)
      • class PrintObject(metaclass=Singleton)
In [20]:
class MyClass:
    pass
In [21]:
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
In [9]:
# 나쁜 예
# 추상 클래스 상속하기
class Elf(Character):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")

    def eat(self):
        print ("no eat")  # <--- 요정은 밥을 안먹지 않을까요? 그래도 선언해줘야 함(상관없는 기능)

        
class Human(Character):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):
        print ("eat foods")

첫 번째 예: 이렇게 작성하는 것이 우선 위 코드보다는 더 좋음1

In [13]:
# 추상 클래스 선언하기
from abc import *

class Character(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass
    
    @abstractmethod
    def move(self):
        pass
In [14]:
# 추상 클래스 상속하기
class Elf(Character):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")
        
class Human(Character):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):  # <--- 메서드 확장
        print ("eat foods")
In [15]:
elf1 = Elf()
human1 = Human()

elf1.attack()
elf1.move()
human1.attack()
human1.move()
human1.eat()
practice the black art
fly
plunge a knife
run
eat foods

두 번째 예: 이렇게 작성하는 것도 처음 코드보다는 더 좋음2

In [4]:
from abc import *

class AttackingWay(metaclass=ABCMeta):
    @abstractmethod
    def attack(self):
        pass

class MovingWay(metaclass=ABCMeta):
    @abstractmethod
    def move(self):
        pass

class EatingWay(metaclass=ABCMeta):
    @abstractmethod
    def eat(self):
        pass

class AbstractHumanCharacter(AttackingWay, MovingWay, EatingWay):
    pass
In [5]:
# 추상 클래스 상속하기
class Elf(AttackingWay, MovingWay):
    def attack(self):
        print ("practice the black art")
    
    def move(self):
        print ("fly")
        
class Human(AttackingWay, MovingWay, EatingWay):
    def attack(self):
        print ("plunge a knife")
    
    def move(self):
        print ("run")

    def eat(self):
        print ("eat foods")
In [6]:
elf1 = Elf()
human1 = Human()

elf1.attack()
elf1.move()
human1.attack()
human1.move()
human1.eat()
practice the black art
fly
plunge a knife
run
eat foods
한발짝 더 나가보기!(심화 문제)
게임 캐릭터 클래스 설계 예제 상기해봅니다. 다음 세 캐릭터의 다양한 메서드를 위 두번째 방법과 유사하게 작성해볼까요?
게임 캐릭터는 다음과 같이 3명이 존재하고, 각각의 메서드는 다음과 같음
Warrior
 - 공격하면 칼로 찌른다를 출력
Elf
 - 공격하면 마법을 쓴다를 출력
Wizard
 - 공격하면 마법을 쓴다를 출력
In [10]:
from abc import *

class UsingKnife(metaclass=ABCMeta):
    @abstractmethod
    def use_knife(self):
        pass

class UsingWizard(metaclass=ABCMeta):
    @abstractmethod
    def use_wizard(self):
        pass

class Warrior(UsingKnife):
    def use_knife(self):
        print ('칼로 찌른다')

class Elf(UsingWizard):
    def use_wizard(self):
        print ('마법을 쓰다')

class Wizard(UsingWizard):
    def use_wizard(self):
        print ('마법을 쓰다')

warrior1 = Warrior()
elf1 = Elf()
wizard1 = Wizard()

warrior1.use_knife()
칼로 찌른다

12.5. DIP(Dependency Inversion Principle) 의존성 역전 법칙

  • 부모 클래스는 자식 클래스의 구현에 의존해서는 안됨
    • 자식 클래스 코드 변경 또는 자식 클래스 변경시, 부모 클래스 코드를 변경해야 하는 상황을 만들면 안됨
  • 자식 클래스에서 부모 클래스 수준에서 정의한 추상 타입에 의존할 필요가 있음
In [11]:
# 실습 코드
class BubbleSort:
    def bubble_sort(self):
        # sorting algorithms
        pass
In [14]:
# 나쁜 예
class SortManager:
    def __init__(self):
        self.sort_method = BubbleSort() # <--- SortManager 는 BubbleSort에 의존적
        
    def begin_sort(self):
        self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

이렇게 되면 어떤 문제가 생길까요? BubbleSort의 메서드 이름을 바꿔봤습니다.

In [13]:
# BubbleSort의 bubble_sort 메서드 변경
class BubbleSort:
    def sort(self):
        print('bubble sort')
        pass
In [15]:
sortmanager = SortManager()
sortmanager.begin_sort()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-7b6c218e4fe5> in <module>()
      1 sortmanager = SortManager()
----> 2 sortmanager.begin_sort()

<ipython-input-14-eb257b4cc514> in begin_sort(self)
      5 
      6     def begin_sort(self):
----> 7         self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

AttributeError: 'BubbleSort' object has no attribute 'bubble_sort'

그러면, 위 예에서는 상위 클래스인 SortManager도 코드를 바꿔줘야 한다.

  • 하부 클래스 코드를 수정하면 상위 클래스 코드도 바꿔줘야 하므로, 어색한 것은 분명함
  • 이 부분을 의존성 역전 법칙에서 상위 클래스가 하부 클래스에 의존되는 역전현상을 막아야 한다라고 어렵게 써놓은 것임
In [8]:
sorting1 = SortManager()
sorting1.begin_sort()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-8-19868312ab3c> in <module>()
      1 sorting1 = SortManager()
----> 2 sorting1.begin_sort()

<ipython-input-6-eb257b4cc514> in begin_sort(self)
      5 
      6     def begin_sort(self):
----> 7         self.sort_method.bubble_sort() # <--- BubbleSort의 bubble_sort 메서드에 의존적

AttributeError: 'BubbleSort' object has no attribute 'bubble_sort'

의존성을 주입하고, 상위 클래스에서 하위 클래스 활용시 하위 클래스에 따라 변경되지 않도록, 일반화(추상화)된 설계를 하면 됨

In [16]:
# 좋은 예
class SortManager:
    def __init__(self, sort_method):    # <--- 의존성을 주입시킨다고 이야기함
        self.set_sort_method(sort_method)
        
    def set_sort_method(self, sort_method):
        self.sort_method = sort_method
        
    def begin_sort(self):
        self.sort_method.sort()         # <--- 하부 클래스가 바뀌더라도, 동일한 코드 활용 가능토록 인터페이스화
In [17]:
# 실습 코드
class BubbleSort:
    def sort(self):
        print('bubble sort')
        pass

class QuickSort:
    def sort(self):
        print('quick sort')
        pass
In [11]:
bubble_sort1 = BubbleSort()
quick_sort1 = QuickSort()

sorting1 = SortManager(bubble_sort1)
sorting1.begin_sort()

sorting2 = SortManager(quick_sort1)
sorting2.begin_sort()
bubble sort
quick sort
초간단 연습2
selection sort 를 출력하는 SelectionSort 클래스 만들고, SortManager로 begin_sort() 호출해서 출력해보기
In [19]:
# 좋은 예
class SortManager:
    def __init__(self, sort_method):    # <--- 의존성을 주입시킨다고 이야기함
        self.set_sort_method(sort_method)
        
    def set_sort_method(self, sort_method):
        self.sort_method = sort_method
        
    def begin_sort(self):
        self.sort_method.sort()         # <--- 하부 클래스가 바뀌더라도, 동일한 코드 활용 가능토록 인터페이스화

class SelectionSort:
    def sort(self):
        print('selection sort')
        pass

selection_sort = SelectionSort()
sorting3 = SortManager(selection_sort)
sorting3.begin_sort()
selection sort