객체 지향 프로그래밍/원칙
1. 개요
객체지향 5원칙(SOLID).
객체지향에서 꼭 지켜야 할 5개의 원칙을 말한다. 일단 한번 보면 개념은 알아 듣긴 하지만 막상 실현하려면 생각보다 어려움이 따른다. 이 5개의 원칙의 앞글자를 따서 SOLID라고도 부른다.
2. 목록
2.1. '''S'''RP : 단일 책임 원칙
Single Responsibility Principle
객체는 오직 하나의 책임을 가져야 한다. (객체는 오직 하나의 변경의 이유만을 가져야 한다.)
사칙연산 함수를 가지고 있는 계산 클래스가 있다고 치자. 이 상태의 계산 클래스는 오직 사칙연산 기능만을 책임진다. 만일 프로그램이 대대적으로 공사를 들어가게 되더라도 계산 클래스가 수정될만한 사유는 누가 봐도 사칙연산 함수와 관련 된 문제 뿐이다. 이처럼 단일 책임 원칙은 클래스의 목적을 명확히 함으로써 구조가 난잡해지거나 수정 사항이 불필요하게 넓게 퍼지는 것을 예방하고 기능을 명확히 분리할 수 있게 한다.
위의 원칙이 제대로 지켜지지 않으면 어떻게 될까? 어떤 프로그래머가 위의 계산 클래스를 통해 GUI를 가지는 계산기 프로그램을 개발하고 있다. 그런데 중간에 귀찮다고 GUI 관련 코드를 계산 클래스에 넣어버렸다. 이렇게 되면 계산 클래스는 계산과 GUI라는 두 가지 책임을 지게 되는데 만일 GUI 관련 수정 사항이 발생하게 되면 별 상관도 없어보이는 계산 클래스를 고치게 된다. 이처럼 하나의 클래스가 두 가지 이상의 책임을 지니게 되면 클래스의 목적이 모호해지고 기능을 수정할 때 영향을 받는 범위도 커져서 유지보수가 힘들어지며[1] 결국 작성한 본인조차도 이게 정확히 뭐하는 클래스인지 명확히 설명할 수가 없는 스파게티 코드가 되어버린다.
2.2. '''O'''CP : 개방-폐쇄 원칙
Open-Closed Principle
객체는 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 원칙이다. 즉, 객체 기능의 확장을 허용하고 스스로의 변경은 피해야 한다.
예를 들자면, 스타크래프트의 유닛을 만든다고 치자. 당신은 이런저런 공통사항을 생각하며 메소드와 필드를 정의한다. 이 중엔 이동 메소드도 있다. 이동 메소드는 대상 위치를 인수로 받아 속도에 따라 대상 위치까지 유닛을 길찾기 인공지능을 사용해 이동한다. 하지만 잠깐 곰곰히 생각해보니 이러면 브루들링 같은 유닛의 기묘한 움직임[2] 을 구현할 때 애로사항이 생길 것 같다. 당신은 고민하다가 이동 메소드에서 이동 패턴을 나타내는 코드를 별도의 메소드로 분리하고, 구현을 하위 클래스에 맡긴다. 그러면 브루들링 클래스에서는 이동 패턴 메소드만 재정의하면 유닛 클래스의 변경 없이 색다른 움직임을 보여줄 수 있다! '유닛' 클래스의 '이동' 메소드는 수정할 필요조차 없다(수정에 대해선 폐쇄). 그냥 브루들링 클래스의 이동 패턴 메소드만 재정의하면 그만인 것이다(확장에 대해선 개방).
2.3. '''L'''SP : 리스코프 치환 원칙
Liskov Substitution Principle[3]
자식 클래스는 언제나 자신의 부모 클래스를 '''대체'''할 수 있다는 원칙이다. 즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다는 것. 상속의 본질인데, 이를 지키지 않으면 부모 클래스 본래의 의미가 변해서 is-a 관계가 망가져 다형성을 지킬 수 없게 된다.
또다시 예를 들면, 컴퓨터용 '마우스' 클래스가 있다고 치자. 컴퓨터에 있는 PS/2 포트나 USB 포트를 통해 연결할 수 있고, 마우스를 바닥에 대고 움직이면 컴퓨터가 신호를 받아들인다는 것을 안다. 사용 면에서는 왼쪽과 오른쪽 버튼, 그리고 휠이 있어 사용자가 누르거나 굴릴 수 있을 것이다. 마우스가 볼마우스든 광마우스든, 아니면 GPS를 이용하건 간에 아무튼 사용자는 바닥에 착 붙여 움직일 것이고, 모든 마우스는 예상대로 신호를 보내 줄 것이다. 또한 만약 추가적인 특별한 버튼이 있는 마우스(상속)라도 그 버튼의 사용을 제외한 다른 부분은 보통의 마우스와 다를 바 없으므로 사용자는 그 마우스의 그 버튼이 뭔 역할을 하던간에 문제 없이 잘 사용한다. 여기까지 나온 마우스들은 LSP를 잘 지킨다고 볼 수 있다.
하지만 오른쪽/왼쪽 버튼 대신 옆쪽 버튼을 사용하는 펜마우스를 처음으로 접하게 되면 사용자는 평소 보던 버튼을 누를 수 없다며 이상을 호소할 것이다. 이런 경우 LSP를 전혀 지키지 못 하는 것이다.
2.4. '''I'''SP : 인터페이스 분리 원칙
Interface Segregation Principle
클라이언트에서 사용하지 않는 메서드는 사용해선 안된다. 그러므로 '''인터페이스'''를 다시 작게 '''나누어''' 만든다. OCP와 비슷한 느낌도 들지만 엄연히 다른 원칙이다. 하지만 ISP를 잘 지키면 OCP도 잘 지키게 될 확률이 비약적으로 증가한다. 정확히 말하자면 인터페이스의 SRP라고 할 수 있다.
또 예를 들어보자. 게임을 만드는데 충돌 처리와 이펙트 처리를 하는 서버를 각각 두고 이 처리 결과를 (당연히) 모두 클라이언트에게 보내야 한다고 가정하자. 그러면 아마 Client라는 인터페이스를 정의하고 그 안에 충돌전달()과 이펙트전달(이펙트)를 넣어놓을 것이다. 그리고 충돌 서버와 이펙트 서버에서 이 인터페이스를 구현하는 객체들을 모아두고 있으며, 때에 따라 적절히 신호를 보낸다. 하지만 이렇게 해두면 충돌 서버에겐 쓸모없는 이펙트전달 인터페이스가 제공되며, 이펙트 서버에겐 쓸모없는 충돌전달 인터페이스가 제공된다. 이를 막기 위해선 Client 인터페이스를 쪼개 이펙트전달가능 인터페이스와 충돌전달가능 인터페이스로 나눈 뒤, 충돌에는 충돌만, 이펙트에는 이펙트만 전달하면 될 것이다. 또한 Client 인터페이스는 남겨두되 이펙트전달가능과 충돌전달가능 이 둘을 상속하면 된다. 그렇다고 인터페이스 너무 작게 쪼개서 떡칠은 하지 말자. 뭐든지 적당히.
2.5. '''D'''IP : 의존성 역전 원칙
Dependency Inversion Principle
추상성이 높고 안정적인 고수준의 클래스는 구체적이고 불안정한 저수준의 클래스에 의존해서는 안된다는 원칙으로서, 일반적으로 객체지향의 인터페이스를 통해서 이 원칙을 준수할 수 있게 된다. (상대적으로 고수준인) 클라이언트는 저수준의 클래스에서 추상화한 인터페이스만을 바라보기 때문에, 이 인터페이스를 구현한 클래스는 클라이언트에 어떤 변경도 없이 얼마든지 나중에 교체될 수 있다. (디자인 패턴 중 전략 패턴을 떠올리면 된다)
제어의 역전을 의미하는 Inversion of control(IoC)과 용어가 비슷하기에 혼동하기 쉬우나, 혼동해서는 안 된다. IoC는 제어의 흐름에 대한 개념이지만 DIP는 클래스 사이의 의존성에 대한 개념이다.
[1] 위의 GUI 책임이 추가된 계산 클래스에서 GUI 코드를 건드릴 때 계산 코드가 영향을 받지 않으리라는 법이 없고 반대로 계산 코드를 건드릴 때 GUI가 영향을 받지 않는다는 보장도 없다.[2] 등속 이동을 하는 대부분의 유닛과 다르게, 일정 시간 이동하고 일정 시간 멈추는 방식으로 이동을 한다.[3] 이 원칙을 제안한 MIT 컴퓨터 사이언스 교수 Barbara Liskov의 이름에서 따온 것이다.