본문 바로가기
Backend/Java & Kotlin

[Java] 객체 지향 4대 특징-1 (상속, 캡슐화)

by Hyeri.dev 2024. 1. 15.
💡 특징에 대해 처음부터 끝까지 설명된 글이 아닙니다. 
인프런 김영한님의 실전 자바 - 기본편을 바탕으로 제가 알지 못했던 부분, 헷갈렸던 부분, 익숙해져야 하는 부분 등을 위주로 정리한 내용이니 참고 부탁드립니다.

객체 지향 4대 특징으로는 상속, 캡슐화, 추상화, 다형성이 있다. 이 중에서 가장 중요한 것은 다형성이며 다형성을 이해함으로서 상속과 추상화를 이해할 수 있고 더 나아가 여러 가지 디자인 패턴 등을 이해할 수 있다.

상속

상속은 말 그대로 나의 특성을 어떤 클래스에게 물려주어 사용할 수 있게 하는 특징이다. 특성을 물려주는 클래스를 부모 클래스, 특성을 물려받는 클래스를 자식 클래스라고 한다.

 

클래스의 인스턴스를 생성할 때, 해당 메모리에 접근할 수 있는 메모리 주소를 인스터스 명에 저장한다. 그리고 해당 메모리 주소를 통해 인스턴스 정보에 접근할 수 있다. 그렇다면 상속을 받은 클래스의 경우에는 어떤 식으로 부모 클래스의 정보에 접근할 수 있는 것일까?

 

아래와 같이 클래스를 설계했다고 하자.

// 부모 클래스
public class Car {
	public void move() {
    	System.out.print("차가 움직입니다.");
    }
}

// 자식 클래스
public class ElectricCar extends Car {
	public void charge() {
    	System.out.print("차를 충전합니다.");
    }
}
Car car = new Car(); // 메모리 주소: X001

car.move();          // 메서드 호출

부모 클래스인 Car의 인스턴스를 생성하여 메서드를 호출하면 아래 그림과 같이 메모리 주소를 통해 인스턴스 메모리에 접근하여 메서드를 호출한다.

Car를 상속받은 자식 클래스인 ElectricCar의 인스턴스를 생성해 메서드를 호출하면 어떻게 될까?

ElectricCar eCar = new ElectricCar();   // 메모리 주소: X002

eCar.charge();                          // 메서드 호출

 

Car의 인스턴스와 마찬가지로 메모리 주소를 통해 인스턴스 메모리에 접근하여 해당 메서드를 호출한다. 그런데 그림 상으로는 eCar 객체 주소를 따라가면 부모 클래스인 Car와 자신의 클래스인 ElectriCar의 정보가 함께 있는 것을 알 수 있다.

상속과 메모리 구조

자식 클래스의 인스턴스를 생성하게 되면 인스턴스의 메모리 주소에 부모 클래스와 자식 클래스의 정보가 공존하고 있다. 그러므로 단순하게 상속은 부모 클래스의 정보를 물려 받는다는 개념이 아닌 포함하여 객체를 생성한다고 보는 편이 메모리 구조를 이해하기에 더 올바르다. 

💡
상속받지 않은 클래스와 상속을 받은 클래스 중에 어떤 클래스의 인스턴스가 메모리를 더 사용할까?
당연하게 상속을 받은 클래스이다. 당연함. 내 정보뿐만 아니라 내 부모 클래스의 정보도 같이 저장하고 있음

 

여기서 주의해야 할 점은 자식 클래스의 인스턴스 메모리 안에 부모 클래스 정보가  한 공간에 공존하고 있지만 서로 구분되어져 있기 때문에 사용에 접근 제어자와 같은 키워드 사용에 주의해야 한다.

상속 관계에 있는 인스턴스의 메서드를 호출할 경우

상속 관계에서는 부모 클래스의 메서드를 자식 클래스에서 오버라이딩해서 사용할 수 있다. 

// 부모 클래스
public class Car {
	public void move() {
    	System.out.print("차가 움직입니다.");
    }
}

// 자식 클래스
public class ElectricCar extends Car {
	@Override
	public void move() {
    	System.out.print("전기차가 움직입니다.");
    }
    
    public void charge() {
    	System.out.print("차를 충전합니다.");
    }
}

 

 

인스턴스의 메서드를 호출하거나 변수의 값을 가져올 때 기본적으로 해당 인스턴스 타입에 맞는 클래스를 우선 탐색한다.

만약 ElectricCar 타입의 eCar 변수가 오버라이딩된 move() 메서드를 호출하면 어떻게 될까? 위와 마찬가지로 본인 타입을우선 탐색하여 존재하면 해당 메서드를 호출한다. 

본인 타입을 우선 탐색한 후 존재하지 않는다면 부모 클래스로 올라가서 탐색하고 또 존재 하지 않는다면 상위 부모 클래스를 탐색한다. 최상위 부모 클래스를 탐색한 후에 해당 메서드가 존재하지 않는다면 컴파일 오류가 발생한다.

오버라이딩 불가 조건

  • final : 불변(재정의 금지)를 의미하는 키워드이므로 오버라이딩이 불가하다.
  • private : 외부 접근이 불가능하다는 의미의 키워드이므로 오버라이딩이 불가하다.
  • static : 클래스 레벨에서 동작하기 때문에 인스턴스를 생성해 호출해야 하는 오버라이딩 메서드에는 의미가 없다. 그냥 클래스를 통해 호출하면 되기 때문이다.

캡슐화

캡슐화는 데이터를 보호하고, 보완을 위한 기능으로 외부로부터의 접근을 제한한다는 특징을 가진다.

단편으로 외부는 해당 애플리케이션을 사용하는 클라이언트라고 생각했던 적이 있지만 서버 내부에서 특정 메서드를 호출하거나, 변수의 값을 불러와야 하는 경우도 있기 때문에 외부란 객체의 클래스 밖을 의미한다.

또한, 캡슐화는 접근제어자 키워드를 통해 서버 내에서의 사용뿐만 아니라 추후에 코드를 수정, 변경할 때 개발자들에게 가독성 좋은 코드를 제공할 수 있다.

 

캡슐화를 위해 사용하는 접근제어자는 크게 3가지로 나눌 수 있다.

  • public : 외부(누구나) 접근 가능
  • default, protected : 같은 패키지 내에서 호출 허용
  • private : 내 클래스에서만 접근 가능(외부 접근 허용 x)

여기서 같은 역할을 하는 키워드가 2가지가 존재한다. default, protected는 상속 관계의 여부에 따라 그 기능을 달리한다. 

  • default : 상속 관계와 상관없이 같은 패키지 내에서만 접근이 가능하다.
  • protected : 상속 관계에 있다면 패키지가 달라도 호출이 가능하다.

이렇기 때문에 상속 관계에서 접근 제어자 키워드 사용은 신중을 가해야 한다. 본인 타입에 사용하고자 하는 메서드나 변수가 없다면 부모 타입에서 찾게 되는데 한 공간 내에 자식과 부모가 공존하여도 서로 구분지어 있기 때문에 접근 제어자로 인한 접근이 불가능한 일도 발생하기 때문이다.