2 추상화
source: categories/study/oop_programming/opp_programming_2.md
2. 추상화
2.1 다형성과 추상화
- 다형성과 추상화
- 다형성 보다는 추상화에 대해 많이 이야기해볼 생각
- 캡슐화와 더불어서 객체 지향을 할 때 필요한 것이 추상화이다.
- 매우 중요한 내용이다.
다형성(Polymorphism)
- 여러(poly) 모습(morph)을 갖는 것
- 객체 지향에서는 한 객체가 여러 타입을 갖는 것
- 즉 한 객체가 여러 타입의 기능을 제공
- 타입 상속으로 다형성 구현
- 하위 타입은 상위 타입도 됨
다형성 예
public class Timer {
public void start() {
// ...
}
public void stop() {
// ...
}
}
public interface Rechargeable {
void charge();
}
public class IotTimer extends Timer implements Rechargeable {
public void charge() {
// ...
}
}
IotTimer
는Timer
타입도 돼고,Rechargeable
타입도 됩니다.
IotTimer it = new IotTimer();
it.start();
it.stop();
Timer t = it;
t.start();
t.stop();
Rechargeable r = it;
r.charge();
- 그래서 위와 같이
IotTimer
를Timer
에도 할당할 수 있고,Rechargeable
에도 할당할 수 있다. IotTimer
는Timer
타입에 정의되어있는 기능과Rechargeable
에 정의된 기능을 모두 사용할 수 있다.
추상화(Abstraction)
- 그럼 다형성 이야기를 왜 꺼냈냐. 추상화 때문이다.
- 우선 추상화가 뭔지 얘기를 해보자.
- 데이터나 프로세스 등을 의미가 비슷한 개념이나 의미 있는 표현으로 정의하는 과정
- 무언가 개념화 시키거나 표현을 도출하는 것이 추상화이다.
- 두 가지 방식의 추상화
- 특정한 성질, 공통 성질(일반화)
- 첫번째, 특정한 성질을 뽑아내는 방식, 예를 들어서 사용자에서 아이디, 이름, 이메일을 뽑아내서 USER 테이블로 추상화할 수 있다.
- 돈과 관련된 여러 특징에서 통화와 금액을 갖고서
Money
클래스로 추상화할 수 있다. - 두번째, 공통 성질을 뽑아내는 방식
- HP MXXX, 삼성 SL-M2XXX 이 두 모델의 공통점은 ‘프린트'이다.
- 지포스, 라데온의 공통점은 ‘GPU'이다.
- 이렇게 추상화는 무언가 특정한 성질이나 공통점을 뽑아내는 과정이다.
- 그럼 다형성은 무엇과 관련된 것일까?
- 바로 공통 성질을 뽑아내는 추상화 방식과 다형성이 관련되어 있습니다.
- 간단한 예
- DB의 USER 테이블: 아이디, 이름, 이메일
- Money 클래스: 통화, 금액
- 프린터: HP MXXX, 삼성 SL-M2XXX
- GPU: 지포스, 라데온
서로 다른 구현 추상화
- 서로 다른 구현의 공통된 특징이 있으면 이를 추상화 해볼 수 있다.
- 위에서 살펴본 두번째 방식으로 추상화를 해봤다.
SCP로 파일 업로드
HTTP로 데이터 전송 -- 추상화 --> 푸시 발송 요청
DB 테이블에 삽입
- SCP로 파일을 업로드하고, HTTP로 데이터를 보내고, 경우에 따라 DB 테이블에 데이터를 삽입했는데,
- 이 3가지가 알고 봤더니, 푸시를 보내기 위한 구현이었던 것.
- 그럼 위 3가지를 추상화 해보면 푸시 발송 요청이란 것으로 표현할 수 있는 것이다.
타입 추상화
- 그럼 타입 추상화는 위와 같은 구현을 추상화 할 때 사용을 합니다.
- 여러가지 구현 클래스가 공통점이 있을 때, 이를 추상화한 상위 타입을 도출해낼 수 있다.
- 여러 구현 클래스를 대표하는 상위 타입 도출
- 흔히 인터페이스 타입으로 추상화
- 추상화 타입과 구현은 타입 상속으로 연결
<<Interface>>
Notifier <- 기능에 대한 의미 제공
+notify(noti: Notification) 구현은 제공하지 않음
^ 어떻게 구현할지 알 수 없음
|
-------------------------------
| | |
| | |
EmailNotifier SMSNotifier KakaoNotifier <- 콘크리트(concrete) 클래스
- 위와 같이
Notifier
란 타입으로 추상화할 수 있다. - 이때 추상화한 타입은
interface
로 많이 표현을 한다. - 그리고 추상화한 타입과 구현 클래스를 타입 상속으로 연결을 하게됩니다.
- 추상화한 타입은 공통된 특징을 표현하는데, 이 표현은 곧 추상화한 타입이 어떤 기능을 제공하는지 의미하게 된다.
- 추상화한 타입 자체는 구현이 어떻게될지 모르니까 구현은 제공하지 않게 된다.
- 실제 구현은 추상 타입을 상속받는 클래스, 위 예시에서 보면
Notifier
추상 타입을 구현하고 있는EmailNotifier
혹은KakaoNotifier
- 이런 추상 타입을 상속받고 있는 클래스에서 실제 구현을 제공을 하고 있는 것입니다.
- 그리고 구현을 제공하는 클래스, 이런 클래스를 흔히 콘크리트 클래스라고 많이 표현을 합니다.
추상 타입 사용
- 이렇게 콘크리트 클래스에서 추상 타입을 도출하면 추상 타입을 이용해서 프로그래밍을 할 수 있게된다.
- 추상 타입을 이용한 프로그래밍
Notifier notifier = getNotifier(...);
notifier.notify(someNoti);
getNotifier
메소드가 어떤 타입의 객체를 반환하는지 상관 없이- 이 코드는
notifier
라는 추상화 타입을 이용해서 통지 기능을 실행할 수 있다. - 실제 통지가 어떻게 이뤄지는지는 모른다. 모르지만 위 코드를 보면 무언가 통지한다는 의도는 잘 드러난다는 걸 우리가 알 수 있다.
- 추상 타입은 구현을 감춤
- 기능의 구현이 아닌 의도를 더 잘 드러냄
추상 타입 사용에 따른 이점: 유연함
- 그러면 이런 추상 타입을 왜 사용하느냐,
- 그 이유는 바로 변경이 유연해지기 때문
콘크리트 클래스를 직접 사용하면
- 최초에는 주문 처리를 하면 SMS만 보내달라는 요청이 있었다고 하자
- 그래서 SMS를 보내주는
SmsSender
클래스를 만들고 - 주문 취소 기능을 가지고있는
cancel
메소드에서는smsSender
를 이용해서 문자를 보내도록 구현함 - 그런데 요구 사항이 추가가 되는 것
private SmsSender smsSender;
public void cancel(String ono) {
// ... 주문 취소 처리
smsSender.sendSms(...);
}
- 카카오 푸시가 가능하면 카카오로 푸시를 보내 라는 요구가 추가됨
- 그래서 아래와 같은 코드들이 추가되기 시작하는 것
- 카카오 푸시를 보내기 위한
KakaoPush
클래스를 만들고 이 클래스를 사용해서 푸시를 보내는 코드를 추가함 - 하다보니까 email로도 취소 사실을 보낼 필요성이 생김
private SmsSender smsSender;
private KakaoPush kakaoPush;
public void cancel(String ono) {
// ... 주문 취소 처리
if (pushEnabled) {
kakaoPush.push(...);
} else {
smsSender.sendSms(...);
}
}
- 그래서
MailService
라는 클래스를 만들고 그 클래스를 이용해서 mail을 보내는 코드를 추가함
private SmsSender smsSender;
private KakaoPush kakaoPush;
private MailService mailSvc;
public void cancel(String ono) {
// ... 주문 취소 처리
if (pushEnabled) {
kakaoPush.push(...);
} else {
smsSender.sendSms(...);
}
mailSvc.sendMail(...)
}
- 그럼 위 과정에서 무슨 일이 벌어졌는지를 한번 보자.
- 사실 주문 취소라는 본질과 크게 상관없는 sms 전송이나, 카카오 푸시나 메일 전송 같은 것들 때문에
- 주문 취소를 위해 구현한
cancel
메소드의 코드가 함께 변하고 있다. - 요구 사항 변경에 따라 주문 취소 코드도 함께 변경
- 위에 보시면
주문 취소 처리
라는 코드는 하나도 변한게 없습니다.주문 취소 처리
로직 자체는 변한게 없는 것이다. - 그런데 통지 방식이 바뀌면서 위와 같이 코드가 계속해서 바뀌고 있다.
- 즉, 주문 취소 코드라는 본질과는 상관 없는 다른 요구 사항 때문에 그 본질을 구현하고 있는 기능에 변화가 생기는 것이다.
추상 타입 사용 이점: 유연함
- 그럼 같은걸 추상 타입을 활용해 구현해보자.
- 공통점을 도출하면
SMS 전송
카카오톡 보냄 -- 추상화 --> 통지
이메일 발송
-
통지
라는interface
를 사용 - 도출한 추상 타입 사용
- 도출한
통지
라는interface
를 사용해서 코드를 작성하도록cancel
을 변경함
public void cancel(String ono) {
// ... 주문 취소 처리
Notifier notifier = getNotifier(...); // 추상화한 notifier 타입을 사용
notifier.notify(...); // 추상화한 타입을 이용해서 통지 기능을 실행하도록 코드를 수정
}
private Notifier getNotifier(...) { // getNotifier 메소드는 상황에 따라서 알맞은 notifier 객체를 생성하도록 일단 구현함
if (pushEnabled)
return new KakaoNotifier();
else
return new SmsNotifier();
}
추상 타입 사용 이점: 유연함
- 그리고 추상화를 한번 더 해봤다.
- 사용할 대상 접근도 추상화
- 아래
getNotifier
코드를 보면notifier
의 실제 구현을 생성하는 그런 코드이다. - 그래서 객체를 생성하는 기능 자체도
NotifierFactory
로 추상화를 해본 것이다.
public void cancel(String ono) {
// ... 주문 취소 처리
Notifier notifier = getNotifier(...); // 추상화한 notifier 타입을 사용
notifier.notify(...); // 추상화한 타입을 이용해서 통지 기능을 실행하도록 코드를 수정
}
private Notifier getNotifier(...) { // getNotifier 메소드는 상황에 따라서 알맞은 notifier 객체를 생성하도록 일단 구현함
if (pushEnabled)
return new KakaoNotifier();
else
return new SmsNotifier();
}
- 위 코드를 아래와 같이 수정
cancel
메소드는NotifierFactory
를 이용해서notifier
를 구하도록 수정함
public void cancel(String ono) {
// ... 주문 취소 처리
Notifier notifier = NotifierFactory.instance().getNotifier(...);
notifier.notify(...);
}
private 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();
}
}
- 위와 같이 바꾼 뒤에 통지방식을 바꿔야된다는 수정사항이 들어옴
- 그럴 땐
DefaultNotifierFactory
여기만 수정해주면 된다. DefaultNotifierFactory
의getNotifier
여기 어딘가만 바꿔주면 된다.- 또는 새로운
notifierFactory
구현 코드를 만들고 그 객체를 리턴하도록 중간에 있는instance
메소드를 바꿔줘도 된다.
- 여기서 중요한 점은 통지 방식이 바뀌더라도
cancel
메소드는 바뀌지 않는다는 것이다. - 즉, 주문 취소 처리 로직은 바뀌지 않는다는 것이다.
- 이것이 바로 추상 타입을 사용하는 이유이다.
- 즉, 주문 취소 처리 로직은 그대로 이용하면서 통지하는 방식을 바꿀 수 있게 해주는 것, 이런 유연함을 제공하는 것이 바로 추상화를 하는 이유이다.
추상화 결과: 사용 대상 변경 유연함
- 추상화 결과를 한번 보자.
- 우리가 무언가를 추상화하게 되면, 내가 사용하는 대상을 쉽게 변경할 수 있는 유연함을 얻을 수 있다.
- 위 화면에서 설명해드리도록 하겠다.
추상화는 의존 대상이 변경하는 시점에
- 그러면 이런 추상화는 언제 해야될까?
- 사실 무턱대고 하면 안됩니다.
- 추상화를 한다는 것은 새로운 추상 타입이 생긴다는 것을 의미합니다.
- 그리고 타입이 늘어나게되면 그만큼 프로그램의 복잡도는 증가하게 됩니다.
- 그래서 아직 존재하지 않는 기능에 대해서 너무 빨리 추상화를 하면 안된다.
- 그렇게되면 추상화를 잘못할 가능성이 높아지고 괜히 복잡도만 증가하게 되는 것이다.
- 추상화 -> 추상 타입 증가 -> 복잡도 증가
- 아직 존재하지 않는 기능에 대한 이른 추상화는 주의: 잘못된 추상화 가능성, 복잡도만 증가
- 실제 변경, 확장이 발생할 때 추상화 시도
- 그럼 추상화는 언제해야될까?
- 실제 변경, 확장이 발생할 때 추상화 시도
- 최초에 주문 취소 서비스를 아래와 같이 구현을 했다.
- 그런데 요구사항이 바뀌어서 SMS를 보내달라는 요구사항이 추가가됨
public class OrderService {
private MailSender sender;
public void order(...) {
// 주문 취소 로직 수행
sender.send(message); // 주문 취소 로직 수행 후 메일로 이 사실을 알려주는 식으로 구현함
}
}
- 그러면 일단 아래처럼 구현해볼 수 있는데,
- 이렇게 변경이나 확장이 발생하는 시점에 추상화를 시도해보는 것이다.
public class OrderService {
private MailSender sender;
private SmsService smsService;
public void order(...) {
// 주문 취소 로직 수행
sender.send(message);
// ...
smsService.send(smsMsg);
}
}
- 메일 알림도 보내고, SMS도 보내는데 이를 추상화 시도를 해보면,
- 사용자에게 무언가를 통지하는 거구나!
- 무언가를 통지한다는 공통점을 알 수 있겠죠?
- 그럼 그때
notifier
라는 타입으로 추상화를 해보는 것입니다. - 이렇게 추상화는 필요한 시점에, 변경이나 확장이 일어나는 시점에서 추상화를 시도하면
- 프로그램의 복잡도를 증가시키지 않으면서
- 추상화를 통해서 얻을 수 있는 이점인 유연함을 얻을 수 있다.
public class OrderService {
private Notifier notifier;
public void order(...) {
// 주문 취소 로직 수행
notifier.notify(noti);
}
}
추상화를 잘 하려면
- 구현을 한 이유가 무엇 때문인지 생각해야 함
- 앞에서 봤던
SMSSender
,KakaoPush
,MailService
이런 것들을 우리가 구현을 했다. - 그럼 이 세가지가 어떤 공통점이 있는지 생각을 해볼 수 있는 거죠?
- 누군가는 "이거는 고객에게 통지하는 거구나."라는 방식으로 추상화를 해볼 수 있는 것이고,
- 또 누군가는 "이거는 메신저 역할이구나."라고 생각해서 메신저로 추상화를 할 수도 있을 것이다.
- 어떤식으로 추상화를 하던지간에 실제로 그 구현을 한 이유가 무엇 때문인지를 생각해야 추상화를 잘 할 수 있다.
2.2 추상화 예제
- 추상화가 실제로 어떤 식으로 도움을 주는지 조금 더 구체적인 예를 이용해서 살펴보겠습니다.
예시
- 기능 예시
- 클라우드 파일 통합 관리 기능을 개발하는 가상의 프로젝트가 있다고 가정을 해보자
- 이 프로젝트의 초기 버전의 기능은 다음과 같다.
- 대상 클라우드
- 드롭박스, 박스
- 주요 기능
- 각 클라우드의 파일 목록 조회, 다운로드, 업로드, 삭제, 검색
- 대상 클라우드
- 추상화 하지 않은 코드 VS 추상화 한 코드
추상화하지 않은 구현: 파일 목록 조회
- 먼저 추상화를 하지 않았을 때의 구현입니다.
- 우선 클라우드 아이디를 구분하기 위한
CloudId
라는 열거(enum
) 타입을 만들었고, 여기에 DROPBOX와 BOX를 지원하기로 했으니까, 그 두가지를 값으로 가지고 있습니다.
public enum CloudId {
DROPBOX,
BOX,
}
- 그리고 FileInfo라는 클래스를 만들었다.
- 이거는 특정 파일 정보를 표현하기 위한..
- 이 파일이 어느 클라우드의 파일인지 (CloudId)
- 그거를 식별하기 위한 아이디가 뭐고 (fileId)
- 이름은 뭐고 (name)
- 파일 길이가 어떻게 되는지 (length)
- 이런 정보를 담고있는 FileInfo라는 클래스를 만들었다.
public class FileInfo {
private CloudId cloudId;
private String fileId;
private String name;
private long length;
// ...
// get 메서드
}
- CloudFileManager 라는 클래스도 만들었다.
- 이 클래스가 클라우드 파일을 관리하는 기능을 제공하는 클래스이다.
- 이 클래스에서
getFileInfos
라는 메소드를 보시면 파일 목록을 조회하는 그런 기능들을 제공을 하는데, - 어떤 클라우드의 파일 목록을 조회할지의 여부를
CloudId
타입의 파라미터로 받고 있다.- 그래서
CloudId
가DROPBOX
이면,CloudId.DROPBOX
.. DropboxClient
<- 이런게 있다고 칩시다.DropboxClient
를 이용해서db.getFiles()
파일 목록을 가져오고- 그 파일 목록을
FileInfo
로 변환하는 코드이다. -
FileInfo
로 변환한 다음에return
하는식으로DROPBOX
를 구현했다. - 만약에
cloudId
가BOX
라면, 이 경우도 비슷하게BOX
와 연동하기 위한BoxService
클래스가 있다고 쳤을 때, - 이
BoxService
를 이용해서 알맞은 구현을 한다.
- 그래서
public class CloudFileManager {
public List<FileInfo> getFileInfos(CloudId cloudId) {
if (cloudId == CloudId.DROPBOX) {
DropboxClient dc = ...;
List<DbFile> dbFiles = db.getFiles();
List<FileInfo> result = new ArrayList<>();
for (DbFile dbFile : dbFiles) {
FileInfo fi = new FileInfo();
fi.setCloudId(CloudId.DROPBOX);
fi.setFileId(fi.getFileId());
...
result.add(fi);
}
return result;
} else if (cloudId == Cloud.BOX) {
BoxService boxSvc = ...;
... //
}
}
}
추상화하지 않은 구현: 파일 다운로드
- 추상화하지 않은 파일 다운로드 코드는 앞에서 봤던 목록 조회와 비슷하다.
FileInfo file
먼저 파일 정보를 준다. 이 파일을 다운받고 싶다고. 첫번째 파라미터.localTarget
로컬에 어디에다 파일을 다운로드해라. 두번째 파라미터.file.getCloudId() == CloudId.DROPBOX
파일의 클라우드 아이디가 드롭박스이면DropboxClient
를 이용해서 파일 정보를 읽어온다. 그리고 로컬로 타겟을 복사하는..- 이런식으로 드롭박스는 작동한다.
file.getCloudId() == CloudId.BOX
클라우드 아이디가 박스이면- 역시 또 박스에서
BoxService
데이터를 읽어와서 로컬 파일로 복사하는 코드이다.
public void download(FileInfo file, File localTarget) {
if (file.getCloudId() == CloudId.DROPBOX) {
DropboxClient dc = ...;
FileOutputStream out = new FileOutputStream(localTarget);
dc.copy(file.getFileId(), out);
out.close();
} else if (file.getCloudId() == CloudId.BOX) {
BoxService boxSvc = ...;
InputStream is = boxSvc.getInputStream(file.getId());
FileOutputStream out = new FileOutputStream(localTarget);
CopyUtil.copy(is, out);
}
}
- 그런데 드롭박스 연동 모듈과 박스 연동 모듈이, 파일을 읽어와서 저장하는 방식이 같으면 좋은데, 아마 다를거라서 위와 같이 드롭박스인 경우와 박스인 경우의 코드가 미세하게 달라지게 될겁니다.
추상화하지 않은 구현: 기타 기능 추가
- 이와 같이 앞에서 봤던거와 비슷하게 업로드인 경우의 처리
- 모두 비슷한
if
,else
구문이 각 기능에 들어가게 됩니다.
public FileInfo upload(File file, CloudId cid) {
if (cid == CloudId.DROPBOX) {
// ...
} else if (cid == CloudId.BOX) {
// ...
}
}
public void delete(String fileId, CloudId cid) {
if (cid == CloudId.DROPBOX) {
// ...
} else if (cid == CloudId.BOX) {
// ...
}
}
public List<FileInfo> search(String query, CloudId cid) {
if (cid == CloudId.DROPBOX) {
// ...
} else if (cid == CloudId.BOX) {
// ...
}
}
추상화하지 않은 구현: 이어지는 추가
- 무사히 초기 버전을 출시했다.
- 그리고 마침 반응이 좋아서 사용자가 모이기 시작했다. 투자도 받았다.
- 그래서 지원하던 클라우드를 3개 더 늘리기로 결정했습니다.
- 클라우드 추가
- S클라우드
- N클라우드
- D클라우드
- 기능 추가
- 클라우드 간 파일 복사
- 그러면 어떻게 될까?
- 아래 코드처럼
download()
,upload()
,delete()
,search()
도 유사한else-if
블록 추가
public List<FileInfo> getFileInfos(CloudId cloudId) {
if (cloudId == CloudId.DROPBOX) {
// ...
} else if (cloudId == CloudId.BOX) {
// ...
} else if (cloudId == CloudId.SCLOUD) {
// ...
} else if (cloudId == CloudId.NCLOUD) {
// ...
} else if (cloudId == CloudId.DCLOUD) {
// ...
}
}
추상화하지 않은 구현: 클라우드 간 복사
- 클라우드간 복사 기능을 추가하기 위해
copy
라는 메소드를 추가함 FileInfo fileInfo, CloudId to
특정 클라우드에 있는 파일을to
클라우드로 복사해라CloudId from = fileInfo.getCloudId();
어떤 클라우드에서 복사해올지를 결정to == CloudId.DROPBOX
to
가 어디냐에 따라 방식이 약간식 바뀌는 것이다.to
가DROPBOX
라면 드롭박스 클라우드로 복사하는 것from
은 어디 클라우드로부터인지 확인하는 것- 각각의
from
인 경우마다 작성되어있는 코드가 약간씩 다르다.- 우연히 같을 수도 있는데, 다를 가능성이 더 높을 것이다.
public FileInfo copy(FileInfo fileInfo, CloudId to) {
CloudId from = fileInfo.getCloudId();
if (to == CloudId.DROPBOX) {
DropBoxClient dbClient = ...;
if (from == CloudId.BOX) {
cbClient = copyFromUrl('http://www.box.com/files/' + fileInfo.getFileId());
} else if (from == CloudId.SCLOUD) {
ScloudClient sClient = ...;
InputStream is = sClient.getInputStream(fileInfo.getFileId());
dbClient.copyFromInputStream(is, fileInfo.getName());
} else if (from == CloudId.DCLOUD) {
dbClient.copyFromUrl('http://www.dcloud.com/getfile?fileId='+fileInfo.getFileId());
} else if (from == CloudId.NCLOUD) {
NCloudClient nClient = ...;
File temp = File.createTemp();
nClient.save(fileInfo.getFileId(), temp);
InputStream is = new FileInputStream(temp);
dbClient.copyFromInputStream(is, fileInfo.getName());
}
}
}
- 위의 코드는
to
가DROPBOX
인 경우이다.- 전체 클라우드는 5개이죠?
- 그렇다면 위와 유사한 코드가 4개 더 있어야 된다는 이야기이다.
추상화하지 않은 구현: 클라우드 간 복사 (if-else 구조만 표시)
if-else
만 표시를 한건데도, 코드가 위와 같이 복잡해집니다.- 복사 대상이 드롭박스인 경우
- 복사 대상이 박스인 경우
- 복사 대상이 SCLOUD인 경우
- 복사 대상이 DCLoud인 경우
- 복사 대상이 NCLoud인 경우
- 이렇게 각 복사 대상이 다른 경우의 코드 블록이 존재한다.
- 위의 코드는 중간에
...
으로 생략했는데, 만약에 이 위치에 실제 코드가 있다고 생각해 보십시오. - 위 코드는 진짜 복잡할 것이다.
- 그리고 만약 위 상태에서 새로운 클라우드를 2개를 더 추가해야 된다고 한다면, 어떻게될까?
- 전체 블록에
else-if
가 2개가 더 추가될 것이고 - 각 블록에
else-if
가 추가될 것이다.
- 위의 코드는 중간에
- 이런식의 코드를 작성 한다는게, 아무래도 어디를 수정해야되는지 좀 찾아야돼고 실제로 단번에 수정 위치를 찾아내기 힘들기 때문에 열심히 찾아야돼고, 코드도 굉장히 길어진다.
- 그래서 실제 수정하는 그 시간 자체도.. 코드가 기니까 뭔가 코드를 분석하는 시간도 길어지고
- 그래서 수정하는 위치를 찾는데도 시간이 오래걸리게 된다.
- 결과적으로 코드를 수정하는 비용이 증가하는.. 그런 전형적인 코드 구조이다.
- 위와 같은 코드는 시간이 흘러갈수록 기능이 추가될 수록 코드 한줄 수정하는 비용이 점점 증가하는 증상이 나타나게 된다.
개발 시간 증가 이유
- 코드 구조가 길어지고 복잡해짐
- 새로운 클라우드 추가시 모든 메서드에 새로운
if
블록 추가- 중첩
if-else
는 복잡도 배로 증가 if-else
가 많을수록 진척 더딤 (신중 모드)if-else
가 많아질 수록 언제 어디서 어떻게 실행될지를 모르기에 신중해져야된다.
- 중첩
- 새로운 클라우드 추가시 모든 메서드에 새로운
- 관련 코드가 여러 곳에 분산됨
- 한 클라우드 처리와 관련된 코드가 여러 메서드에 흩어짐
- 결과적으로, 코드 가독성과 분석 속도 저하
- 코드 추가에 따른 노동 시간 증가
- 실수하기 쉽고 이로 인한 불필요한 디버깅 시간 증가
추상화해보면
DROPBOX
BOX
SCLOUD ------> 클라우드 파일시스템
DCLOUD
LCLOUD
추상화한 타입(클라우드 파일시스템)을 통해 시스템 설계
- 위와 같이 추상화 타입의 기능을 정의했다.
- 그런데
CloudFileSystem
을 구해주는CloudFileSystemFactory
라는 클래스도 만들었다. - 그럼 이렇게 설계한 결과물을 통해 다시한번 구현을 해보겠습니다.
DROPBOX용 클라우드 파일시스템 구현
public class DropBoxFileSystem implements CloudFileSystem {
private DropBoxClient dbClient = new DropBoxClient(...);
@Override
public List<CloudFile> getFiles() {
List<DbFile> dbFiles = dbClient.getFiles();
List<CloudFile> results = new ArrayList<>(dbFiles.size());
for (DbFile file: dbFiles) {
DropBoxCloudFile cf = new DropBoxCloudFile(file, dbClient);
result.add(cf);
}
return results;
}
}
DropBoxFileSystem
은 추상화한CloudFileSystem
을 상속(implements
)하고 있다.getFiles
메소드에서는 내부적으로는DropBoxClient
를 이용해서 코드를 구현하는데, 실제 결과물은CloudFile
을 리턴하는 식으로 구현을 한겁니다.CloudFile
를 상속한DropBoxCloudFile
을 만들어서 그 목록을 리턴하도록 만들었다.
- 그러면
DropBoxCloudFile
은 어떻게 구현했을까? - 추상화한
CloudFile
타입을implements
상속해서 구현을 했다. - 그리고 각각의 기능을 구현했다.
public class DropBoxCloudFile implements CloudFile {
private DropBoxClient dbClient;
private DbFile dbFile;
public DropBoxCloudFile(DbFile dbFile, dbClient) {
this.dbFile = dbFile;
this.dbClient = dbClient;
}
public String getId() {
return dbFile.getId();
}
public boolean hasUrl() {
return true;
}
public String getUrl() {
return dbFile.getFileUrl();
}
public String getName() {
return dbFile.getFileName();
}
public InputStream getInputStream() {
return dbClient.createStreamOfFile(dbFile);
}
public void write(OutputStream out) {
// ...
}
public void delete() {
dbCount.deleteFile(dbFile.getId());
}
// ...
}
파일 목록, 다운로드 기능 구현
CloudFileManager
에서 다운로드 기능을 어떻게 구현해볼 수 있냐면,- 앞에서 구현한
CloudFileSystemFactory.getFileSystem(cloudId)
기능을 이용해서CloudFileSystem
을 구하고, CloudFileSystem
추상화한 타입으로 코드를 작성하고 있죠?fileSystem
을 구하고 이fileSystem
의getFiles
를 이용해서 파일 목록을 구하도록 했다.
public List<CloudFile> getFileInfos(CloudId cloudId) {
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(cloudId);
return fileSystem.getFiles(); // 이 getFiles도 추상화한 타입인 CloudFile 목록을 리턴하고 있다.
}
// download도 마찬가지로 추상화한 타입을 사용함 CloudFile
public void download(CloudFile file, File localTarget) {
// 추상화한 CloudFile이 제공하는 write 기능을 이용해서 특정 파일을 로컬에 복사하도록 코드를 구현함
file.write(new FileOutputStream(localTarget));
}
BOX 클라우드 지원을 추가하려면
BOX 클라우드 지원 추가
- 위와 같이 추상화한
CloudFileSystem
과CloudFile
이 두 타입을 이용해 콘크리트 클래스를 구현하기만 하면 된다. - 그러면 BOX 클라우드 지원을 추가할 수 있다.
- 그리고
CloudFileSystemFactofy
에서CloudFileId
에 따라서 알맞게BoxFileSystem
을 리턴하도록 하면 된다.
파일 목록, 다운로드 기능 구현
- 이렇게
BOX
클라우드 지원을 추가했다. - 그리고 다시 파일 목록과 다운로드 기능을 봤더니 바뀔게 없다.
- BOX 지원 추가 이후
- BOX 클라우드를 지원하는 기능을 새로 추가했는데도,
getFileInfos
,download
이 두 기능의 코드가 바뀐데가 없다.
public List<CloudFile> getFileInfos(CloudId cloudId) {
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(cloudId);
return fileSystem.getFiles(); // 이 getFiles도 추상화한 타입인 CloudFile 목록을 리턴하고 있다.
}
// download도 마찬가지로 추상화한 타입을 사용함 CloudFile
public void download(CloudFile file, File localTarget) {
// 추상화한 CloudFile이 제공하는 write 기능을 이용해서 특정 파일을 로컬에 복사하도록 코드를 구현함
file.write(new FileOutputStream(localTarget));
}
- 이렇게 바뀌지 않는 이유는 뭐 때문이었죠?
- 바로 추상화 타입을 사용했기 때문이다.
파일 복사 기능
- 파일 복사 기능도 단순하다.
// CloudFile을 받아서 그것을 특정한 target 클라우드로 복사해주는데,
public void copy(CloudFile file, CloudId target) {
// 우선 target에 해당하는 CloudFileSystem을 구하고
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(target);
// 그 fileSystem의 copyFrom에다가 복사할 file을 인자로 전달했다.
fileSystem.copyFrom(file);
}
- 각 클라우드의
copyFrom
메소드에 대해 보자. - 각각 자신의 기능을 사용해 복사하도록, 알맞게 기능을 구현하고 있다.
-- DropBoxFileSystem
private DropBoxClient dbClient = new DropBoxClient(...);
public void copyFrom(CloudFile file) {
if (file.hasUrl())
dbClient.copyFromUrl(file.getUrl());
else
dbClient.copyFromInputStream(file.getInputStream(), file.getName());
}
-- NcloudFileSystem
private NcloudClient nClient = new NCloudClient(...);
public void copyFrom(CloudFile file) {
File tempFile = File.createTemp();
file.write(new FileOutputStream(tempFile));
nClient.upload(tempFile, file.getName());
}
추상화 결과
- 그럼 이렇게 추상화를 했더니 우리가 어떤 결과를 얻을 수 있었냐면,
- 추상화한 타입으로만 핵심 기능을 구현을 했다.
public class CloudFileManager {
public List<CloudFile> getFileInfos(CloudId cloudId) {
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(cloudId);
return fileSystem.getFiles();
}
public void download(CloudFile file, File localTarget) {
file.write(new FileOutputStream(localTarget));
}
public void copy(CloudFile file, CloudId target) {
CloudFileSystem fileSystem = CloudFileSystemFactory.getFileSystem(target);
fileSystem.copyFrom(file);
}
// ...
}
- 위에 보시면
CloudFile
,CloudFileSystem
이 타입들은 모두 추상화한 타입이죠? - 그런 추상화한 타입으로만 핵심 기능을 구현을 했구요.
- 이런 구현의 결과로 무얼 얻을 수 있냐면,
추상화 결과
- 추상화 결과로
CloudFileManager
를 수정하지 않고 새로운 클라우드 지원을 추가할 수 있게 되었습니다.
- 즉, 나를 바꾸지 않고.. 여기서의 ‘나'는
CloudFileManager
이다. - 나를 바꾸지 않고 내가 사용하는 대상, 여기서는
CloudFileSystem
이다. - 내가 사용하는 대상을 바꿀 수 있는 유연함을 얻게 된 것이다.
- 게다가 이 코드는 특정 클라우드와 관련된 코드가 한 곳에 몰리는 이점도 있다.
- 예를 들어, 추상화 하기 전에는
dropbox
와 관련된 목록 조회, 업로드, 다운로드, 검색과 같은 코드가 여러 메소드에, 여러if-else
블록으로 흩어져 있었다. - 그런데 추상화한 결과 구조에서는
dropbox
모듈에 다 모여있다. - 그래서
dropbox
모듈만 수정해야 할 때, 복잡한if-else
블록을 뒤질 필요가 없이 빠르게 변경할 수 있게 됩니다.
이것이 바로 OCP (Open-Closed Principle)
- 이것이 바로 OCP이다.
- OCP는 개방 폐쇄 원칙을 말한다.
- 이 원칙은 확장에선 열려 있어야하고, 변경에는 닫혀 있어야한다는 원칙이다.
- 이 말을 조금 더 구체적으로 풀어보면 기능을 변경하거나 확장할 수 있으면서(즉, 기능 변경 확장에는 열려있으면서)
- 그 기능을 사용하는 코드는 수정하지 않아야 한다. (즉, 수정엔 닫혀있어야 한다는 원칙이다.)
- 앞에서 추상화를 사용해서 설계한 클라우드 파일 관리 기능이 바로 이 원칙을 잘 지키고 있다.
CloudFileManager
를 수정하지 않으면서, 즉, 수정에는 닫혀 있으면서,- 새로운
CloudFileSystem
을 추가할 수 있는, 즉, 확장에는 열려있는 구조를 갖고 있다. - 결국 추상화를 잘 하면 이렇게
OCP
를 따르는 구조를 가질 확률이 높아진다. - 그리고 이는 변경이나 확장하는 비용을 낮춰주게 됩니다.
- 이런 이유로 추상화를 하는 것이다.