22장. 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - List

공부하는 감자·2023년 12월 13일
0

자바의 신 3판

목록 보기
22/30

들어가기 전

『자바의 신 3판』 을 읽고 내용 정리 및 공부한 내용을 정리한 글입니다.
서적: 자바의 신 3판 구입처

내용 정리

자바 컬렉션

자바에서 컬렉션은 목록성 데이터를 처리하는 자료 구조를 통칭한다.

자료 구조란

자료 구조는 영어로 Data Structure라고 한다.

  • 어떤 정보를 담는 것을 의미
  • 하나의 데이터가 아닌 여러 데이터를 담을 때 사용

자료 구조의 대표적 예

  • 배열
    • 가장 기본적인 자료 구조
    • 성능 상이나 메모리 효율 면에서 가장 좋다.
  • DTO

배열의 한계

하지만, 배열은 그 크기가 정해져 있을 때 유용하다. 만약 배열에 담으려는 데이터의 크기가 얼마나 되는지 모르는 경우에는 아래와 같은 방법들을 쓸 수 있다.

  • int의 최대값에 해당하는 크기를 갖는 배열 생성
    • 메모리 낭비가 엄청나기 때문에 절대 취하면 안된다!
  • 배열의 크기가 부족하면, 필요한 개수만큼 더 큰 배열을 하나 더 만들어서 거기에 복사

두 번째 방법을 직접 개발해도 되지만, 그럴 필요는 없다. 이미 이러한 문제를 해결하는 클래스들을 미리 만들어 놓았기 때문이다.

자바의 자료 구조

크게 다음과 같이 분류할 수 있다.

  • 순서가 있는 목록 형 List
  • 순서가 중요하지 않은 셋 형 Set
  • 먼저 들어온 것이 먼저 나가는 큐 형 Queue
  • 키-값으로 저장되는 맵 형 Map

자료 구조에서 구현하고 있는 인터페이스

자바에서는 List, Set, Queue는 java.util.Collection 인터페이스를 구현하고 있다. 이 Collection 인터페이스는 여러 개의 객체를 하나의 객체에 담아 처리할 때 공통적으로 사용되는 여러 메소드들을 선언해 놓았다.

이 목록에서 유일하게 Map만이 Collection과 관련 없는 인터페이스로 선언되어 있다.

💡 자바의 컬렉션은 개발하면서 매우 중요하다. 잘 쓰면 약이 되고 잘 못쓰면 독이 된다. 게다가 대부분의 알고리즘 문제나 면접 시험 문제에서도 매우 큰 비중을 차지한다.

따라서 달달 외울 정도로 잘 알고 있어야만 한다.

컬렉션의 구조

그림 출처: https://techvidvan.com/tutorials/java-collection-framework/

https://techvidvan.com/tutorials/java-collection-framework/

Collection 인터페이스

먼저, List와 Set, Queue의 기본이 되는 Collection 인터페이스는 다음과 같이 선언되어 있다.

public interface Collection<E> extends Iterable<E>

앞 장의 제네릭을 이해했다면, 이 인터페이스의 선언을 이해하는 것은 그리 어렵지 않을 것이다.

이 Collection 인터페이스 선언문에서 특이한 것은 Iterable<E> 라는 인터페이스를 확장했다는 점이다.

Interable<E> 인터페이스에는 iterator() 라는 메소드만 선언되어 있고, 이 메소드는 Iterator 라는 인터페이스를 리턴한다.

리턴 타입메소드 이름 및 매개 변수
Iterator<T>iterator()

Iterator 인터페이스

이 인터페이스에는 아래 메소드들이 있다.

  • 추가 데이터가 있는지 확인하는 hasNext() 메소드
  • 현재 위치를 다음 요소로 넘기고 그 값을 리턴해주는 next() 메소드
  • 데이터를 삭제하는 remove() 메소드

결론적으로, Collection 인터페이스가 Iterable 인터페이스를 확장했다는 의미는, Iterator 인터페이스를 사용하여 데이터를 순차적으로 가져올 수 있다는 의미이다.

