정리/JAVA

상속

포테이토누들 2020. 5. 25. 22:16

상속이란?

상속(Inheritance)이란 상위 개념에서 필요한 개념을 하위 개념에서 그대로 사용하거나, 필요 시 확장해서 사용하는 기능이다. 

 

상위 개념이란 상위 클래스에 있는 필드 또는 메소드를 의미한다.

 

상속의 장점으로는 

  • 하위 클래스에서 상속받은 상위 개념을 상위 클래스에서 제어할 수 있기 때문에 유지보수가 좋아진다.
  • 상위 클래스의 개념을 그대로 또는 확장해서 사용하기 때문에 코드의 재사용성이 좋다.
  • 필요할 때마다 하위 클래스를 생성할 수 있기 때문에 생산성이 좋다.

가 있다.

 

첫 번째 장점인 유지보수는 장점이자 단점이라는 생각도 든다.

 

상속의 단점으로는

  • 하위 클래스가 상위 클래스에게 의존적이기 때문에 상속에 어울리지 않는 환경일 경우 캡슐화를 위배할 수 있다.
  • 상속을 남발할 경우 관계가 복잡해져 유지보수가 어렵다.
  • 상위 클래스에서 오류가 있을 경우, 하위 클래스에서도 동일한 오류가 발생한다.

가 있다.

 

특징으로는

  • extends 키워드를 통해 상속한다.
  • 다중 상속은 불가능하다. 

가 있다.

 

이에 대해 몇 가지 예시를 통해 이해하고자 했다.


유지보수

 

public class AAA {
    public String a = "A 클래스 필드";
    
    public void aa() {
    	System.out.println("AAA 클래스 메소드 aa");
    }
    
    public void aaa() {
    	System.out.println("AAA 클래스 메소드 aaa");
    }
    
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa");
    }
}

 

public class BBB extends AAA {
    public String b = "BBB 클래스 필드";
    
    public void bb() {
    	System.out.println("BBB 클래스 메소드 bb");
    }
    
    public void bbb() {
    	super.aaa();
        super.aaaa();
        System.out.println("BBB 클래스 메소드 bbb");
    }
    
    @Override
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa 재정의");
    }
}

 

AAA 클래스와 AAA 클래스를 상속한 클래스 BBB 가 있다고 가정할 경우,

BBB 클래스의 인스턴스를 생성해서 AAA 클래스의 모든 메소드 및 필드에 접근할 수 있고, 

BBB 클래스 자체의 필드 및 메소드에 접근할 수 있고,

super 키워드를 통해 상위 클래스의 메소드를 그대로 사용할 수 있고, 

오버라이딩을 통해 상위 클래스와 메소드 시그니처가 동일하지만 내부 로직은 다른 메소드를 새로 정의할 수도 있다.

 

이 때 요구사항 등의 이유로 AAA 클래스를 수정했다고 하자. 

 

public class AAA {
    public String a = "A 클래스 필드";
    
    public void aa() {
    	System.out.println("AAA 클래스 메소드 aa");
    }
    
    public void aaa() {
    	System.out.println("AAA 클래스 메소드 aaa 수정");
    }
    
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa 수정");
    }
}

 

이 경우 BBB 클래스 중 aaa()와 aaaa()를 호출하는 bbb() 메소드의 경우, 코드의 수정 없이도 aaa()와 aaaa() 메소드의 수정사항을 그대로 받아들여 이를 적용시킬 수 있다. 

 

물론 BBB 클래스에서 재정의한 aaaa() 메소드는 영향을 받지 않는다.

 

이와 같이 상위 클래스를 상속한 하위 클래스는 일정 부분을 상위 클래스에 의존적으로 처리하기 때문에 상위 클래스에 영향을 받아, 상위 클래스의 코드 수정만으로도 하위 클래스의 로직 변경이 가능하다.

 

그렇기 때문에 상위 클래스의 코드 수정만으로도 수 많은 하위 클래스의 코드를 관리하는 효과를 얻을 수 있다.

 

다만 이러한 장점은  

  • 설계 시 상속을 위한 클래스로 설계한 클래스의 경우
  • 하위 클래스 is a 상위 클래스의 관계를 성립하는 경우 

등의 경우에만 가능하다는 점을 기억해야 할 듯 싶다.

 


코드의 재사용성 

 

코드의 재사용성은 매우 쉽게 이해가 가능했다.

 

public class AAA {
    public String a = "A 클래스 필드";
    
    public void aa() {
    	System.out.println("AAA 클래스 메소드 aa");
    }
    
    public void aaa() {
    	System.out.println("AAA 클래스 메소드 aaa 1");
        System.out.println("AAA 클래스 메소드 aaa 2");
        System.out.println("AAA 클래스 메소드 aaa 3");
        System.out.println("AAA 클래스 메소드 aaa 4");
        System.out.println("AAA 클래스 메소드 aaa 5");
        System.out.println("AAA 클래스 메소드 aaa 6");
        System.out.println("AAA 클래스 메소드 aaa 7");
    }
    
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa");
    }
}

 

