GYUD-TECH

[스터디: 객체지향의 사실과 오해] 추상화기법, 상속, 합성 본문

스터디

[스터디: 객체지향의 사실과 오해] 추상화기법, 상속, 합성

GYUD 2024. 2. 1. 20:43

추상화 기법

추상화를 사용하는 이유는 도메인의 복잡성을 단순화하고, 직관적인 모델을 만들 수 있기 때문이다.

이러한 추상화의 기법에는 3가지가 있다.

 

  1. 분류와 인스턴스화
  2. 일반화와 특수화
  3. 집합과 분해

분류와 인스턴스화

객체를 타입으로 분류하는 것을 의미한다.

내가 주문한 아메리카노와 친구가 주문한 아메리카노는 서로 다른 객체이지만 모두 아메리카노로 분류할 수 있다.

객체의 입장에서는 두 객체는 모두 아메리카노 타입의 인스턴스 이다.

이렇게 객체가 타입의 정의에 부합하면 해당 타입으로 분류되며, 이 객체를 타입의 인스턴스 라고 한다.

 


일반화와 특수화

슈퍼타입과 서브타입이 존재하는 포함 관계를 의미한다.

아메리카노와 카페라떼는 모두 커피라는 공통적인 타입으로 일반화 된다.

커피의 입장에서는 아메리카노와 카페라떼는 특수화의 경우이다.

반대로 아메리카노와 카페라떼의 입장에서는 커피로 일반화 한 경우가 된다.

 

상속

책에서는 일반화 특수화 관계 중 상속의 방법으로 서브클래싱과 서브타이핑의 개념을 설명한다.

 

서브타이핑이란, 서브클래스가 슈퍼클래스를 대체할 수 있는 경우를 뜻한다.

반대로 서브클래싱이란, 서브클래스가 슈퍼클래스를 대체할 수 없는 경우를 뜻한다.

 

서프타이핑은 대체가능을 의미하기 때문에 설계의 유연함을 위한 개념이고, 서브클래싱은 단순히 중복을 제거하는 것이 목적이다.

 

SOLID 원칙의 Liskov Substitution Principle에 따르면 서브타입은 언제나 기반 타입으로 교체 가능 해야 한다고 강조한다.

따라서 상속의 목적은 코드의 재사용과 중복 제거가 아니라 서브타이핑에 해당하는 대체가능성이다.

 

 

만약 상속을 서브클래싱의 목적으로 사용하게 된다면, 아래와 같은 문제가 발생할 수 있다.

 

1. 불필요한 기능을 상속받을 수 있다.

Stack<String> stack = new Stack<>();
stack.push("1st");
stack.push("2nd");
stack.push("3rd");

stack.add(0, "4th");

assertEquals("4th", stack.pop());  //오류 발생

Stack 클래스는 Vector 클래스를 상속받기 때문에, Vector 클래스의 add 메서드를 외부에서 사용할 수 있다.

그래서,  add 메서드를 호출하면, stack의 기능과 달리 index값이 추가가 되어서 pop()의 결과가 예상과 다를 것이다.

 

이 때문에 자바 공식문서에도 Stack 클래스보다 Deque 클래스를 사용할 것을 권장한다.

 

 

2. 설계가 유연하지 않을 수 있다.

상속은 부모 클래스의 내부 구현을 자식 클래스가 알아야 하기 때문에 부모와 자식 클래스 사이의 결합도가 높아진다.

또한 상속 관계는 컴파일 타임에 결정되어서 코드를 실행하는 중에 변경할 수 없어 유연한 설계를 할 수 없다.

 

이로 인해 상속 관계에서 다양한 조합이 필요한 상황이 오면, 조합의 수 만큼 새로운 클래스를 추가해줘야 한다.

이로 인해 오히려 코드의 양이 많아지거나, 많은 수의 클래스를 추가해야하는 클래스 폭발 문제가 발생할 수 있다.

 

 

3. 부모클래스의 수정이 자식 클래스에게도 영향을 미친다.

부모 클래스에서 특정 필드를 추가한다면, 자식 클래스의 필드도 추가해줘야 한다.

또한 부모클래스에 오류가 있다면, 자식클래스에도 오류가 그대로 전달된다.

 

이렇게 단순히 코드 재활용의 목적으로 설계를 하고 싶을 때는 상속이 아닌 합성을 사용해야 한다.

 

 

합성

합성은 상속과 같은 확장이 아니라, 필드로 클래스의 인스턴스를 참조하는 것이다.

상속이 Is-A 관계라면, 합성은 Has-A 관계라고 정의할 수 있다.

 

커피에는 물이 들어가야하지만, 상속관계를 맺기에는 애매하기 때문에 수평적인 합성관계로 설계한다.

class Coffee{

