본문 바로가기
Develop/Design

[OOP] 니객망 1탄 - 객체지향 프로그래밍 이란?

by 코딩의성지 2020. 2. 18.

하이~

 

어렸을 때 부터 나는 농구를 좋아했고, 고향팀인 창원 LG팀을 거의 20년 넘게 응원하고 있다.

 

창원 LG 감독 중에 강을준이라는 감독이 있었는데 작전 타임때 종종 구수한 사투리로 ...! 

 

'니가 갱기를 망치고 있어 ~' 라는 말을 자주 했다.

 

<니갱망 강을준 전 창원 LG 감독님>

ㅎㅎㅎ 오늘 부터는 그동안 내가 잘못 이해하고 있던 .. 그리고 개념을 안다고 생각했던 객체지향에 대해 완벽하고 꼼꼼하게 포스팅을 해보려한다. 이름하여 ....!

 

'니가 객체지향을 망치고 있어~' 

니!! 객!! 망!! 시리즈~~

 

 대부분의 회사에서 프로젝트를 할때 설계를 하고 설계문서를 바탕으로 코딩을 할 것이다. 대표적으로 사용하는 설계방식이 바로 객체지향 설계인듯 하다. 그 만큼 이 개념을 확실히 잘 알고 사용하는게 좋을 듯한데, 개인적인 바램으로는 직접 설계를 하고 코딩을 하는 실무자든 면접을 준비하는 취준생이든 누구든지 이 개념을 잘 알았으면 좋겠다. 한번 시작해보자.

 

캡슐화

학교다니면서 다들 이 캡슐화라는 건 한번쯤은 들어봤을 것이다. 여러분에게 물어보고 싶다. 캡슐화가 뭔가? 쉽게 대답하신 분들은 이 글 안보셔도 될 것 같다. 나는 쉽게 대답하지 못했다. 왜냐... 정확하게 잘 모르고 있었기 때문이다. 확실하게 공부를 하고 정리를 해보니 캡슐화가 뭔지 대충 알 것 같다.

 

훌륭한 클래스는 어떤 부분을 외부에 공개하고 감출지를 잘해줘서 객체의 자율성을 보장한 것이라고 생각한다. 여기서 감추는 것! 이게바로 캡슐화이다. 보통 객체지향 언어에서 코딩은 객체 내부의 메서드에 데이터와 기능을 함께 묶어서 로직을 구현하는데, 캡슐화를 잘 해놓으면 그 메서드를 호출하는 객체는 호출하고자하는 메서드의 데이터와 기능이 어떻게 처리되는지는 몰라도, 무슨일을 할지는 알게 된다.

 

이러한 캡슐화는 어떠한 메커니즘과 합쳐지면 굉장히 강력한 힘을 발휘한다. 바로 접근 수정자 (access modifier)

이 접근 수정자를 이용하면 우리는 객체의 자율성을 훨씬 더 잘 보장할 수 있다.

 

결국 우리는 캡슐화랑 접근제어를 통해서

public interface (public) 와 implementation (private, protected) 으로 객체를 나눌 수 있게 된다.

 

여기서 여러분은 코딩할 때 수정이 될 여지가 없고, 공개를 할 내용은 public 에 구현을 하면되고 수시로 변경이 예상되는 부분은 private에 구현하면 될 것 같다는 생각을 하면 참 잘 한 것이다. 

 

implemntation hiding

자! 여러분. 다음과 같은 상황을 가정해보자. 내가 호텔 예매 어플리케이션을 개발 (client programmer) 하려고 하는데, 타 업체로부터 결제관련 솔루션을 jar 파일로 제공 (class creator) 받아서 쓴다고 생각해보자. 우리는 이 결제 솔루션이 어떻게 동작 하는지는 알 수가 없다. (물론 jar를 decompile 해서 까볼 수는 있겠지만 ... ㅎㅎ) 하지만 우리는 이게 어떤 기능을 하는지는 안다 ! 그 이유는 이 솔루션이 어떤 기능을 하는지는 정확하게 공개되어 있기 때문이다. 이렇게 필요한 부분만 공개하고, 나머지는 꽁꽁 숨기는 방식을 implementation hiding 이라고 한다. 

 

메시지와 메서드

시스템에 어떤 기능을 구현하다보면 객체끼리 상호작용이 일어난다. 이를 Collaboration(협력)이라고 부른다. 이건 메서드를 통해 객체끼리 메시지를 전송하고 수신하는 방법이다. 객체가 다른객체와 상호작용을 할수 있는 방법은 메시지를 전송하는 방법 밖에는 없다. 이러한 메시지를 처리하는 방법을 메서드라고 부른다. 객체지향 설계의 핵심이라 할 수 있는 다형성을 이해하기 위해서는 메시지와 메서드를 정확하게 구분하는게 중요하니... 잘 구분했으면한다. (쭉 읽다보면 무슨말인지 알 것임)

 

 

오버라이딩과 오버로딩

