컬렉션 이란?

동일한 타입을 묶어 관리하는 자료구조를 말한다

우표 수집첩에는 우표만 있듯이 한 컬렉션에는 동일한 타입의 데이터만 모아 둘 수 있다

 

그렇다면 배열도 동일한 타입을 묶어 관리하는데 컬렉션이라 부르지않는다 왜 그런가?

 

컬렉션이 배열과 구분되는 가장 큰 특징은 데이터의 저장 용량을 동적으로 관리할 수 있다는 것이다

 

배열은 생성 시점에 저장 공간의 크기를 확정해야하고 나중에 변경이 불가능하다

컬렉션은 저장 공간의 크기를 데이터의 개수에 따라 얼마든지 동적으로 변화 할 수 있다

 

그래서 컬렉션은 메모리 공간이 허용하는 한 저장 데이터의 개수에 제약이 없다

 

컬렉션 프레임워크란?

라이브러리

일반적으로 단순히 연관된 클래스와 인터페이스들의 묶음

 

프레임 워크

클래스 또는 인터페이스를 생성하는 과정에서 설계의 원칙

또는 구조에 따라 클래스 또는 인터페이스를 설계하고

이렇게 설계된 클래스와 인터페이스를 묶어놓은 개념

 

컬렉션 프레임워크

이러한 컬렉션과 프레임워크가 조합된 개념으로

리스트, 스택, 큐, 트리 등의 자료 구조에

정렬, 탐색 등의 알고리즘을 구조화해 놓은 프레임워크

 

쉽게말해 여러개의 데이터 묶음 자료를 효과적으로 처리하기 위해

구조화된 클래스 또는 인터페이스의 모음이라 생각하면 된다

 

컬렉션 프레임워크의 주요 클래스와 인터페이스는 아래와 같다

List<E>, Set<E>, Map<K, V>, Stack<E>, Queue<E>

 

 

List<E> 컬렉션 인터페이스

배열과 가장 비슷한 구조를 지니고 있는 자료구조다

 

배열과 가장 큰 차이점은 저장 공간의 크기가 고정적이냐 동적으로 변화하느냐다

 

List<E>는인터페이스이기 때문에 객체를 스스로 생성할 수 없다

따라서 객체를 생성하기 위해 List<E>를 상속받아 자식클래스를 생성하고

생성한 자식클래스를 이용해 객체를 생성해야 한다

 

하지만 컬렉션 프레임워크를 이용할 때는 직접 인터페이스를 구현하지 않아도 된다

컬렉션 프레임 워크 안에 이미 각각의 특성 및 목적에 따른 클래스가 구현돼 있기 때문이다

ArrayList<E>, Vector<E>, LinkedList<E>

 

객체를 생성할때는 일반적으로 기본 생성자를 사용하지만

초기 저장용량을 매개변수로 포함하고 있는 생성자를 사용할 수도 있다

 

여기서 저장용량은 실제 데이터의 개수를 나타내는 저장 공간의 크기와는 다르다

데이터를 저장하기 위해 미리 할당해 놓은 메모리의 크기라고 생각하면 된다

기본으로 10만큼의 저장용량을 내부에 확보하고 늘어나면 JVM이 자동으로 늘려준다

// 기본 저장용량 10
List<Integer> aList1 = new ArrayList<Integer>();

// 저장용량 30으로 수동으로 지정
List<Integer> aList2 = new ArrayList<Integer>(30);

 

Arrays.asList()

Arrays 클래스의 asList() 정적 메서드를 사용해서 객체를 생성하면

컬렉션 프레임워크지만 저장용량을 고정해서 사용할 수 있다

저장 용량이 고정되어 있다보니 add나 move는 오류가 나는걸 볼 수 있고

수정은 저장 용량에 영향을 주지 않으니 잘 작동하는것을 볼 수 있다 

List<Integer> aList1 = Arrays.asList(1, 2, 3, 4);

aList1.set(1, 7);	// [ 1 7 3 4]
aList1.add(5);		// 오류
aList1.remove(0);	// 오류

 

ArrayList<E> 구현 클래스

- List<E> 인터페이스를 구현한 구현클래스

- 배열처럼 수집한 원소를 인덱스로 관리하며 저장용량을 동적관리

 

add() : 데이터 추가하기

