5-1. 리스트 추상화

shin·2024년 9월 8일

1. 인터페이스 도입


  • 다형성과 OCP 원칙을 가장 잘 활용할 수 있는 곳 중 하나가 바로 자료 구조

List 자료구조

  • 순서가 있고, 중복을 허용하는 자료 구조를 리스트라고 함
  • ArrayListLinkedList 공통 기능을 인터페이스로 뽑아서 추상화하면 다형성을 활요한 다양한 이득을 얻을 수 있음
  • 같은 기능을 제공하는 메서드를 MyList 인터페이스를 뽑기
  • collection.list 패키지 사용

package collection.list;

public interface MyList<E> {
 
 	int size();
 	void add(E e);
	void add(int index, E e);
 	E get(int index);
 	E set(int index, E element);
 	E remove(int index);
	int indexOf(E o);

}
  • collection.array.MyArrayListV4 코드를 복사해서 MyList 인터페이스를 구현하는 MyArrayList를 만들기
package collection.list;

import java.util.Arrays;

public class MyArrayList<E> implements MyList<E> {
 //...
}
  • collection.link.MyLinkedListV3 코드를 복사해서 MyList 인터페이스를 구현하는 MyLinkedList를 만들기
package collection.list;

public class MyLinkedList<E> implements MyList<E> {
 //...
}

  • 메서드 정보가 다르면 오류가 발생할 수 있음
    • 이때는 MyList 인터페이스에 맞춰야 함
  • 추가로 재정의한 메서드에 @Override 어노테이션도 추가


MyArrayList 전체코드

package collection.list;

import java.util.Arrays;

public class MyArrayList<E> implements MyList<E> {

 	private static final int DEFAULT_CAPACITY = 5;
    
 	private Object[] elementData;
 	private int size = 0;
    
 	public MyArrayList() {
        elementData = new Object[DEFAULT_CAPACITY];
    }
    
	public MyArrayList(int initialCapacity) {
        elementData = new Object[initialCapacity];
    }
    
    @Override
 	public int size() {
 		return size;
    }
    
    @Override
 	public void add(E e) {
 		if (size == elementData.length) {
 			grow();
        }
        elementData[size] = e;
        size++;
    }
    
    @Override
 	public void add(int index, E e) {
 		if (size == elementData.length) {
 			grow();
        }
 		shiftRightFrom(index);
        elementData[index] = e;
        size++;
    }
    
 	//요소의 마지막부터 index까지 오른쪽으로 밀기
	private void shiftRightFrom(int index) {
 		for (int i = size; i > index; i--) {
            elementData[i] = elementData[i - 1];
        }
    }
    
    @Override
    @SuppressWarnings("unchecked")
 	public E get(int index) {
 		return (E) elementData[index];
    }
    
    @Override
 	public E set(int index, E element) {
		E oldValue = get(index);
        elementData[index] = element;
 		return oldValue;
    }
    
    @Override
 	public E remove(int index) {
 		E oldValue = get(index);
 		shiftLeftFrom(index);
        size--;
        elementData[size] = null;
 		return oldValue;
    }
    
 	//요소의 index부터 마지막까지 왼쪽으로 밀기
	private void shiftLeftFrom(int index) {
 		for (int i = index; i < size - 1; i++) {
            elementData[i] = elementData[i + 1];
        }
    }
    
    @Override
 	public int indexOf(E o) {
 		for (int i = 0; i < size; i++) {
 			if (o.equals(elementData[i])) {
 				return i;
            }
        }
 		return -1;
    }
    
 	private void grow() {
 		int oldCapacity = elementData.length;
 		int newCapacity = oldCapacity * 2;
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    @Override
 	public String toString() {
 		return Arrays.toString(Arrays.copyOf(elementData, size)) + " size=" + 
									size + ", capacity=" + elementData.length;
    }
    
}

MyLinkedList 전체코드

package collection.list;

public class MyLinkedList<E> implements MyList<E> {

 	private Node<E> first;
 	private int size = 0;
    
    @Override
 	public void add(E e) {
 		Node<E> newNode = new Node<>(e);
 		if (first == null) {
            first = newNode;
        } 
		else {
 			Node<E> lastNode = getLastNode();
            lastNode.next = newNode;
        }
        size++;
    }
    