주요 메소드들

Collection 인터페이스에 선언된 주요 메소드들의 목록을 표로 살펴보자.

이 표의 내용에서 “요소”라는 것은 영어로 “Element”이며, 컬렉션에 저장되는 각각의 데이터를 말한다.

리턴 타입
메소드 이름 및
매개 변수
설명
booleanadd(E e)요소를 추가한다.
booleanaddAll(Collection)매개 변수로 넘어온 컬렉션의 모든 요소를 추가한다.
voidclear()컬렉션에 있는 모든 요소를 지운다.
booleancontains(Object)매개 변수로 넘어온 객체가 해당 컬렉션에 있는지 확인한다.
동일한 값이 있으면 true를 리턴한다.
booleancontainsAll(Collection)매개 변수로 넘어온 컬렉션의 모든 요소들과 동일한 값들이 모두 있으면 true를 리턴한다.
booleanequals(Object)매개 변수로 넘어온 객체와 같은 객체인지 확인한다.
inthashCode()해시 코드값을 리턴한다.
booleanisEmpty()컬렉션이 비어있는지 확인한다. 비어있으면 true를 리턴한다.
Iteratoriterator()데이터를 한 건씩 처리하기 위한 Iterator 객체를 리턴한다.
booleanremove(Object)매개 변수와 동일한 객체를 삭제한다.
booleanremoveAll(Collection)매개 변수로 넘어온 객체들을 해당 컬렉션에서 삭제한다.
booleanretainAll(Collection)매개 변수로 넘어온 객체들만을 컬렉션에 남겨 둔다.
Object[]toArray()컬렉션에 있는 데이터들을 배열로 복사한다.
<T> T[]toArray(T[])컬렉션에 있는 데이터들을 지정한 타입의 배열로 복사한다.

추가: 데이터의 개수를 구하는 메소드

리턴 타입메소드 이름 및 매개 변수설명
intsize()요소의 개수를 리턴한다.

객체에 들어가 있는 데이터의 개수를 가져오는 메소드다.

  • 배열.length : 배열의 넣을 수 있는 공간의 개수
    • 배열의 저장 공간 개수를 의미한다.
  • length() : String 문자열의 길이
  • size() : Collection을 구현한 인터페이스
    • 들어가 있는 데이터 개수를 의미한다.
    • 예를 들어, 초기 크기가 10개여도 들어가 있는 데이터가 3개이면 3을 반환한다.

향상된 for문

아래처럼 사용하며, Collecton 인터페이스를 구현한 모든 클래스에서 사용할 수 있다.

for(타입이름 임시변수명 : 반복대상객체) {
}

List 인터페이스

배열과 비슷한 “목록”이다.

“목록”은 List 인터페이스로부터 시작되며, 이 List 인터페이스는 Collection 인터페이스를 확장하였다. 따라서, 몇몇 추가된 메소드를 제외하고는 Collection에 선언된 메소드와 큰 차이는 없다.

Collection을 확장한 다른 인터페이스와 List 인터페이스의 가장 큰 차이점은 배열처럼 “순서”가 있다는 것이다.

List 인터페이스를 구현한 클래스

List 인터페이스를 구현한 클래스들은 매우 많다. 그 중 java.util 패키지에서는 ArrayList, Vector, Stack, LinkedList를 많이 사용한다.

ArrayList와 Vector

ArrayList와 Vector 클래스의 사용법은 거의 동일하고 기능도 거의 비슷하다. 이 두 클래스는 “확장 가능한 배열”이라고 생각하면 된다.

  • Vector는 JDK 1.0부터 있었고, ArrayList는 JDK 1.2에서 추가되었다.
  • ArrayList는 Thread safe하지 않고, Vector는 Thread Safe하다.
    • ArrayList의 객체는 여러 명이 달려들어 값을 변경하려고 하면 문제가 발생할 수 있고, Vector는 그렇지 않다.
  • 보통 Vector보다 ArrayList를 선호한다.

