객체지향의 4대 특성

짱J·2023년 3월 27일
0
post-thumbnail

객체지향의 4대 특성은 캡!상추다로 쉽게 외울 수 있다.
각각 캡슐화, 상속, 추상화, 다형성을 의미한다.


붕어빵틀과 붕어빵은 잘못된 비유다 !

클래스와 객체의 관계를 설명할 때 '붕어빵틀과 붕어빵'이라는 비유를 자주 사용한다.
아래 코드를 봐보자.

클래스 객체명 = new 클래스();

붕어빵틀 붕어빵 = new 붕어빵틀();

새로운 붕어빵틀을 하나 만들었더니 붕어빵이 되었다.
뭔가 이상한 문장이다. 🤔

정확히 말하면, 붕어빵틀은 붕어빵을 만드는 팩터리이다.


1️⃣ 추상화

추상(抽象): (심리) 여러 사물 또는 개념 따위의 개별자들로 부터 공통점을 파악하고 추려내는 것.

추상화란 구체적인 것을 분해하여 관심 있는 영역에 대한 특성 특성만 가지고 재조합하는 것이라고 정리할 수 있다.

책에서는 객체와 클래스를 아래와 같이 정의한다.

  • 객체: 세상에 존재하는 유일무이한 사물
  • 클래스: 분류, 집합. 같은 속성과 기능을 가진 객체를 통칭하는 개념

세상에 존재하는 유일무이한 객체를 특성(속성 + 기능)에 따라 분류하여 객체를 통칭할 수 있는 집합적 개념인 클래스가 나오게 된 것이다.

클래스를 이용하여 객체를 만들었다는 것을 강조할 때는 인스턴스라는 표현을 사용한다.
객체(object) = 클래스의 인스턴스

사람을 예시로 들어보자.
시력, 몸무게, 혈액형, 키, 나이와 같이 명사로 표현되는 특성을 속성이라고 하며, 속성은 값을 가질 수 있다.

먹다, 자다, 일하다, 운동하다 등 동사로 표현되는 특성은 기능/행위라 하며, 이를 객체 지향에서는 메서드라고 한다.

클래스를 설계하는데 추상화가 사용되며, 설계를 하기 위해서는 어플리케이션 경계부터 정해야 한다.

  • 어플리케이션 경계(context)란? 내가 만들고자 하는 애플리케이션이 어디에 사용될 것인지
    ex) 병원 어플리케이션, 학교 어플리케이션 ...

어플리케이션에 따라 필요한 특성들이 달라진다.

자바에서는 class 키워드를 통해 추상화를 지원한다.

추상화와 T 메모리

애니메이션의 쥐 캐릭터 관리 프로그램을 만든다고 가정해보자.

// Mouse.java
package abstraction01;

public class Mouse {
	public String name;
    public int age;
    public int countOfTail;
    
    public void sing() {
    	System.out.println(name + "찍찍!!");
    }
}

Mouse 클래스를 생성하였고, 이를 사용하여 객체를 생성해 보고 특성을 활용해볼 수 있도록 main() 메서드를 가진 테스트용 클래스를 만들어보자.

// MouseDriver.java
package abstraction01;

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();
    }
}

Main 메서드를 실행하기 전 T 메모리는 위와 같다.
java.lang 패키지와 마찬가지로, 모든 클래스는 static 영역에 배치된다.
이 때, 각 속성들에 대한 변수 저장 공간이 없는 것을 확인할 수 있다.

객체가 생성되어야지 속성의 값을 저장하기 위한 메모리 공간이 (static 영역이 아닌) heap 영역에 할당된다.

5번째 줄 Mouse mickey = new Mouse();에서는 총 3개의 명령이 수행된다.

  1. Mouse 객체에 대한 참조 변수 mickey를 만든다.
  2. Mouse 클래스 인스턴스를 하나 만들어 heap 영역에 배치한다.
  3. Mouse 객체에 대한 주소(포인터)를 참조 변수 mickey에 할당한다.
    (아래 그림에서 화살표를 사용하여 표현했다.)

