이 글은 최범균님의 Inflearn 강의를 학습한 내용을 정리하였습니다.
다형성은 여러(poly) 모습(morph)을 갖는 것을 말한다.
객체 지향에서 다형성은 한 객체가 여러 타입을 갖는 것이다.
즉, 한 객체가 여러 타입의 기능을 제공한다는 의미다.
이는 타입 상속을 통해 가능하다.
타입 상속에는 클래스 상속과 인터페이스 상속이 있다.
왼쪽에 Timer 클래스
와 Rechargeable 인터페이스
가 있다.
중간의 IotTimer 클래스
는 Timer 클래스와 Rechargeable 인터페이스를 상속하고 있다.
이 경우에 IotTimer 객체
는 Timer 타입과 Rechargeable 타입으로 형 변환이 가능하다.
마지막으로 오른쪽에 작성된 코드에서 IotTimer 객체를 Timer 타입과 Rechargeable 타입에 할당하여 각 타입의 기능을 사용하고 있다.
추상화는 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미있는 표현으로 정의하는 과정이다.
특정한 성질을 묶는다.
공통 성질(일반화)을 뽑아낸다.
추상화는 구현을 추상화할 때 사용한다.
구현의 추상화란 여러가지 구현 클래스의 공통점을 상위 타입으로 뽑아내는 것을 말한다.
(추상화 방법에서 공통 성질을 뽑아낸다
에 해당한다.)
아래의 예를 통해 추상화를 알아보자.
+------------------------------+
| <<Interface>> | 기능에 대한 의미 제공
| Notifier | <--- 구현은 제공하지 않음
+------------------------------+ 어떻게 구현할지 알 수 없음
| +notify(noti : Notification) |
+------------------------------+
^
|
+----------------+-------------------+
| | |
+---------------+ +-------------+ +---------------+
| EmailNotifier | | SMSNotifier | | KakaoNotifier | <--- 콘크리트(concrete) 클래스
+---------------+ +-------------+ +---------------+
| | | | | | *콘크리트 클래스란?
+---------------+ +-------------+ +---------------+ 구현을 제공하는 클래스를 말한다.
하단의 EmailNotifier, SMSNotifier, KakaoNotifier 클래스의 공통점을 뽑아보면 Notifier이다.
이때 추상화는 아래의 과정을 통해 할 수 있다.
콘크리트 클래스에서 추상 타입을 도출하면 추상 타입을 이용한 프로그래밍이 가능하다.
타입 추상화의 이점은 의도를 명확하게 드러낸다는 점입니다.
Notifier notifier = getNotifier(...);
notifier.notify(someNoti);
위 코드는 Notifier 타입으로 객체를 꺼내옴으로써(getNotifier(...);) 알림을 보내겠다는 의도를 명확하게 전달한다.
타입 추상화를 사용하는 이유는 변경에 유연하기 때문이다.
일단 추상화를 사용하지 않을 때 문제점을 알아보자.
결론부터 말하자면 부가적인 로직(알림 전송 방식)의 요구사항 변경으로 메인 로직의 코드가 계속 바뀐다는 것이다.
최초 요구사항 : 주문 취소시 SMS에 알림 전송
코드는 아래와 같다.
//주문 취소시 SMS 알림 전송
private SmsSender smsSender;
public void cancel(String ono) {
...주문 취소 처리
smsSender.sendSms(...);
}
두번째 요구사항 : Kakao 알림이 가능하면 Kakao로 알림 전송
코드는 아래와 같다.
//Kakao로 알림 전송 private SmsSender smsSender; private KakaoPush kakaoPush; public void cancel(String ono) { ... 주문 취소 처리 if (pushEnabled) { kakaoPush.push(...); } else { smsSender.sendSms(...); } }
마지막 요구사항 : 항상 이메일 알림 전송
코드는 아래와 같다.
private SmsSender smsSender; private KakaoPush kakaoPush; private MailService mailSvc; public void cancel(String ono) { ... 주문 취소 처리 if (pushEnabled) { kakaoPush.push(...); } else { smsSender.sendSms(...); } mailSvc.sendMail(...); }
위 예제 코드처럼 요구사항이 추가할 때마다 로직도 계속 변경되는 것을 확인할 수 있다.
이제 추상화를 적용해보자.
추상화 적용을 통해 얻고자 하는 것은 부가적인 로직(알림 전송 방식)의 변경이 생겨도 메인 로직의 코드는 변경 없도록 하는 것이다. (변경을 최소화 시키는 것이 목적)
우선 공통 기능을 뽑아보자.
sms전송, kakao푸시, mail전송의 공통 기능은 알림이다.
+-------------+
| sendSms() | --+
+-------------+ |
+-------------+ | +----------+
| pushKakao() | --+--> | notify() |
+-------------+ | +----------+
+-------------+ |
| sendMail() | --+
+-------------+
공통 기능이 나왔으면 추상화를 적용해보자.
+------------------------------+
| <<Interface>> |
| Notifier |
+------------------------------+
| +notify(noti : Notification) |
+------------------------------+
^
|
+----------------+-----------------+
| | |
+---------------+ +-------------+ +---------------+
| EmailNotifier | | SMSNotifier | | KakaoNotifier |
+---------------+ +-------------+ +---------------+
| | | | | |
+---------------+ +-------------+ +---------------+
위 과정으로 만들어진 추상 타입을 적용한 코드는 아래와 같다.
public void cancel(String ono) { ... 주문 취소 처리 Notifier notifier = getNotifier(...); notifier.notify (...); } private Notifier getNotifier(...) { if (pushEnabled) return new KakaoNotifier(); else return new SmsNotifier(); }
Notifier 객체를 이용해 알림을 보낸다는 의도를 명확하게 전달하고
getNotifier()를 이용해 상황에 맞는 알림 구현체를 생성하도록 하였다.
이제 알림 방식의 변경은 getNotifier()에서만 수정하면 된다.
위 코드에서 알림 방식을 정하는 private Notifier getNotifier(...) {...}
부분도 추상화해보자.
일단 이 과정으로 얻는 이점은 아래와 같다.
변경할 코드는 아래와 같다.
public void cancel(String ono) { ... 주문 취소 처리 Notifier notifier = getNotifier(...); notifier.notify(...); } private Notifier getNotifier(...) { if (...) return new KakaoNotifier(); else return new SmsNotifier(); }
기존 코드는 getNotifier()라는 알림 방식을 지정하는 메소드를 사용하고 있다.
알림 방식을 지정하는 로직 자체를 다양하게 선택할 수 있도록 바꿔보자.
public void cancel(String ono) { ... 주문 취소 처리 Notifier notifier = NotifierFactory.instance().getNotifier(...); notifier.notify(...); } public interface NotifierFactory { Notifier getNotifier(...); static NotifierFactory instance() { return new DefaultNotifierFactory(); } } public class DefaultNotifierFactory implements NotifierFactory { public Notifier getNotifier(...) { if (pushEnabled) return new KakaoNotifier(); else return new SmsNotifier(); } }
일단 NotifierFactory 인터페이스를 만든다.
NotifierFactory 인터페이스의 기능은 아래와 같다.
- 알림 방식을 지정하는 로직 객체를 생성한다.
- 알림 방식 구현 클래스를 반환한다.
NotifierFactory 인터페이스를 상속한 DefaultNotifierFactory 클래스를 만든다.
DefaultNotifierfactory 클래스는 NotifierFactory 인터페이스 구현 클래스이다.
getNotifier(...)를 추상화함으로써 즉, NotifierFactory 인터페이스를 만듬으로써 getNotifier(...) 또한 다양한 구현 클래스를 사용할 수 있게되었다.
추상화는 유지보수성을 높이는 장점을 갖는다.
동시에 프로그램의 복잡도를 높이는 단점도 갖고 있다.
추상화로 인해 복잡도가 증가하는 과정
추상화 -> 추상 타입(클래스 또는 인터페이스) 증가 -> 복잡도 증가
따라서, 추상화는 실제 변경·확장이 발생할 때 적용할 것을 권장한다.