Java

[Java] 객체 지향의 4대 개념 추상화/상속/다형성/캡슐화

on1ystar 2024. 7. 13. 21:31
728x90
반응형

세상은 객체(object)들로 이루어져 있다. 여기서 객체는 눈으로 보는 것, 머릿속으로 상상되는 모든 것이 될 수 있다. 즉, 사람이 세상을 인지하는 방식대로 프로그래밍하는 것이 객체 지향의 출발이다. 따라서 객체 지향은 이전의 방식(구조적 프로그래밍)보다 사람 지향적인 방법론이라고들 한다.

객체 지향의 대표적인 4대 특성은 아래와 같다.

  • 캡슐화(Encapsulation)
  • 상속(Inheritance)
  • 추상화(Abstraction)
  • 다형성(Polymorphism)

Java는 대표적인 객체지향 프로그래밍 언어다. 객체지향 언어는 기존의 프로그래밍 언어에 몇 가지 객체지향적인 특징들을 추가한, 보다 발전된 형태의 언어다. 위 객체 지향 특성들이 Java에 어떻게 녹아 들어가 있는지 알아보자.

 

추상화 == 모델링 == 클래스

객체 지향에서 의미하는 추상화를 알아보기 전에 사전적 의미를 먼저 찾아보면,

미술에서 추상화(抽象畫)는 대상의 구체적인 형상을 나타낸 것이 아니라 점, 선, 면, 색과 같은 순수한 조형 요소로 표현한 미술의 한가지 흐름이다.

 

컴퓨터 과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.

 

미술에서의 추상화와 컴퓨터 과학에서의 추상화 모두 비슷한 의미의 맥락을 지니고 있다.

미술에서는 특정 사물을 그릴 때 최대한 그 사물과 비슷하게 모든 생김새를 세세하게 묘사하는 것이 아니라, 그 사물을 대표할 수 있는 몇 가지 특징을 뽑아 간단한 조형 요소로 표현하는 것이 추상화라고 할 수 있다.

컴퓨터 과학에서도 마찬가지로 사물이 모듈이나 시스템으로 바뀌었을 뿐이며 이를 표현하는 방식이 그림이 아닌 UML이나 프로그래밍 언어일 뿐이다.

  • 그릴 대상, 사물 → 모듈, 시스템
  • 점, 선, 면, 색과 같은 순수한 조형 요소로 그려서 표현 → 핵심적인 개념 또는 기능을 UML이나 코드로 표현

핵심은 추상화할 대상을 대표할 수 있는 특징을 뽑아내는 것이다.

객체 지향에서는 추상화할 대상이 객체가 된다. 객체(object)실제로 존재하는 것으로 사물이 될 수 있으며 개념이 될 수도 있다. 예를 들어 책상, 의자, 자동차같은 사물이나 수학 공식, 프로그램 에러같은 개념도 모두 객체다.

객체(object)란 실제로 존재하는 것으로 사물 또는 개념이 될 수도 있다.

 

세상에 실제로 존재하는 객체를 컴퓨터의 가상 세계로 옮겨야 하는데, 그 특징들을 모두 옮길 수도, 옮길 필요도 없기 때문에 추상화를 거치게 된다.

만약 사람을 추상화하기 위해 특징들을 뽑아 보자. 얼굴에 있는 입, 코, 눈부터 시작해서 사람의 키, 몸무게, 성격 등 특징들이 너무 많다. 심지어 사람(객체)의 행위들도 추상화의 대상에 포함된다. 따라서 이 사람을 대표할 수 있는 특징들로 간추려 내야 하는데, 이를 위해서는 어떤 기준이 필요하다. 이를 관심 영역(어플리케이션 경계, Application Boundary)이라고 한다.

관심 영역이 병원이라면 사람을 키, 몸무게, 혈액형, 질병, 질환, 진찰 받기 등으로 추상화 할 것이고, 은행이라면 나이, 직업, 연봉, 대기표 뽑기 등으로 추상화하게 된다. 이러한 과정을 모델링이라 한다.