클래스 속성과 객체 속성은 별도의 초기화를 해주지 않아도
정수형은 0, 부동소수점형은 0.0, 논리형은 false, 객체는 null로 초기화된다.

9번째 줄까지 실행되면, mickey의 각 속성에 값들이 할당된다.

13번째 줄에서 mickey에 null을 할당하면 T 메모리가 아래 그림과 같이 변한다.
mickey는 더 이상 heap 영역에 있는 Mouse 객체를 참조하지 않는다.

그러면 가비지 컬렉터가 아무도 참조하지 않는 Mouse 객체를 쓰레기로 인지하고 수거해 간다.

이 때, jerry가 참조하고 있는 Mouse 객체는 앞에 있던 Mouse 객체와 다르다는 것을 알아두자.

클래스 멤버 vs 객체 멤버

객체는 유일무이하게 존재하므로 속성에 값을 가지고 있지만, 클래스는 개념이면서 분류 체계이므로 속성에 값을 가질 수 없다.

하지만, 모든 객체가 같은 속성을 가지고 있을 때는 어떻게 할까?
(ex. mickey, jerry ... 모든 Mouse의 countOfTail은 1)

모든 객체가 같은 속성을 가지고 있으면 객체 수만큼 메모리를 차지하고 있기 때문에 아깝다.
이 값을 클래스에 저장하여 해결할 수 있다.

// Mouse.java
package abstraction01;

public class Mouse {
	public String name;
    public int age;
    public static int countOfTail = 1;
    
    public void sing() {
    	System.out.println(name + "찍찍!!");
    }
}

변수 유형 정리

이름다른 이름T 메모리 상 위치
static 변수클래스 속성, 정적 변수, 정적 속성 ...static 영역
인스턴스 변수객체 속성, 객체 변수 ...heap 영역
local 변수지역 변수stack 영역 (스택 프레임 내부)

2️⃣ 상속

객체 지향에서의 상속은 상위 클래스의 특성을 하위 클래스에서 상속하고, 거기에 더해 필요한 특성들을 추가하여 확장해서 사용하는 것이다.

이 때, 상위 클래스에서 하위 클래스 쪽으로 가는 것을 구체화, 특수화라고 하며,
하위 클래스에서 상위 클래스 쪽으로 가는 것을 추상화, 일반화라고 한다.

자바에서는 extends 키워드를 통하여 상속을 지원한다.

위와 같은 분류도를 코드로 표현해보자.

// 동물.java
package inheritance01;

public class 동물 {
	String myClass;
    
    동물 () {
    	myClass = "동물";
    }
    
    void showMe() {
    	System.out.println(myClass);
    }
}

// 포유류.java
package inheritance01;

public class 포유류 extends 동물 {
	포유류() {
    	myClass = "포유류";
    }
}

// 조류.java
package inheritance01;

public class 조류 extends 동물 {
	조류() {
    	myClass = "조류";
    }
}

// 고래.java
package inheritance01;

public class 고래 extends 포유류 {
	고래() {
    	myClass = "고래";
    }
}

// 박쥐.java
package inheritance01;

public class 박쥐 extends 포유류 {
	박쥐() {
    	myClass = "박쥐";
    }
}

// 참새.java
package inheritance01;

public class 참새 extends 조류 {
	참새() {
    	myClass = "참새";
    }
}

// 펭귄.java
package inheritance01;

public class 펭귄 extends 조류 {
	펭귄() {
    	myClass = "펭귄";
    }
}

이제 이 클래스들을 사용하여 예제 코드를 작성해보자.

package inheritance01;