 	private Node<E> getLastNode() {
 		Node<E> x = first;
 		while (x.next != null) {
            x = x.next;
        }
 		return x;
    }
    
    @Override
 	public void add(int index, E e) {
 		Node<E> newNode = new Node<>(e);
 		if (index == 0) {
            newNode.next = first;
            first = newNode;
        } 
		else {
 			Node<E> prev = getNode(index - 1);
            newNode.next = prev.next;
            prev.next = newNode;
        }
        size++;
    }
    
    @Override
 	public E set(int index, E element) {
 		Node<E> x = getNode(index);
 		E oldValue = x.item;
        x.item = element;
 		return oldValue;
    }
    
    @Override
 	public E remove(int index) {
 		Node<E> removeNode = getNode(index);
 		E removedItem = removeNode.item;
 		if (index == 0) {
            first = removeNode.next;
        } 
 		else {
 			Node<E> prev = getNode(index - 1);
            prev.next = removeNode.next;
        }
        removeNode.item = null;
        removeNode.next = null;
        size--;
 		return removedItem;
    }
    
    @Override
 	public E get(int index) {
 		Node<E> node = getNode(index);
 		return node.item;
    }
    
 	private Node<E> getNode(int index) {
 		Node<E> x = first;
 		for (int i = 0; i < index; i++) {
            x = x.next;
        }
 		return x;
    }
    
    @Override
 	public int indexOf(E o) {
 		int index = 0;
 		for (Node<E> x = first; x != null; x = x.next) {
 			if (o.equals(x.item))
				return index;
            index++;
        }
		return -1;
    }
    
    @Override
 	public int size() {
 		return size;
    }
    
    @Override
 	public String toString() {
 		return "MyLinkedListV1{" +  "first=" + first +
 				", size=" + size + '}';
    }
    
 	private static class Node<E> {
 		E item;
 		Node<E> next;
        
 		public Node(E item) {
 			this.item = item;
        }
        
        @Override
 		public String toString() {
 			StringBuilder sb = new StringBuilder();
   			Node<E> temp = this;
            sb.append("[");
 			while (temp != null) {
                sb.append(temp.item);
 				if (temp.next != null) {
                    sb.append("->");
            	}
                temp = temp.next;
            }
            sb.append("]");
 			return sb.toString();
        }
        
    }
    
}


2. 의존관계 주입


  • MyArrayList를 활용해서 많은 데이터를 처리하는 BatchProcessor 클래스를 개발하고 있다고 가정
  • 그런데 막상 프로그램을 개발하고 보니 데이터를 앞에서 추가하는 일이 많은 상황이라고 가정
  • 데이터를 앞에서 추가하거나 삭제하는 일이 많다면 MyArrayList보다는 MyLinkedList를 사용하는 것이 훨씬 효율적임

데이터를 앞에서 추가하거나 삭제할 때 빅오 비교

  • MyArrayList : O(n)
  • MyLinkedList : O(1)

구체적인 MyArrayList에 의존하는 BatchProcessor 예시

public class BatchProcessor {

 	private final MyArrayList<Integer> list = new MyArrayList<>();
    
	public void logic(int size) {
 		for (int i = 0; i < size; i++) {
            list.add(0, i); //앞에 추가
        }
    }
    
}
  • MyArrayList를 사용해보니 성능이 너무 느려서 MyLinkedList를 사용하도록 코드를 변경해야 한다면, BatchProcessor의 내부 코드도 MyARrayList에서 MyLinkedList를 사용하도록 함께 변경해야 함

구체적인 MyLinkedList에 의존하는 BatchProcessor 예시

public class BatchProcessor {

 	private final MyLinkedList<Integer> list = new MyLinkedList<>();
    
	public void logic(int size) {
 		for (int i = 0; i < size; i++) {
            list.add(0, i); //앞에 추가
        }
    }
    
}
  • BatchProcessor는 구체적인 MyArrayList 또는 MyLinkedList를 사용하고 있음
  • 이것을 BatchProcessor가 구체적인 클래스에 의존한다고 표현함
  • 이렇게 구체적인 클래스에 직접 의존하면 MyArrayListMyLinkedList로 변경할때마다 여기에 의존하는 BatchProcessor의 코드도 함께 수정해야 함
  • BatchProcessor가 구체적인 클래스에 의존하는 대신 추상적인 MyList 인터페이스에 의존하는 방법도 있음

추상적인 MyList에 의존하는 BatchProcessor 예시

