Search

[자바/스프링 개발자를 위한 실용주의 프로그래밍] - 1부 객체 지향(2) SOLID

SOLID란 객체지향 설계의 5가지 원칙의 앞글자를 따서 만든 단어이다.
SRP (Single Responsibility Principle) - 단일 책임 원칙
OCP (Open/Closed Principle) - 개방/폐쇄 원칙
LSP (Liskov Substitution Principle) - 리스코프 치환 원칙
ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
DIP (Dependency Inversion Principle) - 의존성 역전 원칙
SOLID의 목표는 소프트웨어의 유지보수성과 확장성을 높이는 데에 있다. 그렇다면 설계 관점에서 소프트웨어의 유지보수성을 높인다는 것은 무엇일까?
영향범위 - 코드 변경으로 인한 영향범위가 작아야 함
의존성 - 의존성을 관리해야함
확장성 - 쉽게 확장 가능해야함
SOLID를 따르면 자연스레 위 세 항목을 충족시켜 유지보수성이 올라간다.

단일 책임 원칙

클래스에는 하나의 책임만 갖고 있어야 한다
클래스가 특정 역할을 달성하는데만 집중한다면 수정이 필요할때 특정 클래스나 모듈만 수정하면 되고, 다른 책임과의 충돌을 걱정할 필요 없다.
여기서 말하는 책임이란? - 하나의 모듈은 하나의 액터에 대해서만 책임져야한다.
모듈이나 클래스가 담당라는 액터가 혼자라면 - 단일 책임 원칙
모듈이나 클래스가 담당하는 액터가 여럿이라면 - 단일 책임 위배
액터가 하나 일 수 있다면 클래스를 변경할 이유도 하나로 고정된다. 그러므로 클래스를 변경할 유일한 이유는 액터의 요구사항이 변경될 때로 제한되어야 한다.

개방 폐쇄 원칙

클래스의 동작을 수정하지 않고 확장 할수 있어야 한다
운영중인 코드는 영향범위 때문에 쉽게 수정하기 어렵다. 따라서 코드를 확장하고자 할때 기존코드를 아예 건드리지 않는 것이 최고의 전략이다.
Order가 food라는 구현체 직접 사용
새로운 요구사항이 들어오자 Order 클래스가 영향을 받는다.
Order가 계산 가능한이라는 역할 사용
역할에 의존했더니 새로운 요구사항이 들어와도 Order 클래스가 영향을 받지 않는다. 또 다른 요구사항이 들어와도 해당 프로덕트 클래스를 만들고 Calculable이라는 역할을 구현하기만 하면 되기 때문에 확장에 열려있고 변경에 닫혀있는 구조가 된다.

리스코프 치환 원칙

| 파생 클래스는 기본 클래스를 대체할 수 있어야 한다.
LSP 위반 코드 예시
Squere가 Rectangle을 상속받아 정사각형으로 동작하도록 했지만 Rectangle의 넓이는 구하는 방식과 일관되지 않는 문제 발생
class Rectangle: def __init__(self, width, height): self.width = width self.height = height def set_width(self, width): self.width = width def set_height(self, height): self.height = height def area(self): return self.width * self.height # Rectangle을 상속받은 Square 클래스class Square(Rectangle): def set_width(self, width): self.width = width self.height = width def set_height(self, height): self.width = height self.height = height def print_area(rectangle): rectangle.set_width(5) rectangle.set_height(4) print("Area:", rectangle.area()) rectangle = Rectangle(3, 4) square = Square(5, 5) print_area(rectangle)# 예상: Area: 20 print_area(square)# 예상과 다르게 동작할 가능성 (정사각형인데도 면적 계산이 비정상적)
Python
복사
LSP 준수하도록 코드 수정
class Shape: def area(self): raise NotImplementedError("Subclasses should implement this!") class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def set_width(self, width): self.width = width def set_height(self, height): self.height = height def area(self): return self.width * self.height class Square(Shape): def __init__(self, side_length): self.side_length = side_length def set_side(self, side_length): self.side_length = side_length def area(self): return self.side_length * self.side_length # 사용 예시def print_area(shape): print("Area:", shape.area()) rectangle = Rectangle(3, 4) square = Square(5) print_area(rectangle)# 예상: Area: 12 print_area(square)# 예상: Area: 25
Python
복사
Shape 인터페이스를 만들어 각자의 규칙에 맞게 관리 할 수 있도록 분리. 이처럼 자식 클래스가 부모 클래스의 동작 방식을 깨지 않고 일관성을 유지하도록 설계해야 한다.