public class Driver01 {
	public static void main(String[] args) {
    	동물 animal = new 동물();
        포유류 mamalia = new 포유류();
        조류 bird = new 조류();
        고래 whale = new 고래();
        박쥐 bat = new 박쥐();
        참새 sparrow = new 참새();
        펭귄 penguin = new 펭귄();
        
        animal.showMe();
        mamalia.showMe();
        bird.showMe();
        whale.showMe();
        bat.showMe();
        sparrow.showMe();
        penguin.showMe()
    }
}

상위 클래스에서만 showMe() 메서드를 구현했지만 하위 클래스에서도 모두 사용할 수 있다는 것을 알 수 있다.

package inheritance01;

public class Driver02 {
	public static void main(String[] args) {
    	동물 animal = new 동물();
        동물 mamalia = new 포유류();
        동물 bird = new 조류();
        동물 whale = new 고래();
        동물 bat = new 박쥐();
        동물 sparrow = new 참새();
        동물 penguin = new 펭귄();
        
        animal.showMe();
        mamalia.showMe();
        bird.showMe();
        whale.showMe();
        bat.showMe();
        sparrow.showMe();
        penguin.showMe();
    }
}

Driver01.java에서 객체 생성 부분의 클래스 명을 모두 동물로 바꾸었다.
코드를 실행하면, 결과가 같은 것을 알 수 있다.
이를 통해 하위 클래스는 상위 클래스다 라는 것을 알 수 있다.

클래스가 모두 '동물'로 같기 때문에, 아래와 같이 코드를 줄일 수 있다.

package inheritance01;

public class Driver03 {
	public static void main(String[] args) {
    	동물 animals = new 동물[7];
    	animals[0] = new 동물();
        animals[1] = new 포유류();
        animals[2] = new 조류();
        animals[3] = new 고래();
        animals[4] = new 박쥐();
        animals[5] = new 참새();
        animals[6] = new 펭귄();
        
        for (int index = 0; index < animals.length; index ++) {
        	animals[index].showMe();
        }
    }
}

상속은 is a 관계?

흔히 상속은 is a 관계를 만족해야 한다고 한다.

하위 클래스 is a 상위 클래스

하위 클래스는 하나의 상위 클래스라는 뜻인데, 이는 자칫 객체와 클래스의 관계로 오해될 수 있다.

그러므로 is a보다는 is a kind of 관계라는 표현이 더 적합하다.

다중 상속과 인터페이스

자바는 다중 상속 기능을 지원하지 않는 대신 인터페이스를 도입하였다.

다중 상속

다중 상속이란?
: 한 클래스가 한 번에 두 개 이상의 클래스를 상속받는 경우

그 이유 중 하나가 다이아몬드 문제 때문이다.

그림 속 상황을 코드로 표현하면 아래와 같다. (실제로는 Child 클래스에서 Father 클래스와 Mother 클래스를 동시에 상속할 수 없다)

// Person.java
public abstract class Person {
	public abstract void speak();
}

// Father.java
public class Father extends Person {
	@Override
	public void speak() {
    	System.out.println("speak implementation of Father");
    }
}

// Mother.java
public class Mother extends Person {
	@Override
	public void speak() {
    	System.out.println("speak implementation of Mother");
    }
}

// Child.java
public class Child extends Father, Mother {
	public void test() {
    	speak();
    }
}

위 경우 speak() 메서드를 Father 클래스에서도 구현하고, Mother 클래스에서도 구현했기 때문에 Child는 어떤 speak() 메서드를 호출해야 하는지 알 수 없다.

인터페이스

인터페이스는 is able to 관계이다.

구현 클래스 is able to 인터페이스
(구현 클래스는 인터페이스 할 수 있다.)

위에서 예시를 들었던 상속도에 인터페이스를 적용해보자.

이 때 두 가지를 알아두면 좋다.

  • 상위 클래스는 하위 클래스에세 물려줄 메서드가 풍성할수록 좋다. (리스코프 치환 원칙)
  • 인터페이스는 구현을 강제할 메서드의 개수가 적을수록 좋다. (인터페이스 분할 원칙)

이는 추후 다룰 객체 지향 설계 5원칙과 연관된다.