Stack

Stack 클래스는 Vector 클래스를 확장하여 만들었다. 이 클래스를 만든 가장 큰 이유는 LIFO를 지원하기 위함이다.

LIFO는 Last In First Out의 약자로, 가장 마지막에 추가한 값을 가장 처음 빼 내는 것이다. 프로그래밍 언어에서 “스택”이란 의미는 보통 메소드가 호출된 순서를 기억하는 장소를 말한다.

LinkedList

이 클래스는 “목록”에도 속하지만 “큐”에도 속한다. 자세한 것은 다음 장에서 알아본다.

ArrayList

ArrayList 클래스의 상속

가장 상위 부모인 Object 클래스 다음으로 확장한 부모 클래스들의 이름 앞에는 Abstrat가 붙어 있다. 즉, 이 클래스들은 abstract 클래스다.

ArrayList 클래스가 구현한 인터페이스

인터페이스용도
Serializable원격으로 객체를 전송하거나, 파일에 저장할 수 있음을 지정
CloneableObject 클래스의 clone() 메소드가 제대로 수행될 수 있음을 지정.
즉, 복제가 가능한 객체임을 의미한다.
Iterable<E>객체가 “foreach” 문장을 사용할 수 있음을 지정
Collection<E>여러 개의 객체를 하나의 객체에 담아 처리할 때의 메소드 지정
List<E>목록형 데이터를 처리하는 것과 관련된 메소드 지정
RandomAccess목록형 데이터에 보다 빠르게 접근할 수 있도록,
임의로(random하게) 접근하는 알고리즘이 적용된다는 것을 지정

ArrayList의 생성자와 제네릭

ArrayList의 생성자는 다음 3개가 있다.

생성자설명
ArrayList()객체를 저장할 공간이 10개인 ArrayList를 만든다.
ArrayList(Collection<? extends E> c)매개 변수로 넘어온 컬렉션 객체가 저장되어 있는 ArrayList를 만든다.
ArrayList(int initialCapacity)매개 변수로 넘어온 initialCapacity 개수만큼의 저장 공간을 갖는 ArrayList를 만든다.

보통 ArrayList는 대부분 서로 다른 종류의 객체를 하나의 배열에 넣지 않고, 한 가지 종류의 객체만 저장한다.

💡 여러 종류를 하나의 객체에 담을 때에는 되도록이면 DTO라는 객체를 하나 만들어서 담는 것이 좋다.

그래서, 컬렉션 관련 클래스의 객체들을 선언할 때에는 앞 장에서 배운 제네릭을 사용하여 선언하는 것을 권장한다.

예를 들어, String만 담는 ArrayList를 생성할 때에는 다음과 같이 사용하면 컴파일 시점에 타입을 잘못 지정한 부분을 걸러낼 수가 있다.

ArrayList<String> list = new ArrayList<String>();
ArrayList<String> list = new ArrayList<>();       // JDK 7부터는 이렇게 사용 가능.

ArrayList의 초기 크기

ArrayList 객체를 선언할 때 매개 변수를 넣지 않으면 초기 크기는 10이다. 따라서, 10개 이상의 데이터가 들어가면 크기를 늘이는 작업이 ArrayList 내부에서 자동으로 수행된다.

이러한 작업이 수행되면 애플리케이션 성능에 영향을 주게 된다.

만약 저장되는 데이터의 크기가 어느 정도 예측 가능하다면 다음과 같이 예측한 초기 크기를 지정할 것을 권장한다.

ArrayList에 데이터를 담는 메소드

ArraList는 확장된 배열 타입이기 때문에, 배열처럼 순서가 매우 중요하다.