추상화란 구체적인 대상(객체)로부터 관심 영역(어플리케이션 경계, Application Boundary)에 해당하는 특징만을 뽑아내는 것 = 모델링

 

그리고 위와 같은 특징들을 가진 사람들이 병원에서는 환자라고 분류되고, 은행에서는 고객이라고 분류되며 이를 객체 지향에서는 클래스라고 한다.

클래스는 분류로써, 같은 속성과 기능을 가진 객체들을 총칭하는 개념이다.

 

클래스의 속성은 다른 말로 멤버 변수, 상태 등이 될 수 있고, 기능은 메서드, 함수, 행위 등으로 불릴 수 있다.

다시 정리해보면 아래와 같다.

  • 객체 : 실제로 존재하는 것으로 사물 또는 개념, 구체적인 것
  • 클래스 : 같은 속성과 기능을 가진 객체들의 집합, 분류, 추상적(일반적)인 것
  • 추상화 : 객체로부터 관심 영역에 해당하는 특징만을 뽑아내는 것 = 모델링

객체 지향에서 추상화란 객체를 모델링하는 것이며, 그 결과는 클래스다.

Java에서는 클래스를 class 키워드로 나타낸다. 그리고 class로부터 객체를 만드는 것을 인스턴스화 한다고 하며 보통 객체보단 인스턴스라는 용어를 사용한다.

그렇다면 반대로 인스턴스(객체)들로부터 공통 속성을 뽑아내어 클래스로 모델링하는 것은 추상화가 된다.

클래스 → 인스턴스(객체) ⇒ 인스턴스화(구체화)

인스턴스(객체) → 클래스 ⇒ 추상화

상속 == 재사용 + 확장

처음 class를 배우면 첫 번째로 맞닥뜨리는 관문이 상속이라고 생각한다. 상속이라는 단어가 주는 느낌 때문에 가장 먼저 부모 클래스와 자식 클래스를 떠올리면 현실 세계의 부모-자식 관계를 생각하게 된다. 하지만 처음에 이렇게 이해하려고 하면 당연히 혼란스럽다. 상속 관계에서 무조건 만족해야 할 문장이 있다.

  • 자식 클래스는 부모 클래스다

예를 들어 아버지와 딸의 관계를 생각해 보자.

  • 딸은 아버지다.

딸이 아버지의 일부 특성들을 상속받을 수는 있겠지만 그렇다고 딸이 아버지는 아니다. 때문에 이러한 예시는 틀렸으며 상속을 오해하게 만드는 원인이다. 올바른 예시는 포함관계에 따른 분류다.

  • 포유류는 동물이다.
  • 코끼리는 포유류다.
  • 사자는 동물이다.

따라서 부모 클래스 - 자식 클래스라는 용어도 슈퍼 클래스 - 서브 클래스 또는 상위 클래스 - 하위 클래스로 바꿔 부르는 편이 오해를 줄일 수 있다.

상속은 ‘상속’이라는 단어로 이해하려 하지 말고 재사용 + 확장이라고 이해해는 것이 더 정확하며 편하다.

상속은 재사용 + 확장이다.

 

사실 Java가 상속을 지원하는 키워드도 inheritances가 아닌 extends다. 즉, 상속보다는 확장이 더 올바른 개념이다.

포유류는 동물의 모든 특성을 재사용하면서도 포유류만의 특징을 확장하고 있다.

또한, 코끼리는 포유류의 모든 특성을 재사용하면서도 코끼리만의 특징을 확장하고 있다.

한 가지 더 중요한 개념은 객체 지향에서 클래스는 위에서 설명한 바와 같이 추상화의 결과물로써 분류이며, 같은 속성과 기능을 가진 객체들을 총칭하는 개념이다. 따라서 흔히들 말하는 상속에서의 is a 관계 보다는 is a kind of 관계를 만족해야 한다고 하는 편이 더 정확하다.

