Java

Collections Framework

배열과 컬렉션즈 프레임워크

이전 시간에 배열에 대해서 공부했다. 배열은 연관된 데이터를 관리하기 위한 수단이었다. 그런데 배열에는 몇가지 불편한 점이 있었는데 그 중의 하나가 한번 정해진 배열의 크기를 변경할 수 없다는 점이다. 이러한 불편함을 컬렉션즈 프래임워크를 사용하면 줄어든다.

아래의 예를 보자.

package org.opentutorials.javatutorials.collection;

import java.util.ArrayList;

public class ArrayListDemo {

    public static void main(String[] args) {
		String[] arrayObj = new String[2];
		arrayObj[0] = "one";
		arrayObj[1] = "two";
		// arrayObj[2] = "three"; 오류가 발생한다.
		for(int i=0; i<arrayObj.length; i++){
			System.out.println(arrayObj[i]);
		}
		
		ArrayList al = new ArrayList();
		al.add("one");
		al.add("two");
		al.add("three");
		for(int i=0; i<al.size(); i++){
			System.out.println(al.get(i));
		}
	}

}

아래 코드처럼 배열은 그 크기를 한번 지정하면 크기보다 많은 수의 값을 저장할 수 없다.

String[] arrayObj = new String[2];
arrayObj[0] = "one";
arrayObj[1] = "two";
// arrayObj[2] = "three"; 오류가 발생한다.

하지만 ArrayList는 크기를 미리 지정하지 않기 때문에 얼마든지 많은 수의 값을 저장할 수 있다.

ArrayList al = new ArrayList();
al.add("one");
al.add("two");
al.add("three");

ArrayList는 배열과는 사용방법이 조금 다르다. 배열의 경우 값의 개수를 구할 때 .length를 사용했지만 ArrayList는 메소드 size를 사용한다. 또한, 특정한 값을 가져올 때 배열은 [인덱스 번호]를 사용했지만 컬렉션은 .get(인덱스 번호)를 사용한다.

for(int i=0; i<al.size(); i++){
	System.out.println(al.get(i));
}

그런데 ArrayList를 사용할 때 주의할 점이 있다. 위의 반복문을 아래처럼 바꿔보자.

for(int i=0; i<al.size(); i++){
	String val = al.get(i);
	System.out.println(val);
}

위의 코드는 컴파일 오류가 발생한다. ArrayList의 메소드 add의 입장에서는 인자로 어떤 형태의 값이 올지 알 수 없다. 그렇기 때문에 모든 데이터 타입의 조상인 Object 형식으로 데이터를 받고 있다. 예를들면 아래와 같은 모습일 것이다. (실제와는 다르다)

