객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화

인사이트

객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화

2022년 11월 30일

객체 지향 프로그래밍(Object-Oriented Programming, OOP)이란 컴퓨터 프로그램을 어떤 데이터를 입력받아 순서대로 처리하고 결과를 도출하는 명령어들의 목록으로 보는 시각에서 벗어나 여러 독립적인 부품들의 조합, 즉 객체들의 유기적인 협력과 결합으로 파악하고자 하는 컴퓨터 프로그래밍의 패러다임을 의미합니다.

마치 자동차를 만든다고 했을 때, 수 많은 부품들의 결합과 연결로 하나의 완전한 자동차가 만들어지는 것과 같다고 할 수 있습니다. 객체 지향적으로 소프트웨어를 설계한다는 말의 의미는 어떤 프로그램의 일부분에 해당하는 작은 부품, 즉 객체를 먼저 만들고 이렇게 만들어진 여러 객체들을 조립해서 하나의 완성된 프로그램을 만드는 프로그래밍 방법론을 뜻합니다. 여기서 각 부품에 해당하는 객체에 대해서는 아래에서 좀 더 자세히 알아보도록 하겠습니다.

객체 지향 프로그래밍

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

이제 하나씩 살펴보겠지만, 이렇게 객체 지향적으로 프로그램을 설계하는 데에는 여러 이점들이 있습니다. 가장 큰 이점 중에 하나는 객체 지향적 설계를 통해서 프로그램을 보다 유연하고 변경이 용이하게 만들 수 있다는 점입니다. 마치 컴퓨터 부품을 갈아 끼울 때, 해당하는 부품만 쉽게 교체하고 나머지 부품들을 건드리지 않아도 되는 것처럼 소프트웨어를 설계할 때 객체 지향적 원리를 잘 적용해 둔 프로그램은 각각의 부품들이 각자의 독립적인 역할을 가지기 때문에 코드의 변경을 최소화하고 유지보수를 하는 데 유리합니다.

더 나아가, 코드의 재사용을 통해 반복적인 코드를 최소화하고, 코드를 최대한 간결하게 표현할 수 있습니다. 또한 객체 지향 프로그래밍은 실제 우리가 보고 경험하는 세계를 최대한 프로그램 설계에 반영하기 위한 지속적인 노력을 통해 발전해왔기 때문에, 보다 인간 친화적이고 직관적인 코드를 작성하기에 용이합니다.

객체 지향 프로그래밍의 4가지 특징은 각각 추상화, 상속, 다형성, 캡슐화인데, 모두 이러한 객체 지향적 설계의 이점들을 가장 잘 살릴 수 있는 방향으로 발전되어 왔다고 할 수 있습니다.

이러한 맥락에서, 지금부터 객체 지향 프로그래밍의 가장 기본적인 단위이자 시작점이 되는 객체 개념과 더불어 각 특징들의 내용을 살펴보면서 각 특징이 어떻게 객체 지향적 설계를 하는 데 도움을 줄 수 있는지 이해해보도록 하겠습니다.

객체 지향 프로그래밍

객체(object)란?

앞서 언급한 것처럼, 객체객체 지향 프로그래밍의 가장 기본적인 단위이자 시작점이라 할 수 있습니다. 객체 지향 개념의 가장 기본적인 전제는 실제 세계는 객체들로 구성되어 있으며, 보여지는 모든 현상과 발생하는 모든 사건은 이러한 객체들 간의 상호작용을 통해 발생한다는 것에서 출발합니다.

그렇다면 객체란 무엇일까요?

잠시 컴퓨터에서 눈을 돌려 주위를 살펴봅시다. 무엇이 보이나요? 각자 위치한 장소에 따라 다른 여러 사물과 사람이 보일 것입니다. 책상, 의자, 시계, 전등, 책 등 우리가 주변에서 흔히 볼 수 있는 “모든 실재(實在)하는 대상”을 객체 지향 프로그래밍 언어에서는 객체라고 부릅니다.

객체 지향 프로그래밍

사실 객체의 범주에는 우리 눈에 보이는 유형의 대상뿐만 아니라 눈에 보이지 않는 어떤 논리, 사상, 철학, 개념, 공식 등과 같은 무형의 대상들도 포함될 수 있습니다. 한 마디로, 객체는 우리가 보고 느끼고 인지할 수 있는 그 모든 을 의미한다고 할 수 있습니다.

객체 지향 프로그래밍