상속은 is a kind of 관계를 만족해야 한다.

 

  • 포유류는 하나의 동물이다.
  • 사자는 하나의 포유류다.

위 표현보다는,

  • 포유류는 동물의 한 분류다.
  • 사자는 포유류의 한 분류다.

위 표현들이 더 정확하고 자연스럽다. 즉, 상위 클래스 쪽으로 갈수록 추상화된 것이며, 하위 클래스 쪽으로 갈수록 구체화된 것이다.

상속을 올바르게 사용하면 효울적인 측면에서 코드의 재사용량을 상당히 줄일 수 있다. 상위 클래스에 많은 특징들을 추상화 할 수록 하위 클래스들이 상속받는 특징들이 많아진다. 이는 추후에 유지보수 측면에서도 큰 도움이 된다.

  • 모든 클래스들의 최상위 클래스 Object
  • 하위 클래스의 인스턴스가 생성될 때 상위 클래스의 인스턴스도 함께 생성
  • 상위 클래스의 참조변수로 하위 클래스의 인스턴스를 참조 가능
  • 다중 상속 지원 x, 대신 인터페이스 지원

위 특징들은 Java에서 상속을 사용할 때 알아두면 좋을 특성들이다.

 

다형성 == 사용편의성

객채 지향에서 다형성은 여러 가지 형태를 가질 수 있는 능력을 의미한다. Java에서의 다형성은 대표적으로 참조 변수에서 찾을 수 있다.

참조 변수의 다형성

Java에서는 상위 클래스 타입의 참조 변수로 하위 클래스의 인스턴스를 참조할 수 있다.

class Tv{
	boolean power;
	int channel;

	void power()    { power = !power; }
	void channelUp()    { ++channel; }
	void channelDown()    { --channel; }
}

class IpTv extends Tv {
	String ip;
	void showIp()  { /* 생략 */ }
}

위와 같은 예제가 있다고 하자. IpTvTv 클래스를 상속받고 있다. 따라서 아래와 같은 코드가 가능하다.

  • IpTv c = new IpTv();
  • Tv t = new IpTv();

당연히 의미적으로 IpTv는 Tv이기 때문에 Java에서도 이를 문법으로 구현해 놓은 것이다. 이처럼 객체 지향은 사람이 이해하는 그대로를 코드로 구현할 수 있도록 하고 한다.

그렇다면 IpTv 인스턴스를 IpTv로 참조하는 것과 Tv로 참조하는 것은 어떤 차이가 있을까?

Tv 타입의 참조 변수로는 Tv 클래스의 멤버 변수와 메서드는 사용할 수 있지만 IpTv 클래스의 멤버 변수 및 메서드는 사용할 수 없다. 즉, 참조 변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

참조 변수의 타입을 리모콘에 비유해 보면 이해하기 쉽다.

Tv 리모콘으로는 Tv의 기능을 동작할 수 있는 버튼 밖에 없기 때문에 IpTv의 기능을 사용할 수 없을 것이다. 반대로 IpTv 리모콘으로는 IpTv의 기능을 모두 사용할 수 있다. 여기서 주의해야 할 점은 리모콘을 사용하는 대상(인스턴스)이 IpTv라는 것이다. 만약 대상(인스턴스)이 Tv라면 IpTv 리모콘으로 아무리 IpTv 기능 버튼을 눌러도 동작할 리가 없다. 그래서 Java에서는 하위 클래스 타입의 참조 변수로 상위 클래스 타입의 인스턴스를 참조할 수 없게 했다.

상위 클래스 타입의 참조 변수로 하위 클래스 타입의 인스턴스를 참조할 수 있다. 반대로 하위 클래스 타입의 참조 변수로 상위 타입의 인스턴스를 참조할 수는 없다.

 

