본문 바로가기
카테고리 없음

자바 제네릭과 컬렉션 (Generic, Collection, 타입 안전성)

by ricepuppy9733 2026. 6. 16.

솔직히 처음에 List<String>이나 ArrayList<Integer>를 봤을 때, 저는 그냥 "그냥 따라 쓰면 되는 거 아닌가" 하고 넘겼습니다. 꺾쇠 기호가 뭘 의미하는지, 왜 굳이 타입을 저기에 적어야 하는지 제대로 이해하지 못한 채로요. 그러다 실제로 서버 코드를 짜면서 이 두 개념이 얼마나 긴밀하게 연결되어 있는지 뼈저리게 느꼈습니다.

Generic, Collection

Generic — 타입을 나중에 결정하는 방식이 왜 필요한가

처음 Generic을 마주했을 때 저는 "그냥 Object 타입 쓰면 되는 거 아닌가"라고 생각했습니다. 실제로 Generic 없이 컬렉션을 쓰면 아무 타입이나 넣을 수 있거든요. 그런데 이 생각이 틀렸다는 걸 실습 도중 꽤 창피하게 알게 됐습니다.

Generic(제네릭)이란 클래스나 메서드를 선언할 때 타입을 고정하지 않고, 사용하는 시점에 원하는 타입으로 대입해서 동작하게 하는 문법입니다. 쉽게 말해 타입 자리에 T라는 자리표시자를 끼워두고, 실제 사용할 때 "여기에는 Integer를 쓸게", "여기에는 String을 쓸게" 하고 결정하는 방식입니다.

타입 안전성(Type Safety)이라는 개념이 여기서 핵심입니다. 타입 안전성이란 컴파일 단계에서 잘못된 타입의 데이터가 들어오는 걸 미리 막아주는 성질을 의미합니다. Generic 없이 컬렉션에 아무 데이터나 넣으면, 나중에 꺼낼 때 타입 캐스팅 오류가 런타임에서 터집니다. 런타임 오류는 서버가 돌아가는 도중에 터지기 때문에 훨씬 위험합니다.

제가 직접 써봤는데, Generic을 지정하지 않은 리스트에 숫자와 문자열을 섞어 넣었다가 데이터를 꺼내는 과정에서 ClassCastException이 발생했습니다. 그때 느꼈습니다. 컴파일 시점에 잡아주는 것과 런타임에 터지는 것의 차이가 실제로 얼마나 큰지를요.

제가 실습 중 작성했던 BaseResponse<T> 클래스가 좋은 예입니다. API 응답 객체를 만들 때 결과값의 타입이 요청마다 달라집니다. 어떤 요청은 Boolean을 반환하고, 어떤 요청은 상품 객체를 반환합니다. 이걸 타입마다 따로 클래스를 만들면 코드가 어마어마하게 늘어납니다. BaseResponse<T>처럼 Generic으로 선언해두면 BaseResponse.success(result) 한 줄로 어떤 타입이든 감쌀 수 있습니다.

T라는 알파벳을 굳이 T로 쓰지 않아도 되지만, 관례상 타입(Type)의 T, 키(Key)의 K, 값(Value)의 V처럼 의미 있는 알파벳을 씁니다. 중요한 건 치환을 원하는 모든 자리에 동일한 알파벳을 써야 한다는 점입니다. 이걸 몰랐을 때 저는 한 클래스 안에서 T와 E를 섞어 써서 컴파일 오류를 냈던 적도 있습니다.

T라는 자리표시자를 타입 파라미터(Type Parameter)라고 부릅니다. 타입 파라미터란 클래스나 메서드 정의 시 실제 타입 대신 사용하는 기호로, 사용 시점에 구체적인 타입으로 대체됩니다. 자바 공식 문서에서도 제네릭을 올바르게 사용하면 타입 캐스팅 코드 없이 타입 안전한 코드를 작성할 수 있다고 설명하고 있습니다(출처: Oracle Java Documentation).

Collection Framework — 자료구조를 직접 구현하지 않아도 되는 이유

자료구조를 공부하면 배열, 연결 리스트, 스택, 큐, 해시 테이블 같은 개념이 나옵니다. 이걸 매번 직접 구현하는 건 비효율적입니다. 자바는 이 자료구조들을 Collection Framework(컬렉션 프레임워크)라는 이름으로 이미 구현해서 제공합니다. 컬렉션 프레임워크란 여러 종류의 자료구조를 인터페이스와 클래스로 표준화해 묶어놓은 라이브러리 집합입니다.

