전체/Java

객체지향 프로그래그래밍 5원칙 특징, 요소

effortDev 2019. 1. 27. 13:03

 

1. 객체 지향 프로그래밍이란?

 

객체 지향 프로그래밍(Object-Oriented Programming), 줄여서 OOP.

프로그램을 어떻게 설계해야 하는지에 대한 일종의 개념이자 방법론.

 

프로그램을 단순히 데이터와 처리 방법으로 나누는 것이 아니라, 

프로그램을 수많은 '객체'라는 기본 단위로 나누고 이 객체들의 상호작용으로 서술하는 방식이다. 

 

객체를 데이터의 묶음으로만 착각하기 쉬운데, 그보다는 하나의 '역할'을 수행하는 메소드와 데이터의 묶음으로 봐야 한다.

 

1.1 절차적 프로그래밍 방식

 

입력을 받아 명시된 순서대로 처리한 다음, 그 결과를 내는 것

어떻게 어떤 논리를 어떤 순서대로 써나가는 것인가로 간주되었다. 

즉, 프로그램 자체가 가지는 기능에 대해서만 신경을 썼지, 이 프로그램이 대체 어떤 데이터를 취급하는 것인가에는 

그다지 관심이 없었던 것이다.

 

1.2 구조적 프로그래밍 방식

 

절차적 프로그래밍 방식을 개선하기 위해 나온 방식으로

프로그램을 함수(procedure) 단위로 나누고 

프로시져끼리 호출을 하는 것이 구조적 프로그래밍 방식이다. 

프로그램이라는 큰 문제를 해결하기 위해 그것을 몇개의 작은 문제들로 나누어 해결하기 때문에 

하향식(Top-down) 방식이라고도 한다.

 

1.3 객체 지향 프로그래밍 방식

 

구조적 프로그래밍 박식을 개선하기 위해 나온 방식으로

객체 지향 프로그래밍이다. 

큰 문제를 작게 쪼개는 것이 아니라, 먼저 작은 문제들을 해결할 수 있는 객체들을 만든 뒤, 

이 객체들을 조합해서 큰 문제를 해결하는 상향식(Bottom-up) 해결법을 도입한 것이다. 

이 객체란 것을 일단 한번 독립성/신뢰성이 높게 만들어 놓기만 하면 그 이후엔 

그 객체를 수정 없이 재사용할 수 있으므로 개발 기간과 비용이 대폭 줄어들게 된다.

 

OOP를 사용하면 코드의 중복을 어느 정도 줄일 수 있고 입력 코드, 

계산 코드와 결과 출력 코드 등 코드의 역할 분담을 좀 더 확실하게 할 수 있어서 가독성이 높아질 수 있다.

 

 

2. 

객체 지향 프로그래밍 요소

 

2.1 캡슐화

 

캡슐화는 프로그램의 세부 구현을 외부로 드러나지 않도록 특정 모듈 내부로 감추는 것이다. 

캡슐화는 객체지향 언어를 구성하는 주요 요소이지만 객체지향 언어에서만 사용되는 개념은 아니다. 

내부의 구현은 감추고(=정보 은닉) 모듈 내에서의 응집도를 높이며, 외부로의 노출을 최소화하여 

모듈 간의 결합도를 떨어뜨리는 개념은 거의 대부분의 언어에 녹아있다.

 

많은 객체지향 언어에서 사용되는 클래스를 기준으로 보면, 

클래스 외부에서는 바깥으로 노출된 특정 메소드에만 접근이 가능하며 클래스 내부에서 

어떤 식으로 처리가 이루어지는지는 알지 못하도록 설계된다.

 

일반적으로 세 종류의 접근 제한이 사용된다.

 

public : 클래스의 외부에서 사용 가능하도록 노출시키는 것이다.

 

protected : 다른 클래스에게는 노출되지 않지만, 상속받은 자식 클래스에게는 노출되는 것이다.

 

private : 클래스의 내부에서만 사용되며 외부로 노출되지 않는다.

 

 

2.2 상속

 

상속은 자식 클래스가 부모 클래스의 특성과 기능을 그대로 물려받는 것을 말한다. 

기능의 일부분을 변경해야 할 경우 자식 클래스에서 상속받은 그 기능만을 수정해서 다시 정의하게 되는데, 

이러한 작업을 '오버라이딩(Overriding: 재정의)'이라고 한다. 