public boolean add(Object e) {

따라서 ArrayList 내에서 add를 통해서 입력된 값은 Object의 데이터 타입을 가지고 있고, get을 이용해서 이를 꺼내도 Object의 데이터 타입을 가지고 있게 된다. 그래서 위의 코드는 아래와 같이 바꿔야 한다.

for(int i=0; i<al.size(); i++){
	String val = (String)al.get(i);
	System.out.println(val);
}

get의 리턴값을 문자열로 형변환하고 있다. 원래의 데이터 타입이 된 것이다.

그런데 위의 방식은 예전의 방식이다. 이제는 아래와 같이 제네릭을 사용해야 한다.

ArrayList<String> al = new ArrayList<String>();
al.add("one");
al.add("two");
al.add("three");
for(int i=0; i<al.size(); i++){
	String val = al.get(i);
	System.out.println(val);
}

제네릭을 사용하면 ArrayList 내에서 사용할 데이터 타입을 인스턴스를 생성할 때 지정할 수 있기 때문에 데이터를 꺼낼 때(String val = al.get(i);) 형변환을 하지 않아도 된다.

컬렉션즈 프래임워크란?

그럼 이제부터 컬렉션즈 프래임워크가 무엇인가 본격적으로 알아보자. 컬렉션즈 프래임워크라는 것은 다른 말로는 컨테이너라고도 부른다. 즉 값을 담는 그릇이라는 의미이다. 그런데 그 값의 성격에 따라서 컨테이너의 성격이 조금씩 달라진다. 자바에서는 다양한 상황에서 사용할 수 있는 다양한 컨테이너를 제공하는데 이것을 컬렉션즈 프래임워크라고 부른다. ArrayList는 그중의 하나다.

위의 그림은 컬렉션즈 프래임워크의 구성을 보여준다. Collection과 Map이라는 최상위 카테고리가 있고, 그 아래에 다양한 컬렉션들이 존재한다. 그럼 구체적인 컬렉션즈 프래임워크 클래스들을 살펴보자.

ArrayList를 찾아보자. Collection-List에 속해있다. ArrayList는 LIst라는 성격으로 분류되고 있는 것이다. List는 인터페이스이다. 그리고 List 하위의 클래스들은 모두 List 인터페이스를 구현하기 때문에 모두 같은 API를 가지고 있다. 클래스의 취지에 따라서 구현방법과 동작방법은 다르지만 공통의 조작방법을 가지고 있는 것이다. 익숙한 ArrayList를 바탕으로 나머지 컬렉션들의 성격을 파악해보자.

List와 Set의 차이점은 List는 중복을 허용하고, Set은 허용하지 않는다.

package org.opentutorials.javatutorials.collection;

import java.util.ArrayList;
import java.util.HashSet;

import java.util.Iterator;

public class ListSetDemo {

    public static void main(String[] args) {
		ArrayList<String> al = new ArrayList<String>();
		al.add("one");
		al.add("two");
		al.add("two");
		al.add("three");
		al.add("three");
		al.add("five");
		System.out.println("array");
		Iterator ai = al.iterator();
		while(ai.hasNext()){
			System.out.println(ai.next());
		}
		
		HashSet<String> hs = new HashSet<String>();
		hs.add("one");
		hs.add("two");
		hs.add("two");
		hs.add("three");
		hs.add("three");
		hs.add("five");
		Iterator hi = hs.iterator();
		System.out.println("\nhashset");
		while(hi.hasNext()){
			System.out.println(hi.next());
		}
	}

}

결과는 아래와 같다.

array
one
two
two
three
three
five

hashset
two
five
one
three

우선 값을 가져오는 방법이 조금 달라졌다. (ArrayList에서도 이 방법을 사용할 수 있다)

Iterator ai = al.iterator();
while(ai.hasNext()){
	System.out.println(ai.next());
}

메소드 iterator는 인터페이스 Collection에 정의되어 있다. 따라서 Collection을 구현하고 있는 모든 컬렉션즈 프레임웍크는 이 메소드를 구현하고 있음을 보증한다. 메소드 iterator의 호출 결과는 인터페이스 iterator를 구현한 객체를 리턴한다. 인터페이스 iterator는 아래 3개의 메소드를 구현하도록 강제하고 있는데 각각의 역할은 아래와 같다.

  • hasNext
    반복할 데이터가 더 있으면 true, 더 이상 반복할 데이터가 없다면 false를 리턴한다.
  • next
    hasNext가 true라는 것은 next가 리턴할 데이터가 존재한다는 의미다. 

이러한 기능을 조합하면 for 문을 이용하는 것과 동일하게 데이터를 순차적으로 처리할 수 있다.

그럼 본론으로 돌아와서 Set과 List의 차이를 짚어보자. 위의 결과를 통해서 알 수 있는 것처럼 Set는 중복을 허용하지 않고 순서가 없지만, List는 중복을 허용하고 저장되는 순서가 유지된다는 것을 알 수 있다. 이러한 특징을 고려해서 컬렉션을 선택해야 한다. 그럼 Set에 대해서 조금 더 알아보자.

Set

Set은 한국어로 집합이라는 뜻이다. 여기서의 집합이란 수학의 집합과 같은 의미다. 수학에서의 집합도 순서가 없고 중복되지 않는 특성이 있다는 것이 기억날 것이다. (기억나지 않아도 상관없다) 수학에서 집합은 교집합(intersect), 차집합(difference), 합집합(union)과 같은 연산을 할 수 있었다. Set도 마찬가지다.

이 내용은 수학적 지식을 요구하지 않는다. 겁내지 말자. (프로그래밍과 수학)

아래와 같이 3개의 집합 hs1, hs2, hs3이 있다고 하자. h1은 숫자 1,2,3으로 이루어진 집합이고, h2는 3,4,5 h3는 1,2로 구성되어 있다. set의 API를 이용해서 집합 연산을 해보자.

package org.opentutorials.javatutorials.collection;

import java.util.ArrayList;
import java.util.HashSet;

import java.util.Iterator;

public class SetDemo {

    public static void main(String[] args) {
		HashSet<Integer> A = new HashSet<Integer>();
		A.add(1);
		A.add(2);
		A.add(3);
		
		HashSet<Integer> B = new HashSet<Integer>();
		B.add(3);
		B.add(4);
		B.add(5);
		
		HashSet<Integer> C = new HashSet<Integer>();
		C.add(1);
		C.add(2);
		
		System.out.println(A.containsAll(B)); // false
		System.out.println(A.containsAll(C)); // true
		
		//A.addAll(B);
		//A.retainAll(B);
		//A.removeAll(B);
		
		Iterator hi = A.iterator();
		while(hi.hasNext()){
			System.out.println(hi.next());
		}
	}

}

부분집합 (subset)

System.out.println(A.containsAll(B)); // false
System.out.println(A.containsAll(C)); // true

합집합(union)

A.addAll(B);

교집합(intersect)

A.retainAll(B);

차집합(difference)

A.removeAll(B);

 

Map

이번에는 Map 컬렉션에 대해서 알아보자. Map 컬렉션은 key와 value의 쌍으로 값을 저장하는 컬렉션이다. 아래 코드를 보자.

package org.opentutorials.javatutorials.collection;

import java.util.*;

public class MapDemo {

    public static void main(String[] args) {
		HashMap<String, Integer> a = new HashMap<String, Integer>();
		a.put("one", 1);
		a.put("two", 2);
		a.put("three", 3);
		a.put("four", 4);
		System.out.println(a.get("one"));
		System.out.println(a.get("two"));
		System.out.println(a.get("three"));
		
		iteratorUsingForEach(a);
		iteratorUsingIterator(a);
	}
	
	static void iteratorUsingForEach(HashMap map){
		Set<Map.Entry<String, Integer>> entries = map.entrySet();
		for (Map.Entry<String, Integer> entry : entries) {
		    System.out.println(entry.getKey() + " : " + entry.getValue());
		}
	}
	
	static void iteratorUsingIterator(HashMap map){
		Set<Map.Entry<String, Integer>> entries = map.entrySet();
		Iterator<Map.Entry<String, Integer>> i = entries.iterator();
		while(i.hasNext()){
			Map.Entry<String, Integer> entry = i.next();
			System.out.println(entry.getKey()+" : "+entry.getValue());
		}
	}

}

Map에서 데이터를 추가할 때 사용하는 API는 put이다. put의 첫번째 인자는 값의 key이고, 두번째 인자는 key에대한 값이다. key를 이용해서 값을 가져올 수 있다.

System.out.println(a.get("one"));

이것이 Map의 가장 기본적인 사용법이다. 그럼 Map에 저장된 데이터를 열거할 때는 어떻게 해야할까?

Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
    System.out.println(entry.getKey() + " : " + entry.getValue());
}