리턴 타입
메소드 이름 및 매개 변수설명
booleanadd(E e)매개 변수로 넘어온 데이터를 가장 끝에 담는다.
voidadd(int index, E e)매개 변수로 넘어온 데이터를 지정된 index 위치에 담는다.
booleanaddAll(Collection<? extends E> c)매개 변수로 넘어온 컬렉션 데이터를 가장 끝에 담는다.
booleanaddAll(int index, Collection<? extends E> c)매개 변수로 넘어온 컬렉션 데이터를 index에 지정된 위치부터 담는다.
  • add(E e)
    • 리턴되는 boolean 값은 제대로 추가가 되었는지 여부를 말한다.
    • 보통 false가 떨어지는 경우는 거의 없다.
  • add(int index, E e)
    • 지정된 위치에 데이터를 담으므로, 지정된 위치에 있는 기존 데이터들은 위치가 하나씩 뒤로 밀려난다.
  • addAll(Collection< ? extends E> c)
    • 만약 list의 값을 다른 list에 복사해야 할 일이 생긴다면, 이 메소드를 사용하는 대신 생성자를 사용하는게 편하다.
      ArrayList<String> list2 = new ArrayList<String>(list);
  • addAll(int index, Collection< ? extends E> c)

자바를 개발하다 보면 매우 다양한 타입의 객체를 저장한다. 그리고 모든 개발자들이 모든 프로그램에서 ArrayList만을 사용하여 데이터를 담아 처리하는 것이 아니기 때문에, 이와 같은 Collection을 매개 변수로 갖는 생성자와 메소드를 제공한다.

객체 복사 시 조심할 점

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

위와 같이 사용하면, list2가 list의 값뿐만 아니라, list라는 객체가 생성되어 참조되고 있는 주소까지도 사용하겠다는 말이다.

자바의 모든 객체는 생성되면 그 객체가 위치하는 주소가 내부적으로 할당된다. 앞에서 toString() 메소드를 구현하지 않은 클래스의 toString() 메소드 호출 결과를 출력하면 주소가 나오는 것을 확인했었다.

따라서, 위와 같이 list2 = list 라고 문장을 작성하게 되면, 두 객체의 변수는 다르지만, 하나의 객체가 변경되면 다른 이름의 변수를 갖는 객체의 내용도 바뀐다.

그러므로 하나의 Collection 관련 객체를 복사할 일이 있을 때에는 생성자를 사용하거나, addAll() 메소드를 사용할 것을 권장한다.

💡 Shallow copy와 Deep copy
list2=list 와 같이 다른 객체에 원본 객체의 주소값만을 할당하는 것은 Shallow copy이다.
이와 다르게, 객체의 모든 값을 복사하여 복제된 객체에 있는 값을 변경해도 원본에 영향이 없도록 할 때에는 Deep copy를 수행한다.
예를 들어, 배열을 복사할 때 System 클래스에 있는 arraycopy()와 같은 메소드를 이용하면 Deep copy를 쉽게 처리할 수 있다.

ArrayList에서 데이터를 꺼내기

리턴 타입메소드 이름 및 매개 변수설명
Eget(int index)매개 변수에 지정한 위치에 있는 데이터를 리턴한다.
한 건의 데이터를 꺼내는 메소드는 이 메소드뿐이다.
intindexOf(Object o)매개 변수로 넘어온 객체와 동일한 데이터의 위치를 리턴한다.
intlastIndexOf(Object o)매개 변수로 넘어온 객체와 동일한 마지막 데이터의 위치를 리턴한다.

indexOf와 lastIndexOf 메소드가 있는 이유는, ArrayLsit는 중복된 데이터를 넣을 수 있기 때문이다. 따라서 값의 위치를 앞에서부터 찾을지 뒤에서부터 찾을지 결정하여 사용한다.

  • 앞에서 찾을 때와 뒤에서 찾을 때의 결과는 달라질 수 있다.

ArrayList의 데이터를 배열로 꺼내기

리턴 타입메소드 이름 및 매개 변수설명
Object[]toArray()ArrayList 객체에 있는 값들을 Object[] 타입의 배열로 만든다.
<T> T[]toArray(T[] a)ArrayList 객체에 있는 값들을 매개 변수로 넘어온 T 타입의 배열로 만든다.

