객체지향 설계 5원칙 SOLID
SOLID란?
SOLID란 객체 지향 프로그래밍을 하면서 지켜야하는 5대 원칙이다.
SRP(단일 책임원칙)
OCP(개방-폐쇄원칙)
LSP(리스코프 치환 원칙)
DIP(의존 역전 원칙)
ISP(인터페이스 분리 원칙)
SOLID 원칙을 잘 지키면 유지보수와 확장하기 좋은 소프트웨어를 개발하는데 도움이 된다.
SRP(Single Responsibility Principle - 단일 책임원칙)
로버트 C.마틴은 “어떤 클래스를 변경해야하는 이유는 오직 하나뿐 이어야 한다.” 라는 말을 했다. 여기서 변경의 이유가 한가지라는 것은 해당 클래스가 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다는 것이다.
아래와 같은 클래스가 있다고 가정하자.
class Human{
public void swim() {
// 수영한다
}
public void develop() {
// 개발한다
}
public void coachingFitness() {
// 헬스코칭을 한다
}
}
위의 코드를 보면 Human이라는 클래스가 너무 많은 책임을 가지고 있다. SRP 원칙을 적용해서 다시 적용하면 아래 코드처럼 분리가 된다.
class SwimmingPlayer{
public void swim() {
// 수영한다
}
}
class Developer{
public void develop() {
// 개발한다
}
}
class PersonalTrainer{
public void coachingFitness() {
// 헬스코칭을 한다
}
}
SRP를 제대로 지키면 변경이 필요할 때 수정할 대상이 명확해진다. 이러한 SRP의 장점은 시스템이 커질수록 더 극대화되는데, 시스템이 커져서 서로 많은 의존성을 갖게되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다. SRP를 적용하여 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 추상화함으로써 애플리케이션의 변화에 손쉽게 대응할 수 있다.
OCP(Open-Closed Principle - 개방 폐쇄 원칙)
로버트 C.마틴은 “소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.” 라고 말했다.
확장과 수정에대해 설명하자면,
확장에 대해 열려 있어야 한다.
요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
수정에 대해 닫혀 있어야 한다.
기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.
아래 그림을 봐보자.
그림을 보면 운전자가 2종이냐 1종이냐에 따라서 행동이 달라진다. 이렇게 어떤 변화가 있을 때 바로 운전자에게 영향이 오는 것은 OCP 원칙에 위배된다.
아래 그림은 OCP 원칙을 지켜서 만든 그림이다.
이렇게 상위 클래스 또는 인터페이스를 중간에 두면 어떤 종류의 자동차가 들어와도 자동차에서 수정하여 사용할 수 있다.
즉, 객체가 알아야 하는 지식이 많으면 결합도가 높아지고, 결합도가 높아질수록 OCP원칙을 따르는 구조를 설계하기가 어려워진다. 추상화를 통해 변하는 것들은 숨기고 변하지 않는 것들에 의존하게 하면 기존의 코드 및 클래스들을 따로 수정하지 않고 애플리케이션을 확장할 수 있다.
한가지 더 좋은 OCP원칙의 좋은 예를 봐보자.
아래는 JDBC의 관계도 그림이다.
그림을 보면 알겠지만, 데이터베이스가 Oracle에서 MySQL로 바뀌더라도 JDBC 설정만 바꿔주면 된다.
즉, 자바 애플리케이션은 데이터베이스라고 하는 주변 변화에 닫혀 있고, 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것이다.
LSP(Liskov Substitution Principle - 리스코프 치환 원칙)
로버트 C.마틴은 “서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다” 라고 말했다. 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다.
아래 코드로 설명해보면
public interface Fish{
public void breatheThroughGills() {
// 아가미로 숨을 쉰다
}
}
public class Shark implements Fish{
public void breatheThroughGills() {
// 아가미로 숨을 쉰다
}
}
public class Human implements Fish{
public void breatheThroughGills() {
// 아가미로 숨을 쉰다
}
}
상어는 어류의 한 종류라 Fish 인터페이스의 breatheThroughGills()
규약을 사용할 수 있다. 즉, Shark = Fish가 성립한다.
반대로 Human은 Fish의 인터페이스의 breatheThroughGills() 규약을 지킬 수 없다. 이것은 Human ≠ Fish로 LSP 원칙을 위반한다.
정리하면, 하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다.
ISP(Interface Segregation Principle - 인터페이스 분리 원칙)
로버트 C.마틴은 “클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.” 라고 말했다.
아래 그림은 ISP 원칙을 적용한 예시이다.
즉, ISP란 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이다.
SRP가 클래스의 단일 책임을 강조했다면, ISP는 인터페이스의 단일 책임을 강조한다.
DIP(Dependency Inversion Principle - 의존 관계 역전 원칙)
로버트 C.마틴은 “고차원 모듈은 저차원 모듈에 의존하면 안된다. 이 두 모듈 모두 다른 추상화된 것에 의존해야 한다. 추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다. 자주 변경되는 구체(Concrete) 클래스에 의존하지 마라” 라고 했다.
여기서 고수준 모듈과 저수준 모듈을 설명하자면,
고수준 모듈: 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈
저수준 모듈: 입력과 출력으로부터 가까운(HTTP, 데이터베이스, 캐시 등과 관련된) 구현 모듈
아래 그림을 보면서 설명해보자.
만약 바리스타 객체가 카페라떼 레시피라는 구체적인 클래스에 의존하면 어떻게 될까? 이러면 다른 커피를 만들 때마다 바리스타 객체에서도 계속 같이 변경을 해야한다. 이렇게 되면 DIP 원칙을 위반해 유연성을 떨어트린다.
위 그림처럼 바리스타 객체를 커피 레시피라는 인터페이스에 의존하도록 만들고, 커피 레시피 인터페이스를 구현하는 여러 레시피 클래스들을 만들면 DIP 원칙을 지키게 된다. 이렇게 하면 커피 레시피가 변경이 필요해도 바리스타 객체의 변경을 필요로하지 않아 유연성과 확장성이 높아지게 된다.
이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 관계 역전 원칙이다.
Last updated