프로그래밍 일기 — 컬렉션

배우는 자(Learner Of Life)
20 min readJun 25, 2023

--

컴퓨터의 패션 트랜드를 알아보자

#컬렉션, #자료구조, #리스트, #스택, #큐, #셋, #맵, #프레임워크, #인터페이스, #클래스

컴퓨터는 어떤 옷을 입는 것을 좋아하나?(1)

지금까지 Java를 공부하면서 풀리지 않는 의문이 하나 더 있다. 과연 컬렉션(Collection)이란 무엇일까? 자료구조와 형태를 공부할 때 한번 마추졌던 개념이지만 확실하게 짚고 넘어가지 않았다. 클래스와 인터페이스에 대한 학습을 확실하게 한 이상, 이번 기회에 또 하나의 궁금증을 유발했던 컬렉션이 무엇인지 밝혀야한다는 생각이 들었다.

컬렉션이라는 단어를 들었을 때, 나는 패션에 대한 생각을 가장 먼저 떠올렸다. 패션업계를 내가 잘 아는 것은 아니지만 각 분기 혹은 계절마다 전 세계적으로 유명한 디자이너들이 올해 봄, 여름, 가을, 겨울 패션에 대한 런웨이같은 발표 행사를 가진다. 그 자리에서 사람들이 앞으로 입게 될 패션에 대해 이야기하고 그런 것들을 모아 “OOO디자이너의 2023년 가을 컬렉션”등이라고 이름짓는다. 이런 유행에 민감한 사람들은 그런 유명한 디자이너들이 선보이는 옷들을 계절에 맞게 찾아입는다. 그렇게 세상에서는 트렌드라는 것이 만들어지고, 그것을 따라하는 사람들이 점점 더 많아지면서 하나의 유행이 만들어진다.

컴퓨터를 배우는 사람들도 마찬가지로 유행에 민감해야한다. 컴퓨터의 지식은 패션 못지 않게, 혹은 그 보다 더 빨리 바뀌거나 발전한다. 어떠한 언어가 새로 발표되고, 어떤 언어가 쇠퇴하는지, 어떤 프레임워크가 새로 생겨나고 어떤 것이 유망해지는 지를 알아야한다. 그래야 이 변화 무쌍한 세계에서 능력있는 개발자로 인정받을 수 있다.

그러나 다행스러운 것은 컴퓨터의 기본적인 개념은 쉽게 바뀌지 않는다는 것이다. 컴퓨터공학이라는 학문이 정립된 이래, 아직까지 자료구조나, 네트워크 이론, 객체지향 이론 등 현대 컴퓨터 이론의 중추를 담당하는 개념들은 굉장히 오래 살아남았다. Java도 마찬가지로 90년대에 정립된 이래 거의 30년이 넘는 시간 동안 그 지위를 쉽게 잃지 않았다. 그렇다는 것은 Java에서 통용되는 개념들 또한 여전히 유효하며 현재 컴퓨터 유행의 일부라고 보아도 될 것이다.

컬렉션은 자료 구조이고, 컴퓨터 데이터에 입히는 옷으로 생각하면 조금 더 이해하기 쉬울 것이다. 이런 Java에서 현재 유행하는 옷은 무엇일까? 데이터가 입기 좋고 편한 옷들에는 무엇이 있을까? 그 옷들은 점점 많아지는 추세이고, 이 글에서 모두 다루기 쉽지 않을 정도이므로, 여기서는 가장 기본적으로 널리입히는 옷들이 무엇인지 짚고 넘어가도록 하겠다.

컬렉션(Collection)

먼저 컬렉션에 대한 이해를 하기 전해 짚고 넘어가야하는 개념이 있다.

프레임워크(Framework)(2): 원하는 어플리케이션 개발을 위해 바로 사용할 수 있는 커스터마이징된 컴포넌트(Component) 및 솔루션의 묶음이다. 프레임워크를 건물에 짖는 것에 비유한다면, 건설자들이 직접 야생에서 나무와 석재를 공수해와 일일히 다 다듬을 필요없이, 이미 건설 용으로 1차 가공된 목재와 석재를 바로 가져다가 사용할 수 있는 것과 같다. 마찬가지로 개발자 역시 아무것도 없는 맨땅에 헤딩하는 것이 아니라, 이미 만들어진 프레임워크를 활용하여 웹사이트 혹은 어플리케이션을 만들 수 있다. 프레임워크는 그 목적에 따라 여러 비슷한 종류의 클래스를 묶어 놓는 경우가 많다.