이제 인터페이스를 사용하여 상속도를 코드로 구현해보자.

// 동물.java
package inheritance02;

public class 동물 {
	String myClass;
    
    동물 () {
    	myClass = "동물";
    }
    
    void showMe() {
    	System.out.println(myClass);
    }
}

// 날수있는.java
package inheritance02;

public interface 날수있는 {
	void fly();
}

// 헤엄칠수있는.java
package inheritance02;

public interface 헤엄칠수있는 {
	void swim();
}

// 포유류.java
package inheritance02;

public class 포유류 extends 동물 {
	포유류() {
    	myClass = "포유류";
    }
}

// 조류.java
package inheritance02;

public class 조류 extends 동물 {
	조류() {
    	myClass = "조류";
    }
}

// 고래.java
package inheritance02;

public class 고래 extends 포유류 {
	고래() {
    	myClass = "고래";
    }
    
    @Override
    public void swim() {
    	System.out.println(myClass + "수영 중 어프");
    }
}

// 박쥐.java
package inheritance02;

public class 박쥐 extends 포유류 {
	박쥐() {
    	myClass = "박쥐";
    }
    
    @Override
    public void fly() {
    	System.out.println(myClass + "나는 중 슈웅");
    }
}

// 참새.java
package inheritance02;

public class 참새 extends 조류 {
	참새() {
    	myClass = "참새";
    }
    
    @Override
    public void fly() {
    	System.out.println(myClass + "나는 중 짹짹");
    }
}

// 펭귄.java
package inheritance02;

public class 펭귄 extends 조류 {
	펭귄() {
    	myClass = "펭귄";
    }
    
    @Override
    public void swim() {
    	System.out.println(myClass + "수영 중 푸악");
    }
}

// Driver.java
package inheritance02;

public class Driver {
	public static void main(String[] args) {
    	날수있는 날라리1 = new 박쥐();
        날라리1.fly();
        
        날수있는 날라리2 = new 참새();
        날라리2.fly();
        
        헤엄칠수있는[] 맥주병들 = new 헤엄칠수있는[2];
        맥주병들[0] = new 고래();
        맥주병들[0] = new 펭귄();
        
        for (헤엄칠수있는 맥주병 : 맥주병들) {
        	맥주병.swim();
        }
    }
}

상속과 T 메모리

// Animal.java
package inheritance03;

public class Animal {
	public string name;
    
    public void showName() {
    	System.out.println("안녕 나는 %s야, 반가워\n", name);
    }
}

// Penguin.java
package inheritance03;

public class Penguin extends Animal {
	public string habitat;
    
    public void showHabitat() {
    	System.out.println("%s는 %s에 살아.\n", name, habitat);
    }
}

// Driver.java
package inheritance03;

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";
        
        pingu.showName();
        // pingu.showHabitat();
        
        // Penguin happyfeet = new Animal();
    }
}

Penguin 클래스의 인스턴스뿐만 아니라, Animal 클래스의 인스턴스도 함께 힙 영역에 생겼다.
(그림에서는 생략됐지만, 모든 클래스의 최상위 클래스인 Object 클래스의 인스턴스도 함께 생성되었다.)

하위 클래스의 인스턴스가 생성될 때 상위 클래스의 인스턴스도 함께 생성된다.

pororo와 다르게 pingu 객체 참조 변수가 Animal 인스턴스를 가리키고 있는 것을 볼 수 있다.

// 5번째 줄
Penguin pororo = new Penguin();

// 13번째 줄
Animal pingu = new Penguin();

3️⃣ 다형성

다형성의 가장 기본은 오버라이딩(overriding)과 오버로딩(overloading)이다.

  • overide: 같은 메서드 이름, 같은 인자 목록으로 상위 클래스의 메서드를 재정의
  • overload: 같은 메서드 이름, 다른 인자 목록으로 다수의 메서드를 중복 정의

