이 글은 최범균님의 Inflearn 강의를 학습한 내용을 정리하였습니다.
개발 내용 : 클라우드 파일 통합 관리 기능
대상 클라우드 : 드롭박스, 박스
주요 기능 : 각 클라우드의 파일 목록 조회, 다운로드, 업로드, 삭제, 검색
추상화 예제 코드에서 사용하는 2개의 클래스에 대한 설명이다.
//CloudId.java
public enum CloudId {
DROPBOX,
BOX;
}
//FileInfo.java
public class FileInfo {
private CloudId cloudId;
private String fileId;
private String name;
private long length;
... //getter
}
바로 추상화 미적용 코드의 단점에 대해 알아보자.
추상화 미적용 코드에서 사용하는 클래스는 아래와 같다.
//CloudFileManager.java public class CloudFileManager { //파일들의 정보를 조회 public List<FileInfo> getFileInfos(CloudId cloudId) { if (cloudId == CloudId.DROPBOX) { //DropboxClient에서 가져온 파일을 변환한다. 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 == CloudId.BOX) { //BOXd에서 가져온 파일을 변환한다. BoxService boxSvc = ...; ... // } } //파일 다운로드 public void download(FileInfo file, File localTarget) { if (file.getCloudId() == CloudId.DROPBOX) { //Dropbox에서 가져온 파일을 저장한다. DropboxClient dc = ...; FileOutputStream out = new FileOutputStream(localTarget); dc.copy(file.getFileId(), out); out.close(); } else if (file.getCloudId() == CloudId.BOX) { //BoxService에서 가져온 파일을 저장한다. BoxService boxSvc = ...; InputStream is = boxSvc.getInputStream(file.getId()); FileOutputStream out = new FileOutputStream(localTarget); CopyUtil.copy(is, out); } } //파일 업로드 public FileInfo updload(File file, CloudId cloudId) { if (cloudId == CloudId.DROPBOX) { ... } else if (file.getCloudId() == CloudId.BOX) { ... } } //파일 삭제 public void delete(File file, CloudId cloudId) { if (cloudId == CloudId.DROPBOX) { ... } else if (file.getCloudId() == CloudId.BOX) { ... } } //파일 검색 public List<FileInfo> search(File file, CloudId cloudId) { if (cloudId == CloudId.DROPBOX) { ... } else if (file.getCloudId() == CloudId.BOX) { ... } } }
public List<FileInfo> getFileInfos(CloudId cloudId) {...}
매개변수로 받은 CloudId에 해당하는 파일 목록을 조회하는 메소드이다.
public void download(FileInfo file, File localTarget) {...}
다운로드할 파일 정보와 생성할 파일 정보를 매개변수로 받는 파일 다운로드 메소드이다.
이외에도 업로드, 삭제, 검색과 같은 메소드가 있다.
그리고 모든 메소드는 Cloud가 DROPBOX일 경우와 BOX일 경우에 대한 처리를 하는 로직을 포함한다.
그리고 요구사항 2가지가 추가되었다.
추가된 요구사항은 아래와 같다.
클라우드 추가
기능 추가
추가 요구사항을 반영한 코드는 아래와 같다.
우선 클라우드 추가를 반영하기 위한 코드를 보자.
//파일들의 정보를 조회 public List<FileInfo> getFileInfos(CloudId cloudId) { if (cloudId == CloudId.DROPBOX) { ... } else if (cloudId == CloudId.BOX) { ... } else if (cloudId == CloudId.SCLOUD) { ... } else if (cloudId == CloudId.SCLOUD) { ... } else if (cloudId == CloudId.DCLOUD) { ... } }
download(), upload(), delete(), search()도 유사한 else-if 블록이 추가된다.
//클라우드 간 복사 1 public FileInfo copy(FileInfo fileInfo, CloudId to) { 2 CloudId from = fileInfo.getCloudId(); 3 if (to == CloudId.DROPBOX) { 4 DropboxClient dbClient = ...; 5 if (from == CloudId.BOX) { 6 dbClient.copyFromUrl("http://www.box.com/files/"+fileInfo.getFileId()); 7 dbClient.copyFromInputStream(is, fileInfo.getName()); 8 } else if (from == CloudId.DCLOUD) { 9 dbClient.copyFromUrl("http://www.dcloud.com/getfile?fileId="+fileInfo.getFileId()); 10 } else if (from == CloudId.NCLOUD) { 11 NCloudClient nClient = ...; 12 File temp = File.createTemp(); 13 nClient.save(fileInfo.getFileId(), temp); 14 InputStream is = new FileInputStream(temp); 15 dbClient.copyFromInputStream(is, fileInfo.getName()); 16 } 17 } 18 }
3 번째 줄: if (to == CloudId.DROPBOX) {...}
저장할 클라우드의 종류를 구분한다.
클라우드의 종류마다 if문 내부 코드가 조금씩 다르다.
5, 8, 9 번째 줄: if (from == CloudId.BOX) {...}, else if (from == CloudId.DCLOUD) {...}, ...
각 클라우드에 대한 복사 로직이다.
위 코드는 CloudId가 DROPBOX인 경우만 작성하였다.
추가사항을 반영하기 위해서 이와 유사한 코드가 Cloud의 종류만큼 반복되어 작성된다.
아래의 그림은 반복되는 코드의 내용중에서 if-else 구조만 표시하였다.
if-else만 표시하였지만 여전히 길고 반복 코드가 많이 보인다.
그리고 이 상태에서 새로운 클라우드 몇 개가 더 늘어난다면...?
요구 사항이 추가될수록 코드가 길어지고 구조는 복잡해진다.
관련 코드가 여러 곳에 분산된다.
결과적으로 코드 가독성과 분석 속도 저하를 야기시킨다.
또한 코드 추가에 따른 시간이 증가하며, 실수하기 쉬워 불필요한 디버깅 시간이 증가할 것이다.
일단 클라우드를 추상화(공통 성질을 뽑아내기)하면 아래와 같다.
그리고 클라우드 파일 시스템 설계는 아래와 같다.
//Dropbox File System 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); results.add(cf); } return results; } }
public class DropBoxFileSystem implements CloudFileSystem {...}
DropBoxFileSystem은 CloudFileSystem 인터페이스를 상속한다.
public List<CloudFile> getFiles() {
CloudFileSystem 인터페이스가 제공하는 메소드로 CloudFile List를 반환한다.
실제 구현 코드를 보면 CloudFile을 상속받은 DropBoxCloudFile List를 반환한다.
위 콘크리트 클래스를 사용하여 파일 목록, 다운로드 기능에 추상화를 적용해보자.
//CloudFileManager.java 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 List<CloudFile> getFileInfos(CloudId cloudId) {...}
CloudFileSystemFactory에 cloudId를 넘겨줘서 CloudFileSystem을 가지고 온다.
그리고 CloudFileSystem의 getFiles()를 이용하여 파일 정보를 가져오도록 하였다.
public void download(CloudFile file, File localTarget) {...}
다운로드는 CloudFile이라는 추상 타입을 매개변수로 받는다.
그리고 CloudFile 구현체가 제공하는 write() 기능을 사용하여 다운로드한다.
추상화를 적용하니 의도가 명확하게 전달되고 코드도 간결해졌다.
다음은 클라우드를 추가하는 방법에 대해 알아보자.
CLOUD FILE SYSTEM 모듈에 설계해둔 CloudFileSystem과 CloudFile를 상속한 콘크리트 클래스를 구현하면 된다.
이를 그림으로 표현하면 아래와 같다.
CloudFileManager.java 코드 펼치기/접기이때 중요한 것은 Box 클라우드를 지원하는 기능을 추가하여도 CloudFileManager.java의 코드는 바뀌지 않는다.
바뀌지 않은 이유는 추상 타입을 사용하였기 때문이다.
//CloudFileManager.java 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); }
그리고 각 CloudFileSystem 인터페이스 콘크리트 클래스는 아래와 같다.
//DropBoxFileSystem.java public class DropBoxFileSystem implements CloudFileSystem { 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.java public class NCloudFileSystem implements CloudFileSystem { private NCloudClient nClient = new NCloudClient(...); //클라우드 간 파일 복사 기능 public void copyFrom(CloudFile file) { File tempFile = File.createTemp(); file.write(new FileOutputStream(tempFile)); nClient.upload(tempFile, file.getName()); } ... }
이렇게 추상화를 적용하여 얻을 수 있는 결과를 확인해보자.
추상화 타입만으로 핵심 기능 구현 가능의 의미는 새로운 타입이 추가되더라도 핵심 기능의 코드는 변경이 없다는 말이다.
추가적인 장점으로는 특정 타입에 대한 코드는 해당 타입의 객체에 정의할 수 있다.
아래 그림은 Cloud File System의 타입을 추가에 대한 UML이다.
마지막으로 얻는 이점은 OOP의 주요 원칙중 하나인 OCP(Open/Closed Priciple)가 지켜진다는 것이다.
OCP (Open/Closed Principle)이란?
확장에는 열려있어야하고 변경에는 닫혀있어야한다는 원칙이다.의미를 풀어보면
기능을 변경하거나 확장할 수 있으면서 (확장에는 열려있어야하고)
그 기능을 사용하는 코드는 수정하지 않아야한다. (변경에는 닫혀있어야한다)