객체 지향(OOP) 개념과 특징
객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 컴퓨터 프로그래밍의 패러다임 중 하나이다.
이 패러다임은 프로그램에 필요한 데이터를 독립적입 객체들의 집합으로 모델링하는 방식을 강조한다.
또한 각 객체는 데이터를 추상화 시켜 상태(데이터)와 행위(데이터를 조작하는 메서드)를 가진 객체로 만들고 이 객체(Object)들이 모여 서로 상호 작용하면서 데이터를 처리하는 방식을 일컫는다.
객체 지향 프로그래밍에서 객체는 실제 세계의 개념이나 사물을 모델링한 것으로 생각할 수 있다.
예를 들어, 자동차를 객체로 모델링할 수 있다. 자동차 객체는 속성(데이터)로는 차량 번호, 제조사, 색상 등을 가지고 있을 수 있고, 메서드(기능)로는 주행, 정지, 속도 조절 등을 수행할 수 있다.
이런 객체들은 클래스라는 템플릿을 사용하여 생성되는데 클래스는 객체를 생성하기 위한 설계도이며, 객체의 구조와 동작을 정의하는 역할을 한다.
객체 지향 프로그래밍은 다음과 같은 주요 특징을 갖는다:
- 캡슐화(Encapsulation): 관련된 데이터와 메서드를 하나의 단위로 묶어 캡슐화하고, 외부에는 해당 객체의 일부 정보만 노출시킴으로써 객체의 내부 동작을 숨기고 안전하게 유지한다.
- 상속(Inheritance): 상속을 통해 기존 클래스의 특성을 다른 클래스가 물려받을 수 있다. 이를 통해 코드의 재사용성을 높이고, 클래스 간의 계층 구조를 형성할 수 있다.
- 다형성(Polymorphism): 같은 이름의 메서드를 다양한 객체에서 다르게 동작하도록 구현하는 기능이다. 다형성을 통해 코드의 유연성과 확장성을 높일 수 있다.
객체 지향 프로그래밍은 프로그램의 구조를 모듈화하여 유지보수와 확장을 용이하게 하며, 코드의 가독성을 높일 수 있는 장점을 가지고 있다.
좀더 쉽게 말하면, 프로그램을 묶음 단위로 잘게 쪼개서, 추후에 가져다 쓰기 편하게 만들어 놓은 프로그래밍 방식이라고 보면 된다.
이처럼 레고 블럭 조립하듯이 컴포넌트를 유연하고 변경이 용이하기 때문에, 현업에서 대규모 소프트웨어 개발에 많이 사용된다.
대표적으로 많이 알려진 Java언어를 포함하여 Ruby, Python, C++, Objectivc-C, C#, Kotlin 등이 모두 객체지향 요소를 가진 언어이다.
※ 참고
객체 지향 설계 전략은 꼭 반드시 객체 지향 언어(Java, kotlin 등)에서만 사용되는 것은 아니다.
자바스크립트나 파이썬은 객체 지향 언어는 아니지만, 따로 클래스(class) 문법을 지원하는 것 처럼 대부분의 프로그래밍 언어는 객체 지향 언어 방식을 지원하고 있다.
즉, 객체 지향 프로그래밍 개념을 배우면, 그 기술을 다양한 많은 언어에 적용할수 있다는 뜻이기도 하다
객체 지향 프로그래밍의 반대 개념으로는, 절차적 프로그래밍 (Procedure Programming) 이라는 것이 있다.
절차적 프로그래밍에 대해 말하자면 함수(function)를 이용해서 정리 정돈하는 프로그래밍 기법 이라고 할 수 있다.
대표적인 언어는 C가 있다.
즉 , 함수를 이용해서 작은 부품을 만들고 이것을 결합해서 더 큰 프로그램을 만들어가는 방식이 바로 절차적 프로그래밍 이다.
하지만 기술이 발전됨에 따라, 더 복잡한 어플리케이션에 대한 수요가 증가하면서 절차적 프로그래밍의 한계가 나타나기 시작했다. 복잡한 어플리케이션을 위해서는 실제 세계처럼 더 밀접한 모델링 방식이 필요했기 때문이다.
그래서 서로 연관된 함수(method)와 변수(variable) 을 모아서 박스(레고)를 만들고 거기에 이름을 붙여서 정리정돈을 한 수
납상자를 만들었는데 이것이 바로 Class 이다.
그리고 이런 클래스를 중심으로 프로그램의 구조를 만들어가는 프로그래밍 방법론이 객체 지향 프로그래밍(Object Oriented Programming) 인 것이다.
이 포스팅에서 강조하는 점은 객체 지향 기법은 '사용' 기법이 아닌 '설계' 기법 인 점이다.
대부분 프로그래밍 입문자들은 객체 지향 기법에 대해 잘 와닿지 않는다고 한다.
왜냐하면 우리는 프로그래밍을 배울때 사용하는 법을 배웠고 우리는 그것을 매우 당연하게 사용해왔기 때문이다.
변수 선언, 함수 선언, 조건문, 반복문, 연산자 등을 이해하는데 있어 직접 문제를 구현해봄으로써 필요성을 이해했지만, 객체지향 개념(클래스, 객체, 캡슐화, 상속, 다형성, 재정의, 인터페이스, 추상 클래스 등) 을 이해하는데 있어 충분히 if문 for문으로도 문제를 구현 가능했기 때문에 구조적으로 편하다 정도로만 깨닫지 완벽히 필요한 이유를 알지 못한다
그래서 보통 객체 지향에 대해 요약 정리된 문서를 보면, 추상화 라고 하면 abstract class, 다형성이라 하면 오버로딩 / 오버라이딩, 캡슐화 라고 하면 private 만 알려주는 문서가 많다.
※ 참고
절차적 프로그래밍이 계산기와 같은 아웃풋을 생성하는 프로그램을 만드는 것이라면, 객체지향 프로그래밍은 라이브러리와 같이 다른 개발자가 이용할수 있게 구조를 만드는 것으로 보면 된다.
그리고 거대한 규모의 애플리케이션은 객체지향과 절차적 프로그래밍이 잘 융합하여 만들어진다.
따라서 이번 포스팅에서는 설계 관점에서 왜 추상화, 상속, 다형성, 캡슐화를 사용하는지, 추상화를 하면 어떤 점이 좋은지 , 원론적인 이야기로 풀어 좀 더 객체 지향에 대해 친숙하게 하고 필요성을 느끼게 하는 것을 중점으로 두고 학습을 이어나갈 예정이다.
객체지향 4가지 특성에 대해 확실히 익히게 된다면, 추후에 여러 오픈 소스나 라이브러리들을 접하게 될텐데, 이 라이브러리들이 어떤 기능이 어떻게 동작하는지 보려면 이 클래스, 저 클래스 파일을 넘아들며 상속 원리를 이해하고 다형성 원리를 통해 유기적으로 코드가 돌아가는 구조를 파악해야 하는데 매우 도움이 될 것이다.
추상화 (Abstraciton)
추상화 라는 단어는 미술 시간에 한번쯤은 들어본적이 있을 것이다.
미술에서의 추상화는 사진 처럼 사물을 눈에 보이는 것처럼 자연적, 사실적으로 재현하는 것이 아니라, 점, 선, 면, 색채 등의 단순한 표현을 이용해 그림을 그리는 것을 말한다.
이처럼 컴퓨터 과학에서도 추상화를 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
즉, 정리하자면 보통 알고는 있지만 정확하게 표현하기 힘든 것들을 중요한 부분이나 특징점을 잡아 설명하는 것을 '추상적으로 표현한다' 즉 '추상화한다' 라고 말한다고 볼 수 있다.
객체지향 프로그래밍에서도 이 추상화는 중요한 개념 중에 하나이다.
프로그래밍에서의 추상화는 클래스를 정의할 때 불필요한 부분들을 생략하고 객체의 속성 중 중요한 것에만 중점을 두어 간략화하는 것을 말한다.
즉 클래스들의 중요하고 공통된 성질들을 추출하여 부모(슈퍼) 클래스를 선정하는 개념과, 이벤트 발생의 정확한 절차나 방법을 정의하지 않고 대표할 수 있는 표현으로 대체하는 것을 말한다.
객체 지향 프로그래밍의 추상화는 크게 두가지로 나뉘는데, 불필요한 세부 정보는 '숨긴다' - 제어 추상화 와 객체의 관련 속성 만 '표시' - 데이터 추상화 가 있다.
제어 추상화
제어 추상화는 어떤 클래스의 메소드를 사용하는 사용자에게 해당 메소드의 작동방식과 같은 내부 로직을 숨기는 것을 말한다.
예를들어 소비자가 자동차를 운전할 때, 우리는 자동차의 시동 / 정지, 가속 / 브레이크 등과 같은 자동차 운전 동작에만 신경 쓰면 되지, 실제 시작 / 정지 메커니즘이나 가속 / 제동 프로세스가 내부적으로 어떻게 작동하는지에 대해서는 알 필요가 없다.
소비자는 자동차의 동작 원리 세부 사항을 몰라도 운전하는데는 지장이 없기 때문이다.
만일 자동차를 운전하는데 내부 엔진 원리까지 알아야 된다면 운전하는데 까다로워 지금 처럼 자동차는 많이 팔리지않아 생산 비용에 비해 회사에 적자가 생기게 되고 자동차 종류도 이처럼 많이 발전되지 않았을 것이다.
제어 추상화 이해를 위해 자동차를 빗대어 제어 추상화를 설명했지만, 결국 핵심은 보다 프로그래밍을 빠르게 설계하고 구현하기 위해 추상화를 사용하는 것으로 보면 된다.
만일 여러 국가의 달력을 사용하게 해주는 자바 라이브러리를 다운 받아 사용한다 하자.
달력 라이브러리의 공식 문서를 보더니 달력 사용법은 다음과 같이 되어 있다.
import CountryCalendar; // 라이브러리 클래스를 사용하기 위해 불러옴
CountryCalendar cal = CountryCalendar.getInstance("countryName");
그저 getInstance() 메서드에 나라 이름을 아규먼트로 넣어 호출만 해주면 자동으로 달력 값을 얻을 수 있다.
getInstance() 메서드가 내부에서 어떤 로직으로 각 나라의 달력을 구분해 계산 하는지는 이해하지도 못하며 관심도 없다. 우리가 원하는건 결과값이지 알고리즘 공부가 아니기 때문이다.
실제로 라이브러리 소스 코드를 까보니 클래스 내부에는 다음과 같이 선언되어 있었다.
생전 처음보는 메서드를 호출하여 리턴하거나, 생전 처음 보는 BuddhistCalendar 클래스나 JapaneseImperialCalendar 클래스를 생성하여 할당하고 있다.
public static CountryCalendar getInstance(Locale aLocale) {
return createCalendar(TimeZone.getDefault(), aLocale);
}
private static CountryCalendar createCalander(TimeZone zone, Locale aLocale) {
// ...
if(caltype != null) {
switch (caltype) {
case "buddhist" :
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese" :
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory" :
cal = new GregoryianCalendar(zone, aLocale);
break;
}
}
}
물론 위의 정보들은 라이브러리가 동작하는데 있어 필수적으로 필요한 클래스들 이겠지만, 그냥 라이브러리의 기능을 사용하는 사용자 입장에서는 위와 같은 복잡한 로직이나 각각의 클래스 정보를 일일히 알 필요 없이, 생각 없이 메서드를 사용하면 되는 것이다.
그리고 생각 없이 사용한다라는 말은 구체적이지 않고 추상적으로 메서드 동작을 가늠해 결과값만 받고 끝낸다는 뜻이 되며 이는 곧 추상화의 기능을 이용한다라고 볼 수 있는 것이다.
좀더 단적으로 예를 들자면, 우리가 사용하는 for, while 문도 사실 반복하는 개념을 제어 추상화 한 것이라고 볼 수 있다.
코드를 반복하는데 있어 내부 CPU 동작이 어떻게 이루어지는가를 알 필요없이, 반복 기능이 추상화된 for, while 문법을 정의함으로써, 우리는 복잡한 컴퓨터 지식없이 간단하게 루프 기능을 이용하여 프로그램을 구상할 수 있었 던 것이다.
추상화 라는 개념이 바로 머릿속에 들어오지 않았던 이유는, 우리는 프로그래밍을 처음 배울때 '설계'가 아닌 '사용' 하는 법을 먼저 배웠기 때문이다.
즉, 우리는 처음부터 추상화의 효과를 누리며 코딩해왔던 것이나, 너무나 당연한 것을 거꾸로 거슬러 올라가서 이해하려니 난해했던 것이다.
객체 지향은 설계 이론이지 사용 이론이 아니다. 이점은 유의하며 추상화에 대해 접근하면 이해하기 편하다.
이처럼 제어 추상화를 통해 프로그램을 잘 설계했다면 결과적으로 생산성 증가, 가독성 증가, 에러 감소, 유지 보수시 시간 단축 등의 효과를 얻게 된다.
즉, 추상화는 프로그래밍 노력과 복잡성을 줄여준다고 보면 된다.
데이터 추상화
데이터 추상화란, 대상을 간단한 개념으로 일반화 하는 과정을 말한다.
쉽게 예를 들자면 삼각형, 사각형, 원이라는 객체가 있을 때, 이 객체들을 하나로 묶을 때 객체들의 공통 특징인 도형으로 묶어 이름을 붙이는 것을 데이터 추상화라고 보면 된다.
이처럼 추상화를 하면 할 수록 객체의 디테일함이 사라지고 공통된 특징만 남게 된다.
예를들어 아이폰 객체를 추상화를 통해 객체 정보 분리를 할때, 아이폰 → 휴대폰 → 통신기기 → 전자제품 으로 추상화(abstract)가 이어질 수 있다.
abstract class 전자제품 {
}
abstract class 통신기기 {
}
abstract class 휴대폰 {
}
class 아이폰 {
}
그리고 이렇게 추상화한 상위 요소부터 각 요소에 맞는 기능들을 정의 한다.
전자제품은 전원 기능을, 통신기기는 통화기능, 휴대폰은 카메라, 게임 기능, 아이폰은 애플 앱을 이용할수있는 연동 기능을 요소마다 속성을 배치한다.
그리고 마지막으로 상위 요소가 가진 내용들을 가질 수 있도록 상속 관계(extends)를 설정하여 이어준다.
이렇게 공통된 기능들은 상위 요소에서 미리 구현하기 때문에 아이폰을 만들때 아이폰만의 고유 기능 위주로 개발할 수 있게 된다.
abstract class 전자제품 {
전원기능();
}
abstract class 통신기기 extends 전자제품 {
통화기능();
}
abstract class 휴대폰 extends 통신기기 {
카메라기능();
게임기능();
}
class 아이폰 extends 휴대폰 {
전원기능() { ... }
통화기능() { ... }
카메라기능() { ... }
게임기능() { ... }
애플 제품 연동기능() { ... }
}
// → 최종적으로 아이폰 class는 전원, 통화, 카메라, 게임, 애플 연동 5가지 기능을 정의하여 설계된다
물론 휴대폰을 개발하는데 전자 제품 까지 목록을 추상화 할 필요가 있겠느냐 싶지만, 이런 추상화 구조 분리는 개발을 보다 빠르게 해준다.
아이폰 제품 하나만 만들때는 비효율적으로 보일수 있지만 제품 종류가 늘어날 수록 장점으로 작용 된다.
공통 기능을 미리 개발해 두면, 기능 상속을 통해 빠르게 구조를 확장 할 수 있기 때문이다.
상속 (Inheritance)
자바에서의 상속이란 객체들 간의 관계를 구축하는 방법을 말한다.
위의 아이폰 추상화 예시에서, 추상화를 통해 분리한 추상 개념 정보들을 서로 이었는데 이것이 바로 상속이다.
즉, 상위 클래스의 속성(변수)과 기능(메소드)을 재사용하여(상속) 하위 클래스가 전부 물려받는 것을 말한다.
상속을 사용하기 위해서는 extends 키워드를 상속 받을 클래스에 명시하여 사용한다.
그리고 상속되는 클래스는 super/parent 클래스라 부르고 새롭게 생성된 클래스를 sub/child 클래스라 불리운다.
// super 클래스
class Parent {
String name;
String age;
public void say() {
System.out.println(name + age);
}
}
// sub 클래스 (상속 받음)
class Child extend Parent{
String hair;
public void myHair() {
System.out.println(hair);
}
}
// 상속받은 sub 클래스는 super 클래스의 속성들을 이용이 가능하다.
Child c = new Child();
c.name = "풍성한";
c.age = 17;
c.say();
c.hair = "M자형.."
c.myHair();
중복 속성 제거
상속 기능을 이용하게 되면, 상위 클래스의 특징을 하위클래스에서 상속받아 코드의 중복 제거, 코드 재사용성 증대 효과도 누릴 수 있다.
즉, 자주 사용될 것이 예상되는 기능을 모아놓은 클래스를 한번 만들어 놓으면 편하게 재사용 함으로써 유지보수 효율화를 추구할 수 있는 것이다.
다음은 Dog, Cat, Lion 클래스의 공통된 속성들 teethCount, legCount, tailCount를 Animal 클래스로 하나로 묶어 상속(extends)를 통해 코드량을 줄인 것을 볼 수 있다.
class Dog {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void bark();
}
class Cat {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void meow();
}
class Lion {
int teethCount; // 중복된 속성들
int legCount; // 중복된 속성들
int tailCount; // 중복된 속성들
void roar();
}
class Animal {
int teethCount;
int legCount;
int tailCount;
}
class Dog extends Animal { // 상속을 통해 중복 코드를 제거
void bark();
}
class Cat extends Animal { // 상속을 통해 중복 코드를 제거
void meow();
}
class Lion extends Animal { // 상속을 통해 중복 코드를 제거
void roar();
}
참고