2022. 10. 2. 22:44ㆍTIL💡/Design Pattern
객체지향에서의 상속이란 객체가 다른 객체를 상속받아 상속받은 객체의 요소를 사용하는 것을 의미한다.
이 때 객체를 상속받은 객체는 자식, 상속된 객체는 부모라 칭한다.
자식 객체는 상속된 부모 객체의 은닉화 구성에 따라 정해진 변수, 메소드에 접근할 수 있다.
또한 부모 객체가 추상 객체일 경우 추상 메소드
와 오버라이딩
을 통해 부모 객체의 메소드를 구현하거나 다룰 수 있다.
추상 객체
추상 객체는 하나 이상의 추상 메소드를 포함하는 객체다.
abstract public class Main
{
// 메소드
}
JAVA로 표현한 추상 클래스는 위와 같으며, 클래스의 맨 앞에 abstract
키워드를 적어 해당 객체가 추상 객체임을 표현할 수 있다.
추상 메소드
추상 메소드는 자식 객체에서 구현해야하는 메소드다.
abstract public class Main
{
public void normalMethod()
{
System.out.println("일반 메소드");
}
abstract public void abstractMethod();
}
위는 JAVA로 표현한 추상 객체다. normalMethod()은 일반적인 메소드고, abstractMethod()는 추상메소드다.
추상메소드는 일반적인 메소드와 큰 차이가 있는데, 메소드의 동작이 기술되어있지 않다.
추상 메소드의 구현은 자식 객체가 담당하며, 아래 단계에서 이루어진다.
- 추상 객체의 인스턴스 생성 시
- 추상 객체를 상속받을 시
일반적인 메소드는 자신의 객체에서 선언되어있다.
하지만 추상 메소드의 경우, 추상 객체를 할당받으려는 객체에서 선언되어있다.
이 경우 어떤 메리트가 있을까?
예를 들어, 부모 객체 Main과 이를 상속받은 자식 객체 Sub가 있다고 가정하자.
만약 동작 구조 상 abstractMethod()에서 자식 객체의 변수나 메소드를 사용해야만 한다면?
normalMethod()처럼 동작이 이미 부모 객체에 선언되는 경우 자식 객체의 요소를 반영하기가 매우 어렵다. 인스턴스를 생성하는 방법도 있겠찌만 어떤 객체를 상속받을지 알 수 없는 경우, 예상되는 객체의 인스턴스를 전부 할당받아놓는 게 아니라면 불가능에 가깝다. 그리고 이 방법의 경우 메모리 낭비가 너무 심해진다.
반면 abstractMethod() 같은 추상 메소드의 경우 자식 객체에서 구현되기 때문에 자식 객체의 변수나 메소드에 직접적으로 접근할 수 있다. 때문에 자식 객체의 요소를 활용해서 동작을 구현해야 할 경우, 해당 메소드를 추상으로 정의하면 자식 객체의 특성에 맞게 구현하기 용이하다.
추상 메소드 구현 - 인스턴스 생성 시
public class Sub
{
public void run()
{
Main main = new Main()
{
@Override
public void abstractMethod()
{
System.out.println(text());
}
}
}
private String text()
{
return "Sub 객체의 요소";
}
}
원래대로라면 abstractMethod() 메소드는 Sub 객체의 text()에 접근할 수 없다.
text()는 private 접근제어자를 가지기 때문이다.
하지만 추상 메소드의 경우 구현이 Sub에서 이루어지기 때문에 Sub의 모든 요소에 직접적으로 접근할 수 있다.
즉 private 메소드까지 전부 접근 가능하다.
추상 메소드 구현 - 상속 시
public class Sub extends Main
{
@Override
public void abstractMethod()
{
System.out.println(text());
}
private STring text()
{
return "자식 객체 Sub의 요소";
}
}
부모 객체에 추상 메소드가 있을 경우, 자식 객체는 이를 반드시 오버라이딩해야 한다.
그렇지 않을 경우 컴파일 오류를 일으킨다.
마찬가지로 메소드의 구현이 자식 객체에서 이루어지므로, 자식 객체의 모든 요소에 접근할 수 있다.
추상 메소드는 이처럼 구현의 주체를 자식 객체에게 전가함으로써, 자식 객체의 요소에 제한 없이 접근할 수 있다.
import java.util.Date;
/**
* 컴퓨터 추상 클래스
*
* @author RWB
* @since 2021.08.06 Fri 21:19:19
*/
abstract public class Computer
{
private final String OS;
/**
* Computer 생성자 함수
*
* @param os: [String] OS 이름
*/
public Computer(String os)
{
this.OS = os;
}
/**
* 시작 함수
*/
public void startup()
{
System.out.println(new StringBuilder().append(OS).append(" - started at ").append(new Date().toString()));
}
/**
* 종료 함수
*/
public void shutdown()
{
System.out.println(new StringBuilder().append(OS).append(" - shutdown at ").append(new Date().toString()));
}
/**
* 동작 추상 함수
*/
abstract public void run();
}
여기 Computer라는 추상 객체가 존재한다. 이 객체는 OS라는 상태와 startup, shutdown, run 이라는 동작을 가진다.
이 중 run은 좀 특별한데, 동작은 적혀있으나, 어떤 식으로 동작하는지에 대한 명세는 정해져있지 않다.
이는 추상 객체의 특징 중 하나로, 추상 객체는 하나 이상의 추상 메서드를 포함할 수 있다.
추상 메서드는 구현되지 않은 메서드로, 동작의 개념 정도로만 이해하면 된다.
추상 메서드의 구현은 해당 객체를 상속받은 자식 객체에서 이루어진다.
즉, run 추상 메소드는 자식마다 제각각으로 구현된 동작을 수행한다.
아래의 두 클래스 Asus와 Dell은 Computer 추상 클래스를 상속받은 자식 클래스이다.
/**
* ASUS 컴퓨터 클래스
*
* @author RWB
* @since 2021.08.06 Fri 21:24:50
*/
public class Asus extends Computer
{
/**
* Asus 생성자 함수
*
* @param os: [String] OS 이름
*/
public Asus(String os)
{
super(os);
}
/**
* 동작 함수
*/
@Override
public void run()
{
System.out.println("ASUS 작업 수행");
}
}
/**
* DELL 컴퓨터 클래스
*
* @author RWB
* @since 2021.08.06 Fri 21:26:46
*/
public class Dell extends Computer
{
/**
* Dell 생성자 함수
*
* @param os: [String] OS 이름
*/
public Dell(String os)
{
super(os);
}
/**
* 시작 함수
*/
@Override
public void startup()
{
super.startup();
System.out.println("시스템 안정화 수행");
}
/**
* 종료 함수
*/
@Override
public void shutdown()
{
System.out.println("시스템 프로세스 정리 수행");
super.shutdown();
}
/**
* 동작 함수
*/
@Override
public void run()
{
System.out.println("DELL 작업 수행");
}
}
Asus와 Dell 모두 Computer를 상속받았음을 확인할 수 있다. 또한 모두 run 함수가 제각각 구현된 것 역시 확인할 수 있다.
그런데 Asus와 달리 Dell은 부팅 시와 종료 시 각각 시스템의 안정성을 위한 사전/후 작업이 추가됐다.
이러한 사전/후 작업을 구현하기 위해 startup, shutdown을 오버라이딩한다.
이 과정을 통해 시작과 종료 함수에 각각 원하는 동작을 추가한다.
super?
자식 클래스에서 부모 클래스를 호출할 때 super 키워드를 이용해 호출한다.
Dell의 오버라이딩 메소드 동작에서 활용됨을 알 수 있다.
super.shutdown()은 부모 클래스 Computer의 메소드인 shutdown()을 호출한다.
정리
객체지향은 모든 객체의 모듈화
를 추구한다.
좋은 모듈화는 캡슐화, 은닉화가 적절히 구현되고 유지되는 것을 지향한다.
하지만 포장이 견고하면 뜯기 어렵듯이, 탄탄한 모듈화는 모듈이 경직된다.
재사용의 범위가 제한되는 것 뿐만 아니라, 이를 이용한 확장 또한 어려울 것이다.
만약 객제지향에 이 두 개념만 있었다면 개발자는 재상용성과 모듈화를 적절히 타협하며 객체를 구현했을 것이다.
하지만 상속이라는 개념의 존재로 인해 객체에 지정된 모듈화를 전혀 해치지 않으면서 재사용성, 확장성을 보장받을 수 있다.
객체지향의 모듈화로 인한 딜레마를 상쇄하는 키치한 개념이 아닐 수 없다.
물론 객체지향 중에서도 매우 어려운 개념이지만, 이를 잘 이해하면 조금 더 객체지향다운 코드를 짤 수 있을 것이다.
'TIL💡 > Design Pattern' 카테고리의 다른 글
[OOP] 객체지향 5원칙(SOLID) - 단일 책임 원칙(Single Responsibility Principle) (0) | 2022.10.03 |
---|---|
[OOP] 객체지향의 특징 - 다형성(Polymorphism) (0) | 2022.10.03 |
[OOP] 객체지향의 특징 - 캡슐화(Encapsulation)와 정보 은닉 (0) | 2022.10.02 |
[OOP] 객체 지향 (Object Oriented Programming) (0) | 2022.10.01 |
[컴퓨터구조] RISC, CISC 차이 (0) | 2022.09.30 |