List<Integer> aList1 = new ArrayList<Integer<>();

aList1.add(3)		// 3이라는 값을 추가

aList1.add(1, 2)	// 1번 인덱스에 값 2를 추가

aList1.addAll(aList2)	// aList2 라는 객체를 aList에 추가

 

set() : 데이터 변경하기

List<Integer> aList1 = new ArrayList<Integer<>();

aList1.set(1, 3)	// 1번 인덱스를 값 3으로 수정한다

 

remove(), clear() : 데이터 삭제하기

List<Integer> aList1 = new ArrayList<Integer<>();

aList1.remove(1)		// 1번 인덱스의 값을 삭제

aList1.remove(new Integer(2));	// 정수값 2를 삭제

aList1.clear();			// 값의 개수에 관계없이 모든 값을 한 번에 삭제

 

isEmpty(), size(), get(int index) : 데이터 정보 추출하기

List<Integer> aList1 = new ArrayList<Integer<>();

aList1.isEmpty();	// 값이 있는지 확인 비어있으면 true 반환

aList1.size();		// 저장 데이터의 개수

aList1.get(1);		// 1번 인덱스의 값 가져오기

 

toArray(), toArray(T[ ] t) : 배열로 변환하기

List<Integer> aList1 = new ArrayList<Integer<>();

Object[ ] object = aList1.toArray();

Integer[ ] integer1 = aLists1.toArray(new Integer[0]);	// aList1보다 작게 하면 aList1 크기로 나옴

Integer[ ] integer2 = aLists1.toArray(new Integer[5]);	// aList1보다 크게 하면 늘어난 수에 기본값 0 추가해서 나옴

 

Vector<E> 구현 클래스

List<E>의 공통적인 특성을 모두 갖고 있고 ArrayList<E>와 메서드 기능 및 사용법 또한 완벽히 동일하다

다른 차이점은 Vector<E>의 주요 메서드는 동기화 메서드로 구현돼 있으므로

멀티 쓰레드에 적합하도록 설계되어 있다는 것이다

public synchronized E remove(int index) { }

public synchronized E get(int index) { }

 

동기화 메서드는 하나의 공유 객체를 2개의 쓰레드가 동시에 사용할 수 없도록 만든 메서드이다

 

만약 List<E> 객체가 있을때 하나의 쓰레드는 데이터를 읽고 하나는 삭제하는 작업을 동시에 하면

충돌하는 상황이 발생할 수 있다

 

멀티 쓰레드에서 사용할 수 있도록 기능이 추가된거고 싱글쓰레드에서도 사용할 수는 있다

하지만 굳이 무겁고 많은 리소스를 차지하는 Vector 보다는 ArrayList를 쓴다

 

LinkedList<E> 구현 클래스

이 역시 List<E>의 모든 공통적인 특징을 모두 지니고 있고 동기화 하지 않아서

ArrayList<E>처럼 싱글 쓰레드에 사용하기 적합하다

 

차이점을 보자면

첫번째로 저장 용량을 매개변수로 갖는 생성자가 없기에 객체를 생성할 때 저장용량을 지정할 수 없다

두번째로 내부적으로 데이터를 저장하는 방식이 서로 다르다는 것이다

ArrayList는 위치정보(인덱스)와 값으로 저장하는데

LinkedList는 앞뒤 객체의 정보를 저장한다

즉 모든 데이터가 서로 연결된 형태로 관리되는 것이다

 

이외에는 다 같기에 ArrayList처럼 똑같이 사용하면된다

 

ArrayList<E> 와 LinkedList<E>의 성능 비교

구분 ArrayList<E> LinkedList<E>
추가, 삭제 (add, remove) 속도 느림 속도 빠름
검색 (get) 속도 빠름 속도 느림

 

ArrayList는 인덱스로 값을 저장하고 삭제하니까 만약 1000개의 데이터가 있을때

0번째 인덱스에 추가를 한다고 가정하면 0번 뒤에있는 모든 데이터들이 뒤로 밀린다

그러면 1000개의 데이터를 수정해야 한다는 의미이다 이건 추가나 삭제나 마찬가지이다

LinkedList는 앞뒤 객체의 정보만 바꿔주면 되니까 추가나 삭제에서는 속도가 더 빠른것이다

반대로 검색은 ArrayList는 해당 인덱스만 찾으면 되니까 빠른데

