SOLID? 객체지향?

파이 ఇ·2024년 11월 6일
1
post-thumbnail

SOLID ?

SOLID란 객체 지향 프로그래밍을 하면서 지켜야하는 5가지 원칙으로 각각 SRP(단일 책임 원칙), OCP(개방 폐쇄 원칙), LSP(리스코프 치환 원칙), ISP(인터페이스 분리 원칙), DIP(의존 역전 법칙)의 앞글자를 따서 만들어졌다.
SOLID 원칙을 철저히 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 도움이 되는 것으로 알려져있다.

[ 단일 책임 원칙 ] (SRP, Single Responsibility Principle)

헷갈릴 수 있지만 단일 책임 원칙은 하나의 클래스(이하 객체)가 하나의 책임을 가져야 한다는 모호한 원칙으로 해석하면 안된다. 대신 객체가 변경되는 이유가 한가지여야 함으로 받아 들여야 한다. 여기서 변경의 이유가 한가지 책임이라는 것은 해당 모듈이 여러 대상 또는 액터들에 대해 책임을 가져서는 안되고, 오직 하나의 액터에 대해서만 책임을 져야 한다는 것을 의미한다.

만약 어떤 객체가 여러 액터에 대해 책임을 가지고 있다면 여러 객체들로부터 변경에 대한 요구가 올 수 있으므로, 해당 객체를 수정해야 하는 이유 또한 여러개가 될 수 있다. 반면에 어떤 객체가 단 하나의 책임 만을 가지고 있다면, 특정 액터로 부터 변경을 특정할 수 있으므로 해당 객체를 변경해야 하는 이유와 시점이 명확해진다.

단일 책임의 원칙을 제대로 지키면 변경이 필요할 때 수정할 대상이 명확해진다. 그리고 이러한 단일 책임의 원칙의 장점은 시스템이 커질수록 극대화되는데, 시스템이 커지면서 서로 많은 의존성을 갖게되는 상황에서 변경 요청이 오면 딱 1가지만 수정하면 되기 때문이다.

단일 책임 원칙을 적용하여 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 추상화함으로써 애플리케이션의 변화에 손쉽게 대응할 수 있다.

[ 개방 폐쇄 원칙 ] (OCP, Open-Close Principle)

개방 폐쇄 원칙 확장에 대해 열려있고 수정에 대해서는 닫혀있어야 한다는 원칙으로, 각각이 갖는 의미는 다음과 같다.

  • 확장에 대해 열려있다 : 요구사항이 변경될 때 새로운 동작을 추가하여 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀있다 : 기존의 코드를 수정하지 않고 애플리케이션의 동작을 추가하거나 변경할 수 있다.

OCP가 본질적으로 얘기하는 것은 추상화이다. 추상화를 통해 변하는 것들은 숨기고 변하지 않는 것들에 의존하게 하면 우리는 기존의 코드 및 클래스들을 수정하지 않은 채로 애플리케이션을 확장할 수 있다.

닫혀있는 클래스를 확장하는 방법은 크게 두 가지 방법이 존재한다. 첫 번째 방법은 상속을 활용하는 것이고 두 번째 방법은 포함 관계를 이용하는 것이다. 객체지향에서는 한 클래스를 언제든지 상속하여 필요한 상태와 기능을 추가할 수 있다. 하지만 상속은 is-a 관계가 성립해야 하며, 클래스 간 관계가 고정되기 때문에 유연하지 못할 수 있다. 또 클래스가 상속을 염두하고 설계되어 있지 않다면 해당 클래스를 상속하여 자식 클래스를 정의하기 어려울 수 있다.

닫혀있는 클래스를 더 유연하게 확장하는 방법은 포함 관계를 이용하는 것이다. 클래스의 어떤 기능을 다른 클래스에 위임하고 그 클래스의 객체를 상위 타입에 의존하면 언제든지 유지하고 있는 객체를 바꾸어 기능을 바꿀 수 있다. 또 포함 관계를 이용하면 다양한 것들을 조합하여 새로운 특성의 클래스를 만들 수 있다.