컬렉션은 이러한 프레임워크의 한 종류로써, 여러 데이터를 다루기 위한 자료구조를 표현하고 사용하는 클래스의 묶음이다. 데이터를 다루는데 필요한 클래스와 함수를 여러가지 제공하기 때문에, Java어플리케이션 개발자들에게 매우 유용하게 활용되고 있다.컬렉션 프레임워크의 모든 클래스는 그러므로, Collection 인터페이스를 구현(implement)하는 클래스 혹은 인터페이스라 볼 수 있다.

컬렉션 인터페이스 및 자료구조

Collection 은 모든 자료구조가 구현(implement)하는 인터페이스이므로, 모든 자료구조를 표현할 때 활용된다고 볼 수 있다. 대표적으로 아래의 자료구조들이 있다.

List

순서가 정해진 데이터의 집합이며 데이터의 중복이 가능하다. 예를 들어 아래의 클래스들이 존재한다. Stack은 리스트와 비슷하지만 조금 다른 특성을 가진다(아래 Stack에 대한 설명 참조).

  • ArrayList
  • LinkedList
  • Stack

Set

순서가 없는 데이터의 집합이며 데이터의 중복은 불가능하다. 예를 들어 아래의 클래스들이 존재한다.

  • HashSet
  • TreeSet

Map

“키(Key): 값(Value)” 쌍으로 이루어진 데이터의 집합이며, 데이터의 중복은 불가능하다. Python같은 언어에서는 Dictionary라고 칭하는 자료구조이다. 예를 들어 아래의 클래스들이 존재한다.

  • HashMap
  • TreeMap

Stack

큰 틀에서는 리스트 자료구조 중 한 형태로 볼 수 있다. LIFO(Last in First Out)의 특성을 갖는 구조로써, 가장 나중에 입력된 데이터가 먼저 꺼내지는 자료구조이다. 리스트에 비해서 조금 더 제한이 많은 특성을 가진 자료구조이지만, 상대적으로 구현이 더 쉽고, LIFO의 특성이 잘 활용될 수 있는 문제에 대해서는 더 용이하기도하다. 예를 들어 아래의 클래스들이 존재한다.

  • Stack
  • ArrayDeque

Queue

Stack과는 반대로 FIFO(First in First Out)의 특성을 갖는 구조로써, 먼저 입력된 데이터가 먼저 꺼내지는 구조이다. 예를 들어 아래의 클래스들이 존재한다.

  • Queue(LinkedList)
  • ArrayDeque

컬렉션 인터페이스는 컬렉션 클래스에 저장된 데이터를 읽고(Read), 쓰고(Write), 삭제(Delete)하는 등, 데이터를 조작하는데 필요한 기본적인 매서드를 정의한다. 이제부터는 본격적으로 예를 들어 위에 소개한 각 클래스들을 코드상에서 구현해 보도록 한다.

List

순서가 정해진 데이터를 나열한 자료구조이다.

// Array List 예제

// 필요한 Java 패키지 가져오기
import java.util.*;

// 메인 프로그램에서의 활용
public class Main {
public static void main(String[] args) {
List list = new ArrayList(10); // ArrayList 클래스 인스턴트화 (최대 10개의 데이터 정의)
list.add(1); // 숫자 "1" 데이터 추가
list.add(5); // 숫자 "5" 데이터 추가
list.add(4); // 숫자 "4" 데이터 추가
list.add(11); // 숫자 "11" 데이터 추가
list.add(10); // 숫자 "10" 데이터 추가
System.out.println(list); // [1,5,4,11,10]
Collections.sort(list); // list 정렬
System.out.println(list); // [1,4,5,10,11]
System.out.println(list.size()); // arrayList의 크기 출력
list.remove(4); // 인덱스를 활용하여 해당하는 값 제거
System.out.println(list);
for (int i = 0; i < list.size(); i++) { // index를 활용한 for문
System.out.println(list.get(i)); // get을 이용하여 값 1개씩 출력
}
for (Object current : list) { // Object문을 활용한 for문
System.out.println(current);
}
}
}