 public class BatchProcessor {
 
 	private final MyList<Integer> list;
    
 	public BatchProcessor(MyList<Integer> list) {
 		this.list = list;
    }
    
	 public void logic(int size) {
 		for (int i = 0; i < size; i++) {
            list.add(0, i); //앞에 추가
        }
    }
    
}
main() {
 	new BatchProcessor(new MyArrayList());  //MyArrayList를 사용하고 싶을 때
	new BatchProcessor(new MyLinkedList()); //MyLinkedList를 사용하고 싶을 때
}
  • BatchProcessor를 생성하는 시점에 생성자를 통해 원하는 리스트 전략(알고리즘)을 선택해서 전달하면 됨
  • 이렇게 하면 MyList를 사용하는 클라이언트 코드인 BatchProcessor를 전혀 변경하지 않고, 원하는 리스트 전략을 런타임에 지정할 수 있음
  • 정리하면 다형성과 추상화를 활용하면 BatchProcessor 코드를 전혀 변경하지 않고 리스 전략(알고리즘)을 MyArrayList에서 MyLinkedList로 변경할 수 있음

실제 코드

package collection.list;

public class BatchProcessor {

 	private final MyList<Integer> list;
    
 	public BatchProcessor(MyList<Integer> list) {
 		this.list = list;
    }
    
 	public void logic(int size) {
 		long startTime = System.currentTimeMillis();
 		for (int i = 0; i < size; i++) {
            list.add(0, i); //앞에 추가
        }
 		long endTime = System.currentTimeMillis();
 		System.out.println("크기: " + size + ", 계산 시간: " + (endTime - startTime) + "ms");
    }
    
}
  • logic(int size) 메서드는 매우 복잡한 비즈니스 로직을 다룬다고 가정
  • 이 메서드는 list의 앞부분에 데이터를 추가함
  • 어떤 MyList list의 구현체를 선택할지는 실행 시점에 생성자를 통해 결정함
  • 생성자를 통해서 MyList 구현체가 전달됨
    • MyArrayList의 인스턴스가 들어올 수도 있고, MyLinkedList의 인스턴스가 들어올 수도 있음
  • 이것은 BatchProcessor의 외부에서 의존관계가 결정되어서 BatchProcessor 인스턴스에 들어오는 것 같음
    • 마치 의존관계가 외부에서 주입되는 것 같다고 해서 의존관계 주입이라고 함
  • 참고로 생성자를 통해서 의존관계를 주입했기 때문에 생성자 의존관계 주입이라 함

의존관계 주입

  • Dependency Injection, 줄여서 DI라고 부름
package collection.list;

public class BatchProcessorMain {

	public static void main(String[] args) {
 		MyArrayList<Integer> list = new MyArrayList<>();
 		//MyLinkedList<Integer> list = new MyLinkedList<>();
        
 		BatchProcessor processor = new BatchProcessor(list);
        processor.logic(50_000);
    }
    
}
  • BatchProcessor의 생성자에 MyArrayList를 사용할지, MyLinkedList를 사용할지 결정해서 넘겨야 함
  • 이후에 Processor.logic()을 호출하면 생성자로 넘긴 자료 구조를 사용해서 실행함

MyArrayList - 실행결과

크기: 50000, 계산 시간: 1361ms

 //MyArrayList<Integer> list = new MyArrayList<>();
 MyLinkedList<Integer> list = new MyLinkedList<>();
  • 주석을 지우고 MyArayList에 주석을 추가하여 다시 실행

MyLinkedList - 실행결과

크기: 50000, 계산 시간: 2ms
  • MyLinkedList를 사용한 덕분에 O(n) -> O(1)로 훨씬 더 성능이 개선된 것을 확인할 수 있음
  • 데이터가 증가할수록 성능의 차이는 더 벌어짐


3. 컴파일 타임, 런타임 의존관계


의존관계는 크게 컴파일 타임 의존관계와 런타임 의존관계로 나눌 수 있음

  • 컴파일 타임(compile time) : 코드 컴파일 시점을 뜻함
  • 런타임(runtime) : 프로그램 실행 시점을 뜻함

컴파일 타임 의존관계