인터페이스 분리

클라이언트별로 세분화된 인터페이스를 만들어야 한다.
역할과 책임을 분리하고 역할을 세세하게 나눠 기능적 응집도를 추구하는 것이 목표이다. 하나의 통합된 인터페이스로 모든 것을 해결하려하면 구현체에 불필요한 구현을 생길 수 있다. 인터페이스를 통합하려는 시도는 응집도를 추구하는 것 아닌가? 라고 생각할 수 있지만, 단순히 유사한 코드를 모은다고 응집도가 생기는 것은 아니기 때문이다.

의존성 역전

구체화가 아닌 추상화에 의존해야한다
의존이란 다른 객체나 함수를 사용하는 상태로 단순히 사용하기만 해도 의존하는 것이기 때문에 소프트웨어는 의존하는 객체들의 집합이라고 볼 수 있다. 객체지향에서 객체는 협력을 위해 서로 의존하는데 이때 어떻게 결합하느냐에 따라 약한 의존상태를 만들수 있다.

의존성 주입

햄버거를 만드는 셰프 코드 예시
class HamburgerChef: def make(self): # 필요한 모든 객체를 만들어 사용 bread = WheatBread() meat = Beef() vegetable = Lettuce() sauce = TomatoSauce() return Hamburger.builder().bread(bread).meat(meat).vegetable(vegetable).sauce(sauce).build()
Python
복사
의존성 주입
class HamburgerChef: def __init__(self, bread, meat, vegetable, sauce): # 필요한 재료들을 의존성으로 주입받음 self.bread = bread self.meat = meat self.vegetable = vegetable self.sauce = sauce def make(self): return Hamburger.builder().bread(self.bread).meat(self.meat).vegetable(self.vegetable).sauce(self.sauce).build()
Python
복사
구체적인 클래스에 의존하지 않게해 강한 의존성을 줄인다. 이렇게 하나의 클래스만 두고 봤을 때는 어떤 큰차이가 있는지 알기 힘들다. 실제 프로덕트 코드에서는 사용하는 모듈과 클래스가 엄청 많기 때문에 클래스 내부에서 인스턴스화를 진행한다면 어디서 어떻게 사용하고 있는지 직접 들어가서 보기전에는 어려운 문제가 있다. 따라서 최대한 구현체에 의존하는 이 시점을 미루고 의존성을 관리하기 위해 의존성 주입을 해주는 것이다.

의존성 역전

Restaurant 클래스가 HambuergerChef에 의존
Chef라는 인터페이스 추가
Chef라는 상위 인터페이스를 만들어 기존 두 클래스가 인터페이스에 의존하도록 바꿨다. 이처럼 추상화를 이용해 간접 의존 형태로 변경할 수 있다. 이렇게 되면 상위 하위 모듈을 나눠서, 상위모듈을 그대로 재사용하고 하위모듈을 교체해서 새 기능 제공할 수 있다.
1.
상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
2.
추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.

정리

다시 처음의 유지보수성을 판단하는 세가지 관점을 가져와보면 SOLID의 목적을 이해 할 수 있을 것이다.
영향범위 - 영향범위에 문제가 있다면 응집도를 높이고 적절히 모듈화해서 단일 책임 원칙을 준수하는 코드를 만든다.
의존성 - 의존성에 문제가 있다면 의존성 주입과 의존성 역전 원칙 등을 적용해 약한 의존관계를 만든다.
확장성 - 확장성에 문제가 있다면 의존성 역전 원칙을 이용해 개방 폐쇄 원칙을 준수하는 코드로 만든다.