상세 컨텐츠

본문 제목

SOLID 원칙

똑똑한 개발/Algorithm 과 Data Structure

by 성댕쓰 2022. 2. 3. 22:43

본문

객체 지향 프로그래밍에서 많이 사용하는 SOLID 원칙에 대해 알아보자.

SOLID 원칙은 읽기 쉽고 유연하면서 수정하기 쉬운 코드를 짜기 위한 5가지 원칙을 말한다.


1. Single Responsibility(단일 책임)


모든 함수와 클래스는 하나의 파트에 대해서만 책임을 가져야한다.

 

class Cat:
    def __init__(self, age, name) -> None:
        self.age = age
        self.name = name

    def eat(self):
        print("eating...")
    
    def walk(self):
        print("walking...")

    def speak(self):
        print("meow~")
    
    # cat doesn't have printing or logging behavior
    # def print(self):
    #     print(f"name:{self.name}, age:{self.age}")

    # def log(self):
    #     logger.log(f"name:{self.name}, age:{self.age}")

    # Instead, use reprent behavior outside the cat
    def repr(self):
        return f"name:{self.name}, age:{self.age}"

kitty = Cat(3, "kitty")
print(kitty.repr())
#Logger.log(kitty.repr())

고양이 클래스를 예로 들면, 고양이는 먹고 걷고 말하기의 행동을 가질 수 있다. 하지만 stdout에 출력을 하거나 로그를 남기는 일은 고양이의 행동으로 볼 수 없다. 위 예에서는 repr함수를 정의하고 외부에서 함수를 이용하는 방식으로 single responsibility 원칙을 지키고 있다.



2. Open-Closed(개방-폐쇄)


확장에 대해서는 개방 수정에 대해서는 폐쇄한다는 원칙이다.

 

먼저 Open-Closed 원칙을 지키지 않는 코드이다.

# Open Closed principle 준수하지 않는 Animal
class Animal():
    def __init__(self, type) -> None:
        self.type = type

def hey(animal: Animal):
    if animal.type == 'Cat':
        print('meow')
    elif animal.type == 'Dog':
        print('bark')
    
bingo = Animal('Dog')
kitty = Animal('Cat')

# Cow와 Sheep을 추가하기 위해 hey 함수 수정이 필요
hey(bingo)
hey(kitty)

Animal이 추가 될 때마다, hey함수가 바뀌어야 한다. 확장에 개방적이지 않고 수정에 폐쇄되어 있지 않다.

 

Open-Closed 원칙을 지키기 위해 상속이나 interface를 이용할 수 있다. 다음은 원칙을 지킨 코드 예시이다.

# 상속을 이용한 Animal class. 추가되는 동물에 대해 hey함수의 수정을 필요로 하지 않는다.
class Animal:
    def speak(self): # interface method
        pass

class Cat(Animal):
    def speak(self):
        print("meow")

class Dog(Animal):
    def speak(self):
        print("bark")

class Sheep(Animal):
    def speak(self):
        print("meh")

class Cow(Animal):
    def speak(self):
        print("moo")

def hey(animal: Animal):
    animal.speak();

bingo = Dog()
kitty = Cat()
sheep = Sheep()
cow = Cow()

hey(bingo)
hey(kitty)
hey(sheep)
hey(cow)

Animal이 추가 될 때, hey함수의 수정이 필요없고 따라서 확장에 개방적이고 수정에 폐쇄적이다.



3. Liskov Substitution(리스코프 치환)


T의 서브 타입 S1, S2, S3가 있을 때 T를 서브 타입 S1, S2 또는 S3로 바꿔도 프로그램이 원하는 대로 동작해야 한다는 원칙이다.

 

먼저 해당 원칙을 지킨 코드이다.

class Cat:
    def speak(self):
        print("meow")

class BlackCat(Cat):
    def speak(self):
        print("black meow")

def speak(cat:Cat):
    cat.speak()


cat = Cat()
speak(cat)

BlackCat의 수퍼클래스인 Cat을 BlackCat으로 바꿔도 프로그램이 원하는 대로 동작한다.

 

다음 코드는 Liskov 치환법칙을 지키지 않은 코드이다.

class Fish(Cat):
    def speak(self):
        raise Exception("Fish cannot speak")

cat = Fish()
speak(cat)