>>

[1, 5, 4, 11, 10] // list 정렬 전
[1, 4, 5, 10, 11] // list 정렬 후
5 // list.size로 크기 출력
[1, 4, 5, 10] // list.remove(4)로 값이 하나 제거된 리스트를 출력
1 // 첫 번째 for문으로 리스트 값을 하나씩 출력
4
5
10
1 // 두 번째 for문으로 리스트 값을 하나씩 출력
4
5
10

Set

순서를 유지하지 않는 데이터의 집합이며 데이터의 중복은 없다. Collection 자료형에는 primitive타입의 클래스가 올 수 없다는 것에 주의하라, 그러므로 int, byte, short, long, float, double, boolean, char등의 키워드는 쓰지 않는다. <> 로 보이는 부분은 Generics라는 부분으로써 셋을 비롯한 여려 자료구조의 클래스에서 많이 쓰이는 문법이다. 이에 대한 설명은 추후에 하기로한다.

// HashSet 예제

// 필요한 Java패키지 가져오기
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

// 메인프로그램에서의 활용
public class Main {
public static void main(String[] args) {
Set<Integer> integerSet = new HashSet<>(); // Collection의 자료형에는 primitive 타입은 올 수 없다.
integerSet.add(1);
integerSet.add(3);
integerSet.add(2);
integerSet.add(9); // 하나씩 값을 삽입
System.out.println(integerSet); // 출력 후 순서가 없다는 것을 알 수 있다.
Set<String> stringSet = new HashSet<>(); // String 데이터를 위한 컬렉션 정의
stringSet.add("LA");
stringSet.add("New York");
stringSet.add("LasVegas");
stringSet.add("San Francisco");
stringSet.add("Seoul"); // 하나씩 값을 삽입
System.out.println(stringSet);
stringSet.remove("Seoul"); //Seoul을 HashSet에서 제거
System.out.println(stringSet);
ArrayList<String> target = new ArrayList<String>(); // ArrayList를 하나 생성하기
target.add("New York");
target.add("LasVegas"); //제거할 항목을 ArrayList에 삽입
stringSet.removeAll(target); //제거항목에 삽입된 도시들을 삭제
System.out.println(stringSet);
System.out.println("LA가 포함되어있나요? " + stringSet.contains("LA"));
System.out.println("Las Vegas가 포함되어있나요? " + stringSet.contains("LasVegas"));
//LA나 Las Vegas가 HashSet에 포함되어있으면 true를, 그렇지 않으면 false를 반환
System.out.println("현재 HashSet의 크기는 : " + stringSet.size() + "입니다.");
//HashSet의 크기를 반환
stringSet.clear();//HashSet의 모든 아이템들을 삭제
System.out.println(stringSet);
}
}

>>

[2, 3, 1, 9]
[San Francisco, New York, LasVegas, LA, Seoul]
[San Francisco, New York, LasVegas, LA]
[San Francisco, LA]
LA가 포함되어있나요? true
Las Vegas가 포함되어있나요? false
현재 HashSet의 크기는 : 2입니다.
[]

Map

“Key: value” 구조를 갖는 자료구조이다. HashMap이 가장 널리쓰이는데, Map의 이러한 쌍(pair)구조를 바탕으로 해싱(Hashing)을 수행한다. 이 해싱은 데이터 검색에 있어 매우 뛰어난 성능을 자랑한다. 해싱이란 주어진 키값을 다른 값으로 바꾸는 것을 말한다. 아주 많이 쓰이는 Hash Table에 활용되는 개념이기도하다. Hash Table에서는 key:value 쌍에 hash value라는 index값을 부여한다. Hasing은 특유의 알고리즘을 활용해 값을 생성하는데, 이에 대한 설명을 하려면 너무 길어질 수 있으므로, 자세한 내용은 아래 참조(3)를 통해 더 확인할 수 있다.

// HashMap 예제

// 필요한 Java패키지 가져오기
import java.util.*;