여기서 중요한 것은 매개 변수가 없는 toArray() 메소드는 Object 타입의 배열로만 리턴을 한다는 것이다. 그러므로, 제네릭을 사용하여 선언한 ArrayList 객체를 배열로 생성할 때에는 toArray() 메소드를 사용하는 것은 좋지 않다.

아래처럼 toArray(T[] a) 메소드를 사용하는 것을 적극 추천한다.

ArrayList<String> list = new ArrayList<>();
String[] strArray = list.toArray(new String[0]);

이때, 매개 변수로 넘어가는 “new String[0]”을 유심히 살펴볼 필요가 있다. 매개 변수로 넘기는 배열은 이와 같이 의미 없이 타입을 지정하기 위해서 사용할 수도 있다.

그런데, 실제로는 매개 변수로 넘긴 객체에 값을 담아준다.

  • ArrayList 객체의 데이터 크기 > 매개 변수로 넘어간 배열 객체의 크기
    • ArrayList의 크기만큼 데이터를 담아 반환한다.
    • 책에서는 매개 변수로 넘어간 배열의 모든 값이 null로 채워진다고 하는데, 실제로 돌려보면 ArrayList의 모든 데이터를 반환하는 것을 확인할 수 있다.
  • ArrayList 객체의 데이터 크기 == 매개 변수로 넘어간 배열 객체의 크기
    • 모든 데이터가 정상적으로 들어간다.
  • ArrayList 객체의 데이터 크기 < 매개 변수로 넘어간 배열 객체의 크기
    • 매개 변수로 넘어간 배열에 ArrayList의 데이터를 담고, 남은 공간을 null로 채운다.

따라서, toArray() 메소드를 사용할 때에는 크기가 0인 배열을 넘겨주는 것이 가장 좋다.

ArrayList에 있는 데이터 삭제

리턴 타입메소드 이름 및 매개 변수설명
voidclear()모든 데이터를 삭제한다.
Eremove(int index)매개 변수에서 지정한 위치에 있는 데이터를 삭제하고, 삭제한 데이터를 리턴한다.
booleanremove(Object o)매개 변수에 넘어온 객체와 동일한 첫 번째 데이터를 삭제한다.
booleanremoveAll(Collection<?> c)매개 변수로 넘어온 컬렉션 객체에 있는 데이터와 동일한 모든 데이터를 삭제한다.

어떤 값을 반환하고, 어떻게 동작하는지 기억해두자.

ArrayList 객체에 있는 값을 변경하는 메소드

리턴 타입메소드 이름 및 매개 변수설명
Eset(int index, E element)지정한 위치에 있는 데이터를 두 번째 매개 변수로 넘긴 값으로 변경한다. 그리고, 해당 위치에 있던 데이터를 리턴한다.

만약 이 메소드가 없다면, 특정 위치에 있는 데이터를 삭제(remove())하고, 그 위치에 데이터를 넣어야(add())할 것이다.

이렇게 두 단계를 거쳐야 하는 것을 이 메소드로 한 번에 끝낼 수 있다.

ArrayList 객체의 크기를 줄이기

리턴 타입메소드 이름 및 매개 변수설명
voidtrimToSize()ArrayList 객체 공간의 크기를 데이터의 개수만큼 변경한다. 즉, 데이터가 저장되어 있지 않은 공간을 없애버린다.

만약 ArrayList의 객체를 원격으로 전송하거나, 파일로 저장하는 일이 있을 때 이 메소드를 한 번 호출함으로써 데이터의 크기를 줄일 수 있다는 장점이 있다.

Vector처럼 Thread safe하게 만들기

메소드는 약간 상이하지만 Vector라는 클래스도 있다. Vector는 쓰레드에 안전하고, ArrayList는 쓰레드에 안전하지 않다.

따라서 ArrayList를 쓰레드에 안전하게 만들려면 다음과 같이 객체를 생성해야만 한다.

