절차적, 구조적 프로그래밍의 과정은 인간이 기계를 이해하려는 노력에서 크게 벗어나지 못했다. 특히 포인터의 개념은 기계 수준으로 눈높이를 낮추지 않으면 이해하기 매우 힘들다.
"현실 세계처럼 프로그래밍할 수는 없을까?"라는 고민 속에서 객체 지향의 개념이 탄생했다. 0과 1로 대변되는 기계(컴퓨터)에 맞춰 사고하던 방식을 버리고 현실 세계를 인지하는 방식으로 프로그래밍을 만들고자 한 것이다.
객체지향 프로그래밍은 현실 세계의 개체(Object)와 그 개체들 간의 상호 작용을 모델링하는 개념을 기반으로 하는 프로그래밍 방법이다.
클래스와 객체와의 관계를 생각해보자. 한글로 자바 코드로 작성하면 다음과 같다.
클래스 객체명 = new 클래스();
붕어빵틀 붕어빵 = new 붕어빵틀();
붕어빵틀은 붕어빵을 만드는 팩터리일 뿐이다. 클래스와 객체와의 관계라 보기 힘들다.
클래스는 분류에 대한 개념이지 실체가 아니다. 객체는 실체다. 예시로 생각해보자.
여기서 클래스와 객체를 구분하는 가장 간단한 방법은 나이를 물어보는 것이다.
따라서 사람, 펭귄은 클래스이고 김연아와 뽀로로는 객체인 것이다.
이집트 화가들은 그 사람의 사실적인 모습이 아니라 각 부분의 특징을 가장 잘 표현할 수 있도록 신체를 분해/결합해서 벽화를 그렸다고 한다. 실제 모습이 아닌 추상적인 모습을 그려 오히려 그 인물의 특징을 더욱 정확하게 묘사하려 했던 것이다.
객체 지향의 추상화는 곧 모델링이다. 구체적인 것을 분해해서 관찰자가 관심있는 특성만 가지고 재조합하는 것이다.
컨텍스트는 애플리케이션의 경계이다.
만약 병원 애플리케이션을 만들고 있다면 사람은 환자를 의미하는 구체적인 이름으로 설정할 수 있다. 은행 애플리케이션을 만들고 있다면 사람은 고객이라는 구체적인 이름으로 바꿀 수 있고 클래스의 설계 역시 달라져야 한다.
따라서 다음과 같이 특성을 설계할 수 있다.
애플리케이션 경계 | 내용 | 설명 |
---|---|---|
사람이란 | 사람은 환자이다. | 사람은 고객이다. |
클래스 모델링 - 필드 | 시력 몸무게 혈액형 키 나이 | 나이 직업 연봉 |
클래스 모델링 - 메서드 | 먹다() 자다() 운전하다() 진찰받다() 운동하다() | 일하다() 입금하다() 출금하다() 이체받다() 대출하다() |
추상화란 구체적인 것을 분해해서 관심 영역(목적에 맞게)에 있는 특성만 가지고 재조합하는 것이다. 즉 모델링의 과정이라 볼 수 있다.
자바는 class
키워드를 통해 지원하고 있다.
추상화 = 모델링 = 자바의 class 키워드
클래스 객체_참조_변수 = new 클래스();
새로운 객체를 하나 생성해 그 객체의 주소값을 객체 참조 변수에 할당한다. 예시로 쥐 캐릭터를 활용하여 클래스 설계를 해보자.
쥐 클래스의 논리적 설계 | 쥐 클래스의 물리적 설계 |
---|---|
쥐 | Mouse |
성명 나이 꼬리수 | +name:String +age:int +countOfTail:int |
울다() | +sing():void |
위 클래스 다이어그램을 자바 코드로 변환하면 다음과 같다.
public class Mouse {
public String name;
public int age;
public int countOfTail;
public void sing(){
System.out.println(name + "찍찍!");
}
}
위 Mouse 클래스를 활용하여 객체를 생성해 객체의 특성을 활용해 볼 수 있다.
public class MouseDriver{
public static void main(String[] args){
Mouse mickey = new Mouse();
mickey.name = "미키";
mickey.age = 85;
mickey.countOfTail = 1;
mickey.sing();
mickey = null;
Mouse jerry = new Mouse();
jerry.name = "제리";
jerry.age = 73;
jerry.countOfTail = 1;
jerry.sing();
}
}
위 코드 실행 시, 메모리의 상태는 다음과 같다.
위 예시에서 모든 Mouse 객체의 countOfTail
변수의 값은 1이다. 모든 Mouse 객체 같은 속성이 있음에도 Mouse 객체 수만큼 메모리를 낭비하고 있다. 이러한 경우 static
키워드를 사용하여, 단 하나의 저장 공간을 갖게 할 수 있다.
public class Mouse {
public String name;
public int age;
public static int countOfTail = 1;
public void sing(){
System.out.println(name + "찍찍!");
}
}
정적 메서드는 언제 사용하는 것이 좋을까? 정적 메서드는 객체들의 존재 여부에 관계 없이 쓸 수 있는 메서드이다.
main()
메서드의 경우main()
메서드의 논리를 함수로 분할해서 사용하는 경우
main()
메서드는 당연히 정적 메서드여야 한다. 메모리가 초기화된 순간 객체는 하나도 존재하지 않기 때문에 객체 멤버 메서드를 바로 실행할 수 없다. 따라서main()
메서드는 정적 메서드여야 한다.
이름 | 다른 이름 | 메모리 위치 |
---|---|---|
static 변수 | 클래스(멤버) 속성, 정적 변수, 정적 속성 | 스태틱 영역 |
인스턴스 변수 | 객체(멤버) 속성, 객체 변수 | 힙 영역 |
local 변수 | 지역 변수 | 스택 영역 |
객체 지향에서의 상속은 상위 클래스 특성을 하위 클래스에서 상속(특성 상속)하고 거기에 더해 필요한 특성을 확장을 할 수 있다는 의미이다.
상위 클래스 쪽으로 갈수록 추상화, 일반화되었으며, 하위 클래스 쪽으로 갈 수록 구체화, 특수화됐다고 말한다.
상속의 개념이 조직도나 계층도가 아닌 분류도하는 사실을 기억해두자.
상속 관계에서 반드시 만족해야 할 문장이 있다.
하위 클래스는 상위 클래스이다.
아주 자연스럽게 이해할 수 있다. 위 문장은 객체 지향의 5대 원칙인 LSP(리스코프 치환 원칙)를 나타내는 말이다.
자바는 extends
키워드를 통해 지원하고 있다.
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 Main {
public void static main(String[] args){
동물 animal = new 동물();
포유류 mammalia = new 포유류();
조류 bird = new 조류();
//...
animal.showMe();
mammalia.showMe();
bird.showMe();
//...
}
}
상위 클래스에 showMe()
메서드를 구햔했지만, 모든 하위 클래스의 객체에서 showMe()
메서드를 사용할 수 있다. 이처럼 하위 클래스에서 showMe()
메서드를 다시 작성하지 않아도 되는 장점을 알 수 있다.
public class Main {
public void static main(String[] args){
동물 animal = new 동물();
동물 mammalia = new 포유류();
동물 bird = new 조류();
//...
animal.showMe();
mammalia.showMe();
bird.showMe();
//...
}
}
하위 클래스는 상위 클래스다
라는 표현을 위를 통해 이해할 수 있다.
사람 클래스에 수영한다()
메서드와 물고기 클래스에 수영한다()
메서드가 존재한다면, 인어는 어떤 메서드를 수영하다()
를 출력할지 모른다. 이것이 다이아몬드 문제
이다.
자바에서는 인터페이스를 도입하여 다중 상속의 득은 취하고 실은 버렸다.
상위 클래스는 하위 클래스에 물려줄 수 있는 특성이 많을수록 좋을까? 적을수록 좋을까?
상위 클래스는 물려줄 특성이 풍성할수록 좋다. 리스코프 치환 원칙(LSP)에 의하여 하위 클래스는 상위 클래스여야 한다.
인터페이스는 구현을 강제하는 강제할 메서드가 많을수록 좋을까? 적을수록 좋을까?
인터페이스는 구현을 강제할 메서드의 개수가 적을수록 좋다. 인터페이스 분리 원칙(ISP)에 의하여 인터페이스는 그 역할에 필요한 최소한의 메서드만을 정의하고,하나의 역할을 할 수 있다.
상속 구조에서 객체를 생성할 경우, 메모리 영역의 상태를 알아보자.
public class Animal {
public String name;
public void showName() {
System.out.println("안녕 나는 %s야.\n", name);
}
}
public class Penguine extends Animal {
public String habitat;
public void showHabitat() {
System.out.println("%s는 %s에 살아.\n", name, habitat);
}
}
public class Driver {
public static void main(String[] args){
Penguine pororo = new Penguine();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showHabitat();
Animal pingu = new Penguine();
}
}
Penguin 클래스의 인스턴스만 생기는 것이 아니라, Animal 클래스의 인스턴스도 함께 힙 영역에 생긴 것을 알 수 있다. 즉, 하위 클래스의 인스턴스가 생성될 때 상위 클래스의 인스턴스도 함께 생성된다. 그림에서도 생략했지만 모든 클래스의 최상위 클래스인 Object 클래스의 인스턴스도 함께 생성된다.
Java에서는 오버로딩과 오버라이딩으로 다형성을 지원한다.
public class Animal {
public String name;
public void showName() {
System.out.println("안녕 나는 %s야.\n", name);
}
}
public class Penguine extends Animal {
public String habitat;
public void showHabitat() {
System.out.println("%s는 %s에 살아.\n", name, habitat);
}
//오버라이딩 - 상위 메서드 재정의
public void showName() {
System.out.println("내 이름은 팽귄이다.");
}
//오버로딩 - 인자를 다르게하여 중복 정의
public void showName(String myName) {
System.out.println("내 이름은 %s이고 %s이다.", myName, name);
}
}
public class Driver {
public static void main(String[] args){
Penguine pororo = new Penguine();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showName("초보람보");
pororo.showHabitat();
Animal pingu = new Penguine();
}
}
위 코드를 실행한 후에 메모리의 스냅샷은 다음과 같다.
상위 클래스 타입의 객체 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩(재정의)한 메서드가 호출된다는 사실을 기억하자.
객체 지향에서 접근 제어자로 캡슐화를 지원한다. Java에서 접근 제어자는 private, dafault, protected, public이 존재한다.
다음 접근 제어자들을 통해 캡슐화를 진행할 수 있다.
정적 멤버는 클래스명.정적멤버
형식으로 접근하는 것을 권장한다. 일관된 형식으로 접근할 수 있다.
예를 들어 사람 클래스의 인구와 고양이 클래스 다리 개수를 예를 보자면, 사람.인구, 고양이.다리개수 형식으로 접근하는 것이 홍길동.인구, 러시안블루.다리개수 형식으로 접근하는 것이 더 권장된다. 즉, 정적 멤버에 접근할 때는 객체참조변수명.정적멤버
형식으로 접근하기보다는 클래스명.정적멤버
형식으로 접근하라는 것이다.