LinkedList는 인덱스가 없어서 처음부터 끝까지 다 뒤져서 찾아야하니 검색에서는 속도가 느린것이다

 

시간 확인해보는 방법

long starTime = System.nanoTime();
// 시간 측정 대상 모듈

long endTime = System.nanoTime();
// 측정시간(ns) = endTime - startTime;

 

 

Set<E> 컬렉션 인터페이스

동일한 타입의 묶음이라는 특징은 그대로 가지고 있지만

인덱스 정보를 포함하고 있지 않은 즉 집합의 개념과 같은 컬렉션이다

 

인덱스 정보가 없으므로 데이터를 중복해 저장하면 중복된 데이터 중

특정 데이터를 지칭해 꺼낼 방법이 없다

 

즉 데이터를 구분할 수 있는 유일한 방법이 데이터 그 자체인 것이다

따라서 동일한 데이터의 중복 저장을 허용하지 않는다

 

List는 인덱스가 있어서 "사과" 라는 데이터가 두개가 있어도

인덱스 1번의 사과 인덱스 3번의 사과 이렇게 구분이 되지만

Set은 인덱스가 없어서 "사과"라는 데이터가 두개가 있다면

어떤 데이터를 가리키는지 모른다

 

set<E>의 주요 메서드

List에서 알아보았던 메서드중에 인덱스가 포함된 메서드가 다 사라지고 동일하다고 보면된다

그래서 List에도 있는 메서드인 contains()와 iterator()를 추가로 알아보는것으로 하겠다

 

contains(Object o) : 매개변수로 넘어온 데이터가 객체 내에 포함되어 있는지 확인하고 boolean값 리턴

Set<Integer> aSet1 = new HashSet<Integer<>();

aSet1.contains(3);	// 3이 aSet1안에 존재하는지 확인하고 ture, false로 리턴

 

iterator() : Set<E> 객체에서 데이터를 1개씩 꺼내는 기능을 한다 Iterator<E> 객체를 리턴

Set<Integer> aSet1 = new HashSet<Integer<>();

aSet1.iterator();	//aSet1에 있는 데이터를 1개씩 다 뽑아낸다

 

HashSet<E> 구현 클래스

- 저장 데이터를 꺼낼 때는 입력 순서와 다를 수 있다

- 저장 용량을 동적 관리하며 기본 생성자로 생성 할 때 기본값은 16이다

- 쉽게보면 모든 데이터를 하나의 주머니에 넣어 관리하는것과 동일하다 보면 된다

 

add() : 데이터 추가하기

- 값을 넣을때 데이터가 중복되면 들어가지 않는다

예를들어 "가" 를 두번 "나"를 한번 add() 하면 결과는 [가, 나]

 

- 넣을때 순서와 나올때 순서가 다를 수 있다

예를들어 "가", "나", "다" 순으로 넣었는데 꺼내보면 "다", "가", "나" 이렇게 될 수 도 있다

 

remove(), clear() : 데이터 삭제하기

- remove()는 인덱스가 아닌 실제 삭제할 값을 적어준다

remove("가");

 

- clear()는 ArrayList와 동일하게 다 삭제된다

 

isEmpty(), contains(), size(), iterator()

- isEmpty(), contains(), size()는 ArrayList와 똑같다

- iterator() 대신 for-each문을 사용해서도 꺼낼 수 있다

- iterator()는 아래 예시로 사용법을 설명하겠다

Set<String> hSet1 = new HashSet<String>();

Iterator<String> iterator = hSet1.iterator();
while(iterator.hasNext()) {
    System.out.print(iterator.next() + " ");
}

// hasNext() 는 다음으로 가리킬 원소의 존재 여부를 boolean으로 리턴
// next() 는 다음 원소 위치로 가서 읽은 값을 리턴

 

주의 할 점은 Iterator 객체가 생성되면 객체가 가리키는 위치는 첫 원소가 아닌

첫 원소 바로 이전의 위칫값이다 그래서 next() 를 써서 첫 원소를 가리켜줘야하는 것이다

 

 

toArray(), toArray(T [ ] t) : 배열로 변환하기

- 이또한 ArrayList와 같지만 출력되는 값의 순서가 다를 수 있다는 차이점이 있다

 

