지난 시간에는 SOLID 4번째, 인터페이스 분리 원칙에 대해 알아봤습니다.
이번 시간에는 SOLID 마지막 시간! 의존 관계 원칙에 대해 함께 배워봅시다.
의존 관계 원칙(Dependency inversion principle)은 SOLID의 마지막 D에 해당하는 원칙입니다. 이 원칙의 정의는 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 되고 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다는 것입니다.
코드를 통해 원칙을 이해해 봅시다.
class Sword:
"""검 클래스"""
def __init__(self, damage):
self.damage = damage
def slash(self, other_character):
"""검 사용 메소드"""
other_character.get_damage(self.damage)
class GameCharacter:
"""게임 캐릭터 클래스"""
def __init__(self, name, hp, sword: Sword):
self.name = name
self.hp = hp
self.sword = sword
def attack(self, other_character):
"""다른 유저를 공격하는 메소드"""
if self.hp > 0:
self.sword.slash(other_character)
else:
print(self.name + "님은 쓰러져서 공격할 수 없습니다.")
def change_sword(self, new_sword):
"""검을 바꾸는 메소드"""
self.sword = new_sword
def get_damage(self, damage):
"""캐릭터가 공격받았을 때 자신의 체력을 깎는 메소드"""
if self.hp <= damage:
self.hp = 0
print(self.name + "님은 쓰러졌습니다.")
else:
self.hp -= damage
def __str__(self):
"""남은 체력을 문자열로 리턴하는 메소드"""
return f"{self.name}님은 hp: {self.hp}이(가) 남았습니다."
게임 캐릭터 클래스를 보면 인스턴스 변수로 이름, 체력, 사용하는 검이 있습니다. 메소드로는 다른 유저를 공격하는 attack, 검을 바꾸는 change_sword, 캐릭터가 공격 받았을 때 자신의 체력을 깎는 get_damage, 남은 체력을 문자열로 리턴하는 던더 str이 있습니다.
그 위에 검 클래스에는 공격력을 나타내는 damage를 인스턴스 변수로 가지고 있고 검 사용 메소드인 slash를 가지고 있습니다. 이 메소드는 다른 캐릭터의 체력을 받아서 공격력만큼 그 수치를 깎습니다.
두 클래스를 사용해보겠습니다.
bad_sword = Sword(5)
good_sword = Sword(200)
좋은 검과 나쁜 검 인스턴스를 생성했습니다.
game_character_1 = GameCharacter("타키탸키", 200, bad_sword)
game_character_2 = GameCharacter("파이리", 500, good_sword)
게임 캐릭터 인스턴스도 생성해줍니다.
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_1.attack(game_character_2)
game_character_2.attack(game_character_1)
print(game_character_1)
print(game_character_2)
게임 캐릭터 1, 타키탸키가 공격을 세 번 합니다. 뒤이어 게임 캐릭터 2, 파이리도 공격을 한 번 했습니다. 두 캐릭터의 체력을 출력했습니다.
타키탸키님은 쓰러졌습니다.
타키탸키님은 hp: 0(이)가 남았습니다.
파이리님은 hp: 485이(가) 남았습니다.
여기서 잠깐! 다시 의존 관계 역전 원칙으로 돌아가 봅시다. 의존 관계 원칙에서 상위 모듈과 하위 모듈이라는 개념이 나왔었는데요. 두 클래스가 있을 때 어떤 클래스가 다른 클래스를 사용하는 관계에 있으면 사용하는 클래스를 상위 모듈, 사용 당하는 클래스를 하위 모듈이라고 합니다.
위 정의에 따르면 GameCharacter 클래스가 상위 모듈, Sword 클래스가 하위 모듈입니다.
GameCharacter 클래스의 attack 메소드 속 self.sword.slash(other_character)
부분을 보면 attack 메소드가 Sword 클래스로 만든 인스턴스에 slash 메소드를 사용하는데요. 이는 상위 모듈인 GameCharacter 클래스가 하위 모듈인 Sword를 의존하고 있는 것이고 의존 관계 역전 원칙을 위반하고 있는 것이죠.
attack 메소드가 문제 없이 실행되려면 slash 메소드가 잘 실행된다는 보장이 있어야 하는데요. 만약 Sword 클래스의 slash 메소드가 사라지거나 이름이 바뀌면 GameCharacter 클래스의 attack 메소드가 호출될 때 문제가 생길 것입니다.
이렇게 하위 모듈의 내용이 바뀌면 상위 모듈의 내용도 바뀌어야 하는데요. 그럼 코드를 유지보수 하기가 어려워집니다. 따라서, 이러한 의존 관계를 만들지 말라는 것이 의존 관계 역전 원칙이라 할 수 있습니다.
그렇다면 이 문제의 해결 방법은 무엇일까요? 의존 관계 원칙의 두번째 정의를 보면 알 수 있습니다. 바로 두 모듈 모두를 추상화에 의존시키는 방법인데요. 코드를 통해 알아봅시다.
정의에 따르면 추상 클래스가 필요해 보이니 하나 만들어보겠습니다.
from abc import ABC, abstractmethod
class IWeapon(ABC):
"""무기 클래스"""
@abstractmethod
def use_on(self.other_character):
pass
IWeapon 클래스는 캐릭터가 사용할 수 있는 무기를 나타내는데요. 이 클래스 안에는 다른 캐릭터를 공격하는 걸 나타내는 use_on이라는 메소드가 있습니다.
이제 무기에 해당하는 Sword 클래스가 IWeapon 클래스를 상속받도록 하겠습니다. 이때, 추상 메소드 use_on을 상속받고 오버라이딩 해야 하기 때문에 기존의 slash 메소드를 use_on으로 대체하면 됩니다.
class Sword(IWeapon):
"""검 클래스"""
def __init__(self, damage):
self.damage = damage
def use_on(self, other_character):
other_character.get_damage(self.damage)
이번에는 다른 무기를 하나 더 추가해봅시다. 총을 나타내는 Gun 클래스를 만들어보겠습니다.
def Gun(IWeapon):
"""총 클래스"""
def __init__(self, damage, num_rounds):
self.damage = damage
self.num_rounds = num_rounds
def use_on(self, other_character):
"""총 사용 메소드"""
if self.num_rounds > 0:
other_character.get_damage(self.damage)
self.num_rounds -= 1
else:
print("총알이 없어 공격할 수 없습니다")
총 클래스는 인스턴스 변수로 공격력을 나타내는 damage와 총알 갯수를 나타내는 num_rounds를 가지고 있습니다. 메소드는 IWeapon 클래스로부터 물려받는 추상 메소드를 오버라이딩 하고 있습니다.
무기가 하나 추가 되었으니 GameCharacter 클래스의 코드도 수정이 필요하겠죠?
def __init__(self, name, hp, weapon: IWeapon):
self.name = name
self.hp = hp
self.weapon = weapon
먼저, 이닛 메소드의 파라미터를 sword가 아닌 weapon으로 바꿔야 합니다. 여러 가지 무기가 파라미터로 들어올 것이기 때문입니다. 또한, weapon은 IWeapon의 인스턴스이기 때문에 타입힌팅도 바꿔줍니다. 그리고 인스턴스 변수 부분도 sword에서 weapon으로 바꿔야 합니다.
다음으로 attack 메소드의 sword 부분도 weapon으로 바꿔줍니다. 이제 어떤 무기든 IWeapon 추상 클래스를 상속받아야 하므로 weapon 변수가 어떤 클래스의 인스턴스이든 공격에 간한 내용은 use_on이라는 메소드 안에 있어야 합니다. 따라서, slash 부분도 use_on으로 바꿔줍니다.
이제 이 클래스들을 사용해볼까요?
sword = Sword(5)
gun = Gun(200, 10)
우선, 각 무기 클래스들의 인스턴스를 생성합니다.
game_character_1 = GameCharacter("타키탸키", 200, sword)
game_character_2 = GameCharacter("파이리", 500, gun)
이 부분도 무기를 수정해주었습니다. 그 다음 공격 부분을 같은 코드로 실행하면,
타키탸키님은 쓰러졌습니다.
타키탸키님은 hp: 0(이)가 남았습니다.
파이리님은 hp: 485이(가) 남았습니다.
잘 출력됩니다.
SOLID의 마지막, 의존 관계 원칙을 정리해보겠습니다. 이 원칙의 정의는 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 되고 두 모듈 모두 추상화된 내용에 의존해야 한다는 것이었습니다.
여기서 상위 모듈은 다른 클래스를 사용하는 클래스, 하위 모듈은 사용 당하는 클래스를 말했었죠? 보통 상위 모듈은 프로그램의 메인 흐름에 좀 더 가깝고 하위 모듈은 상대적으로 멀리 있습니다.
의존 관계 원칙을 다시 말하면 상위 모듈이 하위 모듈을 사용할 때 직접 인스턴스를 가져다 쓰지 말라는 뜻입니다. 왜냐하면 그렇게 할 경우, 하위 모듈의 구체적인 내용에 상위 모듈이 의존하게 되어 하위 모듈에 변화가 있을 때마다 상위 모듈의 코드를 자주 수정해야 되기 때문입니다.
이 문제에 대한 해결 방법은 추상 클래스입니다. 추상 클래스는 상위 모듈과 하위 모듈 사이에 추상화 레이어를 만드는 것과 같은데요.
추상 클래스를 사용하면 상위 모듈에는 추상 클래스의 자식 클래스 인스턴스를 사용한다는 가정 하에, 그 하위 모듈을 사용하는 코드를 작성해두면 되고 하위 모듈은 추상 클래스의 추상 메소드들을 오버라이딩하기만 하면 됩니다.
그럼 상위 모듈은 새로운 하위 모듈이 생겨도 기존 코드를 수정할 필요 없이 새 하위 모듈을 자유롭게 가져다 쓸 수 있습니다. 그만큼 유지보수가 편해지기도 하구요.
그런데 이 부분이 낯설지 않습니다. 클래스의 기존 기능을 확장하면서 기존 코드를 수정하지 않아도 되는 상태. 네, 맞습니다. 우리가 앞서 배운 개방-폐쇄 원칙이 떠오르시죠?
사실, 이 의존 관계 역전 원칙은 개방-폐쇄 원칙을 지키는 하나의 방법닙니다. 이처럼 SOLID 원칙은 각 원칙이 서로 별도의 것이 아니라 긴밀한 관계를 맺습니다.
이번 시간에는 SOLID의 마지막, 의존 관계 원칙 정리에 대해 함께 알아봤습니다.
처음에는 이러한 개념들이 익숙치 않으실텐데요. 하지만 이제까지 여러 원칙들을 거쳐오며 결국 추상 클래스의 활용과 상속, 다형성 등을 코드에 적용하는 것이 SOLID 원칙을 지키는 방법이라는 걸 느끼셨을 것 같습니다.
이처럼 객체와 클래스 개념, 객체 지향 프로그래밍의 네 기둥, SOLID 모두 유기적인 관계를 맺고 있습니다. 따라서, 어느 하나를 이해하면 다른 하나를 더 쉽게 이해할 수 있으므로 놓치지 않고 잘 학습하셨길 바랍니다.
이렇게 객체 지향 프로그래밍의 모든 내용이 끝났습니다. 앞으로 더 효과적으로 코드를 작성하실 수 있길 바랍니다.
수고 많으셨습니다😊😊😊
* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.