객체지향 프로그래밍 제대로 이해하기

haero_kim·2021년 10월 1일
65

CS 뿌셔먹기

목록 보기
4/13

💡 객체지향 프로그래밍에 대해서 대충 뭔지는 아시는 분들을 위해 작성되었습니다

객체지향을 이해하려면

객체지향 프로그래밍에 대해 설명해보라 하면 대부분 말문이 막힐 것이다. 그나마 대답한다고 하면, '어... 실세계의 사물 개념을 고대로 옮겨서 프로그래밍하는 방식..?' 이라고 대답할 것이다. 객체지향 프로그래밍의 개념을 이해하려면, 무작정 OOP 가 어떤 것이다 하고 외우기보단 등장 배경에 대해 알 필요가 있다. 따라서 객체지향 프로그래밍이 등장하기 이전의 프로그래밍 패러다임을 몇 가지 톺아보자.


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

정의한 기능의 흐름에 따라 순서대로 동작을 추가하며 프로그램을 완성하는 방식이다. 간단한 프로그램의 경우, 이렇게 코드를 짜게 되면 흐름이 눈으로 보이기 때문에 매우 직관적일 것이다. 그러나, 조금이라도 프로그램의 규모가 커지게 되면 곤란해진다. 만일 A → B → C 라는 동작을 구현하다가, C 에서 A 로 돌아가야할 상황이라면 goto 를 활용해야 한다.

goto 문을 무분별하게 활용하게 되면, 그야말로 스파게티 그 자체가 완성된다. 쭉 나열된 코드 속에서 위로 갔다가 아래로 갔다가 난리도 아니게 된다. 그렇게 되면 동작이 직관적이지 못하게 되고, 유일한 장점이 사라지는 셈이다.

그래서 등장한 것이 절차적, 구조적 프로그래밍이다.


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

절차적 프로그래밍에서 '절차'는 함수를 의미한다. 따라서 절차적 프로그래밍이란, 반복되는 동작을 함수 및 프로시저 형태로 모듈화하여 사용하는 방식이다.

프로시저란?

리턴값이 없는 함수다. 예를 들어 데이터를 출력하는 용도로 사용하는 printf 와 같은 함수를 프로시저라고 한다. (엄밀히 따지면 printf 도 리턴값이 있지만 본래 목적은 이 리턴과 무관하기 때문에 프로시저에 가까움)

반복 동작을 모듈화하여 코드를 많이 줄일 수 있다. 하지만 프로시저라는 것 자체가 너무 추상적이라는 단점이 있다. 무슨 말인지 이해하기 위해 아래 예시를 살펴보자.

도서관의 도서 관리 프로그램을 개발한다고 하자.

  • '책'이라는 자료형을 구현해야 함
  • 책에 대한 함수를 구현해야 함

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

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


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

어떤 개념에 대한 자료형과 함수를 '객체' 형태로 함께 묶어서 관리하기 위해 객체지향 프로그래밍 패러다임이 등장하게 되었다. 핵심 포인트는 객체 내부에 자료형 필드와 함수가 함께 존재하는 것이다. 가능한 모든 물리적, 논리적 요소를 객체로 만드는 것이 객체지향 프로그래밍이다.

위에서 들었던 예시인 '도서 관리 프로그램'도, 객체지향으로 구현하게 되면 책의 제목, 저자, 페이지수와 같은 자료형 필드와 대출하기, 반납하기 등의 메소드를 책이라는 객체에 몽땅 묶어서 관리하는 것이 가능해진다. 이렇게 되면, 추상적이었던 동작도 훨씬 직관적으로 보이게 되어 코드 가독성이 증가한다!

결론적으로 객체 간의 독립성이 뚜렷하게 생기고, 중복되는 코드의 양이 줄어든다. 따라서 유지보수에 용이해질 것이다.


객체지향 프로그래밍의 4가지 특징

컴공 학부생들이라면 누구든 알고 있는 추상화, 캡슐화, 상속, 다형성에 대한 이야기이다. 객체지향 패러다임이 생겨나면서 이러한 4가지 특징을 갖추게 되었고, 이를 잘 이해하여 구현할 필요가 있다. 일반적인 개념은 다들 알고 있을 수 있으니, 각 특징을 조금 더 깊이 파헤쳐보려 한다.

1. 추상화 (Abstraction)

