JAVA/JAVA | Spring 학습기록

[호호 스터디] 다형성과 추상 타입_ 객체지향과 디자인 패턴 Chapter 3

kth990303 2022. 3. 2. 23:22
반응형

호호 스터디에서 Chapter 3: 다형성과 추상 타입을 발표하기 전에, 미리 책을 읽고 공부한 내용을 기록한 포스팅이다.

 

이펙티브 자바, 오브젝트 책에 적힌 내용과 유사한 내용도 많이 있어,

함께 읽으면 좋은 시너지 효과를 낼 수 있을 듯하다! (이펙티브 자바 3판_아이템18, 19, 64)


다형성을 위한 인터페이스

다형성(Polymorphism)이란, 한 객체가 여러 가지 모습을 갖는다는 것을 의미한다.

즉, 한 객체가 여러 가지 타입을 갖는다는 것을 의미한다.

LottoGenerator (인터페이스)와 이를 구현하는 클래스들

위 그림처럼 LottoGenerator에는 여러 방법이 있다.

자동으로 생성하는 방법과, 수동으로 생성하는 방법.

 

따라서 로또 생성 인스턴스 객체를 생성하려 할 때, 아래와 같이 두 가지 방법이 존재한다.

LottoGenerator lottoGenerator = new LottoRandomGenerator();	// LottoGenerator 타입 (인터페이스)

LottoRandomGenerator lottoGenerator = new LottoRandomGenerator();	// LottoRandomGenerator 타입

즉, 여기서 lottoGenerator는 LottoRandomGenerator 타입도 되고, LottoGenerator 타입도 된다.

위 두 코드 중, 여러분들은 어떤 코드가 좀 더 객체지향적으로 좋아보이는가? 위? 아래?

이번 포스팅에선 위에 있는 코드를 좀 더 칭찬하는 얘기를 작성해보려 한다.


추상화를 통한 유연함

그 전에, 왜 LottoRandomGenerator, LottoManualGenerator 두 개의 클래스만 만들지 않고,

LottoGenerator라는 인터페이스를 추가로 만들어준걸까?

 

이는 추상화 때문인데,

추상화(abstraction)란, 데이터나 로직을, 의미가 비슷한 개념이나 표현으로 정의하는 것이다.

이 과정을 왜 하느냐, 바로 캡슐화에도 도움이 되고, 변경에 유연해지기 때문이다!

또한, 의미에서부터 알 수 있듯이, 코드 중복방지 및 재사용성 향상의 효과도 있다.

 

도움된 글: https://developer.mozilla.org/ko/docs/Glossary/Abstraction

 

아래 인터페이스를 보자.

interface LottoGenerator {
    public List<LottoNumbers> generateLottoNumbers();
}

인터페이스를 통해 로또를 생성하는 과정의 시그니처만 정의할 뿐, 실제 구현을 제공하지는 않고 있는 모습이다.

따라서 LottoGenerator를 보고, 어떻게 로또를 생성하는지 상세한 구현에 대해선 알 수 없다.

 

Chapter 2에서의 캡슐화가 생각나지 않는가? 

물론 둘은 좀 다른 의미이다.

캡슐화는 프로덕션 로직을 구현할 때, 다른 객체에 의존하지 않고 자신의 로직을 구현하고 필요에 따라 메시지를 던져 의존성을 최소화하는 방법이고,

추상화는 의미가 비슷한 공통 개념이나 표현으로 정의하여, 외부 변경에 대한 유연함을 얻는 방법이다.

 

왜 외부 변경에 대한 유연함을 얻을 수 있을까?

아까 본 코드를 다시 한 번 보자.

LottoGenerator lottoGenerator = new LottoRandomGenerator();	// LottoGenerator 타입 (인터페이스)

LottoRandomGenerator lottoGenerator = new LottoRandomGenerator();	// LottoRandomGenerator 타입

밑에 코드처럼 구현해도, 구현 순간에는 문제가 되지 않지만, 요구사항이 추가될 경우에 문제가 발생할 수 있다.

밑에 코드는 로또를 생성하는 요구사항의 변화가 생길 때마다 계속 변화가 발생할 수밖에 없다.

반면, 위에 코드는 로또를 자동으로 생성하든, 수동으로 생성하든 간에, 생성에 대한 책임을 다하면 되기 때문에 요구사항이 수정될 때 상대적으로 변경에 유연하다. 이는 구조의 규모가 커질수록 더욱 뚜렷하게 나타난다.

 