// 메인 프로그램에서의 활용
public class Main {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>(); // HashMap 선언
map.put(1, "apple"); // key:value값 쌍을 입력하기
map.put(2, "berry");
map.put(3, "cherry");
System.out.println(map); // HashMap 출력
System.out.println("1st in map: " + map.get(1)); // HashMap의 key "1" 에 대한 값 출력
map.remove(2); // HashMap의 key "2"에 대한 값 삭제
System.out.println(map); // HashMap 출력
System.out.println(map.containsKey(2)); // HashMap이 Key "2"를 가진 값이 있는지 확인
System.out.println(map.containsValue("cherry")); // HashMap이 "cherry"라는 값을 가지고 있는지 확인
map.clear(); // HashMap값 전부 삭제
System.out.println(map); // HashMap 출력
}
}

>>

{1=apple, 2=berry, 3=cherry}
1st in map: apple
{1=apple, 3=cherry}
false
true
{}

Stack

LIFO(Last in First Out)의 구조로써, 마지막에 저장한 데이터가 가장 먼저 꺼내지는 자료구조라 할 수 있다. 프린터 용지함에 쌓인 A4용지들을 상상하면 좋다. 프린터는 구조상 가장 위에 있는 종이부터 사용할 것이다. 가장 아래에 있는 종이가 가장 먼저 놓여진 종이이지만, 가장 나중에 사용되는 것이다. 프로그래밍 상에서는 웹 브라우저상에서 앞페이지로 이동할 때를 상상할 수 있다. 뒤로 갈수록 가장 나중에 본 웹페이지가 먼저 나오는 것과 같다. 아래 그림을 통해서 조금 더 선명하게 볼 수 있다. 데이터를 Stack에 넣는 명령이 push() 이고, 꺼내는 명령이 pop() 이라고 볼 수 있다. 구조상 가장 먼저 입력된 데이터가 가장 아래쌓여 가장 나중에 꺼내지고, 가장 나중에 입력된 데이터가 가장 먼저 꺼내지는 것을 알 수 있다.

가장 나중에 들어간 데이터가 가장 나중에 나온다(4)
// Stack 예제

// 필요한 Java패키지 가져오기
import java.util.*;

// 메인프로그램에서의 활용
public class Main {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>(); // Stack 자료구조 선언
stack.push(1);
stack.push(3);
stack.push(5);
stack.push(7);
System.out.println(stack); // Stack을 출력
System.out.println(stack.peek()); // Stack의 가장 상단 값을 출력(삭제는 하지 않는다.)
stack.pop(); // Stack의 가장 상단 값을 제거
System.out.println(stack); // Stack을 출력
System.out.println(stack.size()); // Stack의 크기를 반환
System.out.println(stack.contains(1)); // Stack에 1이라는 값이 있으면 true를, 그렇지 않으면 false를 반환
System.out.println(stack.empty()); // STack이 비어있으면 true를, 그렇지 않으면 false를 반환
System.out.println(stack); // Stack을 출력
}
}

>>

[1, 3, 5, 7]
7
[1, 3, 5]
3
true
false
[1, 3, 5]

Queue

Stack과는 다르게, 처음에 저장한 데이터가 가장 먼저 꺼내지는 FIFO(First in First Out)구조이다. 어느서건 길게 줄을 서있는 모습을 상상하면 쉽다. 푸드트럭 앞에서있는 사람들은 분명히 먼저 온 손님이 먼저 음식을 가져가도록 되어있다. 이런식으로 먼저 입력된 데이터가 먼저 처리되는 구조라 할 수 있다. 큐의 종류에는 우선순위 큐, 원형 우선순위 큐, 원형 큐 등 다양하지만, 여기서는 기본적인 Queue의 구조만을 다루도록 하겠다.

Queue의 앞부분을 Head(front)라하고 여기에서 데이터를 꺼낼(Dequeue) 수 있다. 뒤부분을 Tail(Rear)라 하고 여기에서 데이터를 입력(Enqueue)할 수 있다.

Queue는 먼저 입력된 데이터가 먼저 꺼내지는 구조이다(5)
// Queue 예제

// 필요한 Java패키지 가져오기
import java.util.*;

// 메인 프로그램에서의 활용
public class Main {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>(); // LinkedList 활용
queue.add(1);
queue.add(3);
queue.add(5); //Queue에 값 삽입(Enqueue)
System.out.println(queue); //Queue 출력
System.out.println(queue.poll()); // Queue에서 객체를 꺼내기 (가장 먼저 입력된 값이 꺼내짐)
queue.add(7);
queue.add(11);
queue.add(9);
System.out.println(queue);
System.out.println(queue.peek()); //Queue에서 가장 앞에있는 데이터를 반환(데이터를 꺼내지 않음)
System.out.println(queue);
}
}