    private Water water;
   
    Coffee(Water water){
    	this.water = water;
    }
}

class Water {

    private double temperature;
    
    Water(double temperature){
    	this.temperature = temperature;
    }
}

이렇게 다른 객체가 필요하다고 해서 상속을 하는 것이 아니라 변수로써 저장하여 사용하는 것이 합성의 방법이다.

 

앞서 소개한 Stack과 Vector의 관계를 합성을 사용하면 아래와 같이 작성할 수 있다.

public class Stack<E> {
    private Vector<E> elements = new Vector<>(); // 합성
    
    public E push(E item) {
        elements.addElement(item);
        return item;
    }

    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() -1);
    }
}

이로 인해 Stack 클래스에서 add를 사용하는 등의 상황은 원천 봉쇄 할 수 있다.

 

이 뿐만 아니라 합성을 사용하면 다양한 장점을 가진다.

1. 객체의 내부는 외부에 공개되지 않기 때문에, 합성을 사용하면 두 객체간 결합도를 낮출 수 있다.

2. 실행시점 동적으로 변경이 가능해져, 유연한 설계가 가능하다.

3. 자바의 특성성 단일상속만 가능한데 합성의 경우에는 여러 필드를 정의하여 사용할 수 있다.

4. 부모 클래스의 변경에 상관없이 자식 클래스만의 기능을 설계할 수 있다.

 

상속은 extends만으로 편하게 부모 클래스의 기능을 사용할 수 있는 장점이 있지만, 많은 문제를 야기할 수 있다.

 

그렇다고 합성을 사용했을 때 장점만 있는 것은 아니다.

합성은 상속에 비해 객체간의 관계에 대한 가독성이 떨어지고, 의도가 불분명해 질 수 있다.

따라서 상황에 맞게 둘을 적절히 잘 활용하는 능력이 중요하다.

 

코드 재사용이 목적이라면 합성과 인터페이스를,
코드의 대체가능성이 목적이라면 상속을 사용한다.

 


집합과 분해

특정 객체의 구성품을 나타내는 관계를 뜻한다.

카페라떼는 물, 우유, 커피 원두로 이루어져 있기 때문에 카페라떼와 물은 집합과 분해 관계이다.

 


 

책에서는 이러한 추상화 기법을 활용하여 객체지향적 설계를 해야한다고 강조한다.

그동안, 객체지향의 장점이라고 하면 재사용성을 가장 먼저 생각했었다.

 

하지만 책을 읽으면서, 복잡한것을 단순하게 표현하고, 미래를 대비하여 변경에 유연한 객체지향적 설계의 매력을 알 수 있었다.

 

객체지향이라는 도구를 잘 활용한다면, 이해하기 쉬우면서, 유지보수에 용이한 프로그램을 설계할 수 있을 것이다.

 


참고자료

https://incheol-jung.gitbook.io/docs/q-and-a/architecture/undefined-2

 

상속보단 합성 - Incheol's TECH BLOG

상속 관계에서는 외부로부터 다형성을 보장하면서 클래스 내부 구현 코드를 모두 구현하지 않고 공통된 로직을 그대로 사용할 수 있고, 클래스 타입에 따라 변경되는 로직만 일부분 구현하면

incheol-jung.gitbook.io

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%9D%98-%EC%83%81%EC%86%8D-%EB%AC%B8%EC%A0%9C%EC%A0%90%EA%B3%BC-%ED%95%A9%EC%84%B1Composition-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

💠 상속을 자제하고 합성(Composition)을 이용하자

상속과 합성 개념 정리 프로그래밍을 할때 가장 신경 써야 할 것 중 하나가 바로 코드 중복을 제거하여 재사용 함으로써 변경, 확장을 용이하게 만드는 것이다. 그런 관점에서 상속과 합성은 객

inpa.tistory.com

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-LSP-%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84-%EC%B9%98%ED%99%98-%EC%9B%90%EC%B9%99#:~:text=%EB%A6%AC%EC%8A%A4%EC%BD%94%ED%94%84%20%EC%B9%98%ED%99%98%20%EC%9B%90%EC%B9%99%EC%9D%80,%EC%9E%88%EC%96%B4%EC%95%BC%20%ED%95%9C%EB%8B%A4%EB%8A%94%20%EA%B2%83%EC%9D%84%20%EB%9C%BB%ED%95%9C%EB%8B%A4.

 

💠 완벽하게 이해하는 LSP (리스코프 치환 원칙)

리스코프 치환 원칙 - LSP (Liskov Substitution Principle) 리스코프 치환 원칙은 1988년 바바라 리스코프(Barbara Liskov)가 올바른 상속 관계의 특징을 정의하기 위해 발표한 것으로, 서브 타입은 언제나 기반

inpa.tistory.com