HashSet<E>의 중복 확인 메커니즘

간단하게 보자면 2단계를 거쳐서 중복인지 아닌지 판단한다

step 1 hashcode() 동일한지 확인

step 2 equal() 결과가 true인지 확인

 

2단계중에 하나라도 아니라는 결과가 나오면 다른 객체로 판단해 버린다

그래서 중복을 확인하려면 두 메서드를 오버라이딩 해줘야한다

class A {
    int data;
    
    public A (int data) {
        this.data = data;
    }
    
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof A) {
            if(this.data == ((C) obj).data)
                return true;
        }
        return false;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(data);
    }
}

public class Test {
    public static void main(String[ ] args) {
        Set<A> hashSet1 = new HashSet<>();
        A a1 = new A(3);
        A a2 = new A(3);
        System.out.println(a1 == a2);					// false
        System.out.println(a1.equals(a2));				// true
        System.out.println(a1.hashCode() + " " + a2.hashCode());	// 34 34
    	hashSet1.add(a1);
        hashSet1.add(a2);
        System.out.println(hashSet1.size());				// 1 (같은객체) 중복처리됨
    }
}

 

LinkedHashSet<E> 구현 클래스

HashSet<E>의 자식 클래스로 HashSet<E>의 모든 기능에

데이터 간의 연결 정보만을 추가로 갖고 있는 컬렉션이다

 

즉 입력된 순서를 기억하고 있는 것이다

따라서 출력 순서가 항상 입력 순서와 동일한 특징을 갖는다

그러나 List<E> 처럼 중간에 데이터를 추가하거나 특정 순서에 저장된 값을 가져오는것은 불가능하다

 

TreeSet<E> 구현 클래스

Set<E>의 기능에 크기에 따른 정렬 및 검색 기능이 추가된 컬렉션이다

데이터를 입력 순서와 상관없이 크기 순으로 출력한다

 

TreeSet<E>는 다른 구현 클래스와 달리 Navigable Set<E>와 SortedSet<E>를 부모 인터페이스로 둔다

이 두 인터페이스에서 TreeSet<E>의 가장 주요한 기능인 정렬과 검색 기능이 추가로 정의된 것이다

그래서 TreeSet 생성자로 객체를 생성해도 Set 타입으로 선언하면

추가된 정렬 및 검색 기능을 사용할 수 없다

TreeSet -> Navigableset -> SortedSet -> Set

// Set 메서드만 사용가능
Set<String> treeSet = new TreeSet<String>();

// Set 메서드와 정렬/검색 기능 메서드도 사용 가능
TreeSet<String> treeSet = new TreeSet<String>();

 

TreeSet<E>의 주요 메서드

first()와 last()

각각 첫번째 값과 마지막 값을 리턴한다

 

lower()

입력매개변수보다 작으면서 입력매개변수에 가장 가까운 수

예를들어 lower(25) -> 24

 

higher()

입력매개변수보다 크면서 입력매개변수에 가장 가까운수

예를들어 higher(25) -> 26

 

floor()와 ceiling()

lower()와 higher()와 비슷하지만 값이 같은 것도 포함한다

 

pollFirst()와 pollLast()

각각 처음과 마지막 값을 꺼내 리턴한다

따라서 검색 메서드와 달리 수행한 후 데이터의 개수가 줄어든다

 

매개변수에 boolean 타입이 들어가지 않으면 sortedSet<E>를 리턴

매개변수에 boolean 타입이 들어가 있으면 Navigableset<E>를 리턴

 

headSet()

입력 매개변수보다 작은 원소로 구성된 Set<E>을 리턴

boolean 타입을 이용해서 입력매개변수값을 포함할지 말지 설정 가능

 

tailSet()

입력 매개변수보다 큰 원소로 구성된 Set<E>를 리턴

boolean 타입을 이용해서 입력매개변수값을 포함할지 말지 설정 가능

 

subSet()

전달되는 2개의 원소값을 최소값과 최대값으로 하는 Set<E>을 구성하는 메서드

boolean값으로 시작과 끝 값의 포함 여부를 설정할 수 있다

 

descendingSet()

현재의 정렬 기준을 반대로 변환하는 메서드다

 

 

Map<K, V> 컬렉션 인터페이스

상속 구조상 List<E>, Set<E>와 분리되어 있다