>>

[1, 3, 5]
1
[3, 5, 7, 11, 9]
3
[3, 5, 7, 11, 9]

ArrayDeque

Stack이나 Queue(LinkedList) 클래스 보다 실무에서 좀 더 많이 활용되는 클래스다. Stack과 Queue의 구조적 장점을 가지면서도 상대적으로 성능이 더 좋다. Queue와는 달리 Head와 Tail에서 모두 데이터의 삽입과 반환이 가능하다.

Deque에서는 Head(Front)와 Tail(Rear)에서 모두 데이터의 삽입과 출력이 가능하다(6)
// Deque예제

// 필요한 Java패키지 가져오기
import java.util.*;

// 메인프로그램에서의 활용
public class Main {
public static void main(String[] args) {
ArrayDeque<Integer> arrayDeque = new ArrayDeque<>(); // ArrayDeque를 이용한 선언(제네릭스 이용)
arrayDeque.addFirst(1);
arrayDeque.addFirst(2);
arrayDeque.addFirst(3);
arrayDeque.addFirst(4); // arrayDeque의 앞에 값을 삽입
System.out.println(arrayDeque);
arrayDeque.addLast(0); // arrayDeque의 끝에 값을 삽입
System.out.println(arrayDeque);
arrayDeque.offerFirst(10); // addFirst와 비슷하지만 큐의 크기 문제가 생길 때, offerFirst는 false를,
// addFrist는 exception을 반환
System.out.println(arrayDeque);
arrayDeque.offerLast(-1); // arrayDeque의 끝에 값을 삽입
System.out.println(arrayDeque);
System.out.println(arrayDeque.size()); // 7
System.out.println(arrayDeque.removeFirst()); // 첫번째 값을 제거하면서 그 값을 리턴
System.out.println(arrayDeque.removeLast()); // 마지막 값을 제거하면서 그 값을 리턴
System.out.println(arrayDeque);
System.out.println(arrayDeque.size()); // 5
System.out.println(arrayDeque.pollFirst()); // 첫번째 값을 반환 및 제거하면서 그 값을 리턴
System.out.println(arrayDeque);
System.out.println(arrayDeque.size()); // 4
System.out.println(arrayDeque.pollLast()); // 마지막 값을 반환 및 제거하면서 그 값을 리턴
System.out.println(arrayDeque);
System.out.println(arrayDeque.size()); // 3
System.out.println(arrayDeque.peekFirst()); // 첫번째 값을 반환, 제거하지 않음
System.out.println(arrayDeque.peekLast()); // 마지막 값을 반환, 제거하지 않음
System.out.println(arrayDeque.size()); // 3
}
}

>>

[4, 3, 2, 1]
[4, 3, 2, 1, 0]
[10, 4, 3, 2, 1, 0]
[10, 4, 3, 2, 1, 0, -1]
7
10
-1
[4, 3, 2, 1, 0]
5
4
[3, 2, 1, 0]
4
0
[3, 2, 1]
3
3
1
3

참조:

(1) https://pixabay.com/photos/silk-tie-sales-man-2846862/

(2) https://www.netsolutions.com/insights/what-is-a-framework-in-programming/#:~:text=A%20framework%20in%20programming%20is,order%20to%20speed%20up%20development.

(3) https://www.techtarget.com/searchdatamanagement/definition/hashing

(4) https://www.javatpoint.com/java-stack

(5) https://jenkov.com/tutorials/java-collections/queue.html

(6) https://www.edureka.co/blog/deque-in-java/

--

--

배우는 자(Learner Of Life)
배우는 자(Learner Of Life)

Written by 배우는 자(Learner Of Life)

배움은 죽을 때까지 끝이 없다. 어쩌면 그게 우리가 살아있다는 증거일지도 모른다. 배움을 멈추는 순간, 혹은 배움의 기회가 더 이상 존재하지 않는 순간, 우리의 삶은 어쩌면 거기서 끝나는 것은 아닐까? 나는 배운다 그러므로 나는 존재한다. 배울 수 있음에, 그래서 살아 있음에 감사한다.

No responses yet