메소드 entrySet은 Map의 데이터를 담고 있는 Set을 반환한다. 반환한 Set의 값이 사용할 데이터 타입은 Map.Entry이다. Map.Entry는 인터페이스인데 아래와 같은 API를 가지고 있다.

  • getKey
  • getValue

위의 API를 이용해서 Map의 key, value를 조회할 수 있다.

앞서 Set이 수학의 집합을 프로그래밍적으로 구현한 것이라고 언급했다. map은 수학의 함수를 프로그래밍화한 것이다. 수학의 함수가 "정의역과 공역 원소들 사이의 단가 대응의 관계"라는 의미를 이해하고 있는 사람이라면 Map의 key와 value의 관계가 함수의 정의역과 공역의 관계와 같다는 것을 이해할 수 있을 것이다.  함수에 대한 이해가 없다면 이 내용은 몰라도 된다. 하지만 프로그래밍을 하게 되면 수학적인 지식들을 매우 구체적으로 경험할 수 있기 때문에 프로그래밍은 수학에 대한 좋은 실습 도구라고 할 수 있다. 수학이 너무 추상적이라서 배움에 어려움이 있는 독자라면 프로그래밍에 익숙해진 후에 수학공부를 시작해보자. 프로그래밍의 많은 장치들이 수학적인 장치들을 빌려온 것임을 알 수 있을 것이고, 수학이 보다 구체적으로 다가올 것이다.

데이터 타입의 교체

