객체 지향에서 가장 중요한 4대 특성 - 캡상추다를 정리해보자.
추상화의 개념은 미술 분야에서의 추상화를 생각하면 된다. 대표적으로 피카소의 그림을 생각한다면, 극사실주의 처럼 눈에 보이는 그대로를 반영하여 그리는 것 아닌 마음 속으로 느껴지는 그 사람의 특징을 살려서 그렸다. 즉, 사실적인 모습이 아니라 각 부분의 특징을 가장 표현하는 것을 의미한다.
객체 지향에서의 추상화를 생각해보자.
공통 특성이나 속성을 추출한다는 점에서는 동일하다고 생각하면 된다. 객체 지향에서의 추상화는 구체적인 것을 분해해서 관심영역(애플리케이션 경계)에 있는 특성만 가지고 재조합하는 것을 의미한다. 단, 설계자마다 보는 시선이 다르기 때문에 주관적이다. (자바에서는 객체 지향의 추상화를 class 키워드로 지원하고 있다! 💪🏻)
💡 애플리케이션 경계(컨텍스트)?
예를 들어, 사람에 대한 공통적인 특징을 모두 모아보면 정말 많다. 하지만 과연 이 특징들이 모두 필요할까? 병원 애플리케이션과 은행 애플리케이션의 입장에서 생각해보자. 애플리케이션마다 가르키는 사람은 각각 환자와 고객을 의미한다. 즉, 동일하게 사람을 지칭했음에도 애플리케이션마다 요구하는 정보는 모두 다르다.
- 병원 애플리케이션에서 필요한 환자 정보 : 시력, 몸무게, 혈액형, 키, 나이, 먹다(), 자다() 등
- 은행 애플리케이션에서 필요한 고객 정보 : 나이, 직업, 연봉, 일하다(), 입금하다(), 출금하다() 등
따라서 애플리케이션의 경계를 기준으로 생각하여 모델링 작업을 해야한다.
보통 상속이라고 하면 Inheritance로 부모와 자식 관계를 많이 떠올린다. 하지만 이는 틀린 표현이다. 상속은 조직와 계층도가 아닌 분류도라는 점에서 재사용과 확장으로 이해해야한다. 쉽게 풀어본다면, 상속은 상위 클래스의 특성을 하위 클래스에서 상속하고 거기에 더해 필요한 특성을 추가하여 확장해서 사용할 수 있다는 의미다. (extends 키워드를 괜히 사용하는게 아니라는 말씀! 😉)
상위 클래스로 갈 수록 추상화, 일반화가 되었다고 하며, 하위 클래스 쪽으로 갈 수록 구체화, 특수화되었다고 말한다.
이 문장은 상속 관계에서는 반드시 만족해야하는 문장이다. 이 문장을 이용한다면, 상속은 조직도가 아닌 분류도라는 것을 쉽게 이해할 수 있다.
💡 클래스명은 분류스럽게! 객체 참조 변수명은 유일무이하게!
안좋은 예를 들어보자.
조류 bird = new 조류();
bird 라는 단어는 분류에 가깝다. bird 의 나이가 무엇인가? 에 대한 답변을 하지 못하는 것이 그 이유다. 유일무이하지 않다는점. 책의 필자가 존경하는 프로그래머의 경우에는 aBird나 theBird 형태로 코딩한다고 한다.
is-a 의 표현은 객체와 클래스의 관계로 오해될 수 있어 is-a-kind-of 의 표현이 더 정확하다고 할 수 있다. 왜 is-a 표현은 알맞지 않은가? 생각해보자.
이 문장을 해석해보자.
또 해석해보자. 여기서 말하는 하나의 상위 클래스는 하나의 객체라고 볼 수 있다. 그럼 하나의 객체로 표현해보면 다음과 같은 문장이 완성된다.
딱 봐도 이상하다. 클래스는 분류를 의미하고 객체는 유일무이한 것이라고 했는데, 이 문장으로 따진다면 분류는 유일무이한 것이 되는 - 논리적으로 이상한 결론이 나오게 된다. 하지만 이를 is-a-kind-of 로 변경하여 해석해본다면, 하위 클래스는 상위 클래스의 하나의 분류! 라는 의미로 논리적으로 알맞게 해석이 되는 것을 알 수 있다.
💡 인터페이스는 어떻게 표현할까?
- 구현 클래스 is able to 인터페이스
인터페이스는 is-able-to 로 문장을 완성하면 된다. 자바 API에서도 이러한 형태의 인터페이스를 많이 확인할 수 있다. 대표적으로
Serializable, Cloneable, Comparable, Runnable, AutoCloseable
가 있다.
상속은 확장이라고 했다. 책에서는 다중 상속을 지원하지 않는 이유로 인어공주를 떠올리라고 했다. 인어공주는 사람과 물고기를 상속하게 되는데, 인어공주는 팔과 다리로 수영을 해야하는 것인가 아니면 물고기처럼 팔과 지느러미로 수영을 해야하는 것인가에 대한 문제가 생긴다. 자바에서는 다중 상속을 지원하지 않는 대신 인터페이스를 도입했다.
public class Animal {
public String name;
public void showName() {
System.out.println(this.name);
}
}
public class Cat {
public String habit;
public void showHabit() {
System.out.println(this.habit);
}
}
public class Driver {
public static void main(String[] args) {
Cat tom = new Cat();
tom.name = "tom";
tom.showName();
tom.habit = "run";
tom.showHabit();
/*
Animal aCat = new Cat();
aCat.name = "tom";
aCat.showName();
// 에러 - aCat.habit = "run";
// 에러 - aCat.showHabit();
((Cat) tom).habit = "run";
((Cat) tom).showHabit();
*/
}
}
다형성은 가장 기본적으로 오버라이딩과 오버로딩을 의미한다.
여기서 주의해야할 점은, 오버로딩을 사용할 때는 정확하게 동일한 기능을 수행하는 메소드에만 적용해야한다. 기능이 다르면 반드시 같은 이름을 적용해서는 안된다.
적절한 비유는 아니지만, 알약을 생각해보자. 알약의 내부에는 가루가 있다는 것은 알고있다. 하지만 우리는 알약을 쪼개지 않는 이상, 가루가 하얀색인지 파란색인지 알 수 없다.
이와 같이 캡슐화는 정보 은닉을 의미한다. 그리고 정보 은닉이라고 하면 가장 먼저 떠올려야하는 것이 접근자와 설정자 메소드다.
private
: 해당 클래스 안에서만default
: 패키지 안에서만protected
: 상속, 패키지 안에서만public
: 어디에서든 가능💡 클래스 멤버 접근과 객체 멤버 접근의 조건?
- 상속을 받지 않았다면 객체 멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야 한다.
- 정적 멤버는 클래스명.정적 멤버 형식으로 접근하는 것을 권장한다.
첫 번째 조건에 대해서는 당연하듯이 이해할 수 있지만, 두 번째 조건에 대해서는 잘 받아들여지지 않을 수 있다. 정적 멤버에 접근하는 방법은 각 위치별로 객체 멤버 메소드에서 접근할 수 있는 방법은 3가지가 있는데, 혼란을 주지 않도록 일관된 형식으로 클래스명.정적 멤버를 사용하는 것을 권장한다는 의미다.
다른 이유도 살펴보자. 하단의 이미지를 보면 한번에 접근을 하느냐(a) 아니면 여러 번 거쳐서 접근을 하느냐(1-2-3)와 같이, 메모리의 물리적 접근성을 보았을 때도 클래스명.정적 멤버가 좋아보이는 것을 알 수 있다.