대충 했지만, 위와 같이 aaa() 메소드에 매우 복잡한 로직이 있다고 가정하자.

 

public class BBB extends AAA {
    public String b = "BBB 클래스 필드";
    
    public void bb() {
    	System.out.println("BBB 클래스 메소드 bb");
    }
    
    public void bbb() {
    	/*
        System.out.println("AAA 클래스 메소드 aaa 1");
        System.out.println("AAA 클래스 메소드 aaa 2");
        System.out.println("AAA 클래스 메소드 aaa 3");
        System.out.println("AAA 클래스 메소드 aaa 4");
        System.out.println("AAA 클래스 메소드 aaa 5");
        System.out.println("AAA 클래스 메소드 aaa 6");
        System.out.println("AAA 클래스 메소드 aaa 7");
        */
        
    	super.aaa();
        
        super.aaaa();
        System.out.println("BBB 클래스 메소드 bbb");
    }
    
    @Override
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa 재정의");
    }
}

이렇게 수 많은 Systout이 아니라 super.aaa()로 상위 클래스의 메소드를 호출하는 것으로 퉁칠 수 있다.

그렇기 때문에 같은 로직을 다른 클래스에서 수행하는 경우, 코드의 중복을 막고 코드의 재사용성을 높인다. 

 


생산성

 

public class AAA {
    public String a = "A 클래스 필드";
    
    public void aa() {
    	System.out.println("AAA 클래스 메소드 aa");
    }
    
    public void aaa() {
    	System.out.println("AAA 클래스 메소드 aaa");
    }
    
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa");
    }
}

 

public class BBB extends AAA {
    public String b = "BBB 클래스 필드";
    
    public void bb() {
    	System.out.println("BBB 클래스 메소드 bb");
    }
    
    public void bbb() {
    	super.aaa();
        super.aaaa();
        System.out.println("BBB 클래스 메소드 bbb");
    }
    
    @Override
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa 재정의");
    }
}

 

이 상태에서 갑자기 aaa() 메소드와 aaaa() 메소드를 수 없이 호출해야 하는 로직이 필요하다면?

그에 맞는 새로운 하위 클래스를 생성하면 된다.

 

public class CCC extends AAA {
	... 
    
    public void ccc() {
    	super.aaa();
        super.aaa();
        super.aaaa();
        super.aaa();
        super.aaaa();
        
        ...
    }
}

 

이 경우 ccc() 메소드 하나만으로 aaa() 메소드와 aaaa() 메소드를 수 없이 호출하는 행위를 대체할 수 있을 것이다.

또한, 유지보수 시에도 코드 수정이 필요하다면 AAA 클래스만 수정하면 ccc() 메소드의 로직 또한 수정될 것이다.

 

이런 식으로 상속하기에 알맞은 클래스라면, 필요할 때마다 하위 클래스를 생성해 복잡한 로직을 처리할 수 있으므로 생산성이 좋아진다.

 

다음으로 단점이다.

 


캡슐화 위배

 

캡슐화를 위배한다는 것은, 코드 간 상호의존도가 높아짐을 의미한다.

상속을 했다는 것은 하위 클래스의 로직이 상위 클래스의 로직에 영향을 받는다는 의미이기 때문에,

매우 조심히 다뤄야만 한다.

 

그렇기 때문에 처음부터 상속을 목적으로 설계하지 않는 이상, 상속을 사용할 경우에는 심사숙고할 시간이 필요할 것이다.

 

상위 클래스의 코드 수정이 하위 클래스에 치명적이지 않거나, 혹은 오히려 필요한 경우가 아니라면 상속으로 인해 상호의존도가 높아져 비효율적인 코드가 될 것이기 때문이다. 

 

public class Robot {
    public void charge() {
    	System.out.println("충전합니다.");
    }

    public void walk() {
    	System.out.println("걸어갑니다.");
    }
}

 

public class Cat extends Robot {
    public class catWalking() {
    	super.walk();
    }
}

 

상위 클래스 Robot과 하위 클래스 Cat이 있다고 가정하자.

애초부터 이는 올바른 상속이 아니다. 

그렇지만 캡슐화를 위배한다는 예시를 위해 좀 억지로 만들었다.

 

아무튼 catWalking()은 현재 논리적으로 큰 문제는 없다.

아무튼 로봇도 걸어가고, 고양이도 걸어간다는 동작은 가능하기 때문이다.

 

그런데 여기서 기술력이 좋아져서 로봇의 걷는 행위가 뛰어나게 바뀌었다고 가정하자.

 

public class Robot {
    public void charge() {
    	System.out.println("충전합니다.");
    }

    public void walk() {
    	System.out.println("기술 발전으로 인해 기존보다 효율적으로 걸어갑니다.");
    }
}

walk() 메소드가 변경된 로봇의 특징에 맞게 수정되었다.

하위 클래스인 Cat 또한 이에 영향을 받을 것이다.

 