List list = Collections.sysnchronizedList(new ArrayList(...));

Stack 클래스

일반적인 웹을 개발할 때에는 별로 사용하지 않지만, 마지막에 들어온 데이터를 가장 처음에 꺼내는 LIFO 기능을 구현하려고 할 때 필요한 클래스다.

💡 LIFO는 Last In First Out의 약자로 한국어로는 후입선출이라고 한다. 나중에 들어온 값을 먼저 처리하는 것을 의미한다.

Stack vs ArrayDeque

하지만 LIFO 기능을 위해 이 클래스를 사용하는 것은 권장하지 않는다. 왜냐하면, 이 클래스보다 더 빠른 ArrayDeque라는 클래스가 존재하기 때문이다.

하지만 ArrayDeque 클래스는 쓰레드에 안전하지 못하다. 따라서 약간 성능은 떨어지지만, 쓰레드에 안전한 LIFO 기능을 원할 때 Stack 클래스를 사용하면 된다.

Stack의 상속

Stack 클래스의 부모 클래스는 Vector인 것을 볼 수 있다. 즉, Vector 클래스에서 제공하는 모든 메소드를 사용할 수 있다.

Stack이 구현한 인터페이스

ArrayList 클래스에서 구현한 인터페이스와 모두 동일하다.

Stack 클래스는 자바에서 상속을 잘못 받은 클래스다

이 클래스가 JDK 1.0 부터 존재했기 때문에 원래의 취지인 LIFO를 생각한다면 Vector에 속해서는 안된다.

하지만, 자바의 하위 호환성을 위해서 이 상속 관계를 계속 유지하고 있다고 생각하면 된다.

생성자

생성자설명
Stack()아무 데이터도 없는 Stack 객체를 만든다.

사용하는 메소드

리턴 타입메소드 이름 및 매개 변수설명
booleanempty()객체가 비어있는지를 확인한다.
Epeek()객체의 가장 위에 있는 데이터를 리턴한다.
Epop()객체의 가장 위에 있는 데이터를 지우고, 리턴한다.
Epush(E item)매개 변수로 넘어온 데이터를 가장 위에 저장한다.
intsearch(Object o)매개 변수로 넘어온 데이터의 위치를 리턴한다.

여기서 peek()과 pop() 메소드의 차이는 꼭 기억해야 한다. 일반적인 Stack 클래스의 용도에는 pop() 메소드가 더 적합하다.

정리해 봅시다.

Q. Collection 인터페이스를 구현하는 대표적인 3개의 자료구조에는 어떤 것들이 있나요?

Me: List, Set, Queue

Q. 배열과 같이 순서가 있는 목록형을 나타내는 대표 인터페이스는 무엇인가요?

Me: List 인터페이스

Q. ArrayList라는 클래스의 생성자 중 매개변수가 없는 기본 생성자를 사용하면 기본적으로 몇 개의 저장공간을 가지나요?

Me: 10개

Q. 만약 ArrayList 클래스의 저장 공간 개수를 처음부터 지정하려면 어떤 생성자를 사용하면 되나요?

Me: ArrayList(int initialNum)

Q. ArrayList 객체를 생성할 때 제네릭을 사용하는 이유는 무엇인가요?

Me: 보통 ArrayList에는 한 가지 타입만 담게 되고, 제네릭을 사용하면 원하는 타입으로 자유자재로 생성해 사용할 수 있다. (틀린 대답. 제네릭을 사용하는 이유에 포커스를 맞춰 답해야 한다.)

Q. ArrayList에 데이터를 담는 메소드 두가지의 이름은 무엇인가요?

Me: add() 메소드와 addAll() 메소드

Q. Collection 인터페이스를 구현한 클래스의 객체에서 사용할 수 있는 for 루프의 구조는 어떻게 되나요? 코드를 작성하세요.

Me: for (객체에 저장된 타입의 변수 : 객체)

Q. Collection 인터페이스를 구현한 클래스의 객체에 저장된 데이터의 갯수를 확인하는 메소드 이름은 무엇인가요?

Me: size()

Q. ArrayList에서 특정 위치에 있는 데이터를 확인하는 메소드는 무엇인가요?

Me: get(index)

Q. ArrayList에서 특정 위치에 있는 데이터를 삭제하는 메소드는 무엇인가요?

Me: remove(index)

Q. ArrayList에서 특정 위치에 있는 데이터를 수정하는 메소드는 무엇인가요?

Me: set(index)

Q. java.util 패키지에 있는 Stack 이라는 클래스는 어떤 클래스를 확장한 것인가요?

Me: List 인터페이스를 구현한 Vector 클래스

Q. Stack 클래스에서 데이터를 담는 메소드는 무엇인가요?

Me: push()

Q. Stack 클래스에서 가장 위에 있는 데이터를 확인만 하는 메소드는 무엇인가요?

Me: peek()

Q. Stack 클래스에서 가장 위에 있는 데이터를 삭제하고 리턴하는 메소드는 무엇인가요?

Me: pop()

질문

💡 책에 있는 내용이 아닙니다.

책을 읽으며 설명이 더 필요하거나, 추가로 궁금한 점에 대해 질문 형식으로 작성 후, 답을 구해보고 있습니다.
참고한 사이트나 영상은 [출처]로 달아두었으며, 오류 지적은 언제나 환영합니다.

Q. 왜 Map만 별도의 인터페이스로 선언되어 있는가?

Map은 다른 컬렉션과는 다르게 Key-Value 형식의 자료를 저장하는 구조를 가지고 있다. 따라서, 별도의 인터페이스로 정의하여 이 구조를 명시적으로 나타냈다.

그리고 이렇게 인터페이스로 선언함으로써, Map 인터페이스의 구현체들은 모두 일관성 있는 사용 방법과 메소드를 공유하게 된다.

Q. Vector는 Thread Safe 함에도 왜 많이 사용하지 않을까?

동기화 오버헤드

Vector의 모든 메소드는 Synchronized 키워드가 붙어 있다.

즉, 수행하려는 모든 작업에 대해 Object lock을 얻어야 한다. 각 작업마다 lock을 얻어야 하는 것은 시간을 많이 소요시키는 일이다.

특히 동기화가 필요없는 단일 스레드 환경에서는 불필요한 동기화 오버헤드를 발생시킨다.

Single Lock을 사용

Vector는 Synchronized Method를 사용 중인데, ChatGPT는 이를 Single Lock을 사용한다고 말한다. 실제로 검색해봤을 때 single lock 이라는 단어를 사용하는 사람은 없는 것 같다.

아무튼, 이 Single Lock이라는 건 아래와 같은 문제가 발생할 수 있다.

  • Vector 객체 하나를 생성 후 여러 스레드에서 해당 객체를 통해 동일한 메소드를 호출하고자 할 때, 다른 메소드가 작업을 끝내기를 기다려야 하므로 병목 현상이 발생할 수 있다.

💡 Lock Splitting과 Lock Striping 이라는 개념이 나오는데, 두 개의 차이는 잘 모르겠다. Thread 관련 내용을 정리할 때 다시 공부해서 적어보자.

더 효율적인 대안들의 등장

Java 5부터는 더 효율적으로 동시성(Concurrency) 문제를 해결하기 위한java.util.concurrent 패키지의 클래스들을 제공한다. 따라서 굳이 Vector를 사용할 이유가 없어졌다.

또한, Vector 클래스는 deprecated 된 클래스이며 Collections.synchronizedList 메소드로 ArrayList나 LinkedList도 동기화 객체로 변환시킬 수가 있다.

그러므로 굳이 내부적으로도 안정되지 않은 Vector 클래스를 사용할 일이 없는 것이다.

💡 동시성(Concurrency) 문제란