컬렉션을 사용할 때는 데이터 타입은 가급적 해당 컬렉션을 대표하는 인터페이스를 사용하는 것이 좋다. 이전 예제의 12행의 내용은 아래와 같다.

HashMap<String, Integer> a = new HashMap<String, Integer>();

이것을 아래와 같이 수정한다. HashMap은 Map 인터페이스를 구현하기 때문에 변수 a의 데이터 타입으로 Map을 사용할 수 있다.

Map<String, Integer> a = new HashMap<String, Integer>();

어떤 필요에 의해서 컬렉션을 HashMap에서 HashTable로 바꾸고 싶다면 아래와 같이 수정하면 된다.

Map<String, Integer> a = new Hashtable<String, Integer>();

정렬

컬렉션을 사용하는 이유 중의 하나는 정렬과 같은 데이터와 관련된 작업을 하기 위해서다. 정렬하는 법을 알아보자. 패키지 java.util 내에는 Collections라는 클래스가 있다. 이 클래스를 사용하는 법을 알아보자.

package org.opentutorials.javatutorials.collection;

import java.util.*;

class Computer implements Comparable{
    int serial;
	String owner;
	Computer(int serial, String owner){
		this.serial = serial;
		this.owner = owner;
	}
	public int compareTo(Object o) {
		return this.serial - ((Computer)o).serial;
	}
	public String toString(){
		return serial+" "+owner;
	}
}

public class CollectionsDemo {
	
	public static void main(String[] args) {
		List<Computer> computers = new ArrayList<Computer>();
		computers.add(new Computer(500, "egoing"));
		computers.add(new Computer(200, "leezche"));
		computers.add(new Computer(3233, "graphittie"));
		Iterator i = computers.iterator();
		System.out.println("before");
		while(i.hasNext()){
			System.out.println(i.next());
		}
		Collections.sort(computers);
		System.out.println("\nafter");
		i = computers.iterator();
		while(i.hasNext()){
			System.out.println(i.next());
		}
	}

}

결과

before
500 egoing
200 leezche
3233 graphittie

after
200 leezche
500 egoing
3233 graphittie

클래스 Collectors는 다양한 클래스 메소드를 가지고 있다. 메소드 sort는 그 중의 하나로 List의 정렬을 수행한다. 다음은 sort의 시그니처다.

public static <T extends Comparable<? super T>> void sort(List<T> list)

sort의 인자인 list는 데이터 타입이 List이다. 즉 메소드 sort는 List 형식의 컬렉션을 지원한다는 것을 알 수 있다. 인자 list의 제네릭 <T>는 coparable을 extends 하고 있어야 한다. Comparable은 인터페이스인데 이를 구현하고 있는 클래스는 아래 메소드를 가지고 있어야 한다.

아래의 메소드는 이러한 제약 조건을 준수하기 위해서 구현한 메소드다.

public int compareTo(Object o) {
	return this.serial - ((Computer)o).serial;
}

메소드 sort를 실행하면 내부적으로 compareTo를 실행하고 그 결과에 따라서 객체의 선후 관계를 판별하게 된다.

이렇게 해서 컬렉션즈 프레임워크에 대한 수업을 마무리 하겠다. 컬렉션즈 프레임워크는 효율적인 에플리케이션을 구축하기 위해서 매우 중요한 내용이다. 그런데 컬렉션은 단순히 사용법을 이해하는 것으로는 부족하고, 소위 알고리즘이나 자료구조(data structure)라고 불리는 분야에 대한 충분한 이해가 필요하다. 컬렉션즈 프레임워크는 이러한 분야의 성취를 누구나 쉽게 사용할 수 있도록 제공되는 일종의 라이브러리라고 할 수 있기 때문이다.

참조

댓글

