객체 지향 프로그래밍

Jemin·2023년 5월 29일
0

Computer Science

목록 보기
12/31
post-thumbnail
post-custom-banner

객체지향 프로그래밍 부분만 읽어도 이해가 가능하다.

객체 지향 이해

객체 지향 프로그래밍(Object Oriented Programming, OOP)은 소프트웨어 개발 패러다임 중 하나로, 현실 세계의 개념과 객체를 소프트웨어로 모델링하는 방식이다.

이렇게 말하면 한 번에 이해할 수 없는게 당연하다. 개념의 정확한 이해를 위해 등장 배경과 등장 이전의 프로그래밍 패러다임 몇 가지를 알아보겠다.

순차적 (비구조적) 프로그래밍

필요한 게 있으면 계속 순서대로 추가해가며 구현하는 방식이다. 직관적일 것이라 생각되지만, 만약 점점 규모가 커지게 되면 어떻게 될까?

이런 비구조적 프로그래밍에서는 goto문을 활용한다. 만약 이전에 작성했던 코드가 다시 필요하면 그 곳으로 이동하기 위한 것이다. 점점 규모가 커지면 goto문을 무분별하게 사용하게 되고, 마치 실뜨기를 하는 것처럼 베베 꼬이게 된다. 나중에 코드가 어떻게 연결되어 있는지 확인조차 하지 못하게 될 문제점이 존재한다.

이런 문제점을 해결하기 위해 탄생한 것이 바로 절차적, 구조적 프로그래밍이다.

절차적 (구조적) 프로그래밍

반복될 가능성이 있는 것들을 재사용이 가능한 함수(프로시저)로 만들어 사용하는 프로그래밍 방식이다.

여기서 보통 절차라는 의미는 함수(프로시저)를 뜻하고, 구조는 모듈을 뜻한다. 모듈이 함수보다 더 작은 의미이긴 하지만, 요즘은 큰 틀로 같은 의미로 쓰이고 있다.

프로시저란?
리턴값이 없는 함수다. 예를 들어 데이터를 출력하는 용도로 사용하는 printf와 같은 함수를 프로시저라고 한다.

하지만 이런 패러다임도 문제점이 존재한다. 바로 너무 추상적이라는 것.

실제로 사용되는 프로그램들은 추상적이지만은 않다. 함수는 논리적 단위로 표현되지만, 실제 데이터에 해당하는 변수나 상수 값들은 물리적 요소로 되어있기 때문이다.

도서관리 프로그램이 있다고 가정해보자.

책에 해당하는 자료형(필드)를 구현해야 한다. 또한 책과 관련된 함수를 구현해야 한다. 구조적인 프로그래밍에서는 이들을 따로 만들어야 한다. 결국 많은 데이터를 만들어야 할 때, 구분하기 힘들고 비효율적으로 코딩할 가능성이 높아진다.

구조적 프로그래밍에서는 이 둘을 따로 생각할 수 밖에 없다. 책은 책이고 책에 대한 함수는 따로 있기 때문에, 같은 소스코드 파일 내에 있더라도 이 둘의 연관 여부는 단 번에 알아치리기 어렵다. 즉, 논리적으로 묶여있을 수 없는 구조이기 때문에 동작이 추상적인 것이다.

C언어가 대표적인 절차적 프로그래밍 언어다.

따라서, 이를 한번에 묶기 위한 패러다임으로 "객체지향 프로그래밍"이 등장하게 된 것이다.

객체지향 프로그래밍의 등장

클래스마다 필요한 필드를 선언하고, getter와 setter로 구성된 모습으로 해결한다. 바로 특정한 개념의 함수와 자료형을 함께 묶어서 관리하기 위해 탄생한 것이다.

가장 중요한 점은, 객체 내부에 자료형(필드)와 함수(메소드)가 같이 존재하는 것이다.

이제 도서관리 프로그램을 만들 때, 해당하는 책의 제목, 저자, 페이지와 같은 자료형과 읽기, 예약하기 등 메소드를 "책"이라는 객체에 한번에 묶어서 저장하는 것이 가능해졌다.

이처럼 가능한 모든 물리적, 논리적 요소를 객체로 만드려는 것이 객체지향 프로그래밍이라고 말할 수 있다.

객체지향으로 구현하게 되면, 객체 간의 독립성이 생기고 중복코드의 양이 줄어드는 장점이 있다. 또한 독립성이 확립되면 유지보수에도 도움이 된다.