오버라이딩은 부모클래스에 정의된 같은 이름과 같은 파라미터를 가진 메서드를 자식클래스에서 내용을 변경하거나 추가하는 재정의 하는 것을 의미한다. 오버로딩은 메서드의 이름은 같지만 제공되는 파라미터가 다른 것을 의미한다. 오버로딩과는 다르게 공존이 가능하다. 이 오버로딩을 잘 이용하면 도메인의 의미를 더욱 풍부하게 표현할 수 있다.

 

 

의존성

객체지향언어를 공부하다 보면 굉장히 많이 듣게되는 말중 하나이다. 의존성이 높다... 또 한번 질문하겠다.. 의존성이 뭐냐? 이번에도 대답을 잘하셨다면 당신은 객체지향 고수 .. ☆ 

 

자. 굉장히 쉽게 이해하자. 의존성이 존재한다? 이 말은 즉, 떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출 할 경우에 '의존성'이 서로 있다고 표현한다.

 

그런데 참 ...! 신기한게 compile time에서의 의존성이랑 run time에서의 의존성이 서로 다를 수 있다. 얼마나 신기하고 아름다운가...

이는 유연하고, 재사용가능하며, 확장 가능한 객체지향만이 가질 수 있는 특징이다. 그런데 이 코드의 의존성과 런타임에서의 의존성이 너무 너무 다르면?? 코드가 점점 어려워지는 걸 느끼실 수 있을 거다.. 여기서 트레이드 오프가 발생하니 다들 이 의존성을 잘 설정하는것을 중요하게 여겨보자.

 

 

상속

부모클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)라고 한다. 이를 가능하게 해주는 것이 바로 상속이다.

 

아까 위에서 메시지와 메서드는 다른 것이라고 했던 것 기억하는가? 객체지향에서 메시지와 메서드는 완전히 다른 개념이다. 객체가 다른객체에게 전송하는게 메서드 같지만 실은 메시지이다. 우리는 어떤 객체의 메서드가 실행 될 지 아무도 모른다.

 

여기서 업캐스팅이라는 개념이 나온다. 업캐스팅은 자식클래스(subclass)가 부모클래스(superclass)를 대신하는 것을 말한다. 업캐스팅이라는 말은 크게 의미는 없다. 다만 클래스다이어그램을 그릴 때 보통 부모클래스를 자식클래스보다 위에 위치시키기 때문에 뭔가 부모클래스로 자동적으로 타입캐스팅이 되는 것 처럼 보여 업캐스팅이라고 한다고 한다.

 

업캐스팅의 개념에 따르면 컴파일 단계에서는 부모클래스에 의존하지만 실제 코드가 실행 될 때 사용되는 건 자식클래스의 기능일 수 있다는 것이다. 자바 컴파일러는 코드 상에 있는 모든 곳에서 부모클래스를 대신해 자식클래스를 사용하는것을 허용한다.

 

상속이 가치있는 또 다른 이유는 부모클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있다는 점이다. 이러한 점에서 대부분의 사람들이 하나의 착각을 하곤 한다. 바로 상속이 인스턴스 변수를 재사용하기 위해 사용한다는 것이다. 상속의 기능은 재사용을 위해 사용되는 것이 아니다. 상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다. 즉 다형성을 구현할 수 있다는 말이다. 그렇다고 해서 다형성을 꼭 상속으로만 구현해야 하느냐? 그건 아니다. 공부를 하다보면 다형성이라는게 굉장히 추상적이라는 것을 깨닫게 될 것이다. 

 

상속은 보통 구현 상속 (implementation inheritance) 과 인터페이스 상속 (interface inheritance) 으로 나눌 수 있다. 전자를 subclassing 이라고도 하고 후자를 subtyping이라고도 한다. 단지 코드를 재사용하는 목적으로 상속을 사용한다면 구현 상속을 사용하는 것이라 할 수 있다. 반대로 다형성을 위해 부모클래스와 자식클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것은 인터페이스 상속을 사용하는 것이다.

 

다시 한번 말하지만 상속은 재사용을 위해 사용하는 게 아니다. 재사용을 위해서만 사용하다 보면 변경이 불가능한 허접한 코드를 만들어내는 자신을 발견할 수 있을 것이다.

 

 

인터페이스 

보통 우리는 상속을 표현할 때 추상클래스를 구현함으로써 자식클래스들이 부모클래스의 내부 구현을 함께 상속 받도록 만들었었다. 그런데 .. 종종 구현을 공유할 필요가 없는 경우가 종종 있다. 순수하게 인터페이스 자체만 공유하고 싶을때가 있다는 말이다. 이를 위해 객체지향언어에서는 인터페이스라는 것을 제공한다. (c++ 에서는 인터페이스 자체를 제공하지는 않지만 추상파생클래스(ABC) 를 통해서 인터페이스의 개념을 구현할 수 있다. 인터페이스 역시 업캐스팅의 개념이 적용되고 협력은 다형적이다.

 

 

다형성

위에서 의존성을 설명할 때 컴파일 시간의 의존성과 실행시간의 의존성이 다르다고 했다. 이러한 특성을 이용해 객체지향 언어에서는 다형성을 구현할 수 있다. 객체지향언어를 공부하다보면 다형성을 구현하는 방법은 꽤 많다는 것을 알 수 있다. 그런데 다형성을 구현하다보면 알게되는 사실이 있는데 모든 다형성은 수신된 메시지에 응답을 하기 위해 실행될 메서드를 컴파일 때가 아닌 실행 때 결정한다는 공통점이 있다. 다시 말해서 응답할 메서드를 실행시점에서 바인딩 한다는 것이다.

 

여기서 바인딩이라는 개념에 대해 잠시 알아보자. 동적 바인딩이니 정적 바인딩이니 바인딩은 전공 공부하다보면 한번쯤은 들어봤을 거다. 아주 간단하고 쉽게 설명드리겠다.  동적 바인딩(dynamic binding)은 위에서 말한 것처럼 메서드를 실행 시점에서 바인딩하는 것을 말한다. 동적 바인딩 은 지연 바인딩(lazy binding) 이라고도 부른다. 반대로 c언어에서 사용하던 함수 호출 처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 정적 바인딩(static binding) 이라 부른다. 다른말로 초기 바인딩(early binding) 이라고도 한다. 객체지향에서는 동적 바인딩 메커니즘을 기본적으로 사용한다. 

 

 

추상화

다형성을 구현하기 위해 사용하는 상속과 인터페이스는 추상화라는 원리를 기반으로한다. 

상속에서 보면 부모클래스가 자식클래스보다 훨씬더 추상적이다. 인터페이스 역시 이를 구현하는 클래스보다 훨씬 추상적이다. 왜 더 추상적이냐?? 프로그래밍 언어적인 측면에서 보면 자식클래스들이 인터페이스에 초점을 맞추기 때문이다.모 클래스나 인터페이스는 자식 클래스가 공통적으로 사용할 수 있는 인터페이스를 정의하고 구현의 일부(추상클래스) 또는 전체(인터페이스)를 자식클래스가 결정할 수 있도록 위임한다.

 

만약 추상화를 이용해 설계를 하면 두가지 장점이 있다. 첫째는 사항의 정책을 굉장히 높은 레벨에서 서술할 수 있다는 것이다. 추상화를 이용해서 상위 레벨의 정책을 서술한다는 말은 기본적인 애플리케이션의 협력 흐름을 서술하는 것을 의미한다. 자식클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름 (추상클래스나 인터페이스)을 그대로 따른다. 이 개념은 굉장히 중요하다. 우리가 들어본 디자인 패턴이나 프레임워크 이런 것들 모두 추상화를 활용하는 객체지향의 메커니즘을 따르고 있다. 

 

두번째는 설계가 굉장히 유연해진다는 것이다. 추상화를 잘 활용해 설계를 해놓으면 우리는 추상화된 녀석을 수정할 필요가 없어진다. 필요하면 하위 레벨의 자식클래스를 만들어 내기만 하면 된다. 기능의 확장이 이렇게 쉬워진다는 이야기이다. 이게 가능한 이유가 무엇인가? 바로객체지향 설계가 특정한 구체적인 상황에 결합되는 것을 사전에 방지하기 때문이다.

 

 

합성

코드를 재사용하는 것에 있어서 우리는 상속을 쓸 수 있다. 하지만 좋은 방법은 아니라고 했다. 우리에겐 대안이 있다. 바로 합성이다. 합성은 상속으로 클래스를 연결하는게 아닌 인스턴스 변수로 느슨하게 결합시키는 방식이다. 뭐 합성이나 상속이나 기능적인 관점에서 동일하다. 그런데 왜 우리는 합성을 써야할까?

 

상속에는 재사용을 위해 쓰기에는 치명적인 단점이 있다. 캡슐화를 위반해버린다. 상속을 이용하려면 자식클래스가 부모클래스의 내부를 이해하고 있어야한다. 이는 부모클래스 내부의 구현 내용이 자식클래스에게 노출되기 때문에 캡슐화가 약해진다. 또 상속은 부모클래스와 자식클래스가 강하게 결합되기 때문에 부모클래스에 변경이 필요하면 자식클래스도 변경될 확률이 커진다. 그리고 강하게 결합되어 있기 때문에 실행시점에서 객체의 종류를 변경하는 것도 불가능하다. 

 

하지만 이런 단점은 합성(composition) 을 이용해서 해결이 가능하다. 합성을 활용하면 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하기에 구현을 효과적으로 캡슐화 할 수 있다. 또한 의존하는 스턴스를 변경하거나 교체하는게 비교적 쉽기에 설계가 유연해진다.

 

이제 우리는 분명히 안다. 코드 재사용이 목적이라면? 합성. 다형성을 위해서라면? 상속.

 

오늘은 그동안 내가 잘 이해가 안됐던 객체지향의 개념에 대해 쭉 확인해봤다. 다음 내용엔 위의 내용에 대한 예제를 포스팅해볼까한다. 그럼 다같이 열공하자 !

 

반응형

댓글