Java Abstract - 직접 구현한 연결, 배열 리스트 의존관계 주입

만약 배열리스트로 수 많은 빅 데이터를 처리 용도로 WatchProcessor 클래스를 개발하고 있는 경우, 앞에서부터 추가, 삭제가 빈번하다면 연결리스트를 도입하는 것이 효율적일 것이다.

데이터 앞에서 추가 및 삭제의 빅오 비교

  • 배열리스트: O(n)
  • 연결리스트: O(1)

WatchProcessor 클래스로 배열리스트로 활용하는 일부 코드

private final CList<Integer> list = new CList<>();

public void proc(int size) {
    for (int i = 0; i < size; i++) {
        ...
        list.add(0, i); // 앞에서 지속적으로 추가
    }
}
  • 배열 앞에서 지속적으로 데이터를 넣는 과정을 볼 수 있다.
    • 프로파일 수행 시 proc() 메소드가 가장 높은 지연시간이 발생하였다.
    • 배열리스트로 구현한 경우 치명적이므로 바꿔줄 필요가 있다.

Watch 클래스 배열리스트에서 연결리스트로 변경

// private final CList<Integer> list = new CList<>();
private final CLinkedList<Integer> list = new CLinkedList<>();
  • 배열리스트를 사용한 것을 연결리스트로 변경하였다.
  • 참고로 Watch 클래스는 구체적인 클래스인 CList 또는 CLinkedList 클래스에게 의존하고 있다.
    • WatchProcessor -> CList 의존 관계
    • WatchProcessor -> CLinkedList 의존 관계
  • 구체적인 클래스에 의존하면 변동사항이 있는 경우, 앞의 코드처럼 매번 변경해주어야 한다.

추상화하고 의존 관계 주입하기

앞에서 WatchProcessor 클래스 의존관계를 설명했던 것처럼 의존 관계를 풀어줄 방법이 있다.

문제가 되는 의존 관계 list 멤버 필드를 추상화하고 뭘 사용할지 나중으로 미뤄 주입 받는 방법이다.

WatchProcessor - 추상적인 CList 인터페이스에 의존하는 클래스

public class WatchProcessor {

    // list = new CList;
    // list = new CLinkedList;
    private final IList<Integer> list;

    public WatchProcessor(IList<Integer> list) {
        this.list = list; // 날 사용하려면 먼저 자료구조를 선택해줘
    }

    public void proc(int size) {
        for (int i = 0; i < size; i++) {
            list.add(0, i);
        }
    }
}

WatchProcessor - 호출부에서 사용 예제

main() {
    new WatchProcessor(new CList); // 배열리스트로 사용하고 싶어
    new WatchProcessor(new CLinkedList) // 연결리스트로 사용하고 싶어
}
  • WatchProcessor 생성하는 시점에서 자료구조 리스트 전략(알고리즘)적으로 선택하여 전달한다.
  • 장점으로 WatchProcessor 내부 코드 변경 없이 자료구조를 자유롭게 변경할 수 있다.
  • 즉, 리스트 전략은 런타임에 지정한다.

호출부에서 WatchProcessor 알고리즘 리스트를 어떤 것을 사용할 지 정하고 내부 코드 작성 없이 알고리즘 변경된 것으로 동작한다.

의존관계 주입

  • Dependency Injection 줄여서 DI 라고 부르며 의존성 주입으로 많이 알려져 있다.

배열리스트, 연결리스트 성능 측정

작성 작성한 WatchProcessor 에서 성능 측정하도록 코드를 추가한다.

WatchProcessor.class

public class WatchProcessor {

    private final IList<Integer> list;

    public WatchProcessor(IList<Integer> list) {
        this.list = list;
    }

    public void proc(int size) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("Processing time: " + (end - start) + "ms");
    }
}
  • CList list 구현체는 실행 시점에 생성자 통해서 결정
  • 생성자 통해서 전해 받은 CList 구현체로 전달 받음.
    • 직접 구현한 CList 일수도 CLinkedList 인스턴스가 들어올 수 있음.
  • 외부의 의존관계가 결정되어 WatchProcessor 인스턴스로 오는 것이다.
    • 의존관계인 구현체가 외부에서 주입되었다. (줄여서 의존관계 주입이다)
    • 생성자 통해서 받았으므로 생성자 의존관계 주입이다.

작성한 코드를 의존관계 주입해 실행시켜보자.

Main.class - 배열 리스트 주입

public class Main {
    public static void main(String[] args) {
        CList<Integer> list = new CList<>();

        WatchProcessor watch = new WatchProcessor(list);
        watch.proc(100_000);
    }
}

Processing time: 6026ms

배열리스트는 데이터가 많을수록 지연시간이 점점 길어진다.
다음으로 연결리스트로 의존관계를 주입해 실행해보자.

Main.class - 연결 리스트 주입

CLinkedList<Integer> list = new CLinkedList<>();

WatchProcessor watch = new WatchProcessor(list);
watch.proc(100_000);

Processing time: 8ms

연결리스트의 성능이 거의 약 800배 차이나는 것을 볼 수 있다.