이제 객체 지향 프로그래밍이란, 프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다. 라고 하면 조금은 이해가 될 것이다.

객체지향 프로그래밍

객체지향 프로그래밍은 실세계의 실체(사물이나 개념)을 인식하는 철학적 사고를 프로그래밍에 접목하려는 시도에서 시작했다. 실체는 특징이나 성질을 나타내는 속성(attribute/property)을 가지고 있고, 이를 통해 실체를 인식하거나 구분할 수 있다.

예를 들어, 사람은 이름, 주소, 성별, 나이, 신장, 체중, 학력, 성격, 직업 등 다양한 속성을 갖는다. 이때 "이름이 아무개이고 성별은 여성이며 나이는 20세인 사람"과 같이 속성을 구체적으로 표현하면 특정한 사람을 다른 사람과 구별하여 인식할 수 있다.

이러한 방식을 프로그래밍에 접목시켜보자. 사람에게는 다양한 속성이 있으나 우리가 구현하려는 프로그램에서는 사람의 "이름"과 "주소"라는 속성에만 관심이 있다고 가정하자. 이처럼 다양한 속성 중에서 프로그램에 필요한 속성만 간추려 내어 표현하는 것을 추상화(abstraction)라고 한다.

"이름"과 "주소"라는 속성을 갖는 person이라는 객체를 자바스크립트로 표현하면 다음과 같다.

const person = {
  name: "Lee",
  address: "Seoul"
};

이때 프로그래머는 이름과 주소 속성으로 표현된 객체인 person을 다른 객체와 구별하여 인식할 수 있다. 이처럼 속성을 통해 여러 개의 값을 하나의 단위로 구성한 복합적인 자료구조를 객체라 하며, 객체지향 프로그래밍은 독립적인 객체의 집합으로 프로그램을 표현하려는 프로그래밍 패러다임이다.

이번에는 원이라는 개념을 객체로 만들어보자. 원에는 반지름이라는 속성이 있다. 이 반지름을 가지고 원의 지름, 둘레, 넓이를 구할 수 있다. 이때 반지름은 원의 상태를 나타내는 데이터이며 원의 지름, 둘레, 넓이를 구하는 것이 동작이다.

const circle = {
  radius: 5, // 반지름
  
  // 원의 지름
  getDiameter() {
    return 2 * this.radius;
  },
  
  // 원의 둘레
  getPerimeter() {
    return 2 * Math.PI * this.radius;
  },
  
  // 원의 넓이
  getArea() {
    return Math.PI * this.radius ** 2;
  }
};

console.log(circle.getDiameter()); // 10
console.log(circle.getPerimeter()); // 31.41592...
console.log(circle.getArea()); // 78.53981...

이처럼 객체지향 프로그래밍은 객체의 상태(state)를 나타내는 데이터와 상태 데이터를 조작할 수 있는 동작(havior)을 하나의 논리적인 단위로 묶어 생각한다. 따라서 객체는 상태 데이터와 동작을 하나의 논리적인 단위로 묶은 복합적인 자료구조라고 할 수 있다. 이때 객체의 상태 데이터를 프로퍼티, 동작을 메서드라 부른다.

각 객체는 고유의 기능을 갖는 독립적인 부품으로 볼 수 있지만 자신의 고유한 기능을 수행하면서 다른 객체와 관계성을 가질 수 있다. 다른 객체와 메시지를 주고받거나 데이터를 처리할 수도 있다. 또는 다른 객체의 상태 데이터나 동작을 상속받아 사용하기도 한다.

객체지향의 패러다임이 생겨나면서 크게 4가지 특징을 갖추게 되었다. 이 4가지 특성을 잘 이해하고 구현해야 객체를 통한 효율적이 구현이 가능해진다.

1. 추상화(Abstraction)

필요로 하는 속성이나 행동을 추출하는 작업

추상적인 개념에 의존하여 설계해야 유연함을 갖출 수 있다.

즉, 세부적인 사물들의 공통적인 특징을 파악한 후 하나의 집합으로 만들어내는 것이 추상화다.
예를 들어, 아우디, BMW, 벤츠는 모두 "자동차"라는 공통점이 있다. 자동차라는 추상화 집합을 만들어두고, 자동차들이 가진 공통적인 특징들을 만들어 활용한다.

  • 객체들의 공통적인 특징(기능, 속성)을 도출하는 것

  • 객체지향성 관점에서는 클래스를 정의하는 것을 추상화라고 할 수 있다.

왜 필요한가?

