객체 지향 프로그래밍/원칙

1 개요

객체지향 5원칙(SOLID).

객체지향에서 꼭 지켜야 할 5개의 원칙을 말한다. 일단 한번 보면 개념은 알아 듣긴 하지만 막상 실현하려면 생각보다 어려움이 따른다. 이 5개의 원칙의 앞글자를 따서 SOLID라고도 부른다.

2 목록

2.1 SRP : 단일 책임 원칙

Single Responsibility Principle
모든 클래스는 단 하나의 책임을 가져야 한다는 원칙이다. 다르게 말하면 클래스를 수정할 이유가 오직 하나여야만 한다는 뜻이기도 하다.

예를 들자면, 계산을 하는 클래스 '계산기'가 있다고 치자. 근데 이 클래스는 GUI 환경에서 사용될 거라고 한다. 그래서 프로젝트에 GUI 코드를 추가해야하는 상황인데 만약 GUI 코드를 '계산기' 클래스에 넣었다면 SRP를 위반하게 된다. 이 클래스는 '계산을 한다'라는 책임과 'GUI로 나타낸다'라는 책임 두개를 지게 되는 것이다! 이렇게 되면 계산용 필드/메소드와 GUI의 필드/메소드가 뒤섞여 사용자도 개발자도 헷갈린다. 버그가 발생해서 한참 찾았더니 GUI용 코드가 아니라 비슷한 이름의 계산용 메소드를 호출하고 있다던지... 또 극단적으로, 갑자기 일정이 변환되어 CUI 환경으로 프로젝트가 전환되었다고 치자. 다른 프로그래머들은 불평하면서도 객체지향적 분리를 잘 해놔서 몇몇 클래스만 수정해 CUI용으로 바로 전환한다. 하지만 이 '계산기' 클래스를 맡은 프로그래머는 GUI 코드의 필드/메소드 뜯어내랴, 뜯어내다 생긴 부작용 치우랴, 클래스의 사용법 바꾸랴... 할일이 많아질 것이다.

2.2 OCP : 개방-폐쇄 원칙

Open Closed Principle
모든 소프트웨어 구성 요소는 확장에 대해서는 개방되어있지만, 수정에 대해서는 폐쇄되어있다는 원칙이다.

이번에도 예를 들자면, 스타크래프트의 유닛을 만든다고 치자. 당신은 이런저런 공통사항을 생각하며 메소드와 필드를 정의한다. 이중엔 이동 메소드도 있다. 이동 메소드는 대상 위치를 인수로 받아 속도에 따라 대상 위치까지 유닛을 길찾기 인공지능을 사용해 이동한다. 하지만 잠깐 곰곰히 생각해보니 이러면 브루들링같은 유닛의 기묘한 움직임을 구현할때 애로사항이 꽃필것 같다. 당신은 고민하다가 이동 함수에서 이동 패턴을 나타내는 함수(혹은 클래스)를 분리해서 구현을 하위 클래스에 맡긴다. 그러면 부르들링 클래스에선 이것만 재정의/설정하면 유닛 클래스의 변경 없이 색다른 움직임을 보여줄수 있다! 이 '유닛'클래스의 '이동' 메소드를 수정할 필요조차 없다(수정에 대해선 폐쇄). 그냥 브루들링 클래스의 이동 패턴만 재정의하면 그만인 것이다(확장에 대해선 개방).

2.3 LSP : 리스코프 치환 법칙

Liskov Substitusion Principle

자식 클래스는 언제나 자신의 부모 클래스를 교체할 수 있다는 원칙이다. 즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다는 것. 상속의 본질인데, 이를 지키지 않으면 부모 클래스 본래의 의미가 변해서 is a 관계가 개발살나며, 그로 인해 다형성 불구자가 된다. 본질적인 구조를 바꾸지 않고 이를 해결하려 하면 높은 확률로 의존성 역전 법칙을 동시에 어기게 된다(...).

