- 다형성과 OCP 원칙을 가장 잘 활용할 수 있는 곳 중 하나가 바로 자료 구조
ArrayList와 LinkedList 공통 기능을 인터페이스로 뽑아서 추상화하면 다형성을 활요한 다양한 이득을 얻을 수 있음
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();
}
}
}
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가 구체적인 클래스에 의존한다고 표현함MyArrayList를 MyLinkedList로 변경할때마다 여기에 의존하는 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 인스턴스에 들어오는 것 같음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<>();
MyLinkedList - 실행결과
크기: 50000, 계산 시간: 2ms
MyLinkedList를 사용한 덕분에 O(n) -> O(1)로 훨씬 더 성능이 개선된 것을 확인할 수 있음의존관계는 크게 컴파일 타임 의존관계와 런타임 의존관계로 나눌 수 있음
- 컴파일 타임(compile time) : 코드 컴파일 시점을 뜻함
- 런타임(runtime) : 프로그램 실행 시점을 뜻함

BatchProcessor 클래스를 보면 MyList 인터페이스만 사용함MyArrayList나 MyLinkedList 같은 정보는 보이지 않음BatchProcessor는 MyList 인터페이스에만 의존함
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 클래스는 구체적인 MyArrayList나 MyLinkedList에 의존하는 것이 아니라 추상적인 MyList에 의존함MyList의 구현체를 얼만든지 선택할 수 있음BatchProcessor에서 사용하는 리스트의 의존관계를 클래스에서 미리 결정하는 것이 아니라, 런타임에 객체를 생성하는 시점으로 미룸MyList의 구현체를 변경해도 BatchProcessor의 코드는 전혀 변경하지 않아도 됨
OCP원칙을 지킴
Open-Closed Principle: 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 함- 클라이언트 코드의 변경 없이, 구현 알고리즘인
MyList인터페이스의 구현을 자유롭게 확장할 수 있음
- 클라이언트 클래스는 컴파일 타임에 추상적인 것에 의존하고, 런파일에 의존 관계 주입을 통해 구현체를 주입받아 사용함으로써 이런 이점을 얻을 수 있음
디자인 패턴 중에 가장 중요한 패턴을 하나 뽑으라고 하면 전략 패턴을 뽑을 수 있음
전략 패턴은 알고리즘을 클라이언트 코드의 변경 없이 쉽게 교체할 수 있음
방금 설명한 코드가 바로 전략 패턴을 사용한 코드
MyList 인터페이스가 바로 전략을 정의하는 인터페이스가 되고, 각각의 구현체인 MyArrayList, MyLinkedList가 전략의 구체적인 구현이 됨BatchProcessor)의 변경 없이 손쉽게 교체할 수 있음