예를 들면, "현대"와 같은 다른 자동차 브랜드가 추가될 수 있다. 이때 추상화로 구현해두면 다른 곳의 코드는 수정할 필요 없이 추가로 만들 부분만 새로 생성해주면 된다.

2. 캡슐화(Encapsulation)

낮은 결합도를 유지할 수 있도록 설계하는 것

쉽게 말하자면, 한 곳에서 변화가 일어나도 다른 곳에 미치는 영향을 최소화 시키는 것을 말한다.(객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것)

결합도가 낮도록 만들어야 하는 이유가 무엇일까? 결합도(coupling)란, 어떤 기능을 실행할 때 다른 클래스나 모듈에 얼마나 의존적인가를 나타내는 말이다.

즉, 독립적으로 만들어진 객체들 간의 의존도가 최대한 낮게 만드는 것이 중요하다. 객체들 간의 의존도가 높아지면 굳이 객체지향으로 설계할 의미가 없어진다.

우리는 소프트웨어 공학에서 객체 안의 모듈 간의 요소가 밀접한 관련이 있는 것을 구성하여 응집도를 높이고 결합도를 줄여야 요구사항 변경에 대처하는 좋은 설계 방법이라고 배운다. 이것이 바로 캡슐화와 크게 연관된 부분이라고 할 수 있다.

  • 실제로 구현되는 부분을 외부에 드러나지 않도록 하여 정보를 은닉할 수 있다.

  • 객체가 독립적으로 역할을 할 수 이도록 데이터와 기능을 하나로 묶어서 관리하는 것

  • 코드가 묶여있어서 오류가 없어 편리하다.

  • 데이터를 보이지 않고 외부와 상호작용을 할 때는 메소드를 이용하여 통신을 한다. 보통 라이브러리로 만들어서 업그레이드해 사용할 수 있다.

그렇다면, 캡슐화는 어떻게 높은 응집도와 낮은 결합도를 갖게 할까?

바로 정보 은닉을 활용한다. 외부에서 접근할 필요가 없는 것들은 private으로 접근하지 못하도록 제한을 두는 것이다.(객체안의 필드를 선언할 때 private으로 선언하라는 말이 바로 이 때문)

3. 상속

일반화 관계(Generalization)라고도 하며, 여러 개체들이 지닌 공통된 특성을 부각시켜 하나의 개념이나 법칙으로 성립하는 과정

일반화(상속)은 또 다른 캡슐화다.

자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이라고 말할 수 있다.

아까 자동차를 통해 예를 들어 추상화를 설명했었다. 여기에 추가로 대리 운전을 하는 사람 클래스가 있다고 생각해보자. 이때, 자동차의 자식 클래스에 해당하는 벤츠, BMW, 아우디 등은 캡슐화를 통해 은닉해둔 상태다.

사람 클래스의 관점으로는, 구체적인 자동차의 종류가 숨겨져 있는 상태다. 대리 운전자 입장에서는 자동차의 종류가 어떤 것인지는 운전하는데 크게 종요하지 않다.

새로운 자동차들이 추가된다고 해도, 사람 클래스는 영향을 받지 않는 것이 중요하다. 그러므로 캡슐화를 통해 사람 클래스 입장에서는 확인할 수 없도록 구현하는 것이다.

이처럼, 상속 관계에서는 단순히 하나의 클래스 안에서 속성 및 연산들의 캡슐화에 한정되지 않는다. 즉, 자식 클래스 자체를 캡슐화하여 "사람 클래스"와 같은 외부에 은닉하는 것으로 확장되는 것이다.

이처럼 자식 클래스를 캡슐화해두면, 외부에선 이러한 클래스들에 영향을 받지 않고 개발을 이어갈 수 있는 장점이 있다.

  • 하나의 클래스가 가진 특징(함수, 데이터)을 다른 클래스가 그대로 물려받는 것

  • 이미 작성된 클래스를 받아서 새로운 클래스를 생성하는 것

  • 기존의 코드를 재활용해서 사용함으로써 객체지향 방법의 중요한 기능 중 하나에 속한다.

상속 재사용의 단점