형변환 역시 서로 상속 관계에 있는 클래스 사이에서는 가능하다. 여기서도 주의해야 할 점은 참조 변수가 가리키는 인스턴스의 하위 클래스 타입으로 형변환은 허용하지 않는다.

형변환은 참조 변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것이 아니다. 따라서 상위 클래스의 인스턴스를 하위 클래스 타입으로 형변환을 하게 될 경우, 해당 인스턴스에는 존재하지 않는 하위 클래스의 멤버들을 사용할 우려가 있기 때문에 Java에서는 애초에 금지하고 있다.

오버라이딩(Overriding)

다형성의 또 다른 예로 오버라이딩이 있다. 오버라이딩은 상위 클래스로부터 상속받은 메서드의 내용을 변경하는 것이다. 이를 위해서는 다음과 같은 조건을 만족해야 한다.

  • 이름이 같아야 한다.
  • 매개변수가 같아야 한다.
  • 반환타입이 같아야 한다.

즉, 함수의 선언부가 같아야 오버라이딩이 성립된다. 오버라이딩을 할 때 주의해야 할 점을 아래 예제를 통해 알아보자.

class Animal {
	public String name;

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

class Penguin extends Animal {
	public String name;
	
	public void showName() {
		System.out.printf("안녕 나는 %s 펭귄이야. 반가워\\n", name);
	}
}

public class Driver {
	public static void main(String[] args) {
		Animal animal1 = new Animal();
		Penguin pororo = new Penguin();
		Animal pingu = new Penguin();

		animal1.name = "동물1";
		pororo.name = "뽀로로";
		pingu.name = "핑구";	

		animal1.showName();
		pororo.showName();
		pingu.showName();
	}
}

실행 결과

안녕 나는 동물1야. 반가워

안녕 나는 뽀로로 펭귄이야. 반가워

안녕 나는 null 펭귄이야. 반가워

animal1.showName()의 결과는 쉽게 예상했을 수 있지만, pingu.showName()의 결과도 올바르게 예상했는지 모르겠다. 이유는 다음과 같다.

상위 클래스 타입의 객체 참조 변수를 사용하더라도 항상 하위 클래스에서 오버라이딩한 메서드가 호출된다. 단, 멤버 변수의 경우 참조 변수의 타입에 따라 달라진다.

 

하위 클래스 PenguinshowName을 오버라이딩했기 때문에 메서드는 하위 클래스(Penguin)의 메서드가 호출된다. 하지만 멤버 변수의 경우 참조 변수 pinguPenguin 인스턴스를 참조하고 있다 하더라도 타입이 Animal이기 때문에 상위 클래스(Animal) 멤버 변수 name에 값 핑구를 저장한다.

따라서 pingu 인스턴스는 Penguin의 인스턴스 메서드 showName을 호출했지만, 사실상 핑구라는 이름은 Animal 인스턴스 변수에 저장했기 때문에 name값이 null로 출력된 것이다.

 

캡슐화 = 정보 은닉

Java에서 캡슐화는 접근 제어자를 통해 지원하고 있다.

  • private : 같은 클래스 내에서만 접근 가능
  • default : 같은 패키지 내에서만 접근 가능
  • protected : 같은 패기지 내에서, 그리고 다른 패키지더라도 상속한 하위 클래스에서는 접근 가능
  • public : 접근 제한이 없음

접근 범위 : public > protected > default > private

 

접근 제어자는 외부로부터 데이터를 보호하며, 클래스 내부적으로 사용되어 외부에는 불필요한 부분을 감추기 위해서 사용된다. 전자의 경우가 일반적으로 객체 지향에서 말하는 캡슐화다. 후자의 경우 프로그램 유지 보수 측면에서 복잡성을 상당히 줄일 수 있으며 이것 역시 캡슐화라고 할 수 있다.

 

References

  • 스프링 입문을 위한 자바 객체지향의 원리와 이해
  • Java의 정석
728x90
반응형