본문 바로가기
프로그래밍 언어/설계 원칙 & 디자인 패턴

저는 SOLID가 처음이라니까요? (2편)

by 조엘 2021. 4. 15.

안녕하세요! 조엘입니다!

 

"처음이라니까요" 시리즈 네 번째 주자 바로 SOLID 원칙입니다. 👏👏

SOLID가 왜 중요한 개념인지 같이 공부해보도록 해요!

 

저번 1편에 이어 2편에서는 LSP / ISP / DIP를 다루도록 하겠습니다!

 

 

*** Liskov Substitution Principle (리스코프 치환 원칙) ***

세 번째 원칙은 LSP, 리스코프 치환 원칙이에요. 정의는 다음과 같아요

"상위 타입의 객체를 하위 타입의 객체로 치환해도, 상위 타입을 사용하는 프로그램은 정상으로 작동해야 한다."

 

LSP는 앞서 살펴본 OCP를 지원하는 원칙이에요. LSP다형성에 대한 원칙이거든요.

 

LSP를 준수한 프로그램에서는 상위 타입의 객체를 사용하는 기존의 코드에,

하위 타입의 객체를 대입해도 프로그램의 작동에 문제가 없어야 해요. 

 

이를 만족하려면 하위 타입의 객체는 상위 타입의 객체가 따르던 계약 사항을 잘 준수하여 작성해야 해요. 

하위 타입의 객체가 상위 타입의 객체의 행동 패턴에 벗어나도록 구현하면 안 된다는 것이에요. 

 

여기서 하위 타입 객체가 준수해야 할 상위 타입 객체의 계약 사항은 다음을 의미해요. 

 

1. 하위 타입 객체에서 오버 라이딩된 메서드는 상위 타입 객체에서 지정한 매개 변수를 받아야 한다. 

상위 타입에서 잘 사용하던 매개 변수는 하위 타입에서도 잘 사용되어야 해요. 

상위 타입의 객체를 하위 타입의 객체로 변경했는데,

기존의 매개변수를 넘겨받고 오류가 난다면, 이는 LSP에 위반되어요.

 

2. 하위 타입 객체에서 오버라이딩 된 메서드의 리턴 값은 상위 타입 객체에서 지정한 리턴 값의 범위를 벗어나지 않는다.

상위 타입에서 반환받은 리턴 값을 활용한 로직을 작성한 경우를 생각해보아요. 

상위 타입의 객체를 하위 타입의 객체로 변경했는데, 

하위 타입의 객체가 기존의 상위 타입의 객체에서 예상치 못한 리턴 값을 반환한다면 프로그램에 오류가 발생해요.

이 역시 LSP에 위반되어요. 

 

LSP는 "확장에 대한 계약"이에요. 

상위 타입의 객체를 하위 타입의 객체로 치환해도 정상 작동 한다면, 자연스레 확장에 유연함을 부여한 것이에요. 

객체 지향적으로 좋은 설계에 한 걸음 다가가는 것이죠. 

중요한 것은 하위 타입 객체로 치환했을 때 정상 작동 되어야 한다는 것! 🎯

 

 

*** Interface Segregation Principle (인터페이스 분리 원칙) ***

네 번째 원칙은 ISP, 인터페이스 분리 원칙이에요. 정의는 다음과 같아요

"인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다"

 

여기서 클라이언트는 실제 로직을 구현한 콘크리트 클래스라고 생각하면 좋아요. 

클라이언트를 구현할 때, 인터페이스를 통해 구현하는 경우를 자주 볼 수 있어요. 

이는 곧 클라이언트가 인터페이스를 의존한다는 것이기도 해요. 

 

그렇다면 인터페이스는 어떤 기준으로 작성해야 할까요? 

ISP 원칙에서는 인터페이스를 클라이언트 기준으로 분리해야 한다고 얘기해요. 

왜 그럴까요? 

 

클라이언트와 인터페이스의 의존은 양면성을 가져요. 

클라이언트가 인터페이스에 의존할 경우,

클라이언트의 변화로 인터페이스가 변화되기도 하지만, 

인터페이스의 변화로 클라이언트가 변화되기도 해요. 

 

여기서 중요한 표현이 나왔는데요, "인터페이스의 변화로 클라이언트가 변화"된다는 표현이에요. 

 

만약 하나의 인터페이스가 다양한 기능을 제공하여,

인터페이스의 기능 중 단지 몇 개가 필요해 이를 구현한 클라이언트가 있다고 가정해봐요. 