객체 지향 프로그래밍에서는 이와 같은 각각의 객체를 추상화시켜 속성(state)기능(behavior)으로 분류한 후에 이것을 다시 각각 변수(variable)함수(function)로 정의하고 있습니다. 자바의 기본적인 문법과 개념은 이 콘텐츠에서 핵심적으로 다루는 주제가 아니므로, 만약 아직 자바 문법에 익숙하시지 않다면 이에 대한 기본적인 학습을 선행하시거나 전체적인 흐름을 중심으로 콘텐츠를 소비해주시길 권장드립니다.

그러면 이제부터 본격적으로 객체 지향의 4가지 특징을 각각 살펴보면서, 각각의 특징들이 앞서 설명한 객체 지향적 설계에 어떻게 녹여지고 반영되어 있는 지 확인해보도록 하겠습니다.

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

앞서, 우리는 객체 지향 프로그래밍이 우리가 보고 인지하는 실제 세계를 흉내 내어 가장 기본적인 단위인 객체들을 만들고, 그것들 간의 유기적인 상호작용을 규정하여 프로그램을 발전시키는 프로그래밍 방법론임을 이해할 수 있었습니다.

또한, 객체 지향적 설계를 통해 소프트웨어를 개발하면 코드의 재사용을 통해 반복적인 코드를 최소화하고, 보다 유연하고 변경이 용이한 프로그램을 만들 수 있다는 사실을 알 수 있었습니다. 마지막으로, 이러한 설계는 앞서 언급한 객체 지향 프로그래밍의 4가지 특징에서 기인하는 것이라 했습니다.

이런 맥락에서, 아래에서는 각각의 객체 지향 프로그래밍의 4가지 특징들에 대해 살펴보도록 하겠습니다. 가장 먼저는 각각의 내용들의 핵심적인 개념에 대한 내용들을 간략하게 소개하고, 이것들이 어떻게 연결되어 객체 지향적인 설계가 가능한 지에 대한 내용을 이해해보도록 하겠습니다.

1. 추상화(Abstration)

추상이라는 용어의 사전적 의미를 보면 “사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것”이라 정의하고 있습니다. 여기서 핵심이 되는 개념은 “공통성과 본질을 모아 추출”한다는 것입니다.

예를 들면, 서울의 지하철 노선도는 서울의 지리를 추상화시켜서 보여주는 대표적인 예라 할 수 있습니다. 중요한 부분을 강조하기 위해 불필요한 세부 사항들은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것과 관련이 있습니다.

ㅊ

같은 맥락에서, 객체 지향 프로그래밍에서 의미하는 추상화는 객체의 공통적인 속성과 기능을 추출하여 정의하는것을 의미합니다.

객체 지향 프로그래밍

위의 예시를 보면, 자동차오토바이는 모두 이동 수단이며 모든 이동 수단은 전진과 후진을 할 수 있다는 공통점을 가집니다. 이것을 자바 문법 요소를 사용하여 표현하면, 자동차오토바이라는 하위 클래스(sub-class)들의 공통적인 기능(전진과 후진)을 추출하여 이동 수단 이라는 상위 클래스(super class)에 정의했습니다. 위의 예제에서는 편의상 공통적인 기능(메서드)만 추출했지만, 공통적인 속성(변수)도 추출하여 선언하는 것이 가능합니다.

위에서 본 살펴본 내용을 코드로 표현해보면 다음과 같습니다. 참고로, 자바에서 추상화를 구현할 수 있는 문법 요소로는 추상 클래스(abstract class)인터페이스(interface)가 있는데, 이번 예제에서는 가장 빈번하게 사용되는 인터페이스를 사용해보겠습니다.

Vehicle 인터페이스

객체 지향 프로그래밍-추상화

가장 먼저 자동차오토바이공통적인 기능 추출하여 이동 수단 인터페이스에 정의합니다. 컴퓨터 프로그래밍에서 인터페이스란 “서로 다른 두 시스템, 장치, 소프트웨어 따위를 서로 이어주는 부분 또는 그런 접속 장치”이라 정의할 수 있는데, 객체 지향적 설계에 있어서 인터페이스는 어떤 객체의 역할만을 정의하여 객체들 간의 관계를 보다 유연하게 연결하는 역할을 담당합니다.

다른 말로 표현하면, 인터페이스에는 추상 메서드나 상수를 통해서 어떤 객체가 수행해야 하는 핵심적인 역할만을 규정해두고, 실제적인 구현은 해당 인터페이스를 구현하는 각각의 객체들에서 하도록 프로그램을 설계하는 것을 의미합니다.