상속을 통한 재사용을 할 때 나타나는 단점도 존재한다.

  1. 상위 클래스(부모 클래스)의 변경이 어려워진다: 부모 클래스에 의존하는 자식 클래스가 많을 때, 부모 클래스의 변경이 필요하다면, 이를 의존하는 자식 클래스들이 영향을 받게 된다.

  2. 불필요한 클래스가 증가할 수 있다: 유사기능 확장시, 필요 이상의 불필요한 클래스를 만들어야 하는 상황이 발생할 수 있다.

  3. 상속이 잘못 사용될 수 있다: 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면, 문제가 발생할 수 있다. 상속 받는 클래스가 부모 클래스와 IS-A관계가 아닐 때 이에 해당한다.

IS-A 관계란?
IS-A 관계는 한 클래스가 다른 클래스의 일종이라는 관계를 나타낸다. 즉, 상속을 통해 부모 클래스의 특성과 동작을 상속받는 자식 클래스가 IS-A 관계를 가진다고 말할 수 있다.

해결책은?

객체 조립(Composition), 컴포지션이라고 부르기도 한다.

객체 조립은, 필드에서 다른 객체를 참조하는 방식으로 구현된다. 상속에 비해 비교적 런타임 구조가 복잡해지고, 구현이 어려운 단점이 존재하지만 변경 시 유연함을 확보하는 데 장점이 매우 크다.

따라서, 같은 종류가 아닌 클래스를 상속하고 싶을 때는 객체 조립을 우선적으로 적용하는 것이 좋다.

상속은 언제 사용하는가?

  • IS-A 관계가 성립할 때

  • 재사용 관점이 아닌, 기능의 확장 관점일 때

4. 다형성(polymorphism)

서로 다른 클래스의 객체가 같은 메시지를 받았을 때 각자의 방식으로 동작하는 능력

객체 지향의 핵심과도 같은 부분이다.

다형성은, 상속과 함께 활용할 때 큰 힘을 발휘한다. 이와 같은 구현은 코드를 간결하게 해주고, 유연함을 갖추게 해준다.

즉, 부모 클래스의 메소드를 자식 클래스가 오버라이딩해서 자신의 역할에 맞게 활용하는 것이 다형성이다.

이처럼 다형성을 사용하면, 구체적으로 현재 어떤 클래스 객체가 참조되는 지는 무관하게 프로그래밍하는 것이 가능해진다.

상속 관계에 있으면, 새로운 자식 클래스가 추가되어도 부모 클래스의 함수를 참조해오면 되기 때문에 다른 클래스는 영향을 받지 않게 된다.

  • 약간 다른 방법으로 동작하는 함수를 동일한 이름으로 호출하는 것

  • 동일한 명령의 해석을 연결된 객체에 의존하는 것

  • 오버라이딩(Overriding), 오버로딩(Overloading)

객체 지향 설계 과정

  • 제공해야 할 기능을 찾고 세분화한다. 그리고 그 기능을 알맞은 객체에 할당한다.

  • 기능을 구현하는데 필요한 데이터를 객체에 추가한다.

  • 그 데이터를 이용하능 기능을 넣는다.

  • 기능은 최대한 캡슐화하여 구현한다.

  • 객체 간에 어떻게 메소드 요청을 주고받을 지 결정한다.

객체 지향 설계 원칙

SOLID라고 부르는 5가지 설계 원칙이 존재한다.

  1. 단일 책임 원칙(SRP, Single Responsibility):

    • 하나의 클래스는 단 하나의 책임만 가져야 한다.

    • 단일 책임 원칙을 지키지 않을 경우 한 책임의 변경에 의해 다른 책임과 관련된 코드에 영향이 갈 수 있다.

  2. 개방-폐쇄 원칙(OCP, Open-Closed):

    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

    • 기능을 변경하거나 확장할 수 있으면서 기능을 사용하는 코드는 수정하지 않는다.

  3. 리스코프 치환 원칙(LSP, Liskov Substitution):

    • 프로그램 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

    • 상위 타입의 객체를 하위 타입의 객체로 치환해도, 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

  4. 인터페이스 분리 원칙(ISP, Interface Segregation):

    • 범용 인터페이스 하나보다 클라이언트를 위한 여러 개의 인터페이스로 구성하는 것이 좋다.

    • 인터페이스는 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

    • 클라이언트가 필요로 하는 인터페이스로 분리함으로써 각 클라이언트가 사용하지 않는 인터페이스에 변경이 있어도 영향을 받지 않도록 만들어야 한다.

  5. 의존관계 역전 원칙(DIP, Dependency Inversion):

    • 추상화에 의존해야지 구체화에 의존하면 안된다.

    • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안되고 저수준 모듈은 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

참고
객체지향 프로그래밍
객체 지향

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.
post-custom-banner

0개의 댓글