해당 클라이언트는 쓸데없이 사용하지도 않는 메서드를 구현해야 할 것이에요. 

 

이 상황에서 해당 클라이언트가 쓰지 않는 인터페이스의 기능에 변경이 일어난다면,

해당 클라이언트 역시 이를 변경을 해줘야 됩니다. 

사용하지도 않는 메서드를 위해 쓸모없는 유지/보수 비용이 투입되는 것이에요. 

 

만약 인터페이스가 클라이언트를 기준으로 분리되어 있다면, 

클라이언트는 인터페이스를 통해 자신에게 꼭 알맞은 필요한 기능만을 구현할 것이고,

클라이언트로부터 발생하는 인터페이스 변경 여파가 오로지 자신을 위한 변화였기 때문에,

타 클라이언트에게 까지 영향을 미치지 않을 것이에요. 

 

따라서 ISP 원칙을 통해 인터페이스를 클라이언트에 특화되도록 분리시킨다면,

유지/보수/확장에 유리한 코드 작성이 가능해 집니다! ✨ 

 

 

*** Dependency Inversion Principle (의존 역전 원칙) ***

다섯 번째 원칙은 DIP, 의존 역전 원칙이에요. 정의는 다음과 같아요

"고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다.

저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다."

 

고수준 모듈 "어떤 의미 있는 단일 기능을 제공하는 모듈"이라고 정의되고,

저수준 모듈 "고수준 모듈의 기능을 구현하기 위해 필요한 하위 기능의 실제 구현"이라고 정의되어요.

 

추상화를 통해서 고수준 모듈을 만들고, 상세 구현인 저수준 모듈을 고수준 모듈에 의존하도록 해요.  

이렇게 추상화를 해놓은 고수준 모듈을 기반으로, 저수준 모듈을 자유롭게 추가/확장해 나갈 수 있어요. 

 

DIP가 잘 드러난 예시로 전략 패턴을 들 수 있는데요.

papimon.tistory.com/75

 

저는 전략 패턴이 처음이라니까요?

안녕하세요! 조엘입니다! "처음이라니까요" 시리즈 두 번째 주자 바로 전략 패턴입니다. 자랑은 아니지만, 사실 디자인 패턴이라는 걸 우테코 들어와서 처음 들어봤어요. 😂 그래서 전략 패

papimon.tistory.com

전략 패턴에서도 우리는 추상화를 해놓은 고수준 모듈전략(Strategy) 과, 

실제 상세 구현을 담고 있는 저수준 모듈전략 콘크리트(Concrete Strategy) 를 만들었어요. 

이를 통해 확장에 유리한 코드를 작성할 수 있었어요. 

 

내용적인 설명이 OCP와 유사하다고 느껴지는 부분이 많은데요!

이는 DIP가 OCP를 지원해주는 원칙이기 때문입니다. 

OCP를 지키려고 노력하다 보면, 자연스레 DIP가 지켜질 것이에요. 🎈

 

 

*** 정리 ***

SOLID 원칙변화에 유연하게 대처할 수 있는 설계 원칙이에요. 

 

SRP(단일 책임 원칙)ISP(인터페이스 분리 원칙) 는 객체가 커지지 않도록 막아줘요. 

객체를 작게 유지하면서, 해당 객체의 변경이 다른 객체의 변경으로 이어지지 않도록 해줘요. 

원하는 객체만 손쉽게 변경할 수 있다면, 변화에 유연한 설계를 해낸 것이지요. 

 

LSP(리스코프 치환 원칙)DIP(역전 의존 원칙)OCP(개방 폐쇄 원칙) 를 지원해요. 

LSP를 통해 다형성을 확보하고, DIP를 통해 변화되는 부분을 추상화해요. 

변화가 추상화된 타입에 여러 객체를 주입/참조시키면서,

확장에는 열려있고, 변경에는 닫혀있는 코드를 쉽게 작성할 수 있게 되어요. 

이 역시 변화에 유연한 설계를 해낸 것이에요. 

 

객체 지향 설계의 가이드라인이 되어주는 SOLID 원칙을 염두에 두고 코드를 작성하다 보면,

차츰차츰 변화에 유연한 코드를 작성할 것 같습니다!! 🤞🤞

 

 

참고

- pizzasheepsdev.tistory.com/9

- stackify.com/solid-design-liskov-substitution-principle/

- huisam.tistory.com/entry/ISP

- wonwoo.ml/index.php/post/1717

- 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴(최범균 | 인투북스)

반응형

댓글