목표
객체지향 프로그래밍(OOP)와 클래스와 객체 그리고 인스턴스에 대해 알아보도록 하겠습니다.
목차 클릭하면 해당 목차로 이동합니다.
개요
자바는 흔히 객체지향 프로그래밍 언어(Obejct-Oriented Programming, OOP)라고 합니다. 객체지향 프로그래밍이란, 실생활에 존재하는 것들 사이의 관계를 매핑하여 컴퓨터에 구현하고자 하는 뜻에서 개발된 것입니다. 컴퓨터에 구현하기 위해 클래스를 이용해 연관 있는
기능(메소드)와 데이터(변수)를 하나의 객체로 묶어서 생성해서 사용하는 것입니다. 이에 대한 자세한 내용을 다뤄보도록 하겠습니다.
1. 클래스와 객체 그리고 인스턴스
먼저, 클래스, 객체, 인스턴스를 이해하기 위해 조금은 이론적으로 다가가 보도록 하겠습니다.
객체지향 프로그래밍(이하 OOP)은 객체(Object)를 이용하는 프로그래밍 설계 기법입니다. 객체라는 것은 무형, 유형의 모든 것(Everything)을 뜻합니다. 이를테면, 실생활에서 볼 수 있는 자전거, 자동차, 핸드폰과 같이 실제로 볼 수 있는(유형) 것부터 동물, 가구와 같은 추상적인 것(무형)도 모두 다 객체입니다. 쉽게 말하면, 동물이라는 카테고리와, 실제로 존재하는 강아지, 고양이와 같은 것들 모두 객체입니다.
이렇게 실제 존재하는 객체를 컴퓨터 프로그램에 생성하기 위해서, 클래스(Class)라는 것에 객체가 어떤 정보를 갖고 어떻게 행동하는지 정의해야 합니다. 한마디로 클래스는 설계도입니다. 마치, 붕어빵 틀에 반죽을 넣고 붕어빵을 찍어내는 것과 같습니다. 붕어빵은 생선 모양이고, 반죽과 팥을 넣고 구워야 한다는 정보를 클래스에 정의해놓고, 붕어빵을 먹고 싶을 때마다 사용하는 것입니다. 이와 같이 생선 모양이고, 반죽과 팥을 넣고 굽는 것을 클래스 내부에 *메소드로 구현하는 것입니다. 반죽과 팥이 얼마나 들어가고, 얼마나 굽는지와 같은 데이터는 변수에 저장되어 있을 것입니다.
*메소드 : 클래스, 구조체 등에 연관되어 있는 함수.
일반적으로 객체는 클래스의 인스턴스(Instance)라고 불립니다. 인스턴스는 객체를 실체화한 것을 의미합니다. 클래스에 정의된 동물이라는 객체에서 사자를 구현하면, 사자는 인스턴스가 되는 것입니다. 프로그램에서 인스턴스는 클래스에서 정의된 것을 메모리에 실제로 구현해낸 것을 의미합니다. 사실, 인스턴스 또한 객체가 될 수 있습니다. 객체를 실체화한 것이 인스턴스인데 어떻게 인스턴스가 객체일까요? 객체는 무엇이든 될 수 있기 때문입니다. 말 그대로 모든 것(Everything)입니다.
그림으로 생각하면 위와 같은 벤다이어그램을 갖고 있습니다.
객체지향 프로그래밍(OOP) : 객체를 이용하는 프로그래밍 설계 기법.
객체 : 모든 것. 일반적으로 동물, 직업 등과 같이 실체화가 가능한 것.
인스턴스 : 클래스에 정의된 객체를 실체화한 것. (사자, 경찰 등)
클래스 : 객체의 역할을 정의하기 위한 틀. (설계도)
강아지, 고양이, 집사 등과 같이 실생활에 무수히 많은 객체들이 존재하고 있습니다. 이 객체들 사이에는 필연적으로 관계라는 것이 생기기 마련입니다. 고양이와 집사는 "기르다"와 같은 관계가 존재하듯 말입니다. 고양이가 집사에게 밥을 달라고 말하듯이, 집사와 고양이는 서로 메세지를 전달하며 무언가 요구합니다. 이러한 기능들을 클래스 내부에 메소드로 구현하는 것입니다.
2. OOP특징(1) - 상속(Inheritance)
위에서 실생활에 많은 객체가 존재한다고 했고, 그 객체들 사이에 관계가 존재한다고 했습니다. 실생활에 존재하는 다양한 객체들을 프로그램에서 구현하기 위해 클래스를 사용합니다. 당연히, 무수히 많은 클래스가 생성될 수 있을 텐데요. 당연히, 이 클래스 사이에서도 관계가 발생할 수 있습니다. 예를 들면, 학생과 직장인 클래스를 구현한다고 가정하겠습니다. 학생은 학교에 등교해서 공부를 해야 하고, 직장인은 회사로 출근해서 업무를 봐야 합니다. 하지만, 둘 다 사람이기 때문에 밥을 먹고, 잠을 자는 등의 같은 기능도 해야 합니다. 이럴 때, 학생과 직장인 클래스에 각각 밥을 먹고, 잠을 자는 메소드를 구현할 필요가 없습니다. 공통적으로 사용하는 메소드는 따로 구현하고 필요할 때 가져다가 사용하는 것입니다.
부모가 자식에게 DNA를 물려주듯, 공통적인 속성은 사람 클래스에 구현해놓고 해당 기능을 상속받아 직장인과 학생 클래스를 구현하는 것입니다. 직장인과 학생 클래스에서는 각각 필요한 구현을 따로 구현하고 있습니다.
사람 클래스와 같이 특성을 상속해주는 클래스를 부모 클래스(Parent class) 혹은 슈퍼 클래스(Super class)라고 하고, 직장인, 학생 클래스와 같이 특성을 상속받는 클래스를 자식 클래스(Child class) 혹은 서브 클래스(subclass)라고 합니다.
이렇게 상속을 통해 클래스를 계층적으로 작성할 수 있습니다. 상속을 활용하면 중복된 코드를 여러 번 작성할 필요가 없기 때문에, 코드의 중복성을 최소화할 수 있습니다.
꼭 부모 클래스가 물려주는 기능을 자식 클래스에서 똑같이 사용할 필요는 없습니다. 자식 클래스에 맞춰서 일부 기능을 수정해서 사용할 수 있습니다. 이 포인트에서 OOP의 가장 중요한 개념인 다형성(Polymorphism)이 나오게 됩니다.
3. OOP특징(2) - 다형성(Polymorphism)
다형성은 감히, OOP에서 가장 중요한 개념이라고 말할 수 있습니다. 일반적으로 다형성은 같은 모양의 코드가 다른 기능을 하는 것을 의미합니다. 우리는 물을 식용으로 마실 수도 있지만, 불이 나면 불을 끄기 위해 뿌리는 소화제가 되고, 음식을 하기 위한 재료가 될 수 있는 것이 일상에 존재하는 다형성의 한 예시라고 할 수 있습니다.
OOP에서 보면 객체가 상속을 통해 기능을 확장하거나 변경해 다른 형태로 재구성되는 것입니다. 다형성을 활용하는 방법은 2가지가 있습니다.
1. 오버로딩(Overloading)
오버로딩은 하나의 클래스에 같은 이름의 메소드를 여러 개를 구현하는 것입니다. 완전히 똑같으면 컴파일러가 메소드를 구분할 수 없습니다. 그래서, 이름은 같지만 매개 변수의 개수나 매개 변수의 타입이 달라야 합니다. 컴파일러는 매개 변수로 메소드를 구분하기 때문에, 매개 변수가 같고 메소드의 반환형(리턴 타입)이 다르면 동작하지 않습니다.
오버로딩은 자바 코드를 작성할 때 IDE를 통해 쉽게 확인할 수 있는데요.
String클래스의 indexOf 메소드는 매개 변수의 종류에 따라 여러 개가 존재합니다. 메소드에 넘어온 매개 변수의 종류에 맞춰 다른 기능을 할 수 있습니다. 오버로딩을 통해 비슷한 기능을 하는 메소드를 같은 이름에 정의할 수 있습니다.
만약, 오버로딩이 없었다면, 다음과 같이 매개 변수의 종류에 따라 메소드를 정의해야 할 것입니다.
/*오버로딩이 없으면 다른 이름으로 각각 구현해야 한다.*/
indexOfInt(int ch);
indexOfString(String str);
indexOfIntInt(int ch, int fromIndex);
indexOfIntString(String str, int fromIndex);
당장에야 큰 필요성을 못 느낄 수 있지만, 프로그램의 크기가 커지고 메소드의 개수가 많아진다면 개발하는 입장에서 효율성이 매우 떨어질 것입니다.
2. 오버라이딩(Overriding)
오버라이딩은 부모 클래스에서 상속받은 메소드를 서브 클래스에서 새롭게 재정의하는 것을 의미합니다. 부모 클래스의 메소드가 가진 이름, 매개 변수와 똑같은 메소드를 자식 클래스에서 새롭게 정의하는 것입니다. 같은 메소드이지만, 부모 클래스와 자식 클래스에서의 역할이 다릅니다.
위 예시에서 봤던 사람, 직장인, 학생 클래스를 다시 보겠습니다. 직장인과 학생이라는 자식 클래스에서는 각각 회사로 출근하고, 학교로 등교하는 메소드를 갖고 있습니다. 사실, 목적지만 다를 뿐, 어디론가 이동한다는 것은 같은 맥락입니다. 만약, 사람이라는 부모 클래스 밑에 어부, 승무원 등 수백 가지의 자식 클래스가 생성되면 어떡할까요? 각각의 목적지에 따라 수백 가지의 메소드를 새로 작성해야 합니다. 같은 내용의 코드를 반복해서 작성해야 하는 상황이 발생하는 것입니다. 이를 오버라이딩을 통해서 해결할 수 있습니다.
위와 같이 부모 클래스의 goTo()라는 메소드 하나로 직장인은 직장으로 출근하고, 학생은 학교로 등교할 수 있게 설계할 수 있습니다. 공통된 기능은 유지하되, 목적지만 변경해서 중복된 코드 작성을 예방할 수 있습니다.
이렇게 OOP는 불필요한 코드의 작성을 최소화하는 것에 초점을 맞추고 있습니다. 상속과 같이 공통된 특성을 묶어 계층화하기 위해서는, 어떤 객체를 생성해야 할지 정확히 설계할 필요가 있습니다. 이때 필요한 개념이 바로 추상화입니다.
4. OOP특징(3) - 추상화(Abstraction)
추상화는 사물들의 공통적인 특징을 파악해서 하나의 개념으로 묶는 것입니다. 다시 말하면, 객체의 공통적인 특징을 파악해서 속성과 기능을 추출하는 것입니다. 객체의 특징을 잘 파악하고 있어야 올바른 설계가 가능합니다.
이때, 주의해야 할 점은 각각의 객체의 구체적인 개념에 의존하지 말고, 추상적인 개념에 의존해야 설계를 유연하게 할 수 있습니다. 예를 들어, K3, 아반떼와 같은 자동차를 설계한다고 가정해보겠습니다. 구체적인 개념에 의존한다면 각각의 K3, 아반떼와 같은 차를 설계할 것입니다.
class K3 {
~~~
}
class Avante {
~~~
}
운전자가 K5라는 차로 바꾸면 어떻게 될까요? K5에 맞게 새롭게 설계해야 하고, 운전자는 사용법을 익혀야 할 것입니다. 새로운 자동차가 출시되면 계속해서 추가되어야 합니다.
이때, 추상적인 개념에 의존해서 설계했다면, 자동차라는 역할에 맞춰 개발하게 되고, K5로 바꿔도 기능만 추가하면 될 뿐, 운전자에게는 영향이 없습니다.
interface Car {
run();
stop();
}
class K3 implements Car {
~~~
}
class Avante implements Car {
~~~
}
한마디로, 역할과 구현을 잘 구분해야 한다는 것입니다. 설계할 때, "자동차"라는 역할에 맞게 인터페이스로 구현하고, K3, 아반떼 등 자동차(구현)는 인터페이스에 맞게 개발해야 합니다. 운전자는 Car라는 인터페이스에 의존하고 있기 때문에, K5로 차를 바꾸어도 새로운 기능만 익히면 될 뿐, 기본적인 기능은 그대로 사용할 수 있습니다. 이렇게 설계하게 되면, 다형성에 맞게 메소드를 오버라이드 하여 자동차에 맞게 기능을 변경할 수 있습니다.
개인적인 생각으로, 이러한 성격들 때문에 OOP에서 가장 중요한 것은 설계라고 생각합니다. 역할과 구현을 잘 분리하여 효율적으로 설계해야 유지 보수하기 쉽고, 코드의 가독성이 높아져 협업할 때 효율을 극대화시킬 수 있다고 생각합니다.
이렇게 설계된 객체를 관리하는 OOP의 마지막 특성은 바로 캡슐화입니다.
5. OOP특징(4) - 캡슐화(Encapsulation)
캡슐화는 데이터와 코드의 형태를 외부로부터 알 수 없게 하고, 데이터의 구조와 역할, 기능을 하나의 캡슐 형태로 묶어주는 방법입니다. JAVA의 경우 접근 제어자(private)를 통해 데이터를 보호하고 getter를 통해 데이터를 얻고 setter를 통해 간접적으로 데이터를 변경할 수 있습니다. 불필요한 정보를 감출 수 있기 때문에 정보를 은닉할 수 있다는 특징이 있습니다.
캡슐화를 고려하여 설계한다면 높은 응집도와 낮은 결합도를 유지할 수 있습니다. 높은 응집도와 낮은 결합도는 추후 유지보수를 수월하게 진행할 수 있습니다.
- 응집도(Cohesion) : 클래스나 모듈 안의 요소들이 얼마나 밀접하게 관련되어 있는지.
- 결합도(Coupling) : 어떤 기능을 실행할 때, 다른 클래스나 모듈에 얼마나 의존적인지.
캡슐화를 통해 외부의 접근을 막을 수 있기 때문에, 낮은 결합도를 유지할 수 있습니다. 만약, 캡슐화를 고려하지 않는다면, 한 클래스의 메소드를 변경하면 이와 관련되어 있는 모든 메소드를 변경해야 하는 상황이 발생할 수 있습니다.
정리
클래스와 객체, 인스턴스에 대해서 알아보고, OOP의 특징에 대해서 알아보았습니다.
결국, 객체지향 프로그래밍(OOP)는 프로그램 개발 시 필요한 객체를 추상화하여 작업하는 방식으로, 객체들이 유기적으로 상호작용하는 프로그래밍 기법입니다. 유기적으로 상호작용한다는 것은, 올바른 설계를 바탕으로 마치 로봇이 조립된 것처럼 맞물려 작동되는 것이라고 생각합니다.
잘 설계된 OOP의 장점은 다음과 같습니다.
- 코드의 재사용성 증가
- 중복된 코드 작성 최소화
- 생산성 향상
- 자연스러운 모델링
- 효율적인 유지보수
실생활에 존재하는 객체를 그대로 대입할 수 있기 때문에, 자연스러운 모델링이 가능합니다. OOP의 특징 중 하나인, 상속을 통해 코드의 재사용성을 높일 수 있습니다. 보다 중요한 점은, 코드의 가독성이 좋아지는 것도 큰 장점이라고 할 수 있습니다.
OOP의 가장 중요한 점은 "설계"라고 생각합니다. 역할과 구현을 구분하여 인터페이스, 추상 클래스 등을 활용해 올바른 설계를 한다면, 불필요한 코드는 줄어들고 가독성이 증가할 수 있습니다. 이는 곧 원활한 유지보수로 이어집니다.
물론, OOP가 장점만 있는 것은 아닙니다. 앞서 말했듯, 설계가 중요하기 때문에 설계하는데 많은 시간이 소모됩니다. 잘못된 설계는 개발의 실패로 이어질 가능성이 큽니다. 또한, 자바와 같은 OOP 언어들은 대부분 실행 속도가 느리고, 메모리 관리가 효율적이지 못합니다.
이상으로 객체지향 프로그래밍에 대한 포스팅을 마무리하도록 하겠습니다.
'개발 > JAVA' 카테고리의 다른 글
[JAVA] 정규표현식으로 문제 해결하기 (0) | 2022.12.10 |
---|---|
[JAVA] 문자열(String)생성, 문자열 비교 Equals(), == 연산자의 차이점 (0) | 2022.01.09 |