들어가기 전에
이번 포스팅은 스프링 입문을 위한 자바 객체 지향의 원리(김종민 저)를 읽으면서 객체 지향 4대 특성에 대해 정리한 글입니다. 이해한 내용을 토대로 정리한 글이기 때문에, 책으로 읽어보는 것을 권장합니다.
기본 개념 잡기
객체(object)와 분류(class)
먼저, 실생활로 객체지향의 객체(object)와 분류(class)에 대해 간단히 살펴보겠습니다.
- 세상에 존재하는 모든 것은 사물(개체, 객체, object)입니다.
- 각각의 사물은 고유하며, 속성 및 행위를 가집니다.
- 사물을 하나하나 인지하기 보다는 사물을 분류(class)하여 이해를 합니다.
- 예) 밤하늘에 반짝이는 사물들을 별이라고 분류합니다.
객체란, 세상에 존재하는 유일무이한 사물(개체)
클래스란, 같은 속성과 기능을 가진 객체를 총칭하는 개념(분류, 집합)
객체 지향은 인간의 인지 및 사고 방식까지 프로그래밍에 접목하였기 때문에 직관적이라고 말합니다. 이번 포스팅에서는 이러한 객체 지향의 4대 특징인 캡! 상추다에 대해 알아보도록 하겠습니다.
어플리케이션 경계(컨텍스트, context)
앞서, 객체를 하나 생성할 때 아래와 같이 선언을 한다고 하였습니다. 이렇게 선언을 하면 사람이라는 클래스(분류)를 이용해 유일무이한 하나의 사람(객체)를 만들어 헬렌켈러(객체 참조 변수)라는 이름을 지어준 것입니다.
사람 헬렌켈러 = new 사람();
앞서 클래스란 같은 속성과 기능을 가진 객체를 총칭하는 개념이라고 말했습니다. 따라서, 위 예시의 사람 클래스는 각각의 사람 객체들이 가진 공통된 특성을 가지고 만들어집니다.
먼저 사람의 공통된 속성 및 기능을 나열해보면 아래와 같이 나열할 수 있습니다.
위 항목뿐 아니라 더 많은 사람의 공통된 속성 및 기능이 있을텐데, 매번 사람 클래스를 만들때마다 사람의 모든 기능과 특성을 넣어야할까?
만약, 매번 모든 기능과 특성을 넣는다면 사용하지 않는 속성 및 기능이 넘쳐날 것입니다.
- 예를 들어, 은행 관련 어플리케이션을 개발한다면 사람의 특성 중 먹다/뛰다/자다/수영하다 등은 필요하지 않은 특성입니다.
이때 생각해야 할 개념으로 어플리케이션 경계가 있으며, 이는 컨텍스트(context)라고도 부릅니다. 즉, 현재 만들고자 하는 범위 내에서의 기능 및 특성에 대해서만 나열하면 됩니다.
캡! 상추다
스프링 입문을 위한 자바 객체 지향의 원리(김종민 저)에서는 객체 지향의 4대 특징을 캡! 상추다라고 하여 좀 더 외우기 쉽도록 하였습니다.
- 캡: 캡슐화(Encapsulation): 정보 은닉
- 상: 상속(재사용)
- 추: 추상화(Abstraction): 모델링
- 다: 다형성(Polymorphism): 사용 편의
클래스와 객체를 구분하는 방법
객체를 생성할 때는 아래와 같이 선언하여 사용합니다.
클래스명 객체명 = new 클래스명();
어떤 항목이 클래스가 되고, 어떤 항목이 객체가 되는지는 나이(제조년월)을 물어보면 됩니다.
- 사람의 나이는 몇살인가요? → 대답 불가(클래스)
- 과자의 제조년월은 언제인가요? → 대답 불가(클래스)
- 핑구의 나이는 몇살인가요? → 대답 가능(1986년도 처음 나왔으므로, 2022년 기준 한국 나이로 37살)(객체)
즉, 클래스는 분류에 대한 개념일 뿐 실체가 아니며 객체는 실체입니다.
추상화(Abstraction) : 모델링
추상화는 모델링이다.
추상화란, 구체적인 것을 분해하여 관심 영역(Application Boundary)에 있는 특성만 가지고 재조합하는 것
추상화 = 모델링 = class
아래 사진은 네이버 국어사전에서 확인한 사전적 의미의 추상입니다. 객체 지향의 추상화는 사전적 의미로의 추상화는 같은 의미로, 공통되는 특성이나 속성을 추출하는 의미로 사용됩니다.
즉, 추상화란 앞서 알아보았던 어플리케이션 경계에 맞게 객체들의 공통되는 특성 및 속성을 추출해 모델링한 것을 의미합니다.
자바는 객체 지향의 추상화를 class 키워드를 통해 지원합니다.
추상화와 T 메모리 구조
앞서 알아본 추상화를 제대로 이해할 수 있도록 T 메모리 구조와 함께 예시를 들어보겠습니다. 애니메이션의 쥐 캐릭터 관리 프로그램을 만든다고 가정해보겠습니다.
public class Mouse {
public String name;
public int age;
public int countOfTail;
public void sing() {
System.out.println("찍찍 울어요");
}
}
- 쥐라는 클래스를 만들 때, 애니메이션 속 쥐의 특징에 맞는 항목들에 대해서만(어플리케이션 경계) 클래스 작성 시 넣으면 됩니다.
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();
}
}
MouseDriver.main() 메소드를 시작점으로 실행된 후, 위 코드의 3번째 줄이 실행되기 직전의 T 메모리 구조는 아래와 같습니다.
위 T 메모리 구조를 보면, java.lang 패키지 및 사용되는 클래스들에 대해 스태틱 영역에 배치되었음을 알 수 있습니다. 이때, Mouse 클래스를 보면, name, age, countOfTail이라는 변수명은 적혀있지만, 따로 변수의 값이 할당될 공간이 없음을 확인할 수 있습니다. 이는 이 3개의 속성이 Mouse 클래스의 속성이 아닌 Mouse 객체의 속성이기 때문입니다.
- 객체의 속성의 변수 저장 공간은 객체가 생성된 후 힙 영역에 할당됩니다.
- T 메모리 구조에서 밑줄이 그어진 변수 또는 메소드는 클래스 멤버(static)이며, 밑줄이 없는 경우 객체 멤버입니다.
이제 3번째 줄의 Mouse mickey = new Mouse();를 실행하게 되면, 아래와 같은 순서에 의해 실행이 됩니다.
- Mouse mickey를 실행함으로써 main() 스택 프레임에 mickey라고 하는 객체 참조 변수 공간 생성(왼쪽 사진)
- new Mouse()를 실행함으로써 Mouse 객체가 생성되어, 힙 영역에 Mouse 클래스의 인스턴스 생성(중간 사진)
- mickey라는 객체 참조 변수의 값은 Mouse 객체의 위치(주소값)를 값으로 초기화(오른쪽 사진) → 객체 참조 변수는 클래스의 인스턴스를 참조
10번째줄까지 실행하게 되면, 기존에 생성한 Mouse 객체와 mickey와의 연결이 끊어지게 됩니다. mickey에 null을 대입하여 Mouse 객체와 연결이 끊어져도, Mouse 객체는 힙 영역에서 제거될 수도 있고, 안될 수도 있습니다. 여기서는 제거되었다고 가정하고 10번째 줄 이후까지 실행하여 12번째 줄까지 실행되면 아래 오른쪽 그림과 같은 형태가 됩니다.
- 가비지 컬렉터(GC, Garbage Collector)에 의해 아무도 참조하지 않는 힙 영역의 객체를 제거해줍니다.
- 다만 가비지 컬렉터가 언제 올지는 알 수 없습니다.
클래스 멤버(static 멤버, 정적 멤버) VS 객체 멤버(인스턴스 멤버)
앞선 예시를 통해 클래스는 개념이면서 분류체계이기 때문에 속성에 대한 값을 가질 수 없음을 알 수 있습니다. 다만, 해당 클래스에 속하는 객체들이 모두 동일한 값을 가질 경우 클래스를 통해 질문을 해도 동일한 값을 뱉어줄 수 있습니다. 이를 클래스 멤버라고 합니다.
- 미키마우스(객체)의 꼬리는 몇개인가요? → 1개
- 제리(객체)의 꼬리는 몇개인가요? → 1개
- 쥐(클래스)의 꼬리는 몇개인가요? → 1개
즉, 위 예시의 countOfTail은 객체 멤버로 갖지 않고, 클래스 멤버로 갖도록 할 수 있습니다. 클래스 멤버로 사용하기 위해서는 static 키워드를 사용해야 하며 아래와 같이 소스를 구성할 수 있습니다.
public class Mouse {
public String name;
public int age;
public static int countOfTail; // 클래스 멤버(정적 멤버)
public void sing() {
System.out.println("찍찍 울어요");
}
}
위와 같이 Mouse 클래스가 변경되면, Mouse 객체를 여러개 생성해도 countOfTail은 스태틱 영역에 변수 공간이 할당될 뿐, 힙 영역에 추가로 변수 공간이 할당되지 않습니다.
- 클래스 멤버의 경우, 객체_참조_변수.클래스멤버로도 접근할 수 있고 클래스명.클래스 멤버로도 접근할 수 있습니다.
위와 같이 정적 속성(static)은 해당 클래스의 모든 객체가 같은 값을 가질 때 사용합니다. 특히, 정적 메소드의 경우 객체들이 존재하지 않아도 사용할 수 있는 메소드로 유틸리티성 메소드를 주로 정적 메소드로 구현합니다.
- Math 클래스에 있는 많은 정적 메소드들을 객체를 생성하지 않고 사용합니다.
- main() 메소드는 항상 정적 메소드여야 합니다.
- T 메모리가 초기화된 순간 객체는 하나도 존재하지 않아 객체 멤버 메소드로 main() 메소드를 실행할 수 없기 때문입니다.
클래스 속성 및 객체 속성은 별도로 초기화하지 않아도 초기화가 됩니다. → 공유 변수의 성격
클래스 멤버(정적 멤버, static 멤버)나 객체 멤버(인스턴스 멤버)의 경우, 따로 초기화를 하지 않으면 아래와 같이 자동으로 초기화됩니다.
- 정수형: 0
- 부동소수점형: 0.0
- 논리형: false
- 객체: null
스택 영역에 생기는 지역변수와 달리 클래스 멤버와 객체 멤버에 대해 자동으로 초기화해주는 이유는, 이 두 멤버들은 공유 변수의 성격을 띄기 때문입니다.
- 클래스 멤버(정적 멤버)는 어디서든 클래스를 임포트할 수만 있다면 사용할 수 있기 때문에 누가 초기화를 해야할지 정할 수 없습니다.
- 객체 멤버는 하나의 객체 안에서 여러 객체 메소드가 공유하는 변수이기 때문에 누가 초기화를 할지 정할 수 없습니다.
- 지역 변수는 해당 지역내에서만 사용되고 소멸되기 때문에 초기화를 누가할지 명확합니다.
상속 : 재사용 + 확장
객체 지향의 상속은 가계도와 같은 개념이 아니며, 재사용과 확장으로 이해해야 합니다. 즉, 아래와 같이 특성을 상속한다고 이해해야 합니다. 예를 들어 동물은 포유류의 부모가 아니며, 참새의 부모가 조류는 아닙니다.
- 그래서, 자바에서는 inheritance라는 키워드 대신 extends라는 확장의 의미의 키워드를 상속의 의미로 사용합니다.
객체지향의 상속이란, 상위 클래스의 특성을 하위 클래스에서 상속(특성 상속, 재사용)하고
추가로 필요한 특성을 더하는 확장한다는 의미입니다.
상속 관계에서 반드시 만족해야 할 문장은 다음과 같습니다.
- 하위 클래스는 상위 클래스이다.
상속은 is a kind of 관계를 만족해야 합니다.
- 펭귄 is a kind of 동물 → 펭귄은 동물의 한 분류입니다.
- 조류 is a kind of 동물 → 조류는 동물의 한 분류입니다.
- 고양이 is a kind of 동물 → 고양이는 동물의 한 분류입니다.
is a 관계라고 하게 되면 a가 어떤 의미로 파악될지 애매하기 때문에 is a kind of 관계라고 보는 것이 좋습니다.
상속(is a kind of)과 인터페이스(is able to)
만약, 자바가 다중 상속을 지원한다면 인어라는 클래스는 사람 클래스와 물고기 클래스 모두를 상속받을 수 있습니다. 이 경우, 숨쉬는 방법과 수영하는 방법은 사람 클래스와 물고기 클래스는 상이합니다. 이러한 문제를 다중 상속의 다이아몬드 문제라고 합니다. 자바는 이러한 문제로 인해 다중 상속을 지원하지 않습니다.
자바는 다중 상속을 지원하지 않는 대신에 인터페이스를 지원합니다.
구현 클래스 is able to 인터페이스
구현 클래스는 인터페이스 할 수 있습니다.
상위 클래스는 하위 클래스에게 특성(속성과 메소드)을 상속해주고,
인터페이스는 클래스가 무엇을 할 수 있다는 기능을 구현하도록 강제합니다.
상속과 T 메모리 구조
Animal을 상속한 Penguin 클래스의 객체를 이용한 예시에 대해 알아보도록 하겠습니다. 소스는 아래와 같습니다.
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 class Driver {
public static void main(String[] args) {
Penguin pororo = new Penguin();
pororo.name = "뽀로로";
pororo.habitat = "남극";
pororo.showName();
pororo.showHabitat();
Animal pingu = new Penguin();
pingu.name = "핑구";
// pingu.habitat = "EBS";
/* 형변환을 이용하여 habitat 호출할 수 있긴 합니다.
Penguin newPingu = (Penguin)pingu;
newPingu.habitat = "EBS";
newPingu.showHabitat();
*/
pingu.showName();
// pingu.showHabitat();
// Penguin happyfeet = new Animal();
}
}
위 코드를 실행하게 되면, main() 메소드가 실행하게 되며 main() 메소드의 3번째 줄을 실행하면, 아래와 같이 Penguin 객체와 Penguin 클래스가 상속 받은 Animal 객체가 함께 힙 영역에 생기게 됩니다.
- 그림에는 생략되어 있지만, 가장 최상단 상속 클래스인 Object 클래스의 인스턴스 역시 힙 영역에 생성된다고 생각하시면 됩니다.
이어서, 11번째 줄을 실행하게 되면, 힙 영역에는 Penguin과 Animal 객체가 2세트가 존재하게 됩니다.
- pingu 객체 참조 변수는 pororo 객체 참조 변수와 다르게 Animal 객체를 가리킵니다. 이는, Penguin 객체를 생성할 때 객체 참조 변수 타입을 Animal로 지정했기 때문입니다.
- pingu 객체 참조 변수는 pororo 객체 참조 변수와 다르게 habitat 정보를 초기화할 수도, habitat 정보를 보여줄 수도 없습니다. → 형변환 연산을 생각해볼 수 있겠지만, 여기서는 위 로직 그대로 사용한다는 가정하에서 사용할 수 없다고 작성했습니다.
다형성 : 사용편의성
객체 지향에서 다형성이라고 하면 오버라이딩(overriding)과 오버로딩(overloading)을 얘기할 수 있습니다. 오버라이딩과 오버로딩을 쉽게 이해하는 방법은 아래 그림을 토대로 쉽게 이해할 수 있습니다.
위 사진을 인공위성 입장에서 내려다본다고 가정합니다.
- 오버라이딩(overriding): 오토바이 위에 사람이 올라타있으면, 오토바이가 아닌 사람만 보입니다. → 맨 위 존재만 보임
- 같은 메소드 이름, 같은 인자 목록으로 상위 클래스의 메소드를 재정의
- 오버로딩(overloading): 트럭에 짐을 여러개 적재하면, 짐들이 모두 보입니다.
- 같은 메소드 이름, 다른 인자 목록으로 다수의 메소드를 중복 정의
다형성과 T 메모리 구조
오버로딩과 오버라이딩을 T 메모리 구조와 함께 예시를 통해 알아보도록 하겠습니다.
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);
}
}
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();
}
}
위 코드를 실행하게 되면, main()문이 실행된 후 pororo 객체 참조 변수를 생성하고 Penguin 객체를 만들어 연결(3번째줄)해주면 아래와 같은 T 메모리 구조를 갖게 됩니다.
- Penguin 클래스에서 showName() 메소드를 오버라이딩(overriding)하여 Animal 클래스의 showName() 메소드가 가려졌습니다.
- 따라서, pororo.showName()을 호출하면 Penguin 클래스에서 재정의한 showName() 메소드가 수행됩니다.
- Penguin 클래스에서 오버로딩(overloading)을 통해 중복 정의한 showName(String)은 Penguin 객체에 추가되어 있습니다.
12번째 줄에서 pingu 객체 참조 변수 생성 및 Penguin 객체 생성시 T 메모리 구조는 아래와 같습니다.
- pingu 객체 참조 변수의 타입은 Animal이기 때문에 Animal 객체를 가리킵니다.
- 다만, 생성한 객체가 Penguin 객체이기 때문에 showName() 메소드는 pororo 때와 마찬가지로 Penguin 클래스에서 재정의한 showName()이 호출됩니다.
- 상위 클래스 타입의 객체 참조 변수를 사용해도, 하위 클래스에서 오버라이딩(재정의)한 메소드가 호출됩니다.
위 예시를 통해 알 수 있는 점은 아래와 같습니다.
다형성은 개발자가 코드를 작성할 때 사용 편의성을 준다.
예를 들어, 아래와 같이 코드를 작성할 때 오버라이딩이 없다면, 각 동물 배열 속 객체의 타입에 맞게 메소드를 다르게 호출해주어야 합니다.
public class Driver {
public static void main(String[] args) {
Animal[] AnimalArray = new Animal[3];
AnimalArray[0] = new Whale();
AnimalArray[1] = new Cat();
AnimalArray[2] = new Dog();
for(Animal animal: AnimalArray) {
animal.crying(); // crying()는 Whale, Cat, Dog 클래스에서 각각 오버라이딩된 메소드
}
}
}
따라서, 다형성은 개발자에게 사용 편의성을 주었음을 알 수 있습니다.
캡슐화 : 정보 은닉
자바에서 정보 은닉이라고 하면, 접근 제어자인 private, [default], protected, public을 떠올릴 수 있습니다.
여기서는 접근 제어자가 객체 멤버(인스턴스 멤버)일 때와 정적 멤버(클래스 멤버)와 함께 쓰일 때 어떻게 다른지 알아보도록 하겠습니다.
먼저 아래의 UML에서 사용되는 표기법에 대해 간단히 정리하였습니다.
- -: private
- ~: default
- #: protected
- +: public
- 밑줄: static 멤버
- 비어있는 화살표 + 실선: 상속(화살촉이 상위 클래스)
객체 멤버(인스턴스 멤버)와 접근 제어자
위 구조로 패키지 및 클래스가 구성되어 있을 때, ClassA의 객체 멤버에 접근권한이 어떤 항목까지 있을지에 대해 정리하였습니다.
- 상속을 받지 않았다면 객체 멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야 합니다.
- 정적 멤버는 클래스.정적멤버로 접근하는 것을 권장합니다.
- 일관된 형식으로 접근하기 위함
- 메모리 접근 방식(객체.정적멤버로 접근할 경우 먼저 객체 참조 변수를 이용해 힙 영역의 객체를 갔다가 스태틱 영역에 가야함)
정적 멤버(클래스 멤버, static 멤버)와 접근 제어자
위 구조로 패키지 및 클래스가 구성되어 있을 때, ClassA의 정적 멤버에 접근권한이 어떤 항목까지 있을지에 대해 정리하였습니다.
참고 자료
'PROGRAMMING LANGUAGE > JAVA' 카테고리의 다른 글
[effective JAVA] 아이템1: 생성자 대신 정적 팩토리 메소드를 고려하라 (0) | 2022.10.28 |
---|---|
[JAVA] 자바가 확장한 객체 지향 (0) | 2022.06.04 |
[JAVA] 자바 프로그램 개발 및 구동 (0) | 2022.06.01 |
[JAVA 개념] ArrayList 초기화 (0) | 2022.02.20 |
[JAVA 개념] 길이 관련 메소드 사용법(length, length(), size()) (0) | 2022.02.20 |