결제 클래스가 있고, 그것을 상속한 PG 결제, 토스페이 클래스가 있을 때 결제 클래스를 상속한 네이버페이, 페이코 클래스를 만든다고 기존 정의한 PG 결제나 토스페이를 수정할 필요는 없다. 또 다형성을 활용하였다면 PG 결제 타입이나 토스페이를 이용하는 기존 코드도 수정할 필요가 없다. 이것이 개방 폐쇄 원칙을 의미하는 것이다.

그렇다고 하여 추후 확장될 가능성이 거의 없는 것들까지 미리 준비하는 것은 과도한 설계가 될 수 있다. 개방 폐쇄 원칙은 "공짜"가 아니다. 따라서 코드의 확장성과 가독성 사이에서 적절한 균형이 필요하다. 따라서 단기간 내에 진행할 수 있는 확장, 구현 비용이 많이 들지 않는 확장에 대해 확장 포인트를 미리 준비하되, 향후 지원 여부가 확실하지 않은 요구사항이나 확장이 오히려 개발에 부하를 주는 경우에는 해당 작업이 실제로 필요할 때 리팩터링 하는 것이 더 나을 수 있다.

[ 리스코프 치환 원칙 ] (LSP, Liskov Substitution Principle)

리스코프 치환 원칙은 1988년 바바라 리스코프가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 하위 타입은 상위 타입을 대체할 수 있어야 한다는 것이다.

상속은 코드를 재사용할 수 있도록 해주고, 코드 중복을 없애준다. 상속의 장점은 이뿐만이 아니다. 상속은 상위 클래스 타입을 후손 클래스를 아우르는 공통 리모컨으로 사용할 수 있도록 해준다. 이를 통해 다형성을 활용할 수 있고, 범용 프로그래밍 또한 가능하다.

이와 같은 장점들이 존재하지만 코드를 재사용할 수 있다고 무조건 상속하면 매우 어색한 코드를 만들 수 있다. 따라서 상속은 반드시 is-a 관계가 성립하는 경우에만 사용해야 한다. 예시로 Pet을 상속하여 사람을 정의하면 "사람은 애완동물이다."라는 명제가 성립하지 않기 때문에 올바른 상속관계라 할 수 없다. 또한 is-a 관계가 성립하더라도 무조건 상속할 필요는 없다. 하여 이 상속이 적절한 상속인지 살펴볼 때 고려해야하는 원리가 바로 LSP이다.

즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경 되어도 차이점을 인식하지 못한 채 상위타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다는 것이다. 즉, 이는 클라이언트와 객체 사이의 계약이 존재하고, 이를 준수해야 한다는 원칙이므로 계약에 따른 설계(design by contract)라고 표현할 수 도 있다. 하위 클래스는 상위 클래스의 동작 규칙(계약)을 따라야 하는 것이다.

다음은 리스코프 치환 원칙을 위반할 수 있는 상황들을 일부 정리한 것이다.

  • 하위 클래스가 상위 클래스에서 선언한 기능을 위반한 경우
    • 상위 클래스가 주문 정렬을 위한 sortOrderByAmount() 함수를 구현해 두었는데, 하위 클래스에서 생성 날짜에 따라 정렬되도록 변경한 경우
  • 하위 클래스가 입력, 출력 및 예외에 대한 상위 클래스의 계약을 위반한 경우
    • 상위 클래스에서 오류가 발생하면 null을, 값을 얻을 수 없으면 빈 컬렉션을 반환하게 해두었는데, 하위 클래스에서 오류가 발생하면 예외를 발생시키고, 값을 얻을 수 없으면 null을 반환하도록 변경 한 경우
    • 상위 클래스에서는 입력 시 모든 정수를 허용하지만, 하위 클래스에서는 음수일 때 예외를 발생시키는 경우
    • 상위 클래스에서 던지는 예외는 ArgumentException뿐이데, 하위 클래스에서는 다른 예외도 던지는 경우
  • 하위 클래스가 상위 클래스의 주석에 나열된 특별 지침을 위반하는 경우
    • 상위 클래스에 예금을 인출하는 withDraw() 메서드에 사용자의 출금 금액이 잔액을 초과해서는 안된다는 주석이 있을 때, 하위 클래스에서는 가능한 경우

