티스토리 툴바


저작권 안내 (펌 하실 때)
  • 저작권자표시 Yes, 상업적이용 No , 컨텐츠변경 No

기능의 재활용이나 기존 기능을 확장하기 위해서 사용하는 가장 손쉬운 방법은 구현 상속을 사용하는 것이다. 수화물 목록을 관리하기 위해 ArrayList를 상속받아서 구현했다고 하자.


public class LuggageCompartment extends ArrayList<Luggage> {

    private int restSpce;

    public void add(Luggage piece) {

        this.restSpace -= piece.getSize();

        super.add(piece);

    }


    public void canContain(Luggage piece) {

        return this.restSpace > piece.size();

    }


    public void extract(Luggage piece) {

        this.restSpace += piece.getSize();

        super.remove(piece);

    }


}


LuggageCompartment는 ArrayList가 제공하는 목록 관리 기능을 재사용하기 위해 ArrayList를 상속받았고, 내부적으로 짐을 넣을 수 있는 여분 공간을 관리하는 기능을 추가하였다. LuggageCompartment 클래스를 정해진 대로만 사용하면 문제가 발생하지 않는다. 하지만, LuggageCompartment는 상위 클래스인 ArrayList의 기능도 함께 제공하기 때문에 다음과 같은 코드를 작성하는 것이 가능하다.


LuggageCompoartment lc = new LuggageCompartment();

lc.add(new Luggage(10));

lc.remove(someLuggage); // 앗!! restSpace가 계산되지 않는다!

lc.extract(anyLuggage);

lc.canContain(aLuggage); // 잘못된 결과


위 코드에서 remove() 메서드는 ArrayList가 제공하는 메서드이므로 LuggageCompartment 객체에서도 호출할 수 있다. 문제는 remove() 메서드를 호출할 경우 LuggageCompartment가 지켜야 하는 규칙(여분 공백 계산)이 지켜지지 않는다는 점이다. 그렇다고 여분 공백 계산을 위해서 ArrayList의 거의 모든 메서드를 오버라이딩하는 것은 배보다 배꼽이 더 큰 상황을 만든다.


상속은 'IS A'에 대한 것


그럼, 왜 이런 문제가 발생하는 걸까? 그 이유는 역할이 같지 않은 클래스를 상속받아 재사용했기 때문이다. 상속은 'IS A' 관계일 때 의미를 갖는다. 예를 들어, ArrayList는 AbstractList이다. (ArrayList is a AbstractList). 따라서, ArrayList가 오버라이딩 하지 않은 AbstractList의 메서드를 호출하더라도 ArrayList 객체는 아무런 문제를 일으키지 않으며, ArrayList는 기대하는 바대로 동작한다.


반면 앞서 LuggageComponent는 ArrayList가 아니다. 즉, LuggageComponent is not a ArrayList 인 것이다. 이런 상황에서 ArrayList의 목록 관리 기능을 재사용하기 위해 ArrayList를 상속받아 구현하면 앞서 살펴본 것 처럼, 상위 클래스의 메서드를 호출할 때 하위 클래스의 기능이 올바르게 동작하지 않는 문제가 발생하게 된다. 즉, 'IS A' 관계가 아닌 두 클래스를 구현 상속으로 연결함으로써 원하지 않은 문제가 발생한 것이다.


따라서, 구현 상속을 이용해서 기능을 재사용하려면 반드시 두 클래스가 같은 역할을 수행하는지 확인해야 한다. 두 클래스가 생성하는 두 객체가 서로 다른 추상 타입을 위한 것이라면 기능 재사용의 방법으로 상속을 선택하면 안 된다.


조립을 통한 기능 재사용


두 클래스가 IS A 관계가 아니라면, 그 두 클래스는 서로 다른 역할을 수행한다는 의미를 갖는다. 앞서 LuggageCompart는 ArrayList처럼 범용적인 객체의 목록을 관리하는 역할을 수행하지 않는다. 단지, 짐의 목록을 관리하기 위한 목적으로 ArrayList의 기능이 필요했던 것이다. 이렇게 역할이 다른 객체의 기능을 재사용하고 싶다면, 조립(composition)을 통해서 재사용하는 것이 좋다.


조립을 통한 재사용은 다음과 같이 필드로 재사용할 객체를 정의하고 메서드 내부에서 해당 필드를 사용하는 식으로 구현된다.


public class LuggageCompartment {


    private List<Luggage> luggages = new ArrayList<Luggage>();

    private int restSpce;


    public void add(Luggage piece) {

        restSpace -= piece.getSize();

        luggages.add(piece);

    }


    public void canContain(Luggage piece) {

        return this.restSpace > piece.size();

    }


    public void extract(Luggage piece) {

        restSpace += piece.getSize();

        luggage.remove(piece);

    }


}


조립 방식으로 구현한 LuggageCompartment 클래스는 ArrayList를 상속받지 않기 때문에 LuggageCompoartment를 사용하는 코드는 remove()와 같은 메서드를 호출할 수 없다. 따라서 LuggageCompartment 클래스는 구현 상속의 경우처럼 원하지 않는 메서드가  호출되어 기능이 비정상적으로 동작하는 걱정을 하지 않아도 된다. 또한, LuggageCompartment가 목록을 관리하기 위해 ArrayList를 사용한다는 점을 클래스 외부에서 알 수 없기 때문에 캡슐화도 향상된다.