상속은 캡슐화를 유지하면서도 클래스의 재사용이 용이하도록 해 준다.

 

 

2.3 다형성

 

하나의 함수명 등이 상황에 따라 다른 의미로 해석될 수 있는 것을 말한다. 

이를 '오버로딩(Overloading: 중복 정의/다중 정의)'이라고 한다.

 

함수 오버로딩

C++과 C#, Java에서는 함수 오버로딩을 통해 동일한 이름의 함수를 매개변수에 따라 다른 기능으로 동작하도록 

할 수 있다.  함수 오버로딩을 너무 자주 사용하면 해당 함수가 어디서 오버로딩되었는지 찾기 어려워질 수 있으므로, 

지나친 남발은 자제하는 것이 좋다.

 

 

2.4 장점

 

이 OOP 특성 덕분에 개발시간 단축(잘 만들어진 클래스는 재사용성을 보장한다) 

정확한 코딩(구현 목적을 위해 클래스를 나눌 수 있으니 구현 단위와 목표가 뚜렷해진다.)이 가능하다.

 

 

3. 객체지향의 원칙[ 5원칙(SOLID) ]

 

객체지향에서 꼭 지켜야 할 5개의 원칙을 말한다. 

일단 한번 보면 개념은 알아 듣긴 하지만 막상 실현하려면 생각보다 어려움이 따른다. 

이 5개의 원칙의 앞글자를 따서 SOLID라고도 부른다.

 

 

3.1 SRP(Single Responsibility Principle) : 단일 책임 원칙 

 

객체는 오직 하나의 책임을 가져야 한다. (객체는 오직 하나의 변경의 이유만을 가져야 한다.)

사칙연산 함수를 가지고 있는 계산 클래스가 있다고 치자. 이 상태의 계산 클래스는 오직 사칙연산 기능만을 책임진다. 

만일 프로그램이 대대적으로 공사를 들어가게 되더라도 계산 클래스가 수정될만한 사유는 누가 봐도 사칙연산 함수와 

관련 된 문제 뿐이다.  이처럼 단일 책임 원칙은 클래스의 목적을 명확히 함으로써 구조가 난잡해지거나 수정 사항이 

불필요하게 넓게 퍼지는 것을 예방하고 기능을 명확히 분리할 수 있게 한다.

위의 원칙이 제대로 지켜지지 않으면 어떻게 될까? 

어떤 프로그래머가 위의 계산 클래스를 통해 GUI를 가지는 계산기 프로그램을 개발하고 있다. 

그런데 중간에 귀찮다고 GUI 관련 코드를 계산 클래스에 넣어버렸다. 

이렇게 되면 계산 클래스는 계산과 GUI라는 두 가지 책임을 지게 되는데 만일 GUI 관련 수정 사항이 발생하게 되면 

별 상관도 없어보이는 계산 클래스를 고치게 된다. 

이처럼 하나의 클래스가 두 가지 이상의 책임을 지니게 되면 클래스의 목적이 모호해지고 기능을 수정할 때 

영향을 받는 범위도 커져서 유지보수가 힘들어지며[1] 결국 작성한 본인조차도 이게 정확히 뭐하는 클래스인지 

명확히 설명할 수가 없는 스파게티 코드가 되어버린다.

 

 

3.2 OCP(Open-Closed Principle) : 개방-폐쇄 원칙

 

객체는 확장에 대해서는 개방적이고 수정에 대해서는 폐쇄적이어야 한다는 원칙이다. 

즉, 객체 기능의 확장을 허용하고 스스로의 변경은 피해야 한다.

 

예를 들면 도롱뇽과 개구리가 있다. 당신은 이런저런 공통사항을 생각하며 메소드와 필드를 정의한다. 

둘다 이동속도는 다르지만 기본 이동은 도룡뇽은 기어다니고 개구리는 기어다니며 점프하고를 반복한다.

하지만 개구리 같은 동물의 움직임을 구현할 때 애로사항이 있을 것 같다.

 

이동 메소드에서 이동 패턴을 나타내는 코드를 별도의 메소드로 분리하고, 구현을 하위 클래스에 맡긴다. 

그러면 개구리 클래스에서는 이동 패턴 메소드만 재정의하면 동물 클래스의 변경 없이 