위에서 우리가 정의한 Vehicle 인터페이스를 실제로 구현한 아래 코드 예제를 보면서 좀 더 이해해보도록 하겠습니다.

Car 클래스

객체 지향 프로그래밍-추상화

MotorBike 클래스

객체 지향 프로그래밍-추상화

위에서 확인할 수 있는 것처럼, Vehicle 인터페이스를 구현한 구현체, CarMotorBike 클래스에서 앞서 우리가 인터페이스에 정의한 역할을 각각의 클래스의 맥락에 맞게 구현하고 있습니다. 즉, 각각 클래스 모두 전진과 후진의 기능을 공통적으로 가지지만, 차는 차의 시동을 걸어야 하고, 오토바이는 오토바이의 시동을 걸어야 하기 때문에 그 구현은 각 클래스에 따라 달라야 할 것입니다.

이것을 객체 지향 프로그래밍에서는 역할과 구현의 분리라고 하며, 이 부분이 아래에서 살펴볼 다형성과 함께 유연하고 변경이 용이한 프로그램을 설계하는 데 가장 핵심적인 부분이라 할 수 있습니다. 정리하면, 객체 지향 프로그래밍에서는 보다 유연하고 변경에 열려있는 프로그램을 설계하기 위해 역할과 구현을 분리하는데, 여기서 역할에 해당하는 부분이 인터페이스를 통해 추상화될 수 있습니다.

이 부분과 관련한 자세한 내용은 아래에서 다른 특징들과 연계하여 좀 더 자세히 살펴보도록 하겠습니다.

2. 상속(Inheritance)

상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미합니다.

앞서 봤었던 추상화의 연장선에서, 상속은 클래스 간 공유될 수 있는 속성과 기능들을 상위 클래스로 추상화 시켜 해당 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있도록 합니다.

즉, 클래스들 간 공유하는 속성과 기능들을 반복적으로 정의할 필요 없이 딱 한 번만 정의해두고 간편하게 재사용할 수 있어 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있도록 합니다.

앞에서 봤었던 예제를 약간 변형하여 상속의 맥락에서 생각해보겠습니다.

위의 그림을 보면 자동차와 오토바이가 있고, 각각의 기능과 속성들이 명시되어 있습니다. 이 중에서 빨간색으로 표시된 속성과 기능들은 자동차와 오토바이의 공통적인 부분들이고, 푸른색으로 표시된 부분들은 그렇지 않은 부분들입니다.

이제 위의 그림을 코드로 표현해보면 다음과 같습니다.

Car 클래스

MotorBike 클래스

객체 지향 프로그래밍-추상화

어떤가요?

위의 코드 예제를 보면, 한 눈에도 같은 코드가 반복적으로 정의되고 있다는 사실을 알 수 있습니다. 좀 더 구체적으로 보면, 각각의 클래스마다 속성으로 model, color, wheels 기능으로 moveForward()moveBackward 가 완전히 동일한 코드임에도 불구하고 계속 반복되고 있다는 점을 확인할 수 있습니다. 또한 하나의 코드에서 변경 사항이 일어나면, 해당 코드의 변경 사항을 다른 클래스에서도 일일이 수정해주어야 하는 어려움이 있습니다.

그러면 이제 앞서 설명했던 추상화와 상속을 활용하여 앞선 코드를 재정의해보도록 하겠습니다. 아래 그림을 참고 하면서 아래 코드를 확인해주세요.

Vehicle 클래스

객체 지향 프로그래밍-추상화

Car 클래스

MotorBike 클래스

객체 지향 프로그래밍-추상화

Main 실행 클래스

객체 지향 프로그래밍-추상화

위의 코드 예제를 보면, CarMotorBike 클래스의 공통적인 속성과 기능들을 추출(추상화)하여 Vehicle클래스(상위 클래스)에 정의하였고, extends 키워드를 통해 각각의 하위 클래스로 확장하여 해당 기능과 속성들을 매번 반복적으로 정의해야 하는 번거로움을 제거했습니다. 또한, 공통적인 코드의 변경이 있는 경우 상위 클래스에서 단 한 번의 수정으로 모든 클래스에 변경 사항이 반영될 수 있도록 만들었습니다.

참고로, MotorBike 클래스에서 확인할 수 있듯이, 상위 클래스의 기능과 속성들을 그대로 사용할 수도 있지만, 각각의 클래스의 맥락에 맞게 메서드 오버라이딩(method overriding)을 사용하여 내용을 재정의할 수도 있습니다.