생선은 말할 수 없으므로 예외를 throw했다. 프로그램이 제대로 작동하지 않는다. Cat sub class로 Fish를 두는 구조가 Liskov substitution을 위반한 클래스 설계인 것이다.



4. Interface Segregation(인터페이스 분리)


프로그램이 사용하지 않을 메소드에 의존하게 해서는 안된다는 원칙이다. 큰 단위의 인터페이스를 더 작은 단위로 만드는게 좋다는 의미이다.

 

수륙양용차 인터페이스를 만들어서 이를 이용해 차나 보트를 만들면 사용하지 않을 메소드를 갖는 클래스가 만들어진다. Interface Segregation을 위반한 것이다.

//No Interface Segregation Principle

//Large Interface
Interface ICarBoatInterface
{
	void drive();
	void turnLeft();
	void turnRight();
	void steer();
	void steerLeft();
	void steerRight();
}

class Avante :ICarBoatInterface
{
	public void drive()
	{
		//implemenetation
	}
	public void turnLeft()
	{
		//implmementation
	}
	public void turnRight()
	{
		//implementation
	}
	public void steer()
	{
		//implemenetation
	}
	public void steerLeft()
	{
		//implmementation
	}
	public void steerRight()
	{
		//implementation
	}
}

큰 interface를 작은 interface로 나누어 원칙을 지킨다.

 

//Interface Segregation Principle
//two small interfaces (Car, Boat)
Interface ICarInterface
{
	void drive();
	void turnLeft();
	void turnRight();
}

Interface IBoatInterface
{
	void steer();
	void steerLeft();
	void steerRight();
}


class Avante : ICarInterface
{
	public void drive()
	{
		//implemenetation
	}
	public void turnLeft()
	{
		//implmementation
	}
	public void turnRight()
	{
		//implementation
	}
}


class CarBoat :ICarInterface , IBoatInterface
{
	public void drive()
	{
		//implemenetation
	}
	public void turnLeft()
	{
		//implmementation
	}
	public void turnRight()
	{
		//implementation
	}
	public void steer()
	{
		//implemenetation
	}
	public void steerLeft()
	{
		//implmementation
	}
	public void steerRight()
	{
		//implementation
	}
}


5. Dependency Inversion(의존관계 역전)


보통 high level module이 low level module에 의존성을 갖는데, 이를 뒤바꾸면 여러 이점이 생긴다는 원칙이다.

전통적인 구조

# low level에 의존
class Cat:
    def speak(self):
        print("meow")

class Dog:
    def speak(self):
        print("bark")


#Zoo depdns on Cat and Dog
class Zoo:
    def __init__(self):
        self.dog = Dog()
        self.cat = Cat()

    def speakAll(self):
        self.cat.speak()
        self.dog.speak()


zoo = Zoo()
zoo.speakAll()

 

Zoo에 동물이 생길면 생길수록 Dependency가 많아져 관리가 어렵다.

 

전통적인 모델과 DI패턴

하지만 Zoo를 추상클래스 Animal에 의존하게 하고 Cat, Dog등은 Animal을 상속하여 구현함으로써 Animal에 의존하게 만들면 동물이 추가될 때 Zoo는 수정하지 않아도 된다.

 

# Dependency Inversion pattern
class Animal: #abstract module
    def speak(self): #interface method
        pass

class Cat(Animal):
    def speak(self):
        print("meow")

class Dog(Animal):
    def speak(self):
        print("bark")


#Zoo depends on Animal.   (Not Cat, Not Dog)
class Zoo:
    def __init__(self):
        self.animals = []

    def addAnimal(self,animal):
        self.animals.append(animal)
  
    def speakAll(self):
        for animal in self.animals:
            animal.speak()

zoo = Zoo()
zoo.addAnimal(Cat())
zoo.addAnimal(Dog())
zoo.speakAll()

 

참조 :

디자인패턴, Single Responsibility, 단일 책임 원칙, 디자인 패턴 - YouTube,

디자인패턴, open Closed principles, 개방 폐쇄 원칙, Design Patterns - YouTube,

리스코프 치환 법칙, Liskov Substitution principle, SOLID, Design pattern, 디자인 패턴 - YouTube,

인터페이스 분리원칙, SOLID, 디자인 패턴, Interface segregation - YouTube

관련글 더보기

댓글 영역