즉 List<E>와 Set<E>가 Collection<E> 인터페이스를 상속받는 반면

Map<K, V>는 별도의 인터페이스로 존재한다

따라서 저장의 형태와 방식이 앞의 두 컬렉션과 다르다

 

- Key와 Value 한 쌍으로 데이터를 저장

- Key는 중복 저장 불가, Value는 중복 가능

 

Map<K, V> 인터페이스의 주요 메서드

put(K key, V value)

(key, value)의 쌍으로 값을 추가한다

 

putAll(Map<? extends K, ? extends V> m)

Map<K, V> 객체를 통째로 추가 할 수 있다

 

replace(K key, V value)

key에 해당하는 값을 value 값으로 변경

해당 key가 없으면 null 리턴

 

replace(K key, V oldValue, V newValue)

key에 해당하는 값에서 oldValue를 newValue로 변경

해당 key가 없으면 false 리턴

 

get(Object key)

매개변수의 key값에 해당하는 oldValue를 리턴

 

containsKey(Object key)

매개변수의 key값이 포함되어 있는지 여부를 boolean 타입으로 리턴

 

containsValue(Object value)

매개변수의 value값이 포함되어 있는지 여부를 boolean 타입으로 리턴

 

keySet()

Map 데이터들 중 Key들만 뽑아 Set 객체로 리턴

 

entrySet()

Map의 각 엔트리들을 Set 객체로 담아 리턴

 

size()

Map에 포함된 엔트리의 개수

 

remove(Object Key)

key를 갖는 엔트리 삭제

단 해당 key가 없으면 아무런 동작을 하지 않음

 

remove(Object key, Object value)

key, value를 갖는 엔트리 삭제

단 해당 key가 없으면 아무런 동작을 하지 않음

 

clear()

Map 객체 내의 모든 데이터 삭제

 

HashMap<K, V>

key값의 중복을 허용하지 않는다

key값의 중복 여부를 확인하는 메커니즘은 HashSet<E> 때와 완벽히 동일하다

 

Hashtable<K, V>

HashMap은 단일 쓰레드에 적합한 반면

Hashtable은 멀티 쓰레드에 안정성을 가진다

즉 Map 객체를 2개의 쓰레드가 동시에 접근 할 때도

모든 내부의 주요 메서드가 동기화 synchronized 메서드로 구현되어 있어

멀티 쓰레드에서도 안전하게 동작한다

 

멀티 쓰레드에도 안전하다는 특징말고는 HashMap과 동일한 특징을 가진다

 

LinkedHashMap<K, V>

HashMap의 기본적인 특성에 입력 데이터의 순서 정보를 추가로 갖고 있는 컬렉션이다

따라서 저장 데이터를 출력하면 항상 입력된 순서대로 출력된다

 

HashSet에서는 Key를 HashSet으로 관리하는 반면

LinkedHashMap은 Key를 LinkedHashSet으로 관리한다

 

출력이 입력 순으로 나오는 것을 제외하면 HashMap과 완벽히 동일하다

 

TreeMap<K, V>

Map의 기본 기능에 정렬 및 검색 기능이 추가된 컬렉션이다

입력 순서와 관계없이 데이터를 key값의 크기 순으로 저장한다

 

TreeSet 때와 마찬가지로 상속구조가 비슷한데

sortedMap<K, V>, NavigableMap<K, V> 인터페이스의 자식 클래스다

 

그래서 TreeMap 생성자로 객체를 생성해도 Map 타입으로 선언하면

추가된 정렬 및 검색 기능을 사용할 수 없다

 

TreeMap<K, V>의 주요 메서드

주요 메서드 종류와 활용법은 다른 구현 클래스와 동일하다

TreeMap에서 추가로 사용할 수 있는 정렬과 검색 관련 메서드에 대해서만 다루겠다

 

firstKey(), lastKey()

Map 원소 중 가장 작은 key값

Map 원소 중 가장 큰 key값 리턴

 

firstEntry(), lastEntry()

Map 원소 중 가장 작은 key값의 key값과 엔트리값을 리턴

Map 원소 중 가장 큰 key값의 key값과 엔트리값을 리턴

 

lowerKey(K key), higherKey(K Key)

입력 키 보다 작으면서 입력 키에 가장 가까운 수

