주요글: 도커 시작하기
반응형
두 번째로 살펴볼 객체 지향 기초 이야기는 추상화와 다형성에 대한 것이다. 이는 모든 패턴의 기본이 되는 내용이므로 이에 대해 이해하는 것은 무엇보다 중요하다. (아래 내용이 완벽하진 않겠지만 기초가 필요한 분들이 입문하는데에 도움이 되었으면 한다.)

추상화(Abstraction)


추상화Abstraction란? 추상화의 사전적 의미는 특정한 개별 사물과 관련되지 않은 공통된 속성이나 관계 등을 뽑아내는 것이다. 이를 컴퓨터 관점에서 생각해보면, 추상화란 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의해 나가는 과정이면서 동시에 각 개별 개체의 구현에 대한 상세함은 감추는 것, 이것이 추상화라고 할 수 있다.

예를 들어, 프로그래밍 언어에서 for, while, foreach 등은 반복을 추상화한 것들이다. 실제로 이것들은 CPU의 이동 명령을 통해서 구현되겠지만, 이 구현으로부터 '반복'이라는 개념을 뽑아내서 for, while 등으로 추상화한 것이다. 비슷하게 OS는 그래픽 기능을 위한 API를 제공하는데, 이 API는 서로 다른 그래픽 카드들의 공통된 기능을 추상화한 결과물이다.

추상화를 하게 되면 상세한 구현이 아닌 공통된 개념과 관계에 집중할 수 있게 되는데, 이는 큰 수준에서 시스템을 이해할 수 있도록 도와준다. 예를 들어, 로그 분석 시스템에서 로그를 다음과 같은 세 가지 방법으로 읽어올 수 있다고 해 보자.
  • 원격 서버의 로그 파일을 FTP로 다운로드한 뒤 로그 조회
  • 원격 서버의 로그 파일을 SCP로 복사한 뒤 로그 조회
  • DB 서버의 로그 테이블로부터 로그 조회
이 세 가지는 개별으로 구현은 다르지만 개념적으로는 '로그 수집'을 처리하는 과정이다. 따라서, 위 세 가지를 '로그 수집'이라는 개념으로 추상화할 수 있으며, 세 개의 상세한 구현이 아닌 한 개의 개념으로 생각할 수 있도록 해 준다.

타입 추상화와 다형성(Polymorphism)

자바에서는 클래스를 이용해서 객체를 표현한다. 예를 들어, FTP 서버로부터 로그 파일을 다운로드해서 로그를 조회하는 객체는 다음과 같은 클래스로 표현할 수 있을 것이다.

public class FtpLogCollector {
    private String ftpServer;
    ... // 기타 다른 필드

    public FtpLogCollector(String ftpServer, String user, String password) {
        this.ftpServer = ftpServer;
        ...
    }

    public FileLogSet collect() {
        ... // FTP에서 파일 다운로드
        ... // 해당 파일의 로그 정보를 담고 있는 FileLogSet 생성 및 리턴
    }
}

FtpLogCollector를 이용해서 로그 정보를 조회하는 코드는 다음과 유사한 형태를 띌 것이다.

FtpLogCollector collector = new FtpLogCollector();
FileLogSet logSet = collector.collect();
FileLogIterator logIter = logSet.iterator();
while (logIter.hasNext()) {
    Log log = logIter.next();
    ...
}
logIter.close();

비슷하게 DB 서버로부터 로그 데이터를 조회하는 객체와 그 기능을 사용하는 객체는 다음과 같은 클래스로 표현될 수 있을 것이다.

public class DBLogCollector {
    private String jdbcUrl;
    ... // 기타 다른 필드

    public DbLogCollector(String jdbcUrl, String user, String password) {
        this.jdbcUrl = jdbcUrl;
        ...
    }

    public JdbcLogSet collect() {
        ... // DB에서 로그 읽어오는 JdbcLogSet 생성 및 리턴
    }
}

DBLogCollector collector = new DBLogCollector();
JdbcLogSet logSet = collector.collect();
JdbcLogIterator logIter = logSet.iterator();
while (logIter.hasNext()) {
    Log log = logIter.next();
    ...
}
logIter.close();

두 코드를 보고나면 너무나도 똑같다는 것을 알 수 있다. 차이점은 FtpLogCollector와 DBLogCollector, FileLogSet과 JdbcLogSet 그리고 FileLogIterator와 JdbcLogIterator의 구현이 다르다는 것일 뿐, 각 클래스가 제공하는 기능은 개념적으로 완전히 동일하다.
  • FtpLogCollector, DBLogCollector : 로그 집합을 특정 자원으로부터 수집하는 기능
  • FileLogSet, JdbcLogSet: 로그에 접근할 수 있는 Iterator를 제공하는 기능
  • FileLogIterator, JdbcLogIterator: 로그를 순차적으로 접근할 수 있는 기능
여기서 추상화의 정의를 다시 살펴보자. 추상화란 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의해 나가는 과정이면서 동시에 각 개별 개체의 구현에 대한 상세함은 감추는 것이라고 했다. 이를 위에 맞춰서 설명하면 FTP나 DB에서 로그를 수집하고 파일이나 테이블에 있는 로그에 접근하는 상세한 구현은 감추고 비슷한 개념인 로그 수집과 로그 접근을 도출해내는 것이 추상화인 것이다.

객체 지향 언어에서는 이렇게 추상화된 개념을 인터페이스로 표현한다. 언어마다 인터페이스를 제공하는 방식이 다른데, 자바의 경우 인터페이스를 interface라는 별도 타입으로 제공하고 있으며 또한 추상 클래스(abstract class)를 이용해서 객체의 인터페이스를 정의할 수도 있다. 앞서 로그 수집과 접근 기능을 추상화한 인터페이스는 다음과 같이 정의할 수 있을 것이다.