사실 이 부분이 앞서 추상화에서 봤었던 인터페이스를 통한 구현과 상속을 구분하는 핵심적인 차이 중에 하나라 할 수 있습니다. 즉, 양자 모두 상위 클래스-하위 클래스의 관계를 전제하면서 공통적인 속성과 기능을 공유할 수 있지만, 상속의 경우 상위 클래스의 속성과 기능들을 하위 클래스에서 그대로 받아 사용하거나 오버라이딩을 통해 선택적으로 재정의하여 사용할 수 있는 반면, 인터페이스를 통한 구현은 반드시 인터페이스에 정의된 추상 메서드의 내용이 하위 클래스에서 정의되어야 합니다.

결론적으로, 상속 관계의 경우 인터페이스를 사용하는 구현에 비해 추상화의 정도가 낮다고 할 수 있습니다. 인터페이스가 역할에 해당하는 껍데기만 정의해두고, 하위 클래스에서 구체적인 구현을 하도록 강제하는 것에 비해, 상속 관계의 경우 상황에 따라 모든 구체적인 내용들을 정의해두고 하위 클래스에서는 그것을 단순히 가져다가 재사용할 수 있습니다.

3. 다형성

이제 객체 지향 프로그래밍의 꽃이라 할 수 있는 다형성에 대해 간략히 살펴보도록 하겠습니다. 다형성(多形性)이란 한자 이름 그대로 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미합니다.

비유적으로 표현하자면, 어떤 중년의 남성이 있다고 했을 때 그 남자의 역할이 아내에게는 남편, 자식에게는 아버지, 부모님에게는 자식, 회사에서는 회사원, 동아리에서 총무 등 상황과 환경에 따라서 달라지는 것과 비슷하다고 할 수 있습니다.

객체 지향 프로그래밍-다형성

객체 지향에서의 다형성도 이와 비슷합니다. 즉, 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할수 있는 객체 지향의 특성을 의미합니다. 대표적인 예로 우리가 앞서 본 메서드 오버라이딩메서드 오버로딩(method overloading)이 있습니다.

Vehicle 인터페이스

객체 지향 프로그래밍-다형성

Car 클래스

객체 지향 프로그래밍-다형성

앞서 추상화에서 본 것처럼 메서드 오버라이딩을 사용하면 같은 이름의 moveForward()moveBackward() 를 각각의 클래스의 맥락에 맞게 재정의하여 사용할 수 있습니다. 즉, 같은 이름의 메서드가 상황에 따라 다른 역할을 수행하는 것입니다. 또한, 하나의 클래스 내에서 같은 이름의 메서드를 여러 개 중복하여 정의하는 것을 의미하는 메서드 오버로딩도 이와 같은 맥락이라 할 수 있습니다.

사실 앞서 언급한 메서드 오버라이딩과 메서드 오버로딩도 다형성의 한 중요한 예시지만, 객체 지향의 맥락에서 이것보다 더 중요한 다형성의 정의는 이것입니다.

객체 지향 프로그래밍에서 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것입니다.

위의 정의만 봐서는 단번에 이해하기는 어렵습니다. 이해를 도울 수 있는 예를 한 가지 들어보겠습니다. 여기 이런 명제가 있습니다.

사람이 음식을 먹습니다.

여기서 음식 은 상황에 따라서 피자가 될 수도, 치킨이 될 수도, 짜장면이 될 수도, 탕수육이 될 수도 있습니다. 또다른 한편으로, 사람 은 옆 집의 철수가 될 수도, 영희가 될 수도, 선생님이 될 수도, 내 동생이 될 수도 있습니다.정리하면, 앞서 쭉 설명한 다형성의 맥락과 마찬가지로 음식사람 은 그 상황과 맥락에 따라서 모습을 바꿀 수있습니다.

지금까지 쭉 사용해왔던 이동 수단 을 다시 한번 더 예시로 들어보면, 이동수단자동차 가 될 수도, 오토바이 가 될 수도 있습니다. 다르게 표현해보면, 자동차는 자동차이다. 라는 명제와 자동차는 이동수단이다. 라는 명제는 모두 참입니다. 오토바이의 경우도 마찬가지 일 것입니다. 이동수단이라는 범위 안에 자동차와 오토바이를 하나로 묶을 수 있게 됩니다. 즉, 이동수단은 작은 개념들을 품을 수 있는 포괄적인 개념이라는 의미입니다.