그런데 이는 Cat 클래스에게 전혀 어울리지 않는 내용이다.

상속 관계가 아니여서 서로 관련이 없었다면 큰 문제가 없겠으나, 상속으로 인해 하위 클래스가 상위 클래스에게 의존적인 관계가 되면서 문제가 발생하게 되었다.

 

이러한 문제를 상속 관계를 유지한 채 바꾸기 위해서는

 

public class Cat extends Robot {
    public class catWalking() {
    	System.out.println("고양이가 걸어갑니다.");
    }
}

아예 새로운 메소드를 만든다.

이 경우 상위 클래스에 있는 charge() 메소드나 walk() 메소드에 그대로 접근할 수 있기 때문에, 적합하지 않기는 하지만 catWalking() 메소드에 집중한다면 논리적인 오류는 해결한 상태이다.

 

public class Cat extends Robot {
    @Override
    public void walk() {
    	System.out.println("고양이가 걸어갑니다.");
    }
}

오버라이딩 하는 방법도 있다.

그러나 이 경우 또한 유지보수에 어려운 점이 많다.

walk()라는 메소드 시그니처는 동일한데 하나는 로봇의 동작을 의미하고, 하나는 고양이의 동작을 의미하니 이를 혼동할 가능성이 있다.

 

그렇기 때문에 상속에 어울리지 않는 클래스를 상속 관계로 만들었을 때, 하위 클래스가 상위 클래스에 의존적이기 때문에 캡슐화의 목적 중 의존도를 낮춘다는 의도에 위배되며, 이는 유지보수의 저하로 이어진다.

 


관계가 복잡해짐 

 

public class A {
	...
}
public class B extends A {
	...
}
public class C extends B {
	...
}
public class D extends C {
	...
}

이런 식으로 Z라는 클래스까지 있을 경우, 클래스 Z의 메소드인 zz()라는 이름의 메소드의 로직을 분석하기 위해 A부터 Y까지의 클래스를 살펴봐야한다. 

 

유지보수 시에도 매우 골치아픈 상태가 된다.

zz() 메소드에서 호출한 yy() 메소드의 코드를 수정하기 위해 이와 관련된 xx() 메소드, 그에 관련된 상위 클래스의 메소드, 그에 관련된 상위 클래스의 메소드로 반복해서 마지막에는 aa() 메소드까지 모조리 수정하게 될 수도 있을 것이다.

 

이 경우 차라리 Z 클래스를 따로 선언하는 것이 코드 분석에도 쉽고, 유지보수도 비교적 간단할 것이다.

 

이런 식으로 상속을 남발할 경우, 관계가 매우 복잡해져 유지 보수가 어려워진다는 단점이 생긴다.

 


상위 클래스의 오류를 공유

 

public class AAA {
    public String a = "A 클래스 필드";
    
    public void aa() {
    	System.out.println("AAA 클래스 메소드 aa");
    }
    
    public void aaa() {
    	System.out.println("AAA 클래스 메소드 aaa");
        System.out.println("매우 심각한 오류가 발생");
    }
    
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa");
    }
}

 

public class BBB extends AAA {
    public String b = "BBB 클래스 필드";
    
    public void bb() {
    	System.out.println("BBB 클래스 메소드 bb");
    }
    
    public void bbb() {
    	super.aaa();
        super.aaaa();
        System.out.println("BBB 클래스 메소드 bbb");
    }
    
    @Override
    public void aaaa() {
    	System.out.println("AAA 클래스 메소드 aaaa 재정의");
    }
}

AAA 클래스의 aaa() 메소드에 매우 심각한 오류가 있다면, 이를 호출하는 bbb() 메소드 또한 매우 심각한 오류가 발생할 것이다.

 

이처럼 상위 클래스의 오류를 하위 클래스에 공유하게 된다는 단점을 가지고 있다.

 

정리

상속의 장점

  • 유지보수가 쉽다.
  • 코드의 재사용성이 높아진다.
  • 생산성이 높다.

 

 

상속의 단점

  • 관계가 복잡해진다.
  • 오류를 공유한다.
  • 캡슐화를 위배한다.

 

상속은 객체 지향 프로그래밍의 특징 중 하나이지만, 잘못 사용했을 경우 객체 지향적으로 프로그램을 설계한 의미를 없애버릴 수 있기 때문에 주의해서 사용할 필요가 있어 보인다.

 

또한 상속을 위해 설계를 아무리 잘한다고 하더라도 하위 클래스는 필연적으로 상위 클래스에 의존적이기 때문에, 상속을 한다면 무조건 캡슐화가 위배된다고 생각하고 캡슐화를 위배한다더라도 큰 문제가 없을 정도로 설계를 완벽하게 해야할 것 같다. 

 

개인적으로 프레임워크에서 지정해준 상속 관계가 아니라면 고려사항이 비교적 적은 컴포지션을 사용할 것 같다.

 


해당 글은 개인이 공부하면서 정리한 글이기 때문에 정확하지 않은 내용이 있을 수 있습니다.