public interface LogCollector {
    LogSet collect();
}

public interface LogSet {
    LogIterator iterator();
}

public interface LogIterator {
    boolean hasNext();
    Log next();
    void close();
}

객체를 추상화했으면, 그 다음으로 할 작업은 실제 기능을 제공하도록 추상화된 타입을 구현하는 것이다. 자바 언어에서는 상속을 통해서 추상화된 타입을 구현할 수 있도록 하고 있다. interface를 정의한 경우에는 인터페이스 상속인 implements를 이용해서 추상 타입을 구현하게 되고, 추상 클래스를 정의한 경우에는 클래스 상속인 extends를 통해서 추상 타입을 구현하게 된다. 다음은 FTP로부터 로그를 수집해서 로컬 파일에 저장하고 로컬 파일로부터 로그 데이터를 읽어오는 구현을 제공하는 구현의 모습을 보여주고 있다.
 
public class FtpLogCollector implements LogCollector {
    private String ftpServer;
    ...
    public FtpLogCollector(...) { ... }
    public LogSet collect() { ... // FileLogSet을 생성해서 리턴 }
}

public class FileLogSet implements LogSet {
    private List<File> files;

    public LogIterator iterator() { ... FileLogIterator를 생성해서 리턴 }
}

public class FileLogIterator implements LogIterator {
    public boolean hasNext() { ... }
    public Log next() { ... }
    public void close() { ... }
}

위 코드에서 FtpLogCollector, FileLogSet, FileLogIterator는 각각 LogCollector, LogSet, LogIterator가 정의한 기능(메서드)의 실제 구현을 제공하고 있다. JDBC를 사용하는 경우에도 비슷한 구성을 갖게 된다. 즉, FTP로 읽어오기 위해 필요한 구현과 DB에서 읽어오기 위해 필요한 구현이 모두 동일한 인터페이스에 맞춰서 개발되는 것이다.

인터페이스 상속이나 클래스 상속을 통해서 추상화 타입을 구현한다고 해서 모든 게 끝나는 것은 아니다. 이런 추상화가 힘을 발휘할 수 있는 것은 다형성 때문이다. 다형성Polymorphism은 이름 그래도 한 객체가 여러 형태를 갖는다는 의미로 다형성으로 인해 추상화가 빛을 발한다.

다형성을 자바 언어와 연결해 설명해 보자면, 한 클래스의 인스턴스(객체)는 그 클래스가 상속 받은 모든 상위 타입도 될 수 있다는 것이다. 앞의 예제로 다시 한 번 설명해 보자. FtpLogCollector는 LogCollector를 상속받고 있으므로, FtpLogCollector 타입의 객체는 LogCollector로서도 동작할 수 있다. 따라서, 다음과 같은 코드 작성이 가능하다.

LogCollector collector = new FtpLogCollector(....);
LogSet logSet = collector.collect();
LogIterator logIter = logSet.iterator();

FtpLogCollector 객체는 LogCollector 타입도 되기 때문에, 위 코드에서와 같이 FtpLogCollector의 객체를 LogCollector 타입 변수에 할당할 수 있다. 또한, collector.collect()는 실제 객체의 타입인 FtpLogCollector 객체의 collect() 메서드를 실행하게 된다.

위 코드에서 FtpLogCollector를 생성하는 코드를 한 번 더 추상화하면 어떻게 될까?

LogCollector collector = LogCollectorFactory.create();
....

LogCollectorFactory 클래스는 시스템 설정에 따라서 FtpLogCollector나 JdbcLogCollector를 생성해주는 기능을 제공하는 팩토리라고 해 보자. 즉, LogCollectorFactory 클래스는 LogCollector 타입의 객체 생성을 추상화한 클래스이다. 이제 위 코드는 더 이상 특정 LogCollector의 구현체(FtpLogCollector와 같은)에 의존하지 않으며, 오직 추상 타입인 LogCollector만 사용하고 있다. 따라서, FtpLogCollector를 사용하다가 DBLogCollector를 사용하더라도 위 코드는 전혀 영향을 받지 않는다. 오직 LogCollectorFactory만 영향을 받을 뿐이다. 이렇게 실제 사용되는 구현 클래스가 변경되었음에도 불구하고 그 타입을 사용하는 코드가 영향을 받지 않는 것이 바로 다형성으로 인해 가능하다.

추상화란 결국 개발자의 뇌를 위한 것

로그 분석 기능을 생각해보면 큰 덩어리로 '로그 파일을 읽어와 파일을 한줄 한줄 파싱한 뒤 계산해서 내용을 DB에 저장한다'일 것이다. 이런 상세함을 한 번에 다 머리속으로 생각할 수 있는 사람은 많지 않다. (물론, 정말 똑똑한 사람은 한 번에 다 생각할 수 있겠지만..) 그래서 필요한 게 생각이 가능한 수준의 덩어리로 나눠서 생각하는 것이다. 상세한 구현을 생각이 가능한 수준의 덩어리로  만들어나가는 과정이 추상화 과정이며, 이를 통해 개발자는 구현의 상세함 속에서 허우적거리지 않고 (머리속에서) 관리 가능한 수준으로 프로그램과 구현을 다룰 수 있게 된다.

관련글

참고자료


제가 쓴 객체 지향 입문서입니다.


http://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8969090010  에서 확인하실 수 있습니다.



+ Recent posts