다시 위의 정의로 돌아가 보면, 객체 지향 프로그래밍에서 다형성이란 앞서 설명한 이동 수단 과 같은 넓은 범위의 타입, 즉 상위 클래스 타입의 참조 변수로 그것과 관계있는 하위 클래스들을 참조할 수 있는 능력입니다. 이제 코드를 통해 좀 더 구체적으로 그 의미를 알아보도록 하겠습니다.

앞서 상속 파트에서 우리가 상위 클래스와 하위 클래스를 상속 관계로 만들고, 객체를 생성하여 어떤 속성과 기능을 사용했을 때 우리는 아래와 같이 객체를 생성한 바 있습니다.

Main 실행 클래스

객체 지향 프로그래밍-다형성

이제 방금 설명한 다형성 개념을 활용하여 객체를 생성하는 방법을 추가해보도록 하겠습니다.

Main 실행 클래스

객체 지향 프로그래밍-다형성

위의 코드를 확인해보면, 상위클래스 타입의 참조변수로 하위클래스 객체를 참조하는 것 의 의미를 조금 더 구체적으로 이해할 수 있습니다. 원래 우리가 사용했던 방식은 하위 클래스의 객체를 생성하여 하위 클래스 타입의 참조 변수에 할당해주었지만, 다형성을 활용한 객체 생성 방식에서는 하위 클래스의 객체를 생성하여 상위 클래스 타입의 참조변수 car2 에 할당해주고 있습니다.

그렇다면 왜 이렇게 다형성을 활용한 방식이 유용할까요?

지금부터 코드로 하나씩 살펴보도록 하겠습니다. 먼저는 다형성을 활용하면 여러 종류의 객체를 배열로 다루는 일이 가능해집니다. 아래 코드를 잠시 보겠습니다.

객체 지향 프로그래밍-다형성

앞서 논의했던 것처럼, 상위 클래스 Vehicle 타입의 객체 배열을 생성해주면, 이제 해당 타입의 참조 변수는 Vehicle 클래스와 상속 관계에 있는 모든 하위 클래스들을 그 안에 담아줄 수 있습니다. 원래 자바에서 배열의개념이 하나의 같은 타입으로 이뤄져 있는 자료구조라는 사실을 기억할 때, 이렇게 다형성을 활용하면 하나의 타입만으로 여러 가지 타입의 객체를 참조할 수 있어 보다 간편하고 유연하게 코드를 작성하는 것이 가능해집니다.

또 다른 예제를 살펴보기 위해 새로운 Driver 클래스를 아래와 같이 정의해보겠습니다.

Driver 클래스

객체 지향 프로그래밍-다형성

Main 클래스

객체 지향 프로그래밍-다형성

위의 예제에서 확인할 수 있듯이, Driver 클래스의 코드는 매우 간단합니다. 즉, 매개변수로 자동차나 오토바이객체를 전달받아 운전하는 것입니다. 이렇게 하나의 객체가 다른 객체의 속성과 기능에 접근하여 어떤 기능을 사용할 때, 우리는 A클래스는 B클래스에 의존한다 라고 표현합니다.

같은 맥락에서, 위의 코드 예제를 도식을 사용하여 표현하면 아래와 같이 그려볼 수 있을 것이고, 우리는 Driver 클래스가 Car 클래스와 MotorBike 클래스에 의존하고 있다 라고 설명할 수 있습니다. 즉 Driver 클래스와 다른 두 개의 클래스가 서로 직접적인 관계를 가지고 있는데, 이러한 상황을 조금 어려운 말로 객체들 간의 결합도가 높다고 표현합니다.

객체 지향 프로그래밍-다형성

하지만 이렇게 결합도가 높은 상태는 객체 지향적인 설계를 하는 데 매우 불리합니다.

만약 지금처럼 이동 수단이 자동차와 오토바이 단 2개가 아니라 수 십, 수 백개라면 어떨까요? 아마 아래와 같이 똑같은 코드를 수 십, 수 백 번 작성해야 할 것입니다.

객체 지향 프로그래밍-다형성

생각만 해도 끔찍하지 않나요?

또 만약에 어떤 새로운 상황이 발생해서, MotorBike 클래스가 다른 클래스 MotorCycle 클래스로 변경되어야 하는 경우는 어떨까요?

Driver 클래스

객체 지향 프로그래밍-다형성

마찬가지로, Driver 안에 매개변수로 전달되는 참조변수의 타입과 참조변수를 수정할 수밖에 없는 상황이 발생합니다. 그리고 코드가 많아질수록 이 작업은 아주 고되고 힘든 작업이 될 수 밖에 없을 것입니다. 이것은 마치 운전자가 운전을 배웠는데, 이동 수단이 바뀔 때마다 새롭게 운전을 배워야 하는 상황과 같다고 할 수 있습니다. 누구에게나 이런 상황은 정말 달갑지 않을 것입니다.