예를들어 lowerKey(25) -> 24

입력 키 보다 크면서 입력 키에 가장 가까운 수

예를들어 lowerKey(25) -> 26

 

lowerEntry(K key), higherEntry(K key)

입력 키 보다 작으면서 입력 키에 가장 가까운 키를 갖는 엔트리

예를들어 lowerKey(25) -> 24=24번째 데이터

입력 키 보다 크면서 입력 키에 가장 가까운 키를 갖는 엔트리

예를들어 lowerKey(25) -> 26=26번째 데이터

 

pollFirstEntry(), pollLastEntry()

가장 작은 key 값의 엔트리와

가장 큰 key 값의 엔트리를 꺼내오는 메서드로

데이터를 꺼내기 때문에 꺼낸만큼 데이터의 개수는 줄어든다

 

나머지 headMap, tailMap, subSet, descendingKeySet, descendingMap

등은 Treeset이랑 같고 메서드 이름으로도 기능을 유추할 수 있어 설명을 생략한다

 

 

Stack<E> 컬렉션 클래스

List, Set, Map, Stack, Queue 컬렉션 중에서 유일하게 클래스이다

즉 자체적으로 객체를 생성할 수 있다

상속 구조를 살펴보면 Vector 클래스의 자식 클래스로 후입선출(LIFO) 자료구조를 구현한 컬렉션이다

 

LIFO (Last In First Out)

말그대로 나중에 입력된 데이터가 먼저 출력되는 것을 말한다

당연히 Vector의 모든 기능을 포함하고 있고

추가로 LIFO 구조를 위한 5개의 메서드가 추가되었다

이들 추가 메서드는 Stack 클래스에서 추가 된거기 때문에

기능을 사용하려면 변수를 Stack 타입으로 선언해야한다

 

Stack<E>의 주요 메서드

push(E item)

데이터를 stack에 추가

 

peek()

가장 위에있는 데이터 읽기 (데이터 변화 없음)

 

search(Object o)

Stack 원소의 위칫값을 리턴

(맨 위의 값이 1, 아래로 내려갈수록 1씩 증가)

(해당 데이터가 없을때 -1 리턴)

 

pop()

최상위 데이터 꺼내기 (데이터의 개수 감수)

 

empty()

Stack 객체가 비어 있는지 여부를 리턴

 

이때 주의해야할 점은 Vector의 자식이다보니 add(), remove() 메서드도 사용이 가능하다

다만 Vector의 메서드를 사용시 LIFO의 특성이 반영되지 않는다

Stack 본연의 특징을 갖기 위해서는 Stack에서 추가한 5가지 메서드를 사용해야 한다

 

 

Queue<E> 컬렉션 인터페이스

List, Set 처럼 Collection에게서 상속된 인터페이스다

LinkedList가 Queue인터페이스의 구현 클래스인 것이다

 

Collection<E> <- Queue<E> <- LinkedList<E>

 

특징은 Stack과 반대인 선입선출(FIFO) 구조를 가진다는 것이다

즉 가장 먼저 저장된 데이터가 먼저 출력된다

 

앞에서 Linked가 붙은 컬렉션은 입력 순서 정보를 저장하기 때문에

입력 순서와 출력 순서가 동일하다고 했었는데

이것이 바로 Queue의 특징이라고 생각하면 된다

 

Queue<E>의 주요 메서드

메서드중 add() 메서드만 java.util.Collection 인터페이스에 정의되어 있고

나머지는 java.util.Queue 인터페이스에 정의되어 있다

FIFO 기능을 부여하는 메서드는 2쌍이 존재한다

차이점은 데이터가 없을때 예외를 발생시키느냐 기본값으로 대체하느냐이다

 

예외 처리 기능 미포함 메서드

add(E item)

데이터를 추가

 

element()

가장 위에 있는 데이터 리턴

(데이터는 변화없음, 데이터없으면 예외발생)

 

remove()

가장 위에 잇는 데이터 꺼내기

(데이터가 없을때 예외발생)

 

예외 처리 기능 포함 메서드

offer(E item)

데이터를 추가

 

peek()

가장 위에 있는 데이터 리턴

(데이터는 변화없음, 데이터없으면 null 리턴)

 

poll()

가장 위에 잇는 데이터 꺼내기

(데이터가 없을때 null 리턴)