JAVA 8 에서 케이크 패턴(Cake Pattern)을 사용해보자

지난 글에서 의존성 주입(Dependency Injection)에 대한 이야기를 했었다. 이번 글에서는 다른 방식의 의존성 주입인 케이크 패턴을 소개한다.

스칼라의 케이크 패턴에 대한 자세한 내용은 여기를 참고 바란다.

케이크 패턴

스칼라의 케이크 패턴을 자바에서 사용해보자. 케이크 패턴은 의존성 주입의 한 방법이다. 자바 8에서는 인터페이스가 구현 코드를 담은 디폴트 메소드를 가질 수 있게 되었다. 이 디폴트 메소드를 이용하면 스칼라의 trait 와 비슷해진다. 비록 trait 의 셀프 타입을 사용할 수는 없지만, 어차피 자바는 스칼라처럼 with 를 사용하여 객체 생성 단계에 와이어링을 할 수 없으니 상관없다.

공통 구성

로그 기록을 담당하는 Logger, 설정 로딩을 담당하는 Configuration 가 있다.

Logger 는 File 에 기록하는 LoggerUsingFile, DB에 기록하는 LoggerUsingDB 가 있다.

Configuration 는 File 에서 읽어오는 ConfigurationFromFile, DB 에서 읽어오는 ConfigurationFromDB 가 있다.

Rabbit 이 Logger 와 Configuration 둘을 사용하는 구조라고 하고, File 을 사용하는 Rabbit 과 DB 를 사용하는 Rabbit 의 인스턴스를 각각 생성해보자.

우선 최상위 인터페이스 코드는 다음과 같다.

1
2
3
4
5
6
public interface Logger {
  void log(String message);
}
public interface Configuration {
  void load();
}

순수 인터페이스

구현이 없는 순수 인터페이스만으로 Rabbit 을 구성하면 다음과 같은 코드가 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class RabbitImplUsingFile implements ConfigurationFromFile, LoggerUsingFile {
  @Override
  public void load() {
    System.out.println("load config from File.");
  }
  @Override
  public void log(String message) {
    System.out.println("write log to File.");
  }
  public void prepare() {
    log("load config");
    load();
  }
}
public class RabbitImplUsingDB implements ConfigurationFromDB, LoggerUsingDB {
  @Override
  public void load() {
    System.out.println("load config from DB.");
  }
  @Override
  public void log(String message) {
    System.out.println("write log to DB.");
  }
  public void prepare() {
    log("load config");
    load();
  }
}

그리고 다음과 같이 인스턴스를 각각 생성할 수 있다.

1
2
3
4
RabbitImplUsingFile rabbitUsingFile = new RabbitImplUsingFile();
rabbitUsingFile.prepare();
RabbitImplUsingDB rabbitUsingDB = new RabbitImplUsingDB();
rabbitUsingDB.prepare();

순수 인터페이스만 사용하면 종속이 걸려 있는 구현을 모든 인터페이스를 구현하는 클래스에서 처리해야 한다.

구현을 가진 인터페이스

이런 상황을 디폴트 메소드를 사용해서 정리하자. 종속이 걸린 구현들을 각 인터페이스의 디폴트 메소드로 올리자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface LoggerUsingFile extends Logger {
  default void log(String message) {
    System.out.println("write log to File.");
  }
}
public interface ConfigurationFromFile extends Configuration {
  default void load() {
    System.out.println("load config from File.");
  }
}
public interface LoggerUsingDB extends Logger {
  default void log(String message) {
    System.out.println("write log to DB.");
  }
}
public interface ConfigurationFromDB extends Configuration {
  default void load() {
    System.out.println("load config from DB.");
  }
}

이제 디폴트 메소드를 가진 인터페이스를 준비했으니 Rabbit 클래스를 구현하자. 구현을 가진 인터페이스로 Rabbit 을 구성하면 다음과 같은 코드가 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
public class RabbitUsingFile implements ConfigurationFromFile, LoggerUsingFile {
  public void prepare() {
    log("load config");
    load();
  }
}
public class RabbitUsingDB implements ConfigurationFromDB, LoggerUsingDB {
  public void prepare() {
    log("load config");
    load();
  }
}

그리고 다음과 같이 인스턴스를 각각 생성할 수 있다.

1
2
3
4
RabbitUsingFile rabbitUsingFileWithDefault = new RabbitUsingFile();
rabbitUsingFileWithDefault.prepare();
RabbitUsingDB rabbitUsingDBWithDefault = new RabbitUsingDB();
rabbitUsingDBWithDefault.prepare();

마치며

어떤가? 조금 달라 보이는가?

Logger, Configuration 에 대한 각 구현 코드들이 Rabbit 클래스에서 사라졌다. 그리고 추후에 Logger, Configuration 의 구현 내용이 변경되더라도 Rabbit 클래스를 수정할 필요가 없다. 디폴트 메소드를 사용하지 않았다면 Logger, Configuration 의 구현 내용을 변경하기 위해서 모든 구현 내용을 가지고 있는 Rabbit 클래스를 수정해야 한다.

자바에서 사용할 수 있는 케이크 패턴은 스칼라의 그것에 비교해서 많이 부족하지만, 용도에 맞게 사용하면 유용할 것이다. 인터페이스 제약과 동시에 기본 구현을 가질 수 있다는 것은 유용한 기능이다. 다른 대안에 대한 내용도 있으니 여기를 참고 바란다.

sample code : https://github.com/prostars/cake-pattern.git

(원글 : http://prostars.net/235)


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.