댓글 본문
  1. coster97
    good
  2. wwwqiqi
    완료
  3. Alan Turing
    22/10/12
  4. PassionOfStudy
    복습 8일차!
  5. PassionOfStudy
    Collections Framework!
  6. aesop0207
    22.03.11. Fri.
    Comparable은 인터페이스인데 이를 구현하고 있는 클래스는 아래 메소드를 가지고 있어야 한다.
    -> 이 부분을 어떻게 강제(?) 하는지 고민해보기
  7. 모찌말랑카우
    22.03.02 완료
  8. 드림보이
    2021.12.31. Collections Framework 파트 수강완료
  9. syh712
    2021-12-15
    Collections Framework
    1. 자바에서는 다양한 상황에서 사용할 수 있는 다양한 컨테이너를 제공하는데 이것을 컬렉션즈 프래임워크라고 부른다.

    2.List와 Set의 차이점은 List는 중복을 허용하고, Set은 허용하지 않는다.

    3. Set(집합)
    부분집합: A.containsAll(B);
    합집합: A.addAll(B);
    교집합: A.retainAll(B);
    차집합: A.removeAll(B);

    4. Map(함수)
    Map 컬렉션은 key와 value의 쌍으로 값을 저장하는 컬렉션
    getKey
    getValue

    5. 컬렉션을 사용하는 이유 중의 하나는 정렬과 같은 데이터와 관련된 작업을 하기 위해서다.
  10. H4PPY
    1128
  11. IaaS
    2021-11-04 수강완료 고생했다!
  12. 윤슬
    210930 다음에 다시 복습할 것!
  13. 오션멍키
    감사합니다 선생님!
  14. 자바가 무섭지 않은 예솔
    늘 감사합니다!
  15. 김요한
    2020.8.14
    자바 기초 강의를 드디어 끝냈습니다.
    휴일 제외한 실질 공부 시간은 한달 정도이고
    휴일 포함하면 36일 정도네요.

    오랜시간이 걸렸지만 마지막까지 포기하지 않고 이해될 때까지 매달렸다는 점에서
    작은 성취감을 느끼고 있습니다.

    선생님 매우 감사드립니다!
  16. AHJSA
    7,8번쨰 영상에서
    package org.opentutorials.javatutorials.collection;
    import java.util.Collection;
    public class MapDemo {
    public static void main(String[] args) {
    HashMap<String, Integer> a= new HashMap<String, Integer>();
    a.put("one", 1);
    a.put("two", 2);
    a.put("three", 3);
    a.put("four", 4);
    System.out.println(a.get("one"));
    System.out.println(a.get("two"));
    System.out.println(a.get("three"));
    iteratorUsingForEach(a);
    iteratorUsingIterator(a);
    }
    }
    collection과 Map은 각각의 최상위 카테고리인데 HashMap을 쓸때 import를 Collection으로 해도 되는 건가요?
  17. 초보
    솔직히 이런건 보는것보단 직접 찾아서 터득해야 하는 건대..

    아 영어 영어 ㅠㅠㅠㅠ내일부터 영어공부시작합니다.
  18. 초보
    어떠한 개념인지 이해는 가지만 알쏭달쏭한 부분이 많네요ㅋㅋㅋㅋㅋ

    듣고나서 그대로 코딩해보면 이해는 가지만

    만약 나 혼자라면 이코딩을 할 수 있을까? 라는 질문에대한 답은 언제나 NO!입니다.

    이해를 하고 그것을 활용할 줄 아는 방법을 터득하는 길은 멀고도 험한 것 같습니다..

    더군다나 언어의 장벽이 가로막고 있는 것 같아서.. 더욱더 힘들지만 한번 오를만한 산이라는걸 느낍니다.

    이고잉님도 처음에 배울땐 이러한 느낌이였겠지요...

    다만 아쉬운 점이 있다면 이러한 코딩을 좀어 어린나이에 접했더라면?

    단순히 프로그래밍분야는 수학잘해야하고 머리좋아야하고 똑똑한 아이들만 할 수있다는 생각을 좀더

    일찍 깰 수 있었더라면? 지금에 나는 어떘을까?? 이러한 생각이 공부하는 내내 들고있네요 ㅋㅋㅋㅋ

    언제나 양질의 수업을 위해 고심하는 이고잉님 화이팅입니다.!
  19. 잘봤습니다!
    오타가 있는거 같아요~

    클래스 Collectors는 -> 클래스 Collections는
  20. silver94
    잘들엇습니다 수고 많으셧습니다
  21. 허공
    감사합니다!
  22. 홍주호
    20191005 완료
  23. doevery
    수강완료
  24. 세모네모
    iteratorUsingForEach(HashMap map) ---> iteratorUsingForEach(HashMap<String, Integer> map)
    대화보기
    • 어디로가야하오
      들을수록 이고잉님의 지식보다 지혜에 더 감탄하게되는 강의들이었습니다. 정말 감사합니다.
    • 집중파와
      감사합니다만 강사님의 중간중간 말 꼬이는 부분이랑 머뭇하시는 부분 때문에 집중과 이해가 안 되는건 사실입니다. 감안하고 잘 듣는 중입니다
    • p.navillera
      map과 함수 그림을 딱 보는 순간 이고잉님이 느끼셨다는 그 희열이 뭔지 깨닫게되었네요. 항상 함수를 잘해야 프로그래밍을 잘한다는 얘기를 들어왔는데 잊고 살다 지금 이순간 그 자물쇠에 맞는 열쇠가 꽂혀 들어가는 기분이네요
    • 라또마니
      한번 쭉~ 보고 드디어 두번 강의를 봤습니다.
      두번 보면서 생활코딩! 자바 프로그래밍 입문 책을 샀는데, 자바를 배우면서 순간순간 헷갈리는 부분이
      나오면 책에서 빨리 찾아 습득할 수 있을 거 같습니다.
      수고하셨고~ 감사합니다.
      혹 스프링 프레임워크 강의를 계획중이셨으면 좋겠습니다.
    • Daydream
      공부완료 좋은강의 감사합니다.
      20181028
    • include_hoany
      전체 코드를 봐야겠지만.
      메인 메소드에서 선언된 해쉬맵 제네릭 타입과 매치가 안되서? 문제가 발생한것 같습니다.
      해쉬맵에 선언된 제네릭의 순서등등을 한번 확인해 보세요!!

      추측입니다.
      대화보기
      • 조서호
        다시보자
      • 이태호
        7/17
      • seokhee
        자바의정석 보기전에 앞서 워밍업으로 듣고 있는데 굉장히 직관적이고 이해가 쉽네요.
        널린게 자바강의인데 그 중 단연 돋보입니다. 감사합니다.
      • 뀨뀨까까보이
        정말 감사합니다...
        하아 이런 강의가 공짜라니...
        돈없는 평범한 대학생은 행복합니다ㅠㅠ
      • 김진홍
        감사합니다.
      • saint
        질문이 있습니다.
        8/9강의중
        Set<Map.Entry<String, Integer>> entries = map.entrySet(); 이부분이 에러가 나던데;;
        어떤 이유에서 인가요??

        에러메세지
        1
        Exception in thread "main" 2
        3
        java.lang.Error: Unresolved compilation problems:
        Map.Entry cannot be resolved to a type
        Map.Entry cannot be resolved to a type

        at collection.Map.iteratorUsingForEach(Map.java:22)
        at collection.Map.main(Map.java:17)
      • vincyper
        3개월에 걸친 학습을 끝냈습니다. 감사합니다.
      • 하면된다하자
        8강 종반과 9강 전체는 거의 이해 못했네요..
        우선 넘기고 다음에 와서 다시 봐야겠어요 ㅎㅎ;

        일단, 종강!!
      • popinbompin
        감사합니다~!
      • 하하하
        종강!!
      • 촌띄기
        제네릭부터 급격히 막히네요 ㅎㅎ...
      • GoldPenguin
        감사합니다.
      • 흑두루미
        밑에 분 댓글에 공감합니다 ~!
      • 신입사원
        책보다 몇 배는 더 낫네요!!
      • 지방대생
        지금 흔히 말하는 세계 4대 컴공 대학들 (버클리, MIT, 스텐포드, 카네기멜론)중 한곳에서 전공수업을 듣고 있습니다. 제가 제가 지금 듣고 있는 수업보다도 내용, 구성, 전달력 모두 더욱 정말 훌룡한 강의인 것 같습니다! 강사님 덕분에 정말 좋은 지식 많이 얻어 갑니다. 감사합니다.
      • 생코
        뭐 이런 무료 강의가 있습니까. 정말 감사합니다.!! 도움 엄청 많이 됐습니다.
      • 생활코딩짱짱맨
        재밌어요
      • 서해규
        너무 너무 감사합니다...ㅜㅜ 여기서 많은정보들을 얻고 갔습니다!
      • J_Project
        감사합니다!
      • 이수인
        와 드디어 종강!!!! ㅠ^ㅠ 진짜 너무 잘들었습니다.
        이런 질 높은 강의를 무료로 제공받을 수 있었다니 정말 영광이었습니다.
        정말 감사합니다 :) !!