  • 컴파일 타임 의존관계는 자바 컴파일러가 보는 의존관계
    • 클래스에 모든 의존관계가 다 나타남
  • 쉽게 이야기해서 클래스에 바로 보이는 의존관계
    • 그리고 실행하지 않은 소스코드에 정적으로 나타나는 의존관계
  • BatchProcessor 클래스를 보면 MyList 인터페이스만 사용함
    • 코드 어디에도 MyArrayListMyLinkedList 같은 정보는 보이지 않음
    • 따라서 BatchProcessorMyList 인터페이스에만 의존함

런타임 의존관계


  • 런타임 의존관계는 실제 프로그램이 작동할 때 보이는 의존관계
    • 주로 생성된 인스턴스와 그것을 참조하는 의존관계임
  • 쉽게 이야기해서 프로그램이 실행될 때 인스턴스 간에 의존관계로 보면 됨
  • 런타임 의존관계는 프로그램 실행 중에 계속 변할 수 있음

MyArrayList<Integer> list = new MyArrayList<>();
BatchProcessor processor = new BatchProcessor(list);
processor.logic(50_000);
  • BatchProcessor 인스턴스의 MyList list는 생성자를 통해 MyArrayList(x001) 인스턴스를 참조함
    • BatchProcessor 인스턴스에 MyArrayList(x001) 의존관계를 주입함
  • 따라서 이후 logic()을 호출하면 MyArrayList 인스턴스를 사용하게 됨

MyLinkedList<Integer> list = new MyLinkedList<>();
BatchProcessor processor = new BatchProcessor(list);
processor.logic(50_000);
  • BatchProcessor 인스턴스의 MyList list는 생성자를 통해 MyLinkedList(x002) 인스턴스를 참조함
    • BatchProcessor 인스턴스에 MyLinkedList(x002) 의존관계를 주입함
  • 따라서 이후 logic()을 호출하면 MyLinkedList 인스턴스를 사용하게 됨


😊 정리


  • MyList 인터페이스의 도입으로 같은 리스트 자료구조를 그대로 사용하면서 원하는 구현을 변경할 수 있게 됨
  • BatchProcessor에서 다음과 같이 처음부터 MyArrayList를 사용하도록 컴파일 타임 의존관계를 지정했다면 이후에 MyLinkedList로 수정하기 위해서는 BatchProcessor의 코드를 변경해야 함
public class BatchProcessor {
 	private final MyArrayList<Integer> list = new MyArrayList(); //코드 변경 필요
}

  • BatchProcessor 클래스는 구체적인 MyArrayListMyLinkedList에 의존하는 것이 아니라 추상적인 MyList에 의존함
    • 따라서 런타임에 MyList의 구현체를 얼만든지 선택할 수 있음
  • BatchProcessor에서 사용하는 리스트의 의존관계를 클래스에서 미리 결정하는 것이 아니라, 런타임에 객체를 생성하는 시점으로 미룸
    • 따라서 런타임에 MyList의 구현체를 변경해도 BatchProcessor의 코드는 전혀 변경하지 않아도 됨
  • 이렇게 생성자를 통해 런타임 의존관계를 주입하는 것을 생성자 의존관계 주입 또는 줄여서 생성자 주입이라고 함
  • OCP 원칙을 지킴
    • Open-Closed Principle : 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 함
    • 클라이언트 코드의 변경 없이, 구현 알고리즘인 MyList 인터페이스의 구현을 자유롭게 확장할 수 있음
  • 클라이언트 클래스는 컴파일 타임에 추상적인 것에 의존하고, 런파일에 의존 관계 주입을 통해 구현체를 주입받아 사용함으로써 이런 이점을 얻을 수 있음

전략 패턴


  • 디자인 패턴 중에 가장 중요한 패턴을 하나 뽑으라고 하면 전략 패턴을 뽑을 수 있음

  • 전략 패턴은 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있음

  • 방금 설명한 코드가 바로 전략 패턴을 사용한 코드

    • MyList 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 구현체인 MyArrayList, MyLinkedList가 전략의 구체적인 구현이 됨
    • 그리고 전략을 클라이언트 코드(BatchProcessor)의 변경 없이 손쉽게 교체할 수 있음


강의 출처 : 김영한의 실전 자바 - 중급 2편

profile
Backend development

0개의 댓글