Post

Chapter 11 - 시스템

시스템 제작과 시스템 사용을 분리하라

소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 ‘연결’하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.

1
2
3
4
5
public Service getService() {
  if (service == null)
    service = new MyServiceImpl(...);
  return service;
}
  • 위의 코드를 초기화 지연(Lazy Initialization) 혹은 계산 지연(Lazy Evaluation)이라는 기법이다.
    • 장점
      • 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다.
      • 어떤 경우에도 null 포인터를 반환하지 않는다.
    • 단점
      • MyServiceImpl의 생성자에 명시적으로 의존한다.(실제 런타임 시 사용하지 않더라도 의존성 해결이 안되면 컴파일이 안된다.)
      • service가 null인 경우와 아닌 경우, 두 방식을 모두 테스트해야한다.
      • MyServiceImpl이 모든 상황에 적합한 객체인지 모른다.

Main 분리

  • 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.

  • 의존성 화살표 방향에 주목하자. 모든 화살표가 main 쪽에서 애플리케이션 쪽을 향한다.
  • 즉, 애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다는 뜻이다.
  • 단지 모든 객체가 적절히 생성되었다고 가정한다.

팩토리 기법

  • 때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다.
  • 예를 들어, (아래 이미지에서는) LineItem을 생성하는 시점은 OrderProcessing가 결정하지만, 구체적인 코드는 알 수 없다.
  • 마찬가지로 의존성 화살표 방향이 main에서 애플리케이션 쪽으로 향한다.

의존성 주입(DI)

  • 의존성 주입은 제어 역전(IoC) 기법을 의존성 관리에 적용한 메커니즘이다.
  • 객체는 의존성 자체를 인스턴스로 만드는 책임을 지는 대신, main루틴이나 특수 컨테이너에게 맡긴다.
    • 의존성을 주입하는 방법으로 setter 메서드나 생성자 인수를 제공한다.
    • DI 컨테이너는 필요한 객체의 인스턴스를 만든 후 필요한 클래스에게 의존성을 설정한다.
1
2
3
4
5
6
7
8
9
10
11
12
// Spring framwork는 자바 DI 컨테이너를 제공한다.
@Service
@RequiredArgsConstructor
public class AdminPostService {
  
  private final PostRepository postRepository;

  public AdminPostService(PostRepository postRepository) {
    this.postRepository = postRepository; // 의존성 주입
  }
  ...
}

확장

  • 작은 마을이 성장하며 도로를 확장 공사할 때 “왜 처음부터 넓게 만들지 않았지?”하는 생각이 들 수 있다.
    • 성장할지 모른다는 기대로 처음부터 거대하게 공사하는게 올바르다고 정당화할 수 있는가?
  • ‘처음부터 올바르게’ 시스템을 만들 수 있다는 믿음은 미신이다.
    • 새로운 스토리가 생기면 거기에 맞춰 시스템을 조정하고 확장하면 된다.
  • TDD, 리팩터링, Clean code는 코드 수준에서 시스템을 조정하고 확장하기 쉽게 만든다.

프록시

  • Client에서 Server을 직접 호출하고, 처리 결과를 직접 받는다. 이것을 직접 호출이라 한다.
    • Client -> Server
  • Client에서 Server을 직접 호출하는 것이 아니라 어떤 대리자를 통해서 대신 간접적으로 서버에 요청할 수도 있다. 이것을 간접 호출이라 한다.
    • 여기서 간접 호출하는 대상을 프록시(Proxy)라 한다.
      • Client -> Proxy -> Server
  • 프록시는 Client와 Server의 중간에 있기 때문에 여러가지 일을 수행 할 수 있다.
    • 권한에 따른 접근 차단, 캐싱, 지연로딩을 수행하는 접근 제어
    • 서버의 기능에 다른 기능까지 추가해주는 부가 기능 추가 (요청, 응답값을 변형해주거나 로그 기록 등)
    • 대리자가 또 다른 대리자를 호출하는 프록시 체인

프록시는 대체 가능해야 한다.

  • 아무 객체나 프록시가 되는것은 아니다.
  • 클라이언트 입장에서는 서버에 요청을 한건지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
  • 즉, 서버와 프록시는 같은 인터페이스를 사용해야 하며, 클라이언트의 코드를 하나도 건들이지 않고 프록시 추가와 런타임 객체 의존 관계 주입만 변경해서 사용할 수 있어야 한다.
  • Client는 ServerInterface를 의존해야 한다. 그리고 ServerInterface의 구현체로 실제 서버와 Proxy가 존재한다. 따라서 DI를 사용해서 대체 가능하다.

JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.

1
2
3
public interface AInterface {
  String call();
}
1
2
3
4
5
6
public class AImpl implements AInterface {
  public String call() {
    System.out.println("A 호출");
    return "a";
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TimeInvocationHandler implements InvocationHandler {
  private final Object target;

  public TimeInvocationHandler(Object target) {
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("TimeProxy 실행");
    long startTime = System.currentTimeMillis();

    Object result = method.invoke(target, args);

    long endTime = System.currentTimeMillis();
    long resultTime = endTime - startTime;
    System.out.println("TimeProxy 종료 resultTime = " + resultTime);
    return result;
  }
}
1
2
3
4
5
6
7
8
9
public class Main {
  public static void main(String[] args) {
    AInterface target = new AImpl();
    TimeInvocationHandler handler = new TimeInvocationHandler(target);

    AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
    proxy.call();
  }
}
1
2
3
4
// 실행 결과
TimeProxy 실행
A 호출
TimeProxy 종료 resultTime = 0

테스트 주도 시스템 아키텍처 구축

  • 소프트웨어 프로젝트에서는 BDUF(Big Design Up Front)는 해롭기까지 한다.
    • 처음 선택한 아키텍처의 사고 방식 때문에 변경을 쉽사리 수용하지 못하기 때문이다.
  • 아주 단순하면서 잘 분리된 아키텍처로 결과물을 빠르게 출신 후, 기반 구조를 추가하며 조금씩 확장해 나가도 괜찮다.
This post is licensed under CC BY 4.0 by the author.