본문 바로가기
개발

SOLID 원칙과 객체 지향

by hjhello423 2021. 9. 19.

클린 코드로 유명한 로버트 마틴이 좋은 객체 지향 설계를 위해 지켜야 하는 5가지 원칙을 정리한 것을 말한다.

먼저 리스트업을 해보자면 아래와 같이 5개의 원칙이 있으며, 각 원칙의 앞 글자를 따서 SOLID라고 부른다.

  • SRP: 단일 책임 원칙 (single responsibility principle)
  • OCP: 개방-폐쇄 원칙 (Open/closed principle)
  • LSP: 리스코프 치환 원칙 (Liskov substitution principle)
  • ISP: 인터페이스 분리 원칙 (Interface segregation principle)
  • DIP: 의존관계 역전 원칙 (Dependency inversion principle)

SOLID는 자바의 객체 지향을 개념을 더 객체 지향적으로 사용 가능하도록 하는 5개의 기본 원칙이다. 자바의 객체 지향 특징과 연관 지어 알아두면 좋다. 간단하게 정리하자면 자바의 객체 지향은 아래의 특징을 가지고 있다.

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

자바를 이용해 개발을 진행하다 보면 복잡하게 얽힌 로직들을 풀어내기 위해 고민해봤을 것이다. 가장 쉬운(어렵기도 한) 해결 방법은 바로 역할과 구현의 분리 적용하는 것이다. 이 특징들과 SOLID를 연관 지어 순서대로 살펴보자


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

한 클래스는 하나의 책임만 가져야 한다.

내가 작성한 클래스가 하나의 책임만 가지도록 해야 한다고 한다. 그럼 하나의 책임이 뭘까?
말이 쉬우면서도 어려운데, 업무를 진행하다 보면 '하나의 책임'이라는 것을 구분 짓는 게 어려울 때가 많다. 그리고 이 책임이라는 것은 상황에 따라 아주 클 수도 아주 작을 수도 있다.

SOLID의 원칙은 더 객체 지향적으로 설계하고 생각하기 위해 나왔다는 점을 다시 생각해 보자.
유지 보수 관점에서 우리는 코드를 자주 수정하게 되는데 이때 수정 대상인 클래스가 여러 개의 책임을 가지고 있다면 어떨까? (유지보수도 객체 지향의 장점 중 하나이다.)
이 클래스를 수정했을 때 그 클래스 내에 있던 연관된 다른 기능에 어떤 영향을 미칠지 모른다. 직원백엔드 개발 하기()를 수정하려다가 다른 업무 함수에도 영향을 미칠 수 있는 것이다.

직원이라는 클래스가 잘 분리되어 있다면 어떨까?
백엔드 개발자, 프론트 개발자, 마케터 3개의 클래스로 역할을 분리하고 각각의 직종에 필요한 역할만 가지고 있도록 하는 것이다.
역할을 분리하여 각 직종에 필요한 하나의 책임을 가지고 있는 클래스를 만들 수 있다.

SRP에서 말하는 책임을 하나만 가진다는 것은 변경이 일어났을 때 다른 역할에 영향을 미치지 않는다는 뜻이기도 하다. 새로운 요구 사항이 생겨나거나 이슈가 있었을 때 우리가 수정하는 코드는 그 책임이 더 작게 분리되어 있을수록 다른 기능에 영향을 주는 범위가 작아질 것이다.

변경사항이 있을 때 그 변경사항으로 인해 벌어지는 사이드이펙트가 적을수록 잘 분리했다고 정리할 수 있다.

 

OCP 개방-폐쇄 원칙 (Open/closed principle)

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

우리가 개발하는 서비스의 요소들이 확장은 가능하나 변경은 닫혀 있어야 한다고 한다.
이게 무슨 말일까?

OCP는 객체 지향의 다형성을 이용하면 쉽게 접근 가능하다. 예를 들어 우리가 개발한 서비스의 상품 정보를 MySQL에 저장하고 있었다고 해보자. 그런데 어느 날 갑자기 조회 성능을 높이기 위해 redis를 도입하라고 한다.ㅎㅎ
어떻게 MySQL에 접근하던 로직을 redis로 최대한 영향을 안 주고 변경할 수 있을까? 위에서 말했다시피 다형성을 이용해 보자. 바로 인터페이스를 이용하는 것이다.

public class ProductService {