이런 맥락에서, 객체 지향 프로그래밍은 지금까지 학습한 추상화, 상속, 그리고 다형성의 특성을 활용하여 프로그래밍을 설계할 때 역할구현을 구분하여 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램 설계를 가능하게 만들었습니다. 이 부분이 사실 객체 지향 프로그래밍의 하이라이트이자 핵심이라 할 수 있습니다.

곧바로 코드로 확인해보겠습니다.

Vehicle 인터페이스

객체 지향 프로그래밍-다형성

Car 클래스

객체 지향 프로그래밍-다형성

MotorBike 클래스

객체 지향 프로그래밍-다형성

Driver 클래스

Main 실행 클래스

객체 지향 프로그래밍-다형성

먼저 앞서 추상화에서 봤었던 것처럼 Vehicle 인터페이스를 통해 이동 수단의 역할을 추상화하고, 각각 Car 클래스와 MotorBike 클래스에서 기능들을 구현하고 있습니다. 여기까지는 앞서 추상화 파트에서 봤던 것과 동일합니다.

위에서 눈여겨봐야 할 가장 핵심은 Driver 클래스입니다. 비교를 위해 인터페이스를 작성하지 않았던 코드를 함께 가져와서 살펴보도록 하겠습니다.

Vehicle 인터페이스 적용 전

객체 지향 프로그래밍-다형성

Vehicle 인터페이스 적용 후

객체 지향 프로그래밍-다형성

한눈에 봐도 코드의 중복이 사라지고, 코드가 훨씬 간결해졌다는 사실을 알 수 있습니다. 핵심은 drive() 메서드로 전달되는 매개변수의 타입을 상위 클래스인 인터페이스 타입 Vehicle 로 변경한 것입니다. 이제 다형성의세례를 받은 drive() 메서드의 매개변수로 인터페이스를 구현한 객체라면 무엇이든 전달이 될 수 있게 되었습니다. 마찬가지로 지금은 편의상 하나의 메서드만 살펴봤지만, 만약 메서드의 수가 많아지고 코드 라인이 길어지면 그 효과는 더 강력하겠죠?

이해를 돕기위해, 앞서 본 코드를 다시 도식으로 나타내보면 다음과 같습니다.

객체 지향 프로그래밍-다형성

어떤가요?

앞서 봤었던 도식에서는 Driver 클래스가 CarMotorBike 클래스 각각과 직접적으로 연결되어 강한 결합도를 보였지만, 이제는 Vehicle 인터페이스를 통해 간접적으로 연결되어 결합도가 낮아졌습니다. 따라서, 이제 Driver 클래스는 더 이상 각각의 클래스 내부의 변경이나 다른 객체가 새롭게 교체되는 것을 신경 쓰지 않아도 인터페이스에만 의존하여 수정이 있을 때마다 코드 변경을 하지 않아도 됩니다.

아까의 비유로 표현하면, 운전자가 운전하는 법을 매번 새롭게 배우지 않아도 변경된 이동 수단 이용하는데 아무런 문제가 발생하지 않습니다. 이것이 바로 지금까지 학습한 추상화와 다형성의 특성을 활용한 역할과 구현의 구분이자, 보다 유연하고 변경이 용이한 소프트웨어 설계를 가능하게 하게 하는 객체 지향 프로그래밍의 꽃이라 할 수 있습니다.

물론 아직 모든 문제가 해결된 것은 아닙니다.

Main 실행 클래스

여전히 실행 클래스의 코드에서 객체를 생성할 때, new Car()new MotorBike() 처럼 객체에 직접적으로 의존하고 있어서, 해당 객체를 다른 객체로 변경할 시 코드의 변경이 불가피합니다. 즉, 다시 객체 간 높은 결합도를 보이는 상황이 초래되었습니다.

이 문제를 해결하기 위해 등장한 것이 바로 의존관계 주입(dependency injection)이라 부르는 스프링 프레임워크의 핵심적인 개념입니다. 스프링 프레임워크에 대한 자세한 내용은 해당 포스팅에서도 확인하실 수 있습니다. 더 자세한 내용이 궁금하시다면 직접 관련된 내용들을 찾아보시는 것도 좋은 공부가 될 것입니다.

