JAVA의 가장 기본이 되는 내용

컬렉션 프레임워크 - List 인터페이스

cwchoiit 2024. 5. 13. 14:43
728x90
반응형
SMALL

참고자료:

 

김영한의 실전 자바 - 중급 2편 | 김영한 - 인프런

김영한 | 자바 제네릭과 컬렉션 프레임워크를 실무 중심으로 깊이있게 학습합니다. 자료 구조에 대한 기본기도 함께 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전

www.inflearn.com

 

ArrayList, LinkedList를 직접 구현해봤더니 공통적으로 사용되는 메서드가 있고 이를 추상화한게 List라는 인터페이스라는 것을 깨달았다. 그래서 항상 사용하길 다음과 같이 사용했구나를 알게됐다.

List<String> list = new ArrayList<>();

 

한번 List도 직접 인터페이스를 만들고 지금까지 만든 ArrayList, LinkedList를 구현체로 변경해보자.

 

MyList

package org.example.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 e);
    E remove(int index);
    int indexOf(E e);
}

 

 

 

자바에서 제공하는 List라는 인터페이스를 직접 만들어봤다. 이제 지금까지 직접 만들어본 ArrayListLinkedList가 이 인터페이스를 구현하게 만들거다.

 

MyArrayList

package org.example.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;

    public MyArrayList() {
        this.elementData = new Object[DEFAULT_CAPACITY];
    }

    public MyArrayList(int capacity) {
        this.elementData = new Object[capacity];
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(E o) {
        if (size == elementData.length) {
            elementData = Arrays.copyOf(elementData, elementData.length * 2);
        }
        elementData[size++] = o;
    }

    @Override
    public void add(int index, E o) {
        if (size == elementData.length) {
            elementData = Arrays.copyOf(elementData, elementData.length * 2);
        }

        for (int i = size - 1; i > index; i--) {
            elementData[i] = elementData[i - 1];
        }
        elementData[size++] = o;
    }

    @SuppressWarnings("unchecked")
    @Override
    public E remove(int index) {
        E oldValue = (E) elementData[index];

        for (int i = index; i < size; i++) {
            elementData[i] = elementData[i + 1];
        }

        elementData[size--] = null;
        return oldValue;
    }

    @SuppressWarnings("unchecked")
    @Override
    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 int indexOf(E o) {
        for (int i = 0; i < size; i++) {
            if (elementData[i].equals(o)) {
                return i;
            }
        }
        return -1;
    }

    public String toString() {
        return Arrays.toString(Arrays.copyOf(elementData, size)) +
                " size = " + size +
                ", capacity = " + elementData.length;
    }
}

 

MyLinkedList

package org.example.collection.list;

public class MyLinkedList<E> implements MyList<E> {
    private Node<E> first;
    private int size;

    @Override
    public int size() {
        return size;
    }

    @Override
    public void add(E item) {
        Node<E> newNode = new Node<>(item);
        if (size == 0) {
            first = newNode;
        } else {
            Node<E> lastNode = getLastNode();
            lastNode.next = newNode;
        }
        size++;
    }

    @Override
    public void add(int index, E item) {
        if (index < 0 || index > size) {
            throw new IndexOutOfBoundsException();
        }

        Node<E> newNode = new Node<>(item);
        if (index == 0) {
            newNode.next = first;
            first = newNode;
        } else {
            Node<E> prevNode = getNode(index - 1);
            newNode.next = prevNode.next;
            prevNode.next = newNode;
        }
        size++;
    }