객체들이 공통적으로 필요로 하는 속성이나 동작을 하나로 추출해내는 작업

추상적인 개념에 의존하여 설계해야 유연함을 갖출 수 있다. 즉, 세부적인 사물들의 공통적인 특징을 파악한 후, 하나의 묶음으로 만들어내는 것이 추상화다.

e.g. 람보르기니, 페라리, 맥라렌, 포르쉐는 모두 '평생 못 사는 차'라는 공통점이 있다. (ㅠㅠ)

'평생 못 사는 자동차'라는 추상화 집합을 만들어두고, 평생 못 사는 자동차들이 가진 공통적인 특징들 (매우 비쌈, 빵빵한 배기음 등) 을 만들어 활용하면 된다.

예를 들어 '부가티'와 같은 다른 '평생 못 사는 자동차'가 추가될 수 있는데, 이 때 추상화로 구현해두면 다른 코드는 건드리지 않고 추가로 만들 부분만 새로 만들어주면 된다.


2. 캡슐화 (Encapsulation)

정보 은닉화를 통해 높은 응집도, 낮은 결합도를 유지할 수 있도록 설계하는 것

쉽게 말하면, 한 곳에서 변화가 일어나도 다른 곳에 미치는 사이드 이펙트를 최소화 시키는 것을 의미한다. 즉, 객체 내부의 어떤 동작에 대한 구현이 어떻게 되어있는지 감추는 것이다. 이를 통해 외부에서 뭔가 잘못 건드려 객체를 손상시키는 일을 방지할 수 있다.

결합도란 어떤 기능을 실행할 때 다른 클래스나 모듈에 얼마나 의존적인지를 나타내는 지표이다. 객체 간의 독립성을 강조하기 위해 객체지향 프로그래밍이 등장했다. 그런데 결합도가 높아서야 객체지향으로 설계하는 의미가 있을까?

따라서 독립적으로 만들어진 객체들 간의 의존도가 최대한 낮게 만드는 것이 중요하다. 때문에 소프트웨어 공학적으로, 객체 내의 모듈 간의 요소가 밀접한 관련이 있는 것으로 구성하여 응집도를 높이고, 결합도를 줄여야 요구사항 변경에 대처하는 좋은 설계라고 배운다.

한 줄 정리

객체 각각은 독립적으로 작용할 수 있도록 응집도 강해야 하고 다른 모듈을 참조하는 결합도는 낮아야 함!

높은 응집도와 낮은 결합도는, '은닉화'를 통해 이루어낼 수 있다. 외부에서 접근할 필요 없는 것들은 접근 지정자를 private 으로 두어 접근에 제한을 두는 것이다. 외부 객체는 객체 내부의 구조를 모르게 하고, 해당 객체가 노출해서 제공하는 필드와 메소드만 이용할 수 있도록 하여 의도하지 않은 동작 오류를 방지하고 유지보수 효율을 높일 수 있다.


3. 상속

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

누구든 상속의 개념은 알고 있을 것이다. 자식 클래스가 부모 클래스의 필드나 메소드를 그대로 물려받아 사용할 수 있게, 혹은 조금 다듬어서 사용할 수 있게 해주는 것이다. 조금 더 엄밀히 따지면 상속은 자식 클래스를 외부로부터 은닉하는 캡슐화의 일종이다.

'평생 못 사는 차'를 통해 추상화를 설명했었는데, 조금 더 나아가 '쌉부자' 클래스가 있다고 가정해보자. 이 때, 평생 못 사는 자동차의 자식 클래스에 해당하는 람보르기니, 페라리, 맥라렌 등은 캡슐화를 통해 은닉해둔 상태이다.

쌉부자의 관점으로는, 구체적인 자동차 종류가 숨겨져 있는 상태다. 자동차를 수집하고 싶은 쌉부자 입장에서는 자동차 종류가 어떤 것이든, 구매하는 데에 크게 중요하지 않다. 그냥 남들이 평생 못 사는 자동차면 된다. 그 어떤 값비싼 차가 추가된다고 해도, 쌉부자에겐 영향이 없어야 한다는 점이 가장 중요하다. 따라서 캡슐화를 통해 쌉부자 입장에선 확인할 수 없도록 하는 것이다.