여러 스레드가 동시에 진행되는 것을 동시성이라고 한다. 실제로 동시에 실행되는 건 아니고, 동시에 실행되는 것처럼 보이게 번갈아가며 실행된다고 한다.
이는 물리적으로 동시에 실행되는 병렬성과는 구분된다고 하니 주의해서 공부하자.

그리고 여러 스레드가 동일한 자원에 동시에 접근하면서, 그 결과를 작업의 실행 순서에 의존하며 예측할 수 없는 동작이 발생할 수 있는 상태를 경쟁 상태(race-condition)라고 한다.

즉, 동시성이 존재할 때 발생할 수 있는 문제 중 하나가 경쟁 상태이다.

Q. 자바의 Stack 이 상속을 잘못 받았다고 하는 이유

Stack은 LIFO라는 특성을 갖는다. 즉, 가장 나중에 들어온 것이 가장 먼저 나가야 한다.

그런데, List 인터페이스를 구현한 Vector를 상속받으면서 Stack과 관련 없는 메소드들도 함께 물려받아 사용할 수 있게 되었다.

즉, LIFO 특성이 깨지면서 Vector의 단점들도 동시에 이어받게 된 것이다.

  • 예를 들어, 자바의 Stack은 무작위 액세스가 가능하다.

Q. clone 메소드는 얕은 복사인가?

Object.clone() 은 해당 객체의 얕은 복사를 수행한다. 객체 자체는 새로 생성하지만, 객체 내부에 있는 참조 자료형 필드들은 같은 객체를 참조하게 된다.

따라서, 깊은 복사가 필요하다면 clone() 메소드를 오버라이딩하여 객체의 내부 참조 자료형 필드들도 새로 생성 후 복사해 줘야 한다.

Q. 사용할 공간을 미리 선언하는 것과 동적으로 늘리는 것의 장단점.

ArrayList에서 동적으로 크기를 조절해준다는 부분을 보고 정리해봤다.

사용할 공간을 미리 선언한다면

  • 초기에 사용하기에 충분한 공간을 할당하면, 요소를 추가할 때 공간을 다시 조정할 필요가 없어 성능이 향상된다.
  • 초기 크기를 정확히 예측하기 어려울 경우에는, 너무 작게 설정하여 재할당이 자주 발생하거나 너무 크게 설정하여 메모리 낭비가 발생할 수 있다.

동적으로 늘린다면

  • 초기 크기를 정확하게 예측할 필요가 없고, 동적으로 크기가 조절되므로 편리하다.
  • 요소를 추가할 때마다 동적으로 크기를 조절하면서 메모리를 재할당해야 하므로 성능 저하가 발생할 수 있다.
  • 메모리 재할당 과정에서 요소들을 다른 위치를 이동시키면서 추가적인 연산 비용이 발생할 수 있다.

Q. Deep Copy와 Shallow Copy의 메모리 공간

Deep Copy는 복사하는 객체와 해당 객체의 참조 타입 필드까지 모두 새로운 메모리 공간에 저장한다.

반면, Shallow Copy는 단순히 복사하는 객체의 참조만을 복사하기 때문에, 같은 메모리 공간을 공유한다.

Q. 배열을 사용하지 않고 ArrayList를 사용하는 이유는?

본문에서도 설명했지만, 배열은 명시적으로 크기를 지정해줘야 한다. 반면 ArrayList는 동적으로 크기를 조절할 수 있다.

또한, ArrayList는 편리한 메소드를 제공하며 다양한 컬렉션과 호환이 된다는 장점이 있다.

상황에 따라 둘 중 하나를 자유롭게 선택하여 사용하면 된다.

참고 사이트

Why is Java Vector (and Stack) class considered obsolete or deprecated?

Why Not To Use Vector Class In Your Code?

Vector Class in Java - GeeksforGeeks

🧱 ArrayList vs Vector 동기화 & 성능 차이 비교

동시성 관련 공부

https://kangmanjoo.tistory.com/13

[Java] 멀티스레드 환경에서의 동시성 문제와 대책

[Java] 동시성 문제 공부 및 정리 (2)

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글