    @Override
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }

        Node<E> currentNode = getNode(index);
        return currentNode.item;
    }

    @Override
    public E set(int index, E item) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }

        Node<E> oldNode = getNode(index);
        E oldItem = oldNode.item;
        oldNode.item = item;
        return oldItem;
    }

    public E remove() {
        if (size == 0) {
            return null;
        }

        Node<E> prevLastNode = getNode(size - 2);
        E removedItem = prevLastNode.next.item;
        prevLastNode.next.item = null;
        prevLastNode.next = null;

        size--;
        return removedItem;
    }

    @Override
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IndexOutOfBoundsException();
        }

        E oldValue;
        if (index == 0) {
            Node<E> newFirst = first.next;
            oldValue = first.item;
            first.item = null;
            first = newFirst;
        } else {
            Node<E> prevNode = getNode(index - 1);
            Node<E> removeNode = getNode(index);
            oldValue = removeNode.item;
            prevNode.next = removeNode.next;

            removeNode.next = null;
            removeNode.item = null;
        }
        size--;
        return oldValue;
    }

    @Override
    public int indexOf(E item) {
        Node<E> x = first;
        for (int i = 0; i < size; i++) {
            if (x.item.equals(item)) {
                return i;
            }
            x = x.next;
        }
        return -1;
    }

    private Node<E> getNode(int index) {
        Node<E> x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        return x;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        Node<E> x = first;

        builder.append("[");
        while (x != null) {
            builder.append(x.item);
            if (x.next != null) {
                builder.append(", ");
            }
            x = x.next;
        }
        builder.append("]");
        return builder.toString();
    }

    private Node<E> getLastNode() {
        Node<E> x = first;
        while (x.next != null) {
            x = x.next;
        }
        return x;
    }

    private static class Node<E> {
        E item;
        Node<E> next;

        public Node(E item) {
            this.item = item;
        }

        @Override
        public String toString() {
            return item.toString();
        }
    }
}

 

이렇게 구현체와 인터페이스를 만들면 어떤 이점이 있을까?

예를 들어, 어떤 Batch 작업을 하는 클래스가 있다고 해보자. 이 클래스는 무수히 많은 데이터를 한번에 어떤 작업을 처리하기 위해 존재하는 클래스인데 그 때 내부적으로 List를 사용한다. 다음 코드를 보자.

 

BatchProcessor

package org.example.collection.list;

public class BatchProcessor {
    private final MyList<Integer> list;

    public BatchProcessor(MyList<Integer> list) {
        this.list = list;
    }

    public void process(int size) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("크기: " + size + ", 계산 시간: " + (end - start) + "ms");
    }
}

 

BatchProcessor라는 클래스는 MyList 타입을 주입받는다. 이렇게 설계해두면 좋은 점이 있다.

  • 의존관계 주입을 통해 의존관계의 결정을 내부에서 하는게 아니라 이후로 미룬다.
  • 이후로 미룬다는 것은 기술(구현체)이 변경되어도 클라이언트 코드는 수정할게 없다. 
  • 클라이언트 코드가 수정할게 없다는것은 유지보수성이 좋아진다.

예를 들어, 이 BatchProcessorprocess() 메서드를 실행할 때 내가 주입한 구현체가 ArrayList 였다고 해보자. 다음과 같이 코드를 작성하고 실행을 할 거다.

 

BatchProcessorMain

package org.example.collection.list;

public class BatchProcessorMain {
    public static void main(String[] args) {
        MyArrayList<Integer> integerArrayList = new MyArrayList<>();

        BatchProcessor batchProcessor = new BatchProcessor(integerArrayList);
        batchProcessor.process(100_000);
    }
}

근데, 실행해보니 속도가 너무 느리다. 왜냐하면 이 배치 작업은 자료구조의 맨 앞에 데이터를 새로 추가한다. 그러면 ArrayList는 맨 앞에 데이터를 추가하는 작업이 O(n)의 시간복잡도를 가지는데, 주어진 사이즈 개수만큼 루프를 돌리는것 또한 O(n)이다. 즉, O(n^2)의 시간 복잡도를 가지게 되는것이다.

실행결과

크기: 100000, 계산 시간: 15258ms

 

 