다시 돌아가서 정리하면, 객체 지향 프로그래밍은 객체 간 관계와 협력을 설계하는 것인데, 다형성은 그 관계를 보다 유연하고 확장이 용이한 설계가 가능하도록 하는데 핵심적인 역할을 한다는 사실이 중요합니다. 또한, 다형성을 제대로 활용하기 위해서 앞서 배웠던 추상화와 상속에 대한 내용들이 함께 존재해야 한다는 사실도 기억해야 합니다. 즉, 추상화가 있어야 각 객체들의 역할 정의가 가능하고, 인터페이스는 상위 클래스-하위 클래스를 전제하기 때문에 상속에서 배웠던 개념들이 함께 필요합니다.

4. 캡슐화(Encapsulation)

이제 객체 지향의 마지막 특징인 캡슐화에 대해 살펴보겠습니다. 캡슐화란 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것을 말합니다.

아래 그림은 캡슐화의 기본 내용을 잘 표현하고 있습니다. 즉 서로 관련 있는 데이터와 이를 처리할 수 있는 기능들을 한곳에 모아 관리하는 것입니다. 자바 객체 지향 프로그래밍에서 이렇게 캡슐화를 하는 이유로 크게 두 가지를 언급할 수 있습니다.

  • 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
  • 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출

우리가 아플 때 한 번씩 먹게 되는 캡슐 약을 떠올려보면, 우리는 캡슐 안에 어떤 색의 내용물이 있는지, 또 어떤 성분의 약이 들어있는지 알 수 없습니다. 또한, 그 안의 내용물은 캡슐을 통해서 외부로부터 오염되지 않고 안전하게 보호될 수 있습니다.

자바의 캡슐화도 이와 같다고 할 수 있습니다. 즉, 외부로부터 클래스에 정의된 속성과 기능들을 보호하고, 필요한부분만 외부로 노출될 수 있도록 하여 각 객체 고유의 독립성과 책임 영역을 안전하게 지키고자 하는 목적이 있습니다.

그렇다면 어떻게 이러한 목적을 달성할 수 있을까요?

자바 객체 지향 프로그래밍에서 캡슐화를 구현하기 위한 방법은 크게 두 가지가 있습니다. 먼저는 접근제어자(access modifiers)를 활용하는 것입니다. 접근제어자는 클래스 또는 클래스의 내부의 멤버들에 사용되어 해당 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제한하는 역할을 합니다. 실생활의 예시를 통해 한번 설명해보겠습니다.

위의 그림은 접근제어자를 우리가 실생활에서 쉽게 접할 수 있는 화장실을 예시로 표현한 것입니다. 모두에게 열려있는 공중화장실, 특정한 멤버십을 가진 사람들에게만 열려있는 호텔 화장실, 그리고 나만 사용할 수 있는 우리 집 개인 화장실은 각각의 다른 접근 범위를 가진다고 할 수 있습니다.

접근제어자도 같은 개념이라고 생각하면 쉽습니다. 자바에는 public, default , protected, private총 4가지 종류의 접근 제어자가 있는데 위 화장실의 예제처럼 오른쪽으로 갈수록 더 좁은 접근 범위를 가집니다.따라서, 어떤 소프트웨어 프로그램을 설계할 때 위의 접근 제어자를 활용하여 어떤 클래스나 그 멤버에 대한 접근범위를 설정하여 데이터를 효과적으로 보호할 수 있습니다.

자바의 기본 문법에 해당하는 내용이지만, 자바의 접근 제어자의 접근 범위를 표로 나타내면 다음과 같습니다.

위의 표에서 확인할 수 있는 것처럼, 접근 제어자의 접근 범위가 각각 클래스 내, 패키지 내, 다른 패키지의 하위 클래스, 그리고 패키지 외까지 각각 다른 것을 확인할 수 있습니다. 위의 내용을 코드로 한번 살펴보겠습니다.

SuperClass 클래스

다른 패키지의 Test2 클래스

이 부분은 자바의 기본 문법에 해당하는 부분이기 때문에, 따로 추가적인 설명을 하지는 않겠습니다. 좀 더 자세한 내용이 궁금하시다면, 사용하고 있는 IDE에 위의 코드를 복제하여 여러 시도를 해보면서 이해를 넓혀가시기를 권장 드립니다.

자바의 캡슐화를 구현하기 위한 두 번째 방법으로는 getter/setter 메서드가 있습니다. 곧바로 예제를 통해 알아보도록 하겠습니다.

Car 클래스