리스코프 치환 원칙 위반 여부를 판단하기 위한 방법으로 상위 클래스의 단위 테스트를 하위 클래스까지 확인하는 방법도 있다. 테스트가 실패하면 상위 클래스의 계약을 완전히 준수하지 않고 하위 클래스가 리스코프 치환 원칙을 위반할 수 있음을 알 수 있다.

[ 인터페이스 분리 원칙 ] (ISP, Interface Segregation Principle)

인터페이스 분리 원칙클래스 자신이 필요하지 않는 메서드를 구현하도록 강요되지 않아야 한다는 원리이다. 이 원리에 충실하기 위해 클래스나 인터페이스가 제공하는 메서드의 수는 최소화 되어야 한다.

객체가 충분히 높은 응집도의 작은 단위로 설계됐더라도, 목적과 관심이 각기 다른 클라이언트가 있다면 인터페이스를 통해 적절하게 분리해주어야 한다는 것이다. 즉, 인터페이스 분리 원칙이란 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것이다. 인터페이스 분리 원칙을 준수함으로써 모든 클라이언트가 자신의 관심에 맞는 퍼블릭 인터페이스(외부에서 접근 가능한 메세지)만을 접근하여 불필요한 간섭을 최소화할 수 있으며, 기존 클라이언트에 영향을 주지 않은 채로 유연하게 객체의 기능을 확장하거나 수정할 수 있다.

인터페이스 분리 원칙을 지킨다는 것은 어떤 구현체에 부가 기능이 필요하다면 이 인터페이스를 구현하는 다른 인터페이스를 만들어서 해결할 수 있다. 예를 들어 파일 읽기/쓰기 기능을 갖춘 구현 클래스가 존재하는데 어떤 클라이언트는 읽기 작업만을 필요로 한다면 별도의 읽기 인터페이스를 만들어 제공해주는 것이다.

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다. 그리고 이렇게 인터페이스를 클라이언트의 기대에 따라 분리하여 변경에 의한 영향을 제어하는 것을 인터페이스 분리 원칙이라고 한다.

여기서 인터페이스는 꼭 하나의 인터페이스 파일에만 해당하지는 않는다. 인터페이스 분리 원칙에서 이야기하는 인터페이스는 넓게 보아 아래의 내용들까지 확장될 수도 있다.

  • API나 기능의 집합
  • 단일 API 또는 기능
  • 객체지향 프로그래밍의 인터페이스

[ 의존 역전 법칙 ] (DIP, Dependency Inversion Principle)

의존 역전 법칙이란 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것이다. 객체 지향 프로그래밍에서는 객체들 사이에 메세지를 주고 받기 위해 의존성이 생기는데, 의존성 역전 원칙은 올바른 의존 관계를 위한 원칙에 해당된다. 여기서 각각 고수준 모듈과 저수준 모듈이란 다음을 의미한다.

  • 고수준 모듈 : 입력과 출력으로부터 먼(비즈니스와 관련된) 추상화된 모듈
  • 저수준 모듈 : 입력과 출력으로부터 가까운(데이터베이스, 캐시 등과 관련된) 구현 모듈

의존 역전 원칙이란 결국 비즈니스와 관련된 부분이 세부 사항에는 의존하지 않는 설계 원칙을 의미한다.

의존 역전 원칙은 개방 폐쇄 원칙과도 밀접한 관련이 있으며, 의존 역전 법칙이 위배되면 개방 폐쇄 원칙 역시 위배될 가능성이 높다. 또한 의존 역전 원칙에서 주의해야 하는 것이 있는데, 의존 역전 원칙에서 의존성이 역전되는 시점이라는 것이다.

결론

위 내용들을 읽으면서 느꼈지만, 5가지 객체 지향 설계 원칙인 SOLID가 얘기하는 핵심은 결국 추상화와 다형성이다. 구체 클래스에 의존하지 않고 추상 클래스(또는 인터페이스)에 의존함으로써 우리는 유연하고 확장 가능한 애플리케이션을 만들 수 있다는 것이다.


재미 업ㅅ어? 재미.있다고 생각하면 되.

profile
⋆。゚★⋆⁺₊⋆ ゚☾ ゚。⋆ ☁︎。₊⋆

0개의 댓글