그래서 자료구조를 이해한다면 맨 앞에 데이터를 계속 추가할 때 유리한 자료구조는 LinkedList 라는 것을 안다. 그럼 어떤것도 변경할 필요없이 주입을 LinkedList로 해주면 끝난다. 왜냐? 다형성의존관계 주입 때문이다. 

BatchProcessorMain

package org.example.collection.list;

public class BatchProcessorMain {
    public static void main(String[] args) {
        MyLinkedList<Integer> integerLinkedList = new MyLinkedList<>();

        BatchProcessor batchProcessor = new BatchProcessor(integerLinkedList);

        batchProcessor.process(100_000);
    }
}

실행결과

크기: 100000, 계산 시간: 11ms

 

속도의 차이가 어마어마하다. 성능은 어마어마하게 증가했지만 클라이언트 코드(BatchProcessor)의 변경은 전혀 없다.

이게 다형성의존관계 주입(DI)를 사용할 때 유지보수성의 극대화다.

 

의존관계 주입에서 핵심은 의존성의 결정을 나중으로 미루는 것에 있다. 유지보수 좋은 코드의 핵심은 기술이 변경되어도 클라이언트 코드의 변경이 없거나 최소화 되는것에 있다. 이 두개가 만나서 이런 시너지를 낼 수 있는것.

 

이 내용을 좀 더 깊이 알려면 다음 내용을 이해해야 한다.

컴파일 타임 의존관계 / 런타임 의존관계

  • 컴파일 타임: 코드 컴파일 시점을 뜻한다.
  • 런타임: 프로그램 실행 시점을 뜻한다. 

컴파일 타임 의존관계라는 건 위에서 말한 의존관계 결정을 나중으로 미루는것과 관련이 있다. 즉, 컴파일 시점에는 다음 코드는 어떤 구현체가 들어올지 알 수가 없다. 그저 추상적인 것(MyList<Integer>)에 의존할 뿐이다.

package org.example.collection.list;

public class BatchProcessor {
    private final MyList<Integer> list;

    public BatchProcessor(MyList<Integer> list) {
        this.list = list;
    }

    public void process(int size) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            list.add(0, i);
        }
        long end = System.currentTimeMillis();
        System.out.println("크기: " + size + ", 계산 시간: " + (end - start) + "ms");
    }
}

 

근데 이 코드가 실행되는 시점인 런타임 시점에서는 코드가 실행하면서 의존관계가 정해진다. 다음 실행하는 코드를 보자.

package org.example.collection.list;

public class BatchProcessorMain {
    public static void main(String[] args) {
        MyLinkedList<Integer> integerLinkedList = new MyLinkedList<>();

        BatchProcessor batchProcessor = new BatchProcessor(integerLinkedList);

        batchProcessor.process(100_000);
    }
}

BatchProcessor 클래스에 MyLinkedList 타입의 리스트가 주입된다. 인스턴스가 생성되는 순간에 의존관계가 정해지는 것이다. 

그러니까 BatchProcessor <-> MyLinkedList는 이 순간에 서로 의존관계가 된다. 그렇다는건 런타임 의존관계는 계속해서 변경될 수 있다는 의미이다. 저 인스턴스를 생성하는 코드 바로 다음라인에 BatchProcessor에게 LinkedList가 아니라 ArrayList를 주면? 그때부터는 ArrayList <-> BatchProcessor가 서로 의존관계가 된다.

 

"이 말이 곧 의존관계 결정을 나중으로 미루는 것으로 인해 클라이언트 코드는 의존관계가 변경이 되어도 코드의 변경이 필요없음을 의미한다".

와 이 말 너무 중요한거 같다. 의존관계 결정을 나중으로 미루면(추상화를 통해) 구현 기술이 변경되어도 클라이언트 코드에는 수정이 필요가 없다. 이게 자바의 객체 지향의 핵심(추상화, 다형성)아닐까? 

 

알고보니, 아무 생각없이 사용했던 List, ArrayList가 이런 심오한 내용을 품고 있었다는게 재밌는거 같다.

728x90
반응형
LIST