(아이템 64. 객체는 인터페이스를 사용해 참조하라)

List<Integer> list = new ArrayList<>();	// ArrayList를 생성할 때, List 타입으로 만들어진다.

우리가 평소에 List를 생성할 때, IDE가 위와 같이 추천해주는 것도 다 이러한 이유 때문이다.


인터페이스를 잘 만들자

이처럼, 추상화를 위해 인터페이스를 잘 만들 줄 알아야된다.

어떻게 하면 잘 만들 수 있을까?

책에서 소개하는 '잘 만드는 방법'에 대해서 간단히 요약하자면 아래와 같다.

 

- 변화되는 부분을 추상화하자.

예를 들어, 공통 로직이 A이고, 이를 구현하는 방법이 여러 갈래로 나뉜다면,

A는 인터페이스로 추상화하는 것을 고려해보자.

 

- 모든 곳에서 인터페이스를 사용하지는 말자.

추상 타입을 증가시키는 과정엔 결국 구조가 복잡해진다는 단점도 존재한다.

인터페이스의 과도한 남발은 오히려 독이 될 수 있다. 

인터페이스를 사용해야 할 때는 변화 가능성이 높은 경우에 한해서 사용하자.

 

- 인터페이스는 인터페이스 사용자 입장에서 만들자.

개발자 입장에서 인터페이스를 작성할 경우,

기능에 집중한 인터페이스명이 아닌, 데이터에 집중한 인터페이스명이 생성될 수 있다.

사용자 입장에서 이 인터페이스가 어떤 기능에 집중하는지 생각하고 인터페이스를 만들자. 

(


상속이 정말 좋을까?

사실 이번 포스팅에서 다형성과 추상화를 얘기하면서 거의 꺼내지 않은 키워드가 있다.

바로 상속이다.

다형성과 상속이 같이 소개되는 포스팅이 많기도 하고, 이 책에서도 다형성과 상속을 함께 설명해주고 있긴 하다.

 

그러나, 상속이 과연 좋을지에 대해선 생각을 해보아야 한다.

특히, 제임스 고슬링도 JAVA를 다시 만든다면 상속을 포함하지 않는다고 말하기도 했고,

비교적 현대 언어에선 상속과 interface inheritence를 분리하여 상속을 지원하지 않는 경우가 많다.

(아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라)

 

상속은 굉장히 강한 의존성을 가진다.

따라서 캡슐화가 깨지는 건 물론이고, 변경에 대한 유연성이 급격하게 떨어질 수 있다.

이러한 강한 의존성 문제 때문에 오히려 다양한 조합수만큼 클래스를 많이 만들 수밖에 없고, 이는 클래스 폭발이라고 불릴 만큼 심각한 문제를 초래할 수 있다. 

 

상속 관계가 존재할 경우,

요구사항 수정/추가로 인해 자식 클래스 메소드를 수정해야 할 때,

부모 클래스의 메소드를 반드시 알고 있어야 하기 때문에 생산성이 떨어진다는 문제도 존재한다.

 

따라서, 상속 관계의 클래스가 존재할 경우, 

여러 조합이 발생할 수 있기 때문에 합성(Composition)으로 코드를 작성하는 것을 고려해보아야 한다.

 

(아이템 18. 상속보다는 컴포지션을 사용하라)

 

아래 글이 도움이 될 것이다.

 

도움된 글: https://mangkyu.tistory.com/199

 

[OOP] 코드의 재사용, 상속(Inheritance)보다 합성(Composition)을 사용해야 하는 이유

객체지향 프로그래밍에서 코드를 재사용하기 위한 방법으로 크게 상속과 합성이 있습니다. 대부분의 경우 상속보다 합성을 이용하는 것이 좋은데, 이번에는 왜 합성을 사용해야 하는지에 대해

mangkyu.tistory.com


추상화를 잘하기 위해선 다양한 경험과 설계 과정이 필수적이기 때문에 결국 프로젝트에 많이 참여하고 열심히 노력하는 것이 중요하다.

 

프로젝트 참여, 코딩 경험을 많이 쌓고,

이론을 병행하면서 시너지 효과를 발생시켜

클린 코드를 만들 수 있도록 열심히 달려보자 :)

반응형