조립 방식의 또 다른 장점은 런타임에 재사용할 기능을 교체할 수 있다는 점이다. 상속은 컴파일타임에 정적으로 관계가 결정된다. 만약 ArrayList가 아닌 LinkedList를 상속받으려면 코드를 수정해서 재컴파일해야 한다. 반면 조립방식의 경우는 런타임에 재사용할 객체를 결정할 수 있다. 예를 들어, 앞서 LuggageCompartment가 생성자를 통해서 기능을 재사용할 List 구현 객체를 전달받는다고 해 보자. 이 방식은 런타임에 얼마든지 구현 객체를 변경할 수 있기 때문에, 소스 코드 수정이 필요한 정적 방식보다 더 유연하게 재사용할 객체를 변경할 수 있게 된다. ('객제 지향 기초 이야기 3, 유연함'http://javacan.tistory.com/entry/OO-Basic-3-Flexibility 을 참고하자.)


구현 상속은 정말 필요할 때 만 쓸 것!


구현 상속은 유지보수를 어렵게 만드는 요인이 된다. 예를 들어, 스프링의 AbstractController를 생각해보자. AbstractWizardFormController, SimpleFormController, MultiActionController 등이 이 클래스의 구현을 상속받고 있다. 이는 AbstractController의 기능을 수정하려면 그 하위 클래스들이 받을 영향에 대해서 고려해야 한다는 것을 뜻한다. 즉, 상속 받은 클래스가 많으면 많을수록 코드 변경시 고려해야 하는 클래스들의 숫자는 비례해서 증가하며, 이는 변경의 어려움이 증가한다는 것을 뜻한다. 따라서, 구현 상속을 통해 기능을 재사용하려거든 정말 필요한 경우에 한해서 적용해야 하며, 조립이나 메타 정보 사용 등의 방법으로 풀 수 있다면 다른 방법을 사용하는 것이 코드 유지보수에 있어 유리하다.





저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.
객체 지향을 하는 이유는? 앞서 '객체 지향 기초 이야기 1, 객체(http://javacan.tistory.com/entry/OO-Basic-1-Object)' 글에서 유지보수 비용을 절감하기 위해 객체 지향적으로 개발한다고 했던 것을 기억할 것이다. 유지보수 비용을 줄일 수 있다는 것은 기능을 쉽게 추가하거나 변경할 수 있다는 것을 의미하는데, 이는 다음의 두 가지를 통해서 얻을 수 있다.
  • 캡슐화: 내부의 구현이 변경되더라도 외부에 영향을 최소화
  • 다형성: 구현체 변경의 유현함을 제공
이번에 살펴볼 내용은 위 두 가지 중 다형성이 주는 유연함에 대한 것이다.

유연하지 않은 코드


유연한 코드에 대해 살펴보기 전에 먼저 유연하지 않은 코드를 살펴보자. 앞서 예제들에서 사용했던 로그 수집을 생각해보자. 최초에 로그 처리는 FTP로부터 파일을 읽어오는 요구만 있었다. 그래서 로그 처리 과정을 제어하는 객체는 FTP로부터 로그를 다운받아오는 FtpLogCollector를 사용해서 구현하였다.


// 흐름 제어

FtpLogCollector logCollector = new FtpLogCollector(....);

FtpLogSet logSet = logCollector.collect();

....


그런데, 요구 사항이 갑자기 바뀌었다. FTP가 아닌 DB에 쌓여 있는 데이터를 로그로 사용해야 한다는 것이었다. 이제 해야 하는 일은? DB에서 로그를 읽어오는 객체를 구현하고 FtpLogCollector 클래스가 출현하는 부분을 모두 DbLogCollector로 바꿔주는 것이다.


DbLogCollector logCollector = new DbLogCollector(....);

DbLogSet logSet = logCollector.collect();

....


로그의 수집 방식이 바뀌면, 로구를 수집하는 구현 클래스가 바뀌는 것 뿐만 아니라 수집 기능을 사용하는 코드도 바뀌게 된다. 즉, 하나의 변화가 다른 코드의 변화를 유발시키는 것이다. 이런 걸 당해본 개발자들은 알겠지만, 이런 종류의 일은 어딜 변경해야 하는지 코드를 검색해야 하고, 노동 집약적인 방식으로 일을 하도록 만든다.


이런 일이 벌어지는 이유는 추상화된 타입이 아닌 실제 구현체를 이용해서 코드를 만들었기 때문이다.


구현체(implementation)가 아닌 인터페이스(interface)에 대고 프로그래밍하기


객체 지향의 기본 규칙 중에 '구현체가 아닌 인터페이스에 대고 프로그래밍'하라는 규칙이 있다. 이 규칙이 나온 이유는 바로 유연함 때문이다. 앞서 봤듯이 추상화된 인터페이스 타입이 아닌 실제 구현을 제공하는 타입을 이용해서 프로그래밍 할 경우, 구현에 변화가 생겼을 때 변화의 영향력이 이곳 저곳으로 퍼져 나가게 된다.


변화가 발생하는 부분이 있다면, 이 부분을 추상화해서 인터페이스 타입을 이용해서 프로그래밍 해 주어야 한다. 인터페이스 타입을 사용해서 구현하게 되면, 실제 구현 객체가 변경되더라도 인터페이스 타입을 이용해서 구현한 코드는 영향을 받지 않게 된다. 예를 들어, 아래는 FtpLogCollector 대신 로그 수집을 추상화한 LogCollector 인터페이스에 대고 프로그래밍을 한 코드이다.


LogCollector logCollector = ... // LogCollector 구현 객체를 어디선가 구함

LogSet logSet = logCollector.collect();

...


LogCollector의 실제 구현 객체를 구하는 부분은 일단 잊어두고 나머지 코드를 살펴보자. 로그 수집하는 방식이 FTP에서 DB로 바뀌더라도 위 코드는 바뀌지 않는다. DB 뿐만 아니라 어떤 방식으로 바뀌더라도, 심지어 FTP와 DB에서 모두 로그를 읽어오도록 구현하더라도 위 코드는 바뀌지 않는다. 즉, 인터페이스에 대고 프로그래밍함으로써 실제 사용되는 구현 객체를 쉽게 변경할 수 있는 유연함을 얻게 된 것이다.


위 코드에서 바뀌는 부분은 실제 LogCollector 구현 객체를 생성하는 부분이다. 실제 사용할 구현 객체를 구하는  부분은 Factory 패턴을 사용하거나 DI(Dependecy Injection)을 통해서 코드 변경을 최소화할 수 있는데, 이 두 가지에 대한 내용은 본 글의 범위를 벗어나므로 패턴 관련 서적 등을 참고하기 바란다.


하지만, 유연함을 얻는 과정은 간단하지 않다. 먼저 실제 구현 객체를 추상화한 인터페이스를 도출해내야 한다. 처음 객체 지향을 접하는 사람들이 어려워 하는 것 중의 하나가 객체를 알맞게 추상화한 인터페이스를 도출하는 것이기 때문에, 유연한 코드를 얻어 내려면 기능(객체)을 추상화하는 연습을 많이 해 주어야 한다.


인터페이스를 도출하기 위한 기본적인 방법은 구현 클래스의 public 메서드를 인터페이스로 변경해보는 것이다. public 메서드는 외부에 기능을 제공할 목적으로 만들어지는 경우가 많기 때문에 인터페이스 후보가 될 수 있다. 내부의 private 메서드 중에서도 인터페이스 후보가 나올 수 있다. private 메서드는 클래스 내부에서 사용되는 기능을 분리한 경우가 많은데, 이 경우 private 메서드를 별도의 인터페이스로 분리함으로써 해당 기능에 대한 유연함을 얻을 수 있게 된다.


인터페이스를 항상 만들어야 하나?


당장 발생하지도 않을 변화를 준비하느라 미리 앞서 인터페이스를 사용할 필요는 없다. 실제로 모든 구현 타입에 대해 인터페이스 타입을 만드는 것은 매우 성가시면서도 불필요한 일이 될 수 있다. 하지만, 그럼에도 불구하고 필자는 인터페이스를 최대한 도출해내려고 노력하는데, 그 이유 중 하나는 단위 테스트에 필요한 Mock 테스트를 할 수 있도록 하기 위함이다.


구현을 하다보면 다른 기능이 구현되지 않아서 현재 작업중인 코드를 테스트 할 수 없는 경우가 발생하는데, 이 경우 Mock 객체를 사용하면 다른 기능의 구현이 되어 있지 않더라도 작업중인 코드를 어느 정도 테스트 할 수 있게 된다. 다른 기능이 만약 인터페이스로 추상화되어 있다면 여러 Mock 라이브러리를 사용해서 다른 기능을 가짜로 제공할 수 있게 되며, 이는 다른 기능의 구현이 완성되어 있지 않더라도 현재 기능을 테스트 할 수 있다는 것을 의미한다.



저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 권남 2012/05/29 12:28  댓글주소  수정/삭제  댓글쓰기

    잘 읽고 있습니다.
    참 쉽게 설명하시네요.

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.
두 번째로 살펴볼 객체 지향 기초 이야기는 추상화와 다형성에 대한 것이다. 이는 모든 패턴의 기본이 되는 내용이므로 이에 대해 이해하는 것은 무엇보다 중요하다. (아래 내용이 완벽하진 않겠지만 기초가 필요한 분들이 입문하는데에 도움이 되었으면 한다.)

추상화(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에 저장한다'일 것이다. 이런 상세함을 한 번에 다 머리속으로 생각할 수 있는 사람은 많지 않다. (물론, 정말 똑똑한 사람은 한 번에 다 생각할 수 있겠지만..) 그래서 필요한 게 생각이 가능한 수준의 덩어리로 나눠서 생각하는 것이다. 상세한 구현을 생각이 가능한 수준의 덩어리로  만들어나가는 과정이 추상화 과정이며, 이를 통해 개발자는 구현의 상세함 속에서 허우적거리지 않고 (머리속에서) 관리 가능한 수준으로 프로그램과 구현을 다룰 수 있게 된다.


저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 지노 2012/10/23 08:41  댓글주소  수정/삭제  댓글쓰기

    좋은 글 잘 읽었습니다. ^^

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

한 번 만들어진 소프트웨어는 얼마나 사용될까? 윈도우 XP는 10년 넘게 사용되고 있고, 포털 사이트의 카페 서비스 역시 10년 넘게 운영을 하고 있다. 여기서 알 수 있는 사실은, 한 번 쓰고 버릴 소프트웨어를 제외한 나머지 다수의 소프트웨어는 개발 기간보다 더 오랜 시간 동안 사용된다는 점이다. 소프트웨어는 살아 있는 생물과도 같아서 지속적으로 변화를 하게 되며, 이는 곧 소프트웨어를 유지하기 위해서는 계속해서 비용이 발생한다는 것을 뜻한다. 소프트웨어를 유지하기 위해 비용이 계속해서 발생하는데, 소프트웨어를 개발하는 시간보다 소프트웨어를 사용하는 시간이 더 길다는 것은, 다시 말해서 소프트웨어를 개발하는 비용보다 소프트웨어를 유지하는 비용이 더 많이 발생한다는 사실을 말해준다.


초기 개발 비용 <<<<<<<< 유지보수 개발 비용


비용을 줄이려면 어떻게 코드를 만들어야 할까? 여러 가지 답이 있겠지만 필자는 다음의 두 가지라고 생각한다.

  • 코드 분석이 용이
  • 코드 변경이 용이
당연한 얘기지만, 코드를 쉽게 읽을 수 있고 변경을 쉽게 할 수 있어야 유지보수 비용이 줄어든다. 근데 왜 객체 이야기를 하는데 비용 얘기를 하느냐? 그건 바로 객체 지향적으로 개발을 하는 것이 위 두 가지를 달성할 수 있는 방법이기 때문이다. 설계를 잘 하느니 객체를 잘 뽑아내느니 등의 말은 겉치레에 불과하다. 필자가 객체 지향을 좋아하는 이유는 뭔가 있어 보이는 것도 있지만(^^;) 그것보다는 전체적으로 유지보수 비용을 감소시켜주기 때문이다. 객체 지향이 비용을 감소시켜 주지 못한다면 객체 지향은 쓸모가 없는 것이나 다름없다.

그렇다면, 왜 객체 지향을 하면 유지보수 비용이 줄어들게 될까? 이는 절차 지향과 객체 지향을 비교해보면 좀 더 명확해질 것 같다.


절차 지향과 객체 지향


절차 지향의 핵심은 데이터를 중심으로 구현을 한다는 데 있다. 각각의 기능을 수행하는 프로시저(함수)들이 데이터를 조작하거나 사용하는 방식으로 구현을 한다.



다양한 프로시저들이 데이터를 공유해서 동작하기 때문에 데이터의 변화는 곧 데이터를 사용하는 코드(프로시저)의 변화로 이어진다. 심지어 한 데이터 타입의 변화가 다른 데이터의 변화를 일으키기는 경우도 있다. 예를 들어, 화폐를 표현하는 데이터의 타입이 int에서 double로 변경되면 화폐를 사용하는 프로시저 뿐만 아니라 잔고, 판매금액 등의 다른 데이터 타입까지도 모두 변하게 된다. 심할 경우, 한 변수의 타입 변화가 수 일이 소용되는 노가다를 만들어내기도 한다. 이는 데이터 중심인 절차 지향으로 구현할 경우 유지보수 비용이 높아진다는 것을 의미한다.


중심에 데이터를 놓고 프로시저를 구현하는 절차 지향과 달리 객체 지향은 데이터와 기능을 함께 담고 있는 객체들이 서로 대화하는 방법으로 구현된다. 객체 간에는 데이터를 (최대한) 주고 받지 않으며, 객체 상호간에 기능을 실행해 달라고 요청하는 방식으로 구현을 하게 된다.



각 객체들은 내부적으로 데이터를 담고 있지만 다른 객체에게 데이터를 노출하지는 않는다. 객체와 객체 간에 데이터가 흐르지 않기 때문에 특정 객체가 담고 있는 데이터에 변화가 생기더라도 해당 객체로만 변화가 한정되며 다른 객체에까지 변하가 전파되지 않게 된다. 이 얘기는 변화에 따른 유지보수 비용이 절차 지향에 비해 상대적으로 더 작다는 것을 의미한다.


프로시저와 객체 지향에 대한 보다 구체적인 예는 필자가 이전에 쓴 '캡슐화 http://javacan.tistory.com/entry/EncapsulationExcerprtFromJavaBook'란 글을 참고하기 바란다.


모든 것을 객체 지향으로 할 필요는 없다. 단순히 한 개의 DB 테이블의 데이터를 읽어서 보여주는 웹 방명록을 생각해보자. 이 기능은 데이터를 중심으로 한 절차 지향으로 구현하는 것이 훨씬 간단하다. 또한 일회성으로 사용될 이벤트 프로그램을 구현하기 위해 객체 지향을 추구할 필요는 없다. 왜냐면 유지보수 비용이 발생하지 않기 때문이다. 하지만, 도메인이 복잡하고 유지보수 기간이 길어질수록 절차 지향 방식은 높은 개발 비용을 발생시키기 때문에, 이런 경우에는 객체 지향적으로 하는 것이 비용측면에서 유리하다. 물론, 코드를 만져야 하는 개발자의 정신 건강 측면에서도 훨씬 유리하다.


객체의 핵심은 기능을 제공하는 것


객체를 가장 간단하게 정의하자면 '기능을 제공하는 것'이라고 할 수 있다. 조금 더 길게 설명하자면 다음과 같다.

  • 제공하는 기능으로 정의된다.
  • 내부적으로 어떤 데이터를 들고 있고 어떻게 기능을 구현하는 지는 숨긴다.

객체는 기능을 제공한다. 예를 들어, TV라는 객체를 생각해보자. TV는 다음의 기능들을 제공할 것이다.

  • 채널 증가
  • 채널 감소
  • 지정 번호로 채널 변경
  • 볼륨 증가
  • 볼륨 감소

TV가 내부적으로 볼륨의 크기를 어떤 데이터 타입으로 관리하는지는 중요하지 않고, 단지 TV 객체에 볼륨을 증가시키라고 요청을 할 뿐이다. 이를 코드로 표현하면 다음과 같다.


TV tv = new TV();

tv.increaseVolume(); // 기능을 제공


만약 데이터가 흘러다니도록 구현을 한다면 다음과 같은 코드가 만들어질 것이다.


TV tv = new TV();

int volume = tv.getVolume();

tv.setVolume(volume + 1);


두 코드의 차이는 명확하다. 앞서 첫 번째 코드의 increaseVolume()는 볼륨을 증가시키는 기능을 제공하며, 내부적으로 볼륨의 타입이 변경되더라도 영향을 받지 않는다. 반면에 두 번째 코드는 볼륨 값을 저장하는 타입이 int 임을 노출하고 있으며, 이 데이터에 기대서 볼륨 조절 기능을 구현하고 있다. 두 번째 코드에서 볼륨을 보관하는 데이터 타입이 short이나 byte와 같은 다른 타입으로 변경될 경우 그 데이터를 사용하는 코드가 함께 변경되게 된다.


객체의 역할(또는 책임)과 메시징


객체는 기능을 제공하는 존재이기 때문에, 객체 지향적으로 설계를 한다는 것은 각각의 객체가 어떤 기능을 제공하는지 정의하는 작업이 된다. 또한, 각 객체들이 서로 어떻게 상대방의 기능을 사용하는지를 설계하는 것이 객체 설계가 된다. 이런 의미에서 객체 지향 설계를 할 때 가정 먼저 해야 할 작업은 객체가 어떤 역할을 수행하는 지 결정하는 것이다.


하나의 객체가 모든 기능을 제공하는 것은 유지보수 측면에서 바람직하지 않기 때문에, 각 기능들을 관련된 것들끼리 분리해서 각각의 객체들에 역할을 분배해준다. 예를 들어, 로그 분석하는 시스템을 생각해보자. 이 경우 다음과 같이 객체 별로 역할을 구분해서 할당할 수 있다.

  • 로그 수집 객체 객체: 로그를 수집해서 한 곳에 모아준다.
  • 로그 분석 객체: 로그 파일로부터 데이터를 추출해서 중간 결과를 생성한다.
  • 리포트 생성 객체: 중간 결과로부터 실제 원하는 리포트를 생성한다.
  • DB 연동 객체: DB에 데이터를 삽입해주는 기능을 제공한다.
  • 흐름 제어 객체: 위 객체들간의 실행 순서를 제어한다.

객체의 역할과 함께 도출되는 것은 객체들 간의 관계이다. 전체 기능이 각각의 객체에 분산되어 있기 때문에, 완성된 기능을 제공하려면 각 객체들이 제공하는 기능을 엮어주어야 한다. 이는 객체 간의 메시징을 통해서 이루어진다. 한 객체는 다른 객체에 기능 실행을 요구하기 위한 요청을 보내는데 이 요청을 보통 메시지라고 표현한다. 다른 객체로부터 메시지를 받은 객체는 메시지에 알맞은 기능을 실행하고 그 결과를 메시지로 보낸다. 자바의 경우 기능 실행을 요청하는 메시지가 메서드 호출로 구현되며, 응답 메시지는 리턴이나 익셉션 등으로 구현된다.


객체의 역할과 메시지로부터 뭔가 떠오르는 것이 있을 것이다. 그것은 바로 커뮤니케이션 다이어그램이다. 커뮤니케이션 다이어그램은 객체 간의 메시지를 주고 받는 과정을 표현하기 위한 다이어그램으로, 이는 객체 지향 설계의 기본이 된다. 실제로 객체를 설계한다는 것은 다음을 반복하는 과정이다.

  • 기능들을 제공할 객체 후보를 선별한다.
  • 각 객체들이 어떻게 메시지를 주고 받는 지 연결한다.
  • 안 좋은 냄새가 나면 위 단계를 되풀이한다.

좋은 객체를 만드는 기본 규칙: 데이터 말고 기능 제공해 줘!


좋은 객체가 한 번에 나오면 좋겠지만, 설계란 그리 호락호락하지 않음을 누구나 알고 있다. 그래서 지속적으로 설계가 바뀌게 되고, 그렇기에 리팩토링이 구현에서 중요한 과정이 되는 것이다. 하지만, 출발점을 좋게 잡을 수는 있다. 그 중 하나가 다음에 따라 객체의 기능을 제공하는 것이다.

  • 데이터를 달라고 하지 않고, 해 달라고 하기!
  • 데이터 조회 기능은 필요한 경우에 한정해서 제공한다.

위 규칙에 따라 객체를 설계하다보면 자연스럽게 객체들 사이로 데이터가 흘러다니는 것을 줄이고, 객체가 내부 내장을 드러내지 않고 (즉, 캡슐화하고) 알맞은 기능을 제공하도록 만들 확률을 높일 수 있게 된다.


내용 정리


간단하게 내용을 정리해 보자.

  • 객체는 기능을 제공하는 역할을 수행하며, 객체의 내부가 어떻게 구현되었는지는 숨긴다.
  • 각 객체는 메시지를 주고 받으며 협업하며, 이를 통해 하나의 완성된 기능을 제공한다.
  • 객체 지향은 변화의 영향이 주변으로 퍼져나가는 것을 감소시켜 유지보수 비용을 줄여준다.

 

저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

Scala 언어가 재미있지만 실제 프로젝트에 적용하려면 콘솔에서 컴파일하거나 실행해 볼 수는 없다. 실제 프로젝트에 Scala를 적용하려면 먼저 편리한 개발 환경을 구축해 주어야 한다. 필자의 경우 개발시 주로 이클립스와 메이븐을 사용하기 때문에, Scala를 이 환경에서 개발하기 위한 내용을 찾아보았으며, 각 내용들은 한 곳에 모아 보았다. (나중에 또 돌아다니면서 찾는 건 귀찮기에 이곳에 정리한다.)


이클립스와 메이븐(m2e 플러그인) 환경을 이용해서 Scala를 적용하려면 다음과 같은 준비를 하면 된다.

  • Scala IDE 설치
  • M2Eclipse Scala 플러그인 설치
  • Maven 프로젝트에 pom.xml 파일에 maven-scala-plugin 설정

Scala IDE 설치하기


먼저 Scala IDE를 설치한다. 이클립스 업데이트 사이트는 아래와 같다. 참고로 아래 주소는 Scala 2.9 버전 기준이다. (참고 사이트는 http://scala-ide.org/index.html 이다.)

  • http://download.scala-ide.org/releases-29/stable/site

M2Eclipse Scala 플러그인 설치


그 다음 할 작업은 Maven 프로젝트와 Scala IDE를 연결해 줄 플러그인을 설치해주는 것이다. 업데이트 주소는 아래와 같다. (참고 사이트는 https://github.com/sonatype/m2eclipse-scala 이다.)


  • http://alchim31.free.fr/m2e-scala/update-site/

pom.xml 파일에 maven-scala-plugin 설정하기


이제 남은 작업은 이클립스에서 실행할 메이븐 프로젝트의 pom.xml 파일에 maven-scala-plugin 설정을 하는 것이다. pom.xml 파일의 설정 예는 https://github.com/sonatype/m2eclipse-scala 사이트에서 찾아볼 수 있다. pom.xml 파일을 알맞게 작성한 뒤에 Scala 소스 코드를 작성하면, 알맞게 컴파일되고, 실행해 볼 수 있고,  자바 코드에서 Scala가 생성한 클래스를 사용하는 등의 작업을 할 수 있다.


참고로, 그럼 내가 왜 Scala를 사용해보려고 하느냐? 여러 이유가 있겠지만 다음이 주요 이유다.

  • 언어 자체의 재미: 언어가 다소 복잡하지만 재미는 있다.
  • Combinator Parser: 뭔가 규칙에 기반해서 텍스트를 파싱해야 할 때, 언어 차원에서 Parser를 지원하기 때문에, ANTLR 등을 사용하지 않아도 쉽게 파싱할 수 있다.
  • DSL(Domain Specific Language): 위의 내용과 더불어 DSL을 만들기에 Ruby나 Groovy 등의 언어들 만큼 좋다.
  • 자바와의 호환성: JVM 기반으로 동작하며 자바와의 호환이 당연히 잘 된다.
  • 함수형 언어이고 Clousure 등을 지원하기에 테스트 코드에 적용하면 테스트 코드에서 발생하는 중복을 많이 줄일 수 있으므로, 그것만으로도 가치가 있다.



저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

회사를 옮길 때 마다 Maven 리포지토리와 관련된 비슷한 짓거리를 하고 그 내용을 동료들에게 알려주어야 하는 귀찮음을 줄이고자 정리해 보았다.


회사에서 내부적으로 사용할 Maven 리포지토리가 필요한 경우가 있다. 예를 들어, 오라클 JDBC 드라이버는 Maven 중앙 리포지토리에 존재하지 않기 때문에 모든 개발자들이 로컬 리포지토리에 따로 등록해주는 수고를 해야 하는데, 내부용 Maven 리포지토리를 만들어서 그곳에 오라클 JDBC 드라이버를 등록하면 한 사람만 내부용 리포지토리에 등록해주는 수고를 해 주면 나머지 개발자는 편하게 오라클 JDBC 드라이버를 Maven의 depedency로 등록해서 사용할 수 있게 된다. (한 사람이 노력으로 많은 개발자들의 중복 작업이 사라진다. DRY!!) 외부의 jar 파일 뿐만 아니라 내부에서 사내의 다수 프롤젝트에서 사용될 모듈을 만드는 경우에도 내부용 리포지토리를 만들면 쉽게 관련 모듈을 사용/관리할 수 있게 된다.

내부용 리포지토리 구축을 쉽게 하고 싶을 때에는 Nexus(http://www.sonatype.org/nexus/)를 사용하면 된다. Nexus는 자바로 만들어져 있으며, WAR로도 배포되기 때문에 손쉽게 설치할 수 있다는 장점이 있다. 설치에 대한 정보는 Nexus 홈페이지를 참고하기 바란다.

Nexus를 설치하면 최초에 다음의 리포지토리를 제공해준다.
  • Releases: 내부에서 만들어진 모듈의 정식 버전을 등록할 때 사용 (releases)
  • Snapshots: 내부에서 만들어진 모듈의 스냅샵 버전을 등록할 때 사용 (snapshots)
  • 3rd party: 외부 업체 제공 모듈을 등록할 때 사용 (예를 들어, 오라클 JDBC 드라이버 등) (thirdparty)
  • 기타 Central, Apache Snapshots, Codehaus Snapshots 등 외부 리포지토리 연결 (central 등)
  • 그리고, 이들을 하나처럼 보이게 해 주는 Public Repositories (public)
위 목록에서 뒤의 괄호안에 있는 이름이 리포지토리 주소에서 사용되는 경로인데, 예를 들어, Releases 리포지토리의 주소는 http://host/nexus-ver/content/repositories/releases/ 의 형식을 가지며, 그룹의 경우는 http://host/nexus-ver/content/groups/public의 형식을 갖는다.

릴리즈하기 위한 Maven 설정

내가 만든 Maven 프로젝트를 리포지토리에 배포하려면 다음의 세 가지 작업을 해 주어야 한다.
  • pom.xml 파일에 배포 대상 리포지토리 설정하기
  • settings.xml 파일에 배포 대상 리포지토리 연결 정보 설정하기
  • 배포하기
먼저 pom.xml 파일에 다음과 같이 <distributionManagement> 태그를 등록한다.

pom.xml 파일

<distributionManagement>
<repository>
<id>mycompany.internal.release</id>
<name>MyCompany Internal Repository</name>
<url>http://host/nexus/content/repositories/releases/</url>
</repository>
</distributionManagement>

두 번째로 settings.xml 파일에 pom.xml 파일에 설정한 리포지토리에 연결할 때 사용할 정보를 추가해주는 것이다. settings.xml 파일은 여러 위치에 만들 수 있는데 필자의 경우 [홈디렉토리]/.m2 디렉토리에 주로 만들어준다. 아래 코드는 settings.xml 파일의 작성 예이다.

settings.xml 파일

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>mycompany.internal.release</id>
<username>deployment</username>
<password>deployment123</password>
</server>
</servers>

</settings>

<server> 태그는 특정 서버에 연결할 때 사용할 아이디/암호를 입력하는데 이때 <id>의 값은 pom.xml에 등록한 리포지토리의 <id> 값과 일치해야 한다.

위와 같이 작업이 끝났다면 이제 배포만 하면 된다. Maven 프로젝트 디렉토리에서 다음과 같이 deploy 명령어를 실행하면 pom.xml 파일에 등록했던 배포 대상 리포지토리에 프로젝트의 artifact가 배포된다.

mvn deploy

등록된 것 사용하기

배포된 artifact를 다른 프로젝트에서 사용하려면 다음과 같이 앞서 구축한 내부용 리포지토리를 등록해주고 depedency를 추가해주면 된다.


<repositories>

<repository>

<id>mycompany.internal.repo</id>

<url>http://host/nexus/content/repositories/releases/</url>

<releases>

<enabled>true</enabled>

</releases>

</repository>

</repositories>


<dependencies>

<dependency>

<groupId>com.mycompany</groupId>

<artifactId>my-common-module</artifactId>

<version>3.0</version>

</dependency>

</dependencies>


Snapshot 등도 동일한 방식으로 등록 및 사용할 수 있다.



저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

미디어서버를 이용해서 동영상 서비스를 제공하고 싶을 때 문제가 되는 부분은 동영상 주소가 웹 소스 상에 그대로 노출된다는 점이다. 예를 들어, 아래 코드를 보자.


<a href="http://somehost/vod/_definst_/mp4:0000/00/1/1_PC1.mp4/playlist.m3u8">


조금만 지식이 있다면 위 코드를 이용해서 아무 곳에서나 동영상을 플레이할 수 있게 된다. 자유롭게 사용할 수 있도록 해 주는 것이 문제가 되지 않는다면 상관없지만, 만약 동영상 컨텐츠를 허용된 방법 이외의 다른 방법으로 재생하거나 동영상 자체를 다운로드 할 수 없도록 하고 싶다면 별도의 방안을 강구해야 한다.


DRM을 사용할 수 있겠지만 DRM 솔루션 자체가 비싸고 플레이어를 별도로 구현해야 한다는 단점이 있다. 또한, 디바이스에 따라 플레이어를 커스터마이징 할 수 없는 경우가 발생할 수도 있다. 그래서 생각해 볼 수 있는 것이 아래와 같이 동영상 주소를 시간에 따라서 다르게 생성해주는 것이다.


<a href="http://somehost/vod/_definst_/E14c1d156a...무지긴문자열...72ea34e4/playlist.m3u8">


위 값을 생성하는 방식에 따라 원하는 기능을 구현할 수 있다. 예를 들어, 암호화할 때 현재 시간 값을 포함시켜서 일정 시간이 지나면 값이 무효화되도록 구현할 수 있다. 이 경우, 위 주소를 복사해서 사용하더라도 일정 시간이 지나면 더 이상 동영상 재생을 할 수 없게 된다.


남은 작업은 Wowza 서버에서 암호화된 주소 값을 인식하도록 하는 것이다. Wowza 서버는 서버 상에 커스텀 모듈을 설치할 수 있도록 지원해주고 있는데, 이 커스텀 모듈을 사용해서 위 기능을 구현할 수 있게 된다. Wowza가 제공하는 서버 API 중 하나인 IMediaStreamNameAliasProvider2 인터페이스를 이용하면 클라이언트가 요청한 변경된 스트림 이름을 실제 스트림 이름으로 변경해 줄 수 있다. 아래 코드는 이 인터페이스를 이용한 구현 예를 보여주고 있다.


public class RaconAliasModule extends ModuleBase implements

        IMediaStreamNameAliasProvider2 {


    public void onAppStart(IApplicationInstance appInstance) {

        appInstance.setStreamNameAliasProvider(this);

        getLogger().info(

                "RaconAliasModule is registered to " + appInstance.getName());

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, IClient client) {

        return resolveAlias(name);

    }


    private String resolveAlias(String name) {

        // name이 암호화된 값

        RequestStreamName rsn = convertAliasToRequestStreamName(name); // name을 복호화 처리

        if (rsn.isTimeout()) {

            return null; // 유효하지 않은 name값. null 리턴

        }

        return si.getRealStreamName(); // 실제 스트림 이름 리턴

    }


    public RequestStreamName convertAliasToStreamInfo(String name) {

        // name을 복호화 처리

        ...

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, IHTTPStreamerSession httpSession) {

        return resolveAlias(name);

    }


    public String resolvePlayAlias(IApplicationInstance appInstance,

            String name, RTPSession rtpSession) {

        return resolveAlias(name);

    }

    .... // 동일하게 구현

}


모듈을 구현한 클래스를 구현했다면, 해당 모듈을 jar 만들어서 [wowza디렉토리]/lib에 복사한다. 이 때, 모듈에서 함께 사용하는 외부 jar 파일도 함께 복사해 주는 것도 잊지 말자.


lib에 복사했다면 위 모듈을 적용하고 싶은 Wowza 어플리케이션의 Application.xml 파일에 다음과 같이 Module 정보를 추가해 주면 된다.


<Module>

        <Name>customaccesscontrol</Name>

        <Description>CustomAccessControl</Description>

        <Class>com.custom.wowza.module.CustomAliasModule</Class>

</Module>


모듈을 개발하려면 Wowza 관련 jar 파일이 필요한데, 이 jar 파일들은 [wowza디렉토리]/lib에 위치한다. 이 파일들을 별도로 클래스패스로 잡기 귀찮다면 Wowza IDE를 이용해도 된다. Wowza IDE에 대한 자료는 아래 참고자료 링크를 확인해보기 바란다.


참고자료




저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. nwow 2012/07/09 18:13  댓글주소  수정/삭제  댓글쓰기

    setStreamNameAliasProvider 안하고 왜 안되나 하고 있었는데, 여기서 답을 찾았네요.

    좋은 정보 감사합니다. ~

  2. min 2013/02/12 16:26  댓글주소  수정/삭제  댓글쓰기

    초보라... 자세히 만들어서 주시면 안되나요? 샘플이라도... 부탁드립니다.

    • madvirus 2013/02/15 07:08  댓글주소  수정/삭제

      위 코드가 거의 샘플입니다.
      누락된 부분은
      1. 실제로 암호화된 URL을 만드는 것과
      2. 암호하된 URL을 복호화하는 convertAliastToStreamInfo(String name) 부분이에요.

      암호화된 URL을 만들어주는 기능은 Wowza가 아닌 웹 서버나 기타 다른 프로그램에서 만들어주셔야 할 것 같구요,

      위 코드에서 만든 Wowza 모듈의 convertAliasToStreamInfo 는 암호화된 이름을 복호화 처리하면 됩니다.

      실제 암/복호화와 관련된 건 자바의 Security API를 찾아보시면 될 것 같습니다.

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

요즘 저녁에 집에서 짬이 생길 때마다 공부겸 취미겸 간단한 웹 기반 어플리케이션을 만들고 있는데, 만들던 중 아래와 같은 기능이 필요하게 되었다.

  • WAR로 배포하고, 데이터 디렉토리를 외부에서 변경할 수 있어야 함
  • JNDI나 시스템 프로퍼티 값을 이용해서 디렉토리 경로를 지정할 수 있어야 함
위 기능을 직접 구현할까 하다가 누군가도 위와 같은 기능을 필요로 할 것 같아서 검색을 해 보았다. 아니나 다를까, 딱 들어맞는 기능을 제공하는 모듈이 있어 간단하게 기능을 정리해보았다. 이 모듈의 이름은 Data directory locator tool, 줄여서 datadirlocator (http://simplericity.org/datadirlocator)로서 사용법도 매우 간단하다.

모듈 다운로드

홈페이지에서 다운로드 받거나 Maven을 사용하는 경우 다음과 같이 의존을 추가해주면 된다.

<dependency>
    <groupId>org.simplericity.datadirlocator</groupId>
    <artifactId>datadirlocator</artifactId>
    <version>1.10</version>
</dependency>

지원하는 설정 방식

datadirlocator는 설정 파일이 위치하는 디렉토리나 어플리케이션의 홈 디렉토리와 같이 디렉토리 경로를 구하는 기능을 제공하며, 다음과 같이 4가지 방식으로 설정 경로를 구할 수 있도록 지원하고 있다.
  • JNDI 설정 이용 (기본 JNDI 명: java:com/env/dataDirectory)
  • 서블릿 컨텍스트 파라미터 이용 (기본 컨텍스트 파라미터 명: dataDirectory)
  • 시스템 프로퍼티 이용 (기본 시스템 프로퍼티  명: dataDirectory)
  • 환경 변수 이용 (기본 환경 변수 명: DATADIRECTORY)
JNDI부터 순서대로 값이 존재하는지 검색하고 값이 존재하면 그 값을 사용하고 존재하지 않으면 그 다음 방식의 값이 존재하는 검사한다. 위의 네 가지 경우에 대해 모두 값이 존재하지 않으면 기본 디렉토리로 $HOME/datadirectory를 사용한다.

사용법1, 직접 모듈 사용하기

가장 간단한 사용방법은 다음과 같다.
  • ServletContextListener를 추가한다.
  • ServletContextListener에서 DefaultDataDirectoryLocator를 사용해서 경로 값을 구한다.
예를 들어, 아래와 같은 코드를 구현해서 JNDI나 시스템 프로퍼티에 지정된 경로값을 구해서 시스템을 초기화하는데 사용할 수 있다.

public class ConfigInitializerServletContextListener implements ServletContextListener {

@Override
public void contextInitialized(ServletContextEvent sce) {
DefaultDataDirectoryLocator locator = new DefaultDataDirectoryLocator();
locator.setServletContext(sce.getServletContext());
locator.setJndiName("java:comp/env/rr4s/home");
locator.setSystemProperty("rr4s.home");
locator.setContextParamName("rr4shome");
locator.setEnvVarName("RR4SHOME");
locator.setDefaultDir("$HOME/rr4s.home");
File homeDirectory = locator.locateDataDirectory();
// homeDirectory를 이용한 설정 초기화
}
....
}

사용법2, 스프링 빈으로 사용하기

또 다른 방법은 스프링 빈으로 사용하는 것이다. DefaultDataDirectoryLocator를 스프링 빈 객체로 설정해서 사용할 수 있고, 만약 서블릿 컨텍스트 파라미터에 접근해야 한다면, ServletContextAware 인터페이스를 구현한 ServletContextAwareDataDirectoryLocator를 사용하면 된다. 다음은 설정 예이다.

<bean id="dataDirectoryLocator"
class="org.simplericity.datadirlocator.spring.ServletContextAwareDataDirectoryLocator">
<property name="jndiName" value="java:comp/env/rr4s/home" />
<property name="systemProperty" value="rr4s.home" />
</bean>

<bean id="contextReloader" class="org.chimi.rr4s.setup.ContextReloader">
<property name="dataDirectoryLocator" ref="dataDirectoryLocator" />
</bean>

위 코드에서 ContextReloader 클래스는 인젝션을 통해서 전달받은 dataDirectoryLocator를 이용해서 설정에 필요한 디렉토리 경로를 받아올 것이다. 

public class ContextReloader implements ApplicationContextAware,
ApplicationListener<ContextRefreshedEvent> {

private DataDirectoryLocator dataDirectoryLocator;
...
private File locateHomeDirectory() {
return dataDirectoryLocator.locateDataDirectory();
}
...
}

 




저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.

요즘 유행하는 빅데이터류의 기술을 사용하고 있지는 않지만, 빅이 아닌 나머지 분야에서의 대부분 자바 개발자들은 아마 웹 관련 프로젝트에 주로 참여하고 있을 거라 생각되어 최근에 진행중인 프로젝트에서 사용한 오픈 소스들에 대한 초간단 리뷰를 한번 해 보고자 한다. 이들 목록은 아래와 같다.

  • Spring Data JPA
  • Apache Shiro
  • Sitemesh
  • Bootstrap
  • Solr
  • Easyrec
Spring Data JPA

필자는 ORM 매니아이다. 아니 매니아를 넘어 ORM 신봉자에 가깝고 심지어 SQL은 (물론 필요할 땐 사용하지만) 쳐다보기도 싫을 정도이다. 이런 필자에게 Spring Data JPA는 하이버네이트에서 JPA로 넘어가는 계기를 만들어줬다. Spring Data를 사용하면 다음의 편리함들이 있다.
  • (거의 모든 리포지토리에 대해) 리포지토리 인터페이스만 정의하면 Spring Data가 런타임에 구현객체를 만들어 준다. 그래서 잡다하고 지겨운 코드 작성을 줄일 수 있다.
  • DDD의 Specification을 지원해서 검색 조건을 도메인 용어로 잘 표현할 수 있게 된다.
    • 덤으로 이들 스펙의 조합도 쉽게 할 수 있다.
  • 페이징, 정렬 등의 표준화된 인터페이스 제공

DB 연동과 관련된 지겨운 코드 타이핑을 덜 하게 해 주고 이는 더 중요한 부분에 시간을 더 많이 쏟을 수 있다는 걸 의미한다. 물론, DB 연동 관련 코딩 시간이 주니까 전반적인 개발 시간도 줄어드는 효과가 있다.


Apache Shiro


Apache Shiro는 인증과 권한을 위한 프레임워크로서 웹 URL 기반의 접근 제어나 코드에서 직접 권한 검사를 하기 위한 기능을 제공한다. 단, Shiro를 알맞게 커스터마이징해서 사용하려면 Shiro의 구조와 동작 방식에 대한 이해가 필요하다. 이와 관련해서는 예전에 필자가 정리한 http://javacan.tistory.com/entry/Apache-Shiro-Core-Diagram 글을 참고하기 바란다. 필자의 프로젝트의 경우는 권한 검사 부분을 커스터마이징 해서 사용했다. 예를 들어, DB로부터 역할과 기능 정보를 로딩하도록 커스텀 클래스를 구현했고, 쿠키를 이용해서 인증을 수행하도록 구현했다.


Sitemesh


예전부터 Tiles보다 Sitemesh가 좋았다. Sitemesh가 좋은 이유는 데코레이터를 적용하지 않아도 결과물이 완전한 HTML이 된다는 점이다. 예를 들어, Tiles를 사용하는 경우에는 내가 만드는 JSP가 Tiles 템플릿의 일부 영역을 만드는 것이기 때문에 완전한 HTML이 아니며, 따라서 필요한 자바 스크립트가 <head> 안에 들어가는 것이 아니라 <body> 태그 어딘가에 들어가게 된다. <head>에 넣으려면 별도의 JSP 파일에 넣어야 하는 불편함이 따른다. 반면에 Sitemesh를 사용하면 내가 만드는 코드가 완전한 HTML을 생성하게 된다. 즉, 데코레이터 적용 여부에 상관없이 완전한 하나의 결과물을 만들어내기 때문에, UI 관련 코드가 불필요하게 이 파일 저 파일에 쪼개지는 현상을 줄일 수 있다.


Bootstrap


프로토타입을 만들더라도 UI나 UX나 너무 개발자스러우면(^^;) 뭔가 만든 것 같지 않은 느낌이 들기 마련이다. 필자도 이걸로 고민을 좀 했는데, 아는 지인의 소개로 Bootstrap이란 걸 알게 되었다. Twitter에서 오픈한 CSS 소스인데, Bootstrap의 사용법을 조금만 익히면 최소한 개발자스러운 껍데기를 벗어날 수 있게 된다. 게다가 약간의 이미지만 곁들이면 있어 보이기까지 한다. 필자처럼 UI에 대한 감이 없는 개발자들이 디자인의 도움없이 뭔가 껍데기를 입혀야 한다면 적극 추천한다.


Apache Solr


Solr는 그 유명한 Lucene을 이용한 검색 서비스이다. 웹 서비스로 제공되기 때문에 플랫폼에 상관없이 쉽게 연동할 수 있다. 설치도 쉽고, 검색을 위한 스키마 설계만 간단하게 해주면 거의 바로 사용할 수 있다. 게다가 (필자처럼) 검색에 대한 지식이 약해도 빠르게 적용해 볼 수 있다는 장점이 있다. 한글 검색을 제대로 하려면 별도의 분석기가 필요하고 사전도 필요하겠지만, 단순 키워드 매칭 수준의 검색 용도르는 충분하다. 물론, 유사단어, 검색어 오류 수정 등의 기능을 제공하고 싶지만 많은 노력이 필요할 것이다.


Easyrec


이번에 PoC 성격의 프로젝트를 진행하면서 뭔가 개인화 추천 기능을 넣고 싶었다. CI(Collective Intelligence) 관련 내용은 이전부터 틈틈히 봤지만 그렇다고 이걸 직접 구현하고 싶진 않았다. 게다가 Mahout 같은 걸 삽질해 가면서 사용하고 싶진 않았다. 그런 와중에 지인(좋은 지인 열 개발자 안 부럽다인가요..)의 소개로 Easyrec라는 걸 알게 됐다. 정말이지 딱 필요한 기능만 제공하고 있어 이거다 싶을 정도였다. 내부 DB로는 MySQL을 사용하고 있고 자바 기반의 웹 어플리케이션으로 만들어졌기 때문에, 어지간한 환경에서 다 사용할 수 있다. 웹기반으로 동작하기 때문에 자바가 아닌 다른 언어에서도 쉽게 연동할 수 있다. 이쪽 분야의 전문가가 아니기에 품질이 어느 정도인지 아직 확인은 안 되지만, '빅'이 아닌 사이트에서 작게 사용하기에는 충분할 거라 생각된다.



저작자 표시 비영리 변경 금지
Posted by 최범균 madvirus

댓글을 달아 주세요

페이스북 친구들과 댓글을 공유하고 싶다면 아래를 이용해주세요.