위의 예제를 보면, 모든 속성값들이 private 접근 제어자로 선언되어 있고, getter/setter 메서드의 접근제어자만이 public 으로 열려있습니다. 따라서 선택적으로 외부에 접근을 허용할 속성과 그렇지 않을 속성을 getter/setter 메서드를 통해 설정해줄 수 있습니다.

이제 조금 더 구체적으로 앞서 설명한 캡슐화가 어떻게 객체 지향의 핵심적인 이점과 연결될 수 있는지 코드 예제를 통해 좀 더 알아보도록 하겠습니다. 먼저 아래 코드를 살펴보겠습니다.

Car 클래스

Driver 클래스

Main 실행 클래스

위의 코드는 아무런 문제 없이 잘 동작하는 코드처럼 보이지만, 치명적인 약점을 가지고 있습니다. Driver 클래스의 drive() 메서드의 바디를 살펴보면, 해당 메서드가 호출되었을 때 Car 클래스의 메서드들이 순차적으로 실행되고 있는 것을 확인할 수 있습니다.

겉으로 봤을 때는 아무런 문제가 없어 보이는 데 이게 왜 치명적인 약점이 될까요?

만약에 Car 클래스의 3가지 메서드들에 어떤 변경이 생겼다고 가정해봅시다. 그러면 해당 메서드들을 사용하고 있는 Driver 클래스의 drive() 메서드의 수정이 불가피합니다. 다른 말로, Driver 클래스가 Car 클래스 의 세부적인 내부 로직을 속속히 너무 잘 알고 있고, 이것은 앞서 우리가 계속 피하고자 했던 객체 간의 결합도가 높은 상태를 의미합니다.

이럴 때 우리는 캡슐화를 활용하여 객체의 자율성, 즉 하나의 객체가 해당 객체의 속성과 기능에 대한 독점적인 책임을 담당하도록 만들고, 이를 통해 객체 간의 결합도를 낮게 유지할 수 있습니다. 코드로 한번 살펴보도록 하겠습니다.

Car 클래스

Driver 클래스

Main 실행 클래스

아까와 출력값은 동일하지만, 기존의 Driver 클래스가 하나하나 호출해줬던 메서드들을 모두 operate() 메서드로 묶어 Car 클래스로 옮겨두었고, Driver 클래스에서는 내부 동작을 전혀 신경쓰지 않아도 단순히 operate() 메서드를 호출하여 사용하고 있습니다.

또한, operate() 메서드 내부의 메서드들은 외부에서 호출되어 사용할 일이 없으므로 접근 제어자를 모두 private으로 변경해주었습니다. 정리하면, Car 클래스와 관련된 기능들은 온전히 Car 에서만 관리되도록 하였고, 불필요한 내부 동작의 노출을 최소화하였습니다. 이제 Driver 클래스의 입장에서는 더 이상 Car 클래스의 내부 로직을 알지 못하고, 알 필요도 없어졌습니다.

이렇게 캡슐화를 활용하면, 객체 내부의 동작의 외부로의 노출을 최소화하여 각 객체의 자율성을 높이고, 이를 통해 객체 간 결합도를 낮추어 앞서 설명한 객체 지향의 핵심적인 이점을 잘 살리는 방법으로 프로그램을 설계하는 일이 가능합니다.

객체 지향 프로그래밍과 코딩의 이해,
코드스테이츠 백엔드 부트캠프에서

이로써 객체 지향 프로그래밍의 기본적인 개념과 그 설계를 바르게 하기 위해 필요한 4가지 특징에 대한 내용을 모두 살펴보았습니다.

코드스테이츠 백엔드 개발 부트캠프는 안정적인 웹 서비스를 효율적으로 구축하는 백엔드 개발자 양성 과정으로 자바, 스프링, 데이터베이스 등 백엔드 전반에 대한 기초 지식을 쌓아 배운 내용을 바탕으로 실전 프로젝트까지 진행하는 커리큘럼으로 설계되어 있습니다. 1주차부터 4주차까지는 Java 기초 문법을 통해 객체 지향 프로그래밍과 코딩을 이해할 수 있는 시간이 마련되어 있어요.

상대적으로 적은 분량의 콘텐츠만으로 객체 지향 프로그래밍이라는 방대하고 깊은 주제의 모든 내용을 이해하기 어렵겠지만, 이 콘텐츠를 하나의 작은 이정표라고 생각하면서 앞으로의 학습 여정에 있어서 조금이나마 도움이 되었으면 좋겠습니다.

나태웅 Educational Software Engineer (Software Engineering)


👩‍💻 개발자 커리어의 시작,
코드스테이츠 부트캠프가 더 궁금하다면?

목록 보기

추천글