객체 지향은 인간 지향이다
프로그래밍 언어의 발전사를 보면 개발자를 더욱 편하고 이롭게 하기 위한 과정임을 알 수 있다.
그러나 절차적/구조적 프로그래밍까지의 과정은 인간이 기계를 이해하려는 노력에서 크게 벗어나지 못했다.
특히 포인터의 개념은 기계 수준으로 눈높이를 낮추지 않으면 이해하기 매우 힘든 부분이다.
그런데 “왜 우리가 기계 종속적인 개발을 해야 하는가?”라고 하는 의문이 생겼고 “우리가 눈으로 보고, 느끼고, 생활하는 현실 세계처럼 프로그래밍할 수는 없을까?”라는 고민 속에서 객체 지향의 개념이 탄생했다.
객체 지향이 현실 세계를 반영한다는 말은 이미 오래 전에 객체 지향 언어의 틀을 만든 누군가가 한 말이다.
그 증거가 바로 ‘객체’이다.
눈으로 보여지는 것, 손으로 만져지는 것, 머릿속으로 상상되는 모든 것은 사물이다. 사물을 조금 더 멋진 용어로 객체, 영어로는 Object라고 한다.
그리하여 “우리가 주변에서 사물을 인지하는 방식대로 프로그래밍할 수 있지 않겠는가”하는 것이 바로 객체 지향의 출발이다.
객체 지향의 4대 특성 - 캡! 상추다
캡 - 캡슐화(Encapsulation): 정보 은닉(information hiding)
상 - 상속(inheritance): 재사용
추 - 추상화(Abstraction): 모델링
다 - 다형성(Polymorphism): 사용 편의
inheritance에 취소선이 그어져 있는 이유는 오해의 소지가 다분한 단어이기 때문이다.
해당 내용은 아래에서 다시 정리하겠다.
클래스 vs. 객체 = 붕어빵틀 vs. 붕어빵 ???
대부분의 사람들이 클래스와 객체의 관계를 붕어빵틀과 붕어빵의 비유로 설명한다.
붕어빵틀 붕어빵 = new 붕어빵틀();
이 코드를 보고도 잘 이해가 가지 않는다면 붕어빵틀과 붕어빵을 클래스와 객체 관계라는 논리로 조금 더 풀어보자.
붕어빵 틀을 생산하는 금형 기계가 있다고 했을 때 붕어빵틀이 붕어빵을 찍어내는 클래스라고 한다면 같은 논리로 금형 기계는 붕어빵틀을 찍어내는 클래스가 된다.
이를 코드로 나타내면 다음과 같다.
금형기계 붕어빵틀 = new 금형기계();
위 코드를 인간적인 말로 번역해보면 다음과 같다.
“새로운 금형기계를 하나 만들었더니 붕어빵 틀이 되었다”
절대 금형기계와 붕어빵틀이 클래스와 객체 관계가 아니듯 붕어빵틀과 붕어빵도 클래스와 객체 관계가 아니다.
붕어빵틀은 붕어빵을 만드는 팩터리였던 것이다.
클래스는 분류에 대한 개념이지 실체가 아니다. 객체는 실체다.
붕어빵틀과 붕어빵이라는 잘못된 메타포 대신 사람과 김연아라는 올바른 메타포를 사용하자.
추상화: 모델링
피카소는 극사실주의와 같이 눈에 보이는 그대로의 사물(객체)을 그린 것이 아니라 마음 속에 느껴지는 그 사람의 특징을 그렸다.
이집트 화가들은 그 사람의 사실적인 모습이 아니라 각 부분의 특징을 가장 잘 표현할 수 있도록 신체를 분해/결합해서 벽화를 그렸다고 한다.
추상(抽象) [명사][심리] 여러 가지 사물이나 개념에서 공통되는 특성이나 속성 따위를 추출하여 파악하는 작용.
추상의 사전적 의미를 보면 알 수 있듯이 객체 지향의 추상화와 그림으로서의 추상화, 사전적 의미로서의 추상화가 같은 의미인 것이다.
객체 지향의 4대 특성은 클래스를 통해 구현된다. 또는 객체라고 할 수도 있다.
세상에 존재하는 유일무이한 객체를 특성(속성 + 기능)에 따라 분류해 보니 객체를 통칭할 수 있는 집합적 개념, 즉 클래스(분류)가 나오게 된다.
사람이라는 클래스를 설계한다고 해보자. 사람 클래스를 만들기 위해 주변에서 보이는 실체들, 즉 사람 객체들을 관찰해서 공통된 특성을 찾아야 한다.
속성 | - 시력, 몸무게, 혈액형, 키, 나이, 직업, 취미 등의 명사로 표현되는 특성 - 값을 가질 수 있다. |
기능/행위 | - 먹다, 자다, 일하다, 침 뱉다, 운전하다, 울다 등의 동사로 표현되는 특성 |
이런 특성을 가지고 클래스를 설계할 때 과연 모든 특성이 필요할까?
여기서 또 하나의 개념이 나오는데 바로 ‘애플리케이션 경계’다.
애플리케이션 경계를 알기 위해서는 단순한 질문 하나만 던져 보면 된다.
“내가 만들고자 하는 애플리케이션은 어디에서 사용될 것인가?”
모델은 실제 사물을 정확히 복제하는 게 아니라 목적에 맞게 관심 있는 특성만을 추출해서 표현하는 것이다.
바로 모델은 추상화를 통해 실제 사물을 단순하게 묘사하는 것이다.
상속: 재사용 + 확장
객체 지향의 상속은 영어 단어를 그대로 옮기면서 생긴 오해라고 할 수 있다.
객체 지향의 상속은 재사용과 확장으로 이해하는 것이 맞다.
객체 지향에서의 상속은 상위 클래스의 특성을 하위 클래스에서 상속하고 거기에 더해 필요한 특성을 추가, 즉 확장해서 사용할 수 있다는 의미다.
아래의 두 가지 코드는 상속의 강력함을 보여준다.
public class 동물 {
String myClass;
동물() {
myClass = "동물";
}
void showMe() {
System.out.println(myClass);
}
}
public class 포유류 extends 동물 {
포유류() {
myClass = "포유류";
}
public class 고래 extends 포유류 {
고래() {
myClass = "고래";
}
}
public class 박쥐 extends 포유류 {
박쥐() {
myClass = "박쥐";
}
}
public class Driver01 {
public static void main(String[] args) {
동물 animal = new 동물();
고래 whale = new 고래();
박쥐 bat = new 박쥐();
animal.showMe();
whale.showMe();
bat.showMe();
}
}
상위 클래스에서만 showMe() 메서드를 구현했지만 모든 하위 클래스의 객체에서 showMe() 메서드를 사용할 수 있다.
public class 동물 {
String myClass;
동물() {
myClass = "동물";
}
void showMe() {
System.out.println(myClass);
}
}
public class 포유류 extends 동물 {
포유류() {
myClass = "포유류";
}
public class 고래 extends 포유류 {
고래() {
myClass = "고래";
}
}
public class 박쥐 extends 포유류 {
박쥐() {
myClass = "박쥐";
}
}
public class Driver01 {
public static void main(String[] args) {
동물[] animals = new 동물[3];
animals[0] = new 동물();
animals[1] = new 고래();
animals[2] = new 박쥐();
for(int index = 0; index < animals.length; index++) {
animals[index].showMe();
}
}
}
또한 이렇게 상위 클래스 타입의 참조변수로 하위 클래스 타입을 참조하여 인스턴스의 생성과 관리가 가능하다.
여기서 상속에 대한 또 하나의 오해를 짚고 넘어가자. 상속은 is-a 관계를 만족해야 한다고 들어왔을 것이다.
하지만 is-a 관계는 객체와 클래스의 관계로 오해될 소지가 많기 때문에 is a 관계보다 is a kind of 관계가 명확하다고 할 수 있다.
- 김연아 is a 사람 → 김연아는 한 명의 사람이다.
- 뽀로로 is a 펭귄 → 뽀로로는 한 마리 펭귄이다.
그렇다면 왜 자바는 다중 상속을 지원하지 않는가?
예를 들어, 사람 클래스와 물고기 클래스를 동시에 상속 받는 인어 클래스가 있다고 가정해보자.
인어에게 “수영해!”라고 한다면 사람처럼 팔과 다리를 저어 수영을 해야 할까? 아니면 물고기처럼 헤엄쳐야 할까?
public class 생물 {
...
void swim() {
...
}
}
public class 사람 extends 생물 {
...
void swim() {
System.out.println("수영한다");
}
}
public class 물고기 extends 생물 {
...
void swim() {
System.out.println("헤엄친다");
}
}
public class 인어 extends 사람, 물고기 {
...
}
public class Driver01 {
public static void main(String[] args) {
인어 mermaid = new 인어();
mermaid.swim();
}
}
이처럼 두 개의 상위 클래스가 같은 이름의 메서드를 각각 다르게 오버라이딩 했을 때 그 하위 클래스에서 메서드를 사용한다면 어떤 메서드가 호출되어야 할까?
이와 같은 문제를 다중 상속의 다이아몬드 문제라고 한다.
앞서 상속은 is a kind of 관계라고 설명했다.
그럼 다중 상속을 포기하고 대신 인터페이스를 도입한 자바에서 인터페이스는 어떤 관계를 나타내는 것일까?
인터페이스는 be able to, 즉 “무엇을 할 수 있는”이라는 표현 형태로 만드는 것이 좋다.
다형성: 사용편의성
객체 지향에서 다형성이라고 하면 오버라이딩(overriding)과 오버로딩(overloading)이라고 할 수 있다.
오버라이딩 - 재정의: 상위 클래스의 메서드와 같은 메서드 이름, 같은 인자 리스트
오버로딩 - 중복정의: 같은 메서드 이름, 다른 인자 리스트
public class Animal {
public String name;
public void showName() {
System.out.printf("안녕 나는 %s야. 반가워\\n", name);
}
}
public class Penguin extends Animal {
public String habitat;
public void showHabitat() {
System.out.printf("%s는 %s에 살아\\n", name, habitat);
}
//오버라이딩
public void showName() {
System.out.println("어머 내 이름은 알아서 뭐하게요?");
}
//오버로딩
public void showName(String yourName) {
System.out.printf("%s 안녕, 나는 %s라고 해\\n", yourName, name);
}
}
Animal 클래스를 상속 받는 Penguin 클래스에서 showName() 메서드를 오버라이딩, 오버로딩했다.
public class Driver {
public static void main(String[] args) {
Penguin pororo = new Penguin();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showName("초보람보");
pororo.showHabitat();
Animal pingu = new Penguin();
pingu.name = "핑구";
pingu.showName();
}
}
//결과
어머 내 이름은 알아서 뭐하게요?
초보람보 안녕, 나는 뽀로로라고 해
뽀로로는 남극에 살아
어머 내 이름은 알아서 뭐하게요?
Driver.java를 실행한 결과를 보면 다음 두 가지를 알 수 있다.
- 오버라이딩 된 메서드를 호출하면 하위 클래스의 것이 호출된다.
- 상위 클래스 타입의 객체 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩한 메서드가 호출된다.
캡슐화: 정보 은닉
자바에서 정보 은닉이라고 하면 접근 제어자가 생각날 것이다.
접근 제어자가 객체 멤버와 쓰일 때와 정적 멤버와 함께 쓰일 때를 비교해서 살펴보자.
보통 객체 멤버에 접근할 때 참조변수명.멤버로 흔히 사용하지만 정적 멤버인 경우 일관된 형식으로 접근하기 위해 클래스명.정적멤버 형식으로 접근하는 걸 권장한다.
참조 변수의 복사
기본 자료형 변수를 복사하는 경우 Call By Value(값에 의한 호출)에 의해 그 값이 복사되며 두 개의 변수는 서로에게 영향을 주지 않는다.
int a = 10;
int b = a;
b = 20;
System.out.println(a);
System.out.println(b);
//결과
10
20
변수 a에 10을 대입한 후, 변수 b에 변수 a가 가진 값을 복사하고, 다시 변수 b에 20을 할당한 것을 볼 수 있다.
이 때 a가 가진 값이 단순히 b에 복사된 것이고 a와 b 변수는 아무런 관계도 없는 것을 알 수 있다.
그렇다면 기본 자료형이 아닌 객체를 저장하고 있는 객체 참조 변수를 복사하는 경우는 어떨까?
많은 책에서는 이 경우 Call By Reference(참조에 의한 호출) 또는 Call By Address(주소에 의한 호출)라고 설명하면서 Call By Value와 다르게 설명하고 있다.
public class CallByReference {
public static void main(String[] args) {
Animal ref_a = new Animal();
Animal ref_b = ref_a;
ref_a.age = 10;
ref_b.age = 20;
System.out.println(ref_a.age);
System.out.println(ref_b.age);
}
}
class Animal {
public int age;
}
//결과
20
20
큰 차이가 느껴질 수도 있겠지만 사실 Call By Value와 Call By Reference는 본질적으로 차이가 없다.
다만 차이라면 기본 자료형 변수는 저장하고 있는 값을 그 값 자체로 해석하는 반면, 객체 참조 변수는 저장하고 있는 값을 주소로 해석한다는 차이가 있을 뿐이다.
Call By Value에 의해 변수를 복사하든 Call By Reference에 의해 참조 변수를 복사하든 결국은 변수가 가진 값이 그대로 복사되기 때문이다.
그 값을 값 자체로 해석하느냐 아니면 주소값으로 해석하느냐의 차이일 뿐이다.
'Reading > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
7. 스프링 삼각형과 설정 정보_IoC/DI (0) | 2023.04.01 |
---|---|
5. 객체 지향 설계 5원칙 - SOLID (0) | 2023.03.29 |
4. 자바가 확장한 객체 지향 (0) | 2023.03.23 |
2. 자바와 절차적/구조적 프로그래밍 (0) | 2023.03.21 |
1. 사람을 사랑한 기술 (0) | 2023.03.21 |