이처럼, 상속 관계에서는 단순히 하나의 클래스 안에서 속성, 메소드들의 캡슐화에 한정되지 않는다. 즉 자식 클래스 또한 캡슐화되어 '쌉부자'와 같은 외부 클래스에 은닉하는 것으로 확장되는 것이다. 이렇듯 자식 클래스를 캡슐화해두면, 외부에선 개별적인 자식 클래스들과 무관하게 개발을 이어갈 수 있는 장점이 있다.


상속을 활용하면 상위 클래스의 구현을 활용함으로써, 코드 재사용이 용이해진다.
그러나, 상속을 통한 재사용을 할 때 나타나는 단점이 명백하다. 따라서 객체지향 프로그래밍에서 '코드 재사용'을 목적으로 하는 상속 행위는 엄격히 금한다.

  1. 부모 클래스의 변경이 불편해짐
    → 부모 클래스에 의존하는 자식 클래스가 많을 때 부모 클래스의 변경이 필요하다면, 이를 의존하는 자식 클래스들이 영향을 받게 됨

  2. 불필요한 클래스의 증가
    → 유사 기능 확장시, 필요 이상의 불필요한 클래스를 만들어야할 수 있음

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

그렇다면 이러한 문제는 어떻게 해결할까?
구성 (Composition) 을 통해 해결

객체 구성은 객체 내부 필드에서 다른 객체를 참조하는 방식으로 구현한다.
상속에 비해 런타임 구조가 복잡하고 구현이 어렵지만, 변경시 유연함을 확보할 수 있다는 장점이 크다.
같은 종류가 아닌 클래스를 상속하고 싶을 땐, 객체 컴포지션을 먼저 적용해볼 것


상속은 반드시

  • IS-A 관계 성립할 때
  • 재사용 관점 X, 기능의 확장 관점 O

이러한 상황에서만 사용해야 한다. 상속을 코드 재사용의 개념으로 이해하면 안 된다. 코드를 재사용할 수 있다고 무지성으로 상속을 사용하는 경우가 있는데, 이렇게 되면 클래스간 결합도가 너무 높아져 유지보수 효율이 똥망한다. 일반적인 개념을 구체화하는 상황에서 상속을 사용하자.

IS-A

말 그대로 포함 관계를 의미한다. 한 클래스 A 가 다른 클래스 B 의 자식 클래스임을 이야기한다.
e.g. 햄스터는 동물이다. (소의 부모 클래스는 동물)

HAS-A

상속이 아닌 구성 (Composition) 관계를 의미한다. 한 객체가 다른 객체에 속한다는 이야기다.
e.g. 컴퓨터안에는 CPU 가 있다. (컴퓨터 객체가 CPU 객체를 구성함)


4. 다형성 (Polymorphism)

서로 다른 클래스의 객체가 같은 동작 수행 명령을 받았을 때, 각자의 특성에 맞는 방식으로 동작하는 것

객체지향 패러다임의 핵심이다. 다형성은 상속과의 시너지가 엄청나다. 다형성 구현을 통해 코드를 간결하게 해주고, 유연함을 갖추게 해준다. 또한, 구체적으로 현재 어떤 클래스 객체가 참조되는 지는 무관하게 헐렁하게 프로그래밍하는 것이 가능하다.

평생 못 사는 차들은 배기음이 각기 다르고 고유의 감성이 있어 자동차 덕후들에게 사랑받는다.

위 예시에서 '평생 못 사는차'라는 개념을 일반화(상속)하여 람보르기니, 페라리 등의 객체를 만들었는데,
평생 못 사는 차라는 클래스의 '배기음 재생(?)' 이라는 메소드를 실행했을 때, 자식 클래스들이 각기다른 배기음을 내뿜는 것이 다형성이 부각된 부분이다.

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


지금까지 객체지향 패러다임의 등장 배경과 4가지 특성에 대해 알아보았다. 다음 포스팅에선 객체지향 설계원칙 5가지 (SOLID) 에 대해 알아보고자 한다.

profile
어려울수록 기본에 미치고 열광하라

7개의 댓글

comment-user-thumbnail
2021년 10월 4일

항상 잘 보고 도움 많이되고있습니다. 사진쓰시는게 항상 센스있으셔서 재밌게 볼 수 있네요 ㅋㅋ

1개의 답글
comment-user-thumbnail
2021년 10월 6일

때려박는 예제들...ㅋㅋㅋ 이해가 잘되네요! ㅎㅎ

2개의 답글
comment-user-thumbnail
2021년 10월 8일

잘봤어요

1개의 답글