	private MySQLRepository repository = new MySQLRepository();

}

-> redis로 바꾸면?..

public class ProductService {

	private RedisRepository repository = new RedisRepository();

}

 

ProductRepository 인터페이스를 만들었다면 위의 요구사항에 더 유연하게 대처가 가능하다.

public class ProductService {

	private ProductRepository repository = new MySQLRepository();

}

-> redis로 변경

public class ProductService {

	private ProductRepository repository = new RedisRepository();

}

 

OCP의 문제점

자! 인터페이스를 이용해서 더 유연하게 확장 가능하도록 처리해 보았다. 그런데 어딘가 좀 이상하다...

private ProductRepository repository = new MySQLRepository();
private ProductRepository repository = new RedisRepository();

인터페이스를 이용하더라도 결국 구현체를 선언하는 new xxxRepository는 수정을 해줘야 한다...
객체 지향적 코드를 위해 다형성을 이용했고 OCP를 지키려 했지만 결국 코드는 수정을 해줘야 한다.

결국 이렇게 직접 코드를 수정하지 않도록 별도의 처리기를 만들어야 한다는 말이다.
이 별도의 처리기는 대표적으로 자바 개발자들이 가장 많이 사용하는 스프링이다. 스프링의 IOC, DI 개념에 대해 알고 있을 것이다. 스프링이 위 코드에서 경험한 문제를 해결해 주는 역할을 한다.

 

LSP 리스코프 치환 원칙 (Liskov substitution principle)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

다형성을 지원하기 위 한 원칙으로, 다형성에서 하위 클래스는 인터페이스 규약을 꼭 지켜야 한다는 것이다.
인터페이스를 구현한 구현체가 인터페이스에서 정의한 역할을 잘 수행하고 있다고 믿고 사용하기 위한 원칙이다.

위의 사진을 보면 프론트, 마케팅 직원은 직원 인터페이스에서 정의한 저녁 인사하기()를 구현하고 있지 않다. 따라서 백엔드 직원 클래스만이 LSP를 지키고 있다고 말할 수 있는 것이다.

한 가지 더 있다. 백엔드 직원이 인터페이스의 메서드를 모두 구현은 했지만 아침 인사하기()에서 사실은 저녁 인사를 하고 있었다면 어떨까? 인터페이스가 정의한 역할을 제대로 수행하고 있지 않은 것이다. 이러한 경우 또한 LSP를 지키고 있지 않다고 할 수 있다.

 

ISP 인터페이스 분리 원칙 (Interface segregation principle)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

인터페이스가 많은 역할을 가지고 있지 않도록 하라는 얘기이다. 자동차 인터페이스가 있다고 해보자. 자동차 인터페이스를 운전 인터페이스, 정비 인터페이스로 분리가 가능하다.

이때 SRP와 조금 다른 점이 있다면 대상이 인터페이스라는 것이다. ISP는 인터페이스를 클라이언트를 기준으로 작은 단위로 나누어 설계하도록 요구하는 것이다.

 

DIP 의존관계 역전 원칙 (Dependency inversion principle)

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안 된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존하라는 것이다.
쉽게 풀어 보자면 구현 클래스에 의존하지 말고, 역할(인터페이스)에 의존하라는 뜻이다. 어떤 클래스가 도움을 받을 때 혹은 의존할 때 구체적인 클래스는 변화할 확률이 높기 때문에 이를 추상화한 인터페이스나 추상 클래스와 의존관계를 맺도록 설계해야 한다.

객체도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워진다.
그런데 OCP에서 설명한 MemberService는 인터페이스에 의존하지만, 구현 클래스도 동시에 의존한다. (new MySQLRepository())

public class ProductService {

	private ProductRepository repository = new MySQLRepository();

}

결국 위의 소스도 DIP 원칙을 위반하고 있는 것이다.

 


여기까지 SOLID 원칙에 대해 정리해 보았다.
다만 OCP, DIP에 대해서는 해결하지 못한 부분들이 있다. 이런 문제점들을 해결하기 위한 코드를 작성하려면 꽤 많은 시간과 노력이 들어간다.
하지만 걱정할 필요가 없다. 이러한 노력을 스프링이라는 프레임워크가 대신해주고 있으니까.

 


참고

반응형

'개발' 카테고리의 다른 글

[다이어그램] 순차 다이어그램 작성법(Sequence Diagram)  (0) 2021.11.27

댓글