오버라이딩과 오버로딩이 헷갈리 때는 '타다(ride)'와 '적재하다(load)'의 차이를 생각하자.
위로 쌓을 것인가? 아래로 쌓을 것인가?

코드로 예시를 들어보자

// Animal.java
package polymorphism01;

public class Animal {
	public string name;
    
    public void showName() {
    	System.out.println("안녕 나는 %s야, 반가워\n", name);
    }
}

// Penguin.java
package polymorphism01;

public class Penguin 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 yourName) {
    	System.out.println("%s 안녕, 나는 %s야", yourName, name);
    }
}

// Driver.java
package polymorphism01;

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();
    }
}

// 결과
// 내 이름은 알아서 뭐하게요?
// 초보람보 안녕, 나는 뽀로로라고 해
// 뽀로로는 남극에 살아
// 내 이름은 알아서 뭐하게요?

다형성과 T 메모리

위 예제 코드로 T 메모리 구조를 살펴보자.

10번째 줄의 pororo.showName();을 실행하면 Penguin 객체에서 재정의한 showName() 메서드가 호출된다.
11번째 줄의 pororo.showName("초보람보");을 실행하면 중복 정의한 showName(yourName) 메서드가 호출된다.

17번째 줄의 pingu.showMe() 메서드가 실행되면 Penguin 객체에 의해 재정의된 showMe() 메서드가 실행된다.

상위 클래스 타입의 객체 참조 변수를 사용하더라도 하위 클래스에서 오버라이딩한 메서드가 호출된다는 사실을 기억하자.

오버로딩은 함수명 하나로 인자 목록만 다르게 하여 다수의 함수를 구현한 효과를 낼 수 있다.
오버라이딩도 하위 클래스가 재정의한 메서드를 알아서 호출해 줌으로써 형변환이나 instanceof 연산자를 써서 하위 클래스가 무엇인지 신경쓰지 않아도 된다.

오버로딩과 오버라이딩을 통해 다형성을 제공하여 개발자가 프로그램을 작성할 때 편리하게 할 수 있다.


4️⃣ 캡슐화 (정보 은닉)

자바에서 정보 은닉이라고 하면 접근 제어자인 private, [default], protected, public과 접근자 및 설정자 메서드가 생각날 것이다.

  • private - 외부에 공개되지 않으며, 외부에서 직접 접근할 수 없다.
    • 해당 객체의 public 메서드를 통해서만 접근 가능
  • public - 외부로 공개되며, 해당 객체를 사용하는 프로그램 어디에서나 접근 가능
  • default - 같은 클래스의 멤버와 같은 패키지에 속하는 멤버에서만 접근 가능
  • protected - 아래와 같은 영역에서 접근할 수 있다.
    • 해당 멤버를 선언한 클래스의 멤버
    • 해당 멤버를 선언한 클래스가 속한 패키지의 멤버
    • 해당 멤버를 선언한 클래스를 상속받은 자식 클래스의 멤버
접근 제어자같은 클래스의 멤버같은 패키지의 멤버자식 클래스의 멤버그 외의 영역
publicOOOO
protectedOOOX
defaultOOXX
privateOXXX
  • 상속을 받지 않았다면 객체멤버는 객체를 생성한 후 객체 참조 변수를 이용해 접근해야 한다.
  • 정적멤버는 클래스명.정적멤버 형식으로 접근하는 것을 권장한다.
    • 사람.인구수(O), 홍길동.인구수(X)

참조 변수의 복사

  • 기본 자료형 변수는 값을 값 자체로 판단한다.
  • 참조 자료형 변수는 값을 주소(포인터)로 판단한다.
  • 기본 자료형 변수를 복사할 때, 참조 자료형 변수를 복사할 때 일어나는 일은 같다. (= 가지고 있는 값을 그대로 복사해서 넘겨 준다.)

☘️ Reference

profile
[~2023.04] 블로그 이전했습니다 ㅎㅎ https://leeeeeyeon-dev.tistory.com/

0개의 댓글