또다시 예를 들면, 컴퓨터용 '마우스' 클래스가 있다고 치자. 이 컴퓨터용 '마우스'는 어떤 마우스를 사오던 컴퓨터에 있는 ps/2 포트나 usb 포트에 연결할수 있을 것이다. 사용 면에서는 왼쪽과 오른쪽 버튼, 그리고 휠이 있어 사용자가 누르거나 굴릴수 있을 것이다. 또한 무언가 바닥에 대고 움직이면 컴퓨터가 이를 받아들인다는 것도 안다. 마우스가 볼마우스던 광마우스던, 아니면 GPS를 이용하건 외계인 테크놀러지로 무중력 상태에서도 쓸수 있건 간에 암튼 사용자는 바닥에 착 붙여 움직일것이고, 모든 마우스는 예상대로 신호를 보내 줄 것이다. 또한 만약 추가적인 특별한 버튼이 있는 마우스(상속)라도 그 버튼의 사용을 제외한 다른 부분은 보통의 마우스와 다를 바 없으므로 사용자는 그 마우스의 그 버튼이 뭔 역할을 하던간에 무심한듯 시크하게 아무 문제 없이 잘 사용한다. 하지만 오른쪽/왼쪽 버튼 대신 뒤쪽/앞쪽 버튼을 사용하는 기묘한 마우스가 나왔다고 치자. 사용자는 평소 보던 버튼을 누를수가 없으므로 "현기증 난단 말이에요 빨리 '제대로 된' 마우스 가져다 주세요"라고 이상을 호소할 것이다. 위의 볼마우스나 광마우스는 LSP를 휼륭하게 지키지만[1] 바로 위의 예제는 전혀 그렇지 못하다고 볼 수 있다.

2.4 ISP : 인터페이스 분리 원칙

Interface Segregation Principle

클라이언트에서 사용하지 않는 메서드는 사용해선 안된다. 그러므로 인터페이스를 다시 작게 나누어 만든다. OCP와 비슷한 느낌도 들지만 엄연히 다른 원칙이다. 하지만 ISP를 잘 지키면 OCP도 잘 지키게 될 확률이 비약적으로 증가한다.

이젠 좀 지겹겠지만 또 예를 들어보자. 게임을 만드는데 충돌 처리와 이팩트 처리를 하는 서버를 각각 두고 이 처리 결과를 (당연히) 모두 클라이언트에게 보내야 한다고 가정하자. 그러면 아마 Client라는 인터페이스를 정의하고 그 안에 충돌전달()과 이펙트전달(이펙트)를 넣어놓을 것이다. 그리고 충돌 서버와 이펙트 서버에서 이 인터페이스를 구현하는 객체들을 모아두고 있으며, 때에 따라 적절히 신호를 보낸다. 하지만 이렇게 해두면 충돌 서버에겐 쓸모없는 이펙트전달 인터페이스가 제공되며, 이펙트 서버에겐 쓸모없는 충돌전달 이펙트가 제공된다. 이를 막기 위해선 Client 인터페이스를 쪼개 이펙트전달가능 인터페이스와 충돌전달가능 인터페이스로 나눈 뒤, 충돌에는 충돌만, 이펙트에는 이펙트만 전달하면 될 것이다. 또한 Client 인터페이스는 남겨두되 이펙트전달가능과 충돌전달가능 이 둘을 상속하면 된다.

2.5 DIP : 의존성 역전 법칙

Dependency Inversion Principle

상위 클래스는 하위 클래스에 의존해서는 안된다는 법칙이다. 당연하다고 생각되면서도 잘 어겨지는것 1순위(...). 팩토리 패턴이 귀찮다고 별도 클래스 분리 없이 상위 클래스에 정적 메소드로 구현하는 경우가 대표적인 어기는 사례이다 [2].
  1. 물론 현실인만큼 '완벽하게'까진 아니다. 광마우스는 무언가 패드 같은거에 대야 잘 움직인다던지...
  2. 여담으로 이 짓거리는 단일 책임 원칙도 씹어먹는다(...)