기어다니며 점프하는 움직임을 보여줄 수 있다! '동물' 클래스의 '이동' 메소드는 수정할 필요조차 없다(수정에 대해선 폐쇄). 

그냥 개구리 클래스의 이동 패턴 메소드만 재정의하면 그만인 것이다(확장에 대해선 개방).

 

 

3.3 LSP(Liskov Substitution Principle) : 리스코프 치환 원칙

 

자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있다는 원칙이다. 

즉 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 계획대로 잘 작동해야 한다는 것. 

상속의 본질인데, 이를 지키지 않으면 부모 클래스 본래의 의미가 변해서 다형성을 지킬 수 없게 된다.

 

또다시 예를 들면, 컴퓨터용 '마우스' 클래스가 있다고 치자. 

컴퓨터에 있는 ps/2 포트나 usb 포트를 통해 연결할 수 있고, 마우스를 바닥에 대고 움직이면 

컴퓨터가 신호를 받아들인다는 것을 안다.  사용 면에서는 왼쪽과 오른쪽 버튼, 그리고 휠이 있어 사용자가 

누르거나 굴릴수 있을 것이다.  마우스가 볼마우스든 광마우스든, 아니면 GPS를 이용하건 간에 아무튼 

사용자는 바닥에 착 붙여 움직일 것이고, 모든 마우스는 예상대로 신호를 보내 줄 것이다. 

 

또한 만약 추가적인 특별한 버튼이 있는 마우스(상속)라도 그 버튼의 사용을 제외한 다른 부분은 

보통의 마우스와 다를 바 없으므로 사용자는 그 마우스의 그 버튼이 뭔 역할을 하던간에 문제 없이 잘 사용한다. 

여기까지 나온 마우스들은 LSP를 잘 지킨다고 볼 수 있다.

 

하지만 오른쪽/왼쪽 버튼 대신 옆쪽 버튼을 사용하는 펜마우스를 처음으로 접하게 되면 사용자는 

평소 보던 버튼을 누를 수 없다며 이상을 호소할 것이다. 이런 경우 LSP를 전혀 지키지 못 하는 것이다.

 

 

3.4 ISP(Interface Segregation Principle) : 인터페이스 분리 원칙

 

클라이언트에서 사용하지 않는 메서드는 사용해선 안된다. 그러므로 인터페이스를 다시 작게 나누어 만든다. 

OCP와 비슷한 느낌도 들지만 엄연히 다른 원칙이다. 하지만 ISP를 잘 지키면 OCP도 잘 지키게 될 확률이 

비약적으로 증가한다.

 

게임을 만드는데 충돌 처리와 이팩트 처리를 하는 서버를 각각 두고 

이 처리 결과를 (당연히) 모두 클라이언트에게 보내야 한다고 가정하자. 

그러면 아마 Client라는 인터페이스를 정의하고 그 안에 충돌전달()과 이펙트전달(이펙트)를 넣어놓을 것이다. 

그리고 충돌 서버와 이펙트 서버에서 이 인터페이스를 구현하는 객체들을 모아두고 있으며, 때에 따라 적절히 신호를 보낸다. 

하지만 이렇게 해두면 충돌 서버에겐 쓸모없는 이펙트전달 인터페이스가 제공되며, 

이펙트 서버에겐 쓸모없는 충돌전달 인터페이스가 제공된다.  이를 막기 위해선 Client 인터페이스를 쪼개 

이펙트전달가능 인터페이스와 충돌전달가능 인터페이스로 나눈 뒤, 충돌에는 충돌만, 

이펙트에는 이펙트만 전달하면 될 것이다. 

또한 Client 인터페이스는 남겨두되 이펙트전달가능과 충돌전달가능 이 둘을 상속하면 된다.

 

 

3.5 DIP(Dependency Inversion Principle) : 의존성 역전 원칙

 

추상성이 높고 안정적인 고수준의 클래스는 구체적이고 불안정한 저수준의 클래스에 의존해서는 안된다는 원칙으로서, 

일반적으로 객체지향의 인터페이스를 통해서 이 원칙을 준수할 수 있게 된다. 

(상대적으로 고수준인) 클라이언트는 저수준의 클래스에서 추상화한 인터페이스만을 바라보기 때문에, 

이 인터페이스를 구현한 클래스는 클라이언트에 어떤 변경도 없이 얼마든지 나중에 교체될 수 있다..