컬렉션 프레임워크에서 자주 쓰는 구현체를 정리하면 다음과 같습니다.

  • ArrayList: 내부적으로 배열로 구현된 리스트. 인덱스로 빠르게 접근 가능하지만 중간 삽입·삭제가 느림
  • LinkedList: 노드가 서로 연결된 구조. 중간 삽입·삭제가 빠르지만 특정 위치 접근은 느림
  • HashMap: 키와 값을 쌍으로 저장. 키를 해시 함수로 변환해 빠른 검색 지원
  • HashSet: 중복을 허용하지 않는 집합. 내부적으로 HashMap을 기반으로 동작
  • TreeSet: 정렬된 순서로 데이터를 유지하는 집합

리스트를 쓸 때 저는 처음에 ArrayList만 써야 한다고 고집했습니다. 그런데 사실 List<Integer> list = new ArrayList<>()처럼 인터페이스 타입으로 선언해두면, 나중에 ArrayList를 Vector나 Stack으로 바꾸고 싶을 때 선언 한 줄만 고치면 됩니다. 이게 다형성(Polymorphism)이 가져다주는 실질적인 이점입니다. 다형성이란 하나의 인터페이스로 다양한 구현체를 교체해가며 사용할 수 있는 객체지향의 특성입니다.

제가 직접 써봤는데, 처음에는 이 차이를 그냥 "선언 방식 차이"로만 봤습니다. 그런데 레포지토리 클래스를 짤 때 인터페이스 없이 BoardRepositoryImplJdbc를 직접 가져다 쓰다가 DB를 교체해야 하는 상황이 생겼고, 그때 연결된 모든 코드를 하나씩 고쳐야 했습니다. 솔직히 이건 예상 밖이었습니다. 인터페이스를 사이에 두었다면 구현 클래스만 바꿨을 텐데, 직접 의존하게 짜놓으니 변경 비용이 몇 배로 늘어났습니다.

HashMap의 경우 키(Key)가 중복되면 기존 값을 덮어씁니다. 이 동작 방식을 처음에 놓쳐서 상품 정보를 저장할 때 동일한 키로 put을 두 번 해서 데이터가 사라진 줄 알고 꽤 당황했던 기억이 납니다. HashSet은 중복을 차단하는 특성상 Set 내부에서는 equals()와 hashCode() 메서드가 기준이 됩니다. 이 두 메서드를 오버라이딩하지 않으면 객체가 내용이 같아도 다른 것으로 판단합니다. 이는 자바 공식 API 명세에서도 명시하고 있는 동작 규칙입니다(출처: Oracle Java SE API).

타입을 지정하지 않은 컬렉션은 아무 타입이나 섞어 넣을 수 있어서 처음에는 편해 보입니다. 그런데 꺼낼 때 매번 타입 캐스팅을 해야 하고, 잘못 넣은 데이터는 런타임에서야 오류가 납니다. Generic과 컬렉션을 함께 쓰는 이유가 바로 여기에 있습니다.

자바에서 제네릭과 컬렉션 프레임워크를 함께 익히는 건 선택이 아니라 필수라고 느낍니다. 타입 파라미터, 타입 안전성, 다형성, 인터페이스 기반 설계까지 이 두 개념을 제대로 이해해야 이후에 스프링이나 실무 코드를 읽을 때 막히는 부분이 확연히 줄어듭니다. 저처럼 "그냥 따라 쓰면 되겠지" 하고 넘어가면 레포지토리나 서비스 계층 코드를 짤 때 같은 실수를 반복하게 됩니다. 개념을 잡고 나서 코드를 다시 보면, 왜 그렇게 짰는지가 보이기 시작합니다. 그 순간이 꽤 뿌듯합니다.


참고: https://dltldnr2563.tistory.com/entry/%EC%BD%94%EB%94%A9%EA%B3%B5%EB%B6%8020250724-stack-%EC%A4%91%EC%9C%84%EC%8B%9D%EC%9D%84-%ED%9B%84%EC%9C%84%EC%8B%9D%EC%9C%BC%EB%A1%9C-%EB%B3%80%EA%B2%BD-%ED%9B%84-%EA%B3%84%EC%82%B0-%EA%B7%B8%EB%9E%98%ED%94%84%ED%83%90%EC%83%89DFS-%EC%97%AC%ED%96%89%EA%B2%BD%EB%A1%9C-%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98-%ED%95%98%EB%85%B8%EC%9D%B4


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 자동식단생성 연관 블로그