객체지향 프로그래밍(1)

5 minute read

Object oriented programming

흔히들 말하는 OOP(Object oriented programming) 제대로 알고 있는 개발자들이 얼마나 될까? 대부분의 주니어, 중니어(?)ㅋㅋ 심지어 자칭 시니어라고 부르는 개발자들도 설명하는 내용을 보면 학부때 배웠던 붕어빵 틀 예시나, 실세계를 모방한다 정도의 얘기를 하는 경우가 많다. 어느정도 틀린 말은 아니지만.. 그래서 OOP을 따르기 위해선 구체적으로 무엇을 어떻게 명확하게 설명해준 사람은 단 1명도 없없다.

그러던 중 오브젝트 라는 책을 접하면서 OOP에 대해 스킬업을 할 수 있게 되었다. 책을 다 읽은지 시간이 좀 지나 슬슬 까먹기 전에 1장부터 리마인드!

class가 아닌 객체를 초점에 맞춰 생각

객체지향적으로 프로그래밍을 할 때 가장 먼저 생각해야되는것은 class가 아니라 어떤 객체가 필요한지이다. class가 아닌 객체에 초점을 맞추고 생각해봐야한다. class는 공통적인 상태와 행동을 공유하는 객체들을 추상화 한 것 따라서 class의 설계를 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야된다.

각 객체들은 독립적인 것이 아닌 어떠한 기능을 구현하기 위해 협력하는 공동체의 일원으로 생각

객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 가반으로 클래스를 구현하다.

도메인의 구조를 따르는 프로그램 구조

도메인이란 사용자가 프로그램을 사용하는 분야를 의미한다. 객체지향적으로 생각하였을때 요구사항 설계와 코드 레벨 설계에서 객체라는 동일한 추상화 대상을 사용하므로서 요구사항 설계에서 부터 코드 설계까지 매끄럽게 연결될 수 있다.

도메인을 구성하는 개념을 객체라는 관점에서 설계하고 각 객체를 클래스로 구현한다. 이 때 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사하게 지어야한다.

클래스 구현하기

클래스를 구현하거나 사용할때 가장 중요한 것은 클래스의 경계를 구분 짓는 것 클래스 설계의 핵심은 캡슐화이다. 외부에서는 직접적으로 객체의 속성에 접근 할 수 없도록 하며, public 메소드를 통해 접근할 수 있도록 해야된다. 왜냐 경계의 명확성이 객체의 자율성을 보장하고, 프로그래머에게 구현의 자유를 제공하기 때문이다.

객체의 자율성 보장이란

객체는 상태와 행동을 함께 갖고 있는 복합적인 존재이다. 또한 객체는 스스로 판단하고 행동하는 자율적인 존재이다. 객체지향에서 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디러를 적절하게 표현할 수 있게 한다. 이처럼 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 한다. 객체에게 원하는 것을 요청하고 객체가 스스로 최선의 방법을 결정할 수 있을 것이라는 것을 믿고 기달려야한다. 그러기 위해서는 외부에서 객체의 내부 상태를 직접적으로 변경하거나 볼 수 있도록하면 안된다. 일반적으로 객체의 상태는 숨기고 행동만 외부에 공개한다. 이를 위해 인터페이스와 구현을 적절하게 분리하도록 한다. 여기서 인터페이스는 외부에 공개된 public 메소드이고, 구현은 내부에서만 접근 가능한 부분을 말한다.

프로그래머에게 구현의 자유를 제공하기란

프로그래머는 클래스작성자와 클라이언트 프로그래머로 구분지을 수 있다. 클라이언트 프로그래머는 클래스 작성자가 만든 클래스를 조합하여 빠르게 어플리케이션을 구현하는 것이며, 클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 최대한 숨겨야한다. 이를 구현 은닉이라고 부르며 이렇게 하므로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고 내부 구현을 마음대로 변경 할 수 있다. 구현은닉의 장점은 클라이언트 프로그래머는 인터페이스만 알아도 클래스를 사용할 수 있고, 클래스 작성자는 인터페이스를 변경하지만 않으면 내부 구현을 자유롭게 수정 할 수 있다.

클래스를 개발할때는 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야된다. 설계가 필요한 이유는 변경을 관리하기 위함이라는 점도 기억해야된다. 접근제어는 객체의 변경을 관리할 수 있는 기법 중 가장 대표적인 것 중 하나이다. 변경 가능성이 있는것은 private으로 감춰 외부에서 접근 하지 못하도록 해야된다.

객체간의 협력

어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다. 객체가 다른 객체와 상요작용할 수 있는 유일한 방법은 메시지를 전송하는 것이고, 이렇게 수신된 메시지를 어떻게 처리할지는 객체 스스로 메소드라는 것을 통해서 처리한다.

tip

  • template method 패턴을 이용하여 적절하게 공통 코드를 관리하자.
  • 생성자를 통해 객체가 올바른 상태를 갖고 생성될 수 있도록 보장하자.

의존성

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 이 말은 클래스 사이의 의존성 관계와 객체 사이의 의존성이 동일하지 않을 수 있다.

유연, 재사용, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다라는 점이다. 하지만 의존성이 달라질수록 코드를 이해하기 어려워질 수 있다. 왜냐 객체를 생성하고 연결하는 부분을 찾아야하기 때문이다.

차이에 의한 프로그래밍(programming by difference) - 상속관련

상속은 코드의 재사용성을 이용하기 위한 방법중 하나이다. 새로운 클래스를 작성할 때 기존 클래스와 거의 동일한 코드가 있다면 상속을 이용하여 기존 클래스는 그래도 둔채 새로운 클래스에서 기존 클래스를 상속 받아 필요한 부분만 수정하여 사용하면 재사용성이 높아진다. 이처럼 부모 클래스에서 다른 부분만 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 한다.

상속과 인터페이스(public메소드)

상속한다는것은 부모의 모든 인터페이스를 자식도 포함한다라는 것이다. 이렇게 됨으로써 외부에서는 상속을 통해 나온 새로은 클래스가 부모의 타입과 같다고 볼 수 있다. 호출하는 쪽 입장에서는 해당 인터페이스를 이해할 수 있기만 하면 어떤 타입인지 상관이 없기 때문이다. 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고 한다.

tip

상속에서 주의해야될 점은 상속을 코드의 재사용성에 초점을 맞춰 하기보다는 인터페이스 상속을 위해 사용해야된다는 점이다. 코드의 재사용성(구현의 재사용)을 위해 상속을 한다면 변경에 취약한 코드를 낳는 결과를 초래할 수 있다.

다형성

호출하는 쪽에서는 동일한 인터페이스로 호출하지만 실제 어떤 메소드가 실행될지는 인터페이스를 호출 받는 객체에 따라 달라질수 있다 이것을 다형성이라고 한다.

다형성은 컴파일 시점의 의존성과 실행 시점의 의존성이 달라질수 있다는 사실을 기반으로 한다. 컴파일 시점에서는 추상 클래스나, 어떤 부모 클래스에 의존 할 수 있지만 실제 실행 시점에서는 컴파일 시점에 의존성을 갖고 있던 클래스를 상속 받는 객체와 의존성을 갖을 수도 있다. 이와 같은 것을 지연 바인딩 또는 동적 바인딩이라고 부른다. 반대의 개념의로 정적 바인딩 초기 바인딩이 있다.

인터페이스와 추상 클래스

인터페이스와 추상 클래스를 이용하여 다형성을 구현할 수 있다. 그러면 이 둘은 어떤 기준에 따라 나눠서 사용해야되는가? 인터페이스의 경우 공유할 코드없이 순수하게 인터페이스만 공유하고 싶을 경우 사용한다. ( 외부에 공유 가능한 인터페이스로 정의 ) 반면에 알고리즘을 공유할 필요가 있을 경우 추상 클래스로 공통된 부분을 작성한 후 상속을 이용하여 다른 부분을 구현하도록 한다.

추상화

추상화의 장점

  • 요구사앙의 정책을 높은 수준에서 서술할 수 있다. 이 말은 세부적은 내용을 포함하는 추상화 계층을 통해 설명하여 간단한 표현만으로 포괄적인 내용을 설명할 수 있다라는 말이다.
  • 유연한 설계를 할 수 있다. 추상화를 이용하여 상위 정책들을 표현하게 되면 기존 구조를 수정하지 않고 새로운 기능을 쉽게 추가하고 확장 할 수 있다. 상위 정책이라 함은 기존적인 애플리케이션의 협력 흐름을 기술한다는 것으로 추상화면 협력 계층에 맞춰 각 정책에 맞는 인터페이스에 해당하는 기능만 구현하여 바꿔 넣어주면 되기 때문이다.

유연함이 필요한 곳에 추상화를 사용하라

코드의 재사용성 관점에서 상속과 합성

상속은 코드의 재사용을 하기 위한 한가지 방법이며, 다른 클래스를 맴버 변수로 두고 사용하는 합성이라는 방법도 있다. 객체 지향에서 대부분의 사람들이 상속보다는 합성을 통한 재사용을 권장하고 있다. 그 이유는 무엇?

  1. 캡슐화를 위반한다. 상속을 이용하기 위해서는 부모 클래스의 내부 구조를 알아야하며, 부모 클래스의 구현이 자식 클래스에 노출되므로 캡슐화가 약해진다. 부모클래스의 구현이 자식에게 노출되므로 부모 클래스의 변경이 자식에게 큰 영향을 미치게된다.

  2. 설계가 유연하지 않다는점 대부분의 언어에서 컴파일 시점에 상속관계에 있는 인스턴스를 변경하지 못하기 때문에 개념적으로 추상화 되어있다라고 해도 유연하게 객체를 변경할 수 가 없다.

합성을 통한 코드의 재사용

상속은 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는 데 비해, 합성은 클래스의 맴버 변수로 갖고 있으면서 사용하고자 하는 객체의 인터페이스를 통해서만 결합되어 있어 약하게 결합되어 있다. 이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고한다.

합성은 인터페이스를 통해서만 의존성을 갖고 있기 때문에 캡슐화 원칙을 잘 지키며, 의존하는 인스턴스를 교체하는 것도 비교적 쉽기 때문에 설계를 유연하게 만든다.

그렇다고 해서 무조건 상속을 사용하지 말라는 것은 아니다. 추상클래스로 특정 알고리즘을 공통적으로 관리해야되는 경우와 같이 보통 설계에서는 상속과 합성을 같이 사용한다.

객체지향 설계의 핵심은 객체간의 적절한 협력을 식별하고, 협력에 필요한 역할을 정의한 후 역할을 수행할 수 있는 적절한 객체에 책임을 할당하는 것이다. 즉 객체가 각각의 존재로 생각하기 보단 상호 협력적인 상태로 생각해야된다.

Updated: