심화 1 AOP📚 전체 맵심화 3 예외처리
05b · 3차 심화과정 · STEP 2 / 5

Java 컬렉션

List · Map · Set · Iterator · 정렬 · Collections 유틸 — 우리 프로젝트에서 이미 쓰고 있었지만 이론 정리가 안 됐던 것들을 한 번에 정복

이 파일의 학습 목적
STEP 2 — 컬렉션은 왜 지금 정리하나?

사실 컬렉션은 1차~3차 프로젝트 전체에서 이미 사용하고 있었다. List<BoardDto>, Map.of("token", token), Page<Board> — 이것들이 전부 컬렉션이다. 의식하지 못하고 쓴 것들을 이번에 이론부터 체계적으로 정리한다. STEP 2는 새 코드를 추가하는 단계가 아니라, 이미 쓴 코드를 이해하는 단계다.

심화 5단계 중 지금 여기 — STEP 2
STEP 1 ✅
AOP
로깅 자동화
📌 지금 여기
STEP 2
컬렉션 정리
STEP 3
예외처리
전역 핸들러
STEP 4
@Transactional
readOnly 최적화
STEP 5
@Builder
DTO 생성 개선
이미 쓰고 있었던 컬렉션들
  • · List<BoardDto> — 게시글 목록 (BoardService)
  • · List<GrantedAuthority> — 권한 목록 (CustomUserDetailsService)
  • · Map.of("token", token) — API 응답 (UserController)
  • · Page<Board> — 페이징 목록 (BoardService)
이 파일에서 배울 것
  • · List / Map / Set 비교 — 언제 무엇을 쓰나
  • · Iterator — for-each 중 삭제할 때 왜 필요한가
  • · Comparable vs Comparator — 정렬 기준 지정법
  • · Collections 유틸 · Stream API · Page<T> 상세
Overview
컬렉션 한눈에 비교

우리 프로젝트에서 이미 List, Map, Page를 쓰고 있었다. 이론 정리 없이 그냥 쓴 것들. 이번 STEP에서 기초부터 제대로 정리한다.

컬렉션특징언제 쓰나우리 프로젝트 사용 예
List<T> 순서 O, 중복 O 게시글 목록처럼 순서 있는 데이터 List<BoardDto>, List<GrantedAuthority>
Map<K,V> 키-값 쌍, 키는 중복 불가 API 응답 메시지, 설정값, 토큰 반환 Map.of("message","성공","token",token)
Set<T> 순서 X, 중복 X 중복 제거가 필요한 데이터 태그 기능 등 (현재 미사용)
Page<T> JPA 페이징 — Spring Data 제공 게시판 목록 페이징 처리 Page<Board> boards, Page<BoardDto>
Iterator<T> 순회 중 삭제 안전하게 가능 for-each 도중 remove() 해야 할 때 현재 미사용 (ConcurrentModificationException 방지용)
📌 Java 컬렉션 계층 구조

Collection 인터페이스 ← List, Set, Queue 가 구현
Map 인터페이스 ← HashMap, LinkedHashMap, TreeMap 등이 구현 (Collection과 별도 계층)

실무에서 변수 선언은 인터페이스 타입으로, 생성은 구현체로: List<String> list = new ArrayList<>(); → 나중에 구현체를 LinkedList로 바꿔도 사용 코드 변경 불필요

Details
컬렉션별 상세 — 메서드 + 우리 프로젝트 연결
List
순서 있는 목록
add/get/remove/size/contains 기본 메서드. Stream과 함께 쓰면 강력. ArrayList가 가장 많이 쓰임.
List 기본 사용법
List<String> list = new ArrayList<>();
list.add("홍길동");
list.add("라희");
list.get(0);               // "홍길동"
list.size();               // 2
list.contains("라희");  // true
list.remove("홍길동");   // 삭제
list.isEmpty();           // false

// 불변 리스트 (추가/삭제 불가)
List<String> immutable = List.of("a", "b");

// 우리 프로젝트 사용 예
List<GrantedAuthority> authorities =
    List.of(new SimpleGrantedAuthority(role));

List<BoardDto> dtoList = boardPage.getContent()
    .stream().map(this::toDto)
    .collect(Collectors.toList());
Map
키-값 쌍
put/get/containsKey/remove/keySet/values. API 응답 JSON에 가장 많이 사용. HashMap이 기본, 순서 유지는 LinkedHashMap.
Map 기본 사용법
Map<String, String> map = new HashMap<>();
map.put("token", token);
map.put("role", "ROLE_USER");
map.get("token");           // token 값
map.containsKey("role");   // true
map.remove("role");        // 삭제
map.keySet();               // 키 목록 Set
map.values();               // 값 목록 Collection

// 불변 Map (수정 불가)
Map.of("key1", "val1", "key2", "val2");

// 우리 프로젝트 사용 예
return Map.of("token", token, "role", role);
return ResponseEntity.ok(
    Map.of("message", "글쓰기 성공"));
Set
중복 없는 집합
add/contains/remove. 중복 자동 제거. 순서 보장 없음. 순서 유지는 LinkedHashSet, 정렬은 TreeSet.
Set 기본 사용법
Set<String> set = new HashSet<>();
set.add("Java");
set.add("Java"); // 중복! 무시됨
set.add("Spring");
set.size();      // 2
set.contains("Java"); // true

// List → Set으로 중복 제거
List<String> withDup = List.of("a", "b", "a");
Set<String> noDup = new HashSet<>(withDup);
// noDup = {a, b}

// Set → List로 다시 변환
List<String> result = new ArrayList<>(noDup);
정렬
Comparable vs Comparator
Comparable: 클래스 자체에 기본 정렬 기준 내장. Comparator: 외부에서 정렬 기준 지정 — 여러 조건 조합 가능. 실무에서 Comparator 많이 사용.
정렬 방법들
// 1. Comparable — 클래스에 내장
public class Board implements Comparable<Board> {
    public int compareTo(Board other) {
        return this.createdAt.compareTo(other.createdAt);
    }
}

// 2. Comparator — 외부에서 기준 지정
boards.sort(Comparator
    .comparing(Board::getCreatedAt)
    .reversed());   // 최신순

// 다중 조건 정렬
boards.sort(Comparator
    .comparing(Board::getViewCount).reversed()
    .thenComparing(Board::getCreatedAt).reversed());

// 우리 프로젝트 — JPA에서
Sort.by(Sort.Direction.DESC, "createdAt");
PageRequest.of(page, 10, Sort.by("createdAt")
    .descending());
Collections
유틸 클래스
정렬/최대최소/뒤집기/빈도수 등 컬렉션 관련 정적 메서드 모음. java.util.Collections — 인터페이스 Collection과 다름!
Collections 유틸 메서드
List<Integer> nums = new ArrayList<>(
    List.of(3, 1, 4, 1, 5));

Collections.sort(nums);       // [1,1,3,4,5] 오름차순
Collections.reverse(nums);    // [5,4,3,1,1] 뒤집기
Collections.shuffle(nums);    // 랜덤 섞기
Collections.max(nums);        // 5
Collections.min(nums);        // 1
Collections.frequency(nums, 1); // 1의 개수: 2

// 불변 컬렉션으로 감싸기 (수정 시 예외)
List<String> locked =
    Collections.unmodifiableList(nums);

// 빈 컬렉션
Collections.emptyList();   // 빈 List
Collections.emptyMap();    // 빈 Map
Collections.emptySet();    // 빈 Set
Iterator
안전한 순회/삭제
순회 중 삭제가 필요할 때 사용. for-each 도중 remove()를 직접 호출하면 ConcurrentModificationException 발생!
Iterator 안전한 삭제
// ❌ 잘못된 방식 — for-each 중 remove() 예외 발생!
for (String s : list) {
    if (s.equals("삭제대상"))
        list.remove(s); // ConcurrentModificationException!
}

// ✅ 올바른 방식 1 — Iterator 사용
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next();
    if (s.equals("삭제대상"))
        it.remove(); // 안전! Iterator의 remove()
}

// ✅ 올바른 방식 2 — removeIf (Java 8+, 더 간결)
list.removeIf(s -> s.equals("삭제대상"));
Stream
Stream API — 우리 프로젝트에서 쓴 것들

Stream은 컬렉션을 다루는 파이프라인. .stream() → 중간 연산들 → 최종 연산 순서로 연결한다. 우리 프로젝트의 toDto(), getAllUsers() 등에서 이미 사용하고 있다.

종류메서드역할우리 프로젝트 사용 예
중간 연산.map()각 요소를 변환 — Board → BoardDto.map(this::toDto), .map(user -> UserDto.builder()...build())
중간 연산.filter()조건에 맞는 요소만 통과특정 역할 유저만 필터링 등 (현재 미사용)
중간 연산.sorted()정렬 — Comparator 지정 가능.sorted(Comparator.comparing(...))
중간 연산.distinct()중복 제거
중간 연산.limit(n)앞에서 n개만
최종 연산.collect()List, Set, Map 등으로 수집.collect(Collectors.toList())
최종 연산.forEach()각 요소에 동작 수행 (반환 없음)
최종 연산.count()요소 개수
최종 연산.findFirst()첫 번째 요소 Optional로 반환
최종 연산.anyMatch()조건에 맞는 요소가 하나라도 있으면 true
Stream 사용 패턴 — 우리 프로젝트 기준
// 패턴 1: 엔티티 리스트 → DTO 리스트 변환 (가장 많이 씀)
List<BoardDto> dtoList = boardPage.getContent()
    .stream()
    .map(this::toDto)          // 각 Board → BoardDto
    .collect(Collectors.toList());

// 패턴 2: Builder와 함께 (AdminService)
List<UserDto> result = userRepository.findAll()
    .stream()
    .map(user -> UserDto.builder()
        .id(user.getId())
        .username(user.getUsername())
        .build())
    .collect(Collectors.toList());

// 패턴 3: filter + map 조합
List<UserDto> admins = users.stream()
    .filter(u -> u.getRole().equals("ROLE_ADMIN"))
    .map(this::toDto)
    .collect(Collectors.toList());
💡 메서드 레퍼런스 — this::toDto

.map(board -> this.toDto(board)).map(this::toDto) 는 완전히 같다.
this::toDto는 "this 객체의 toDto 메서드를 사용해라"는 뜻의 메서드 레퍼런스 표현식.
Board::getCreatedAt도 마찬가지 — b -> b.getCreatedAt()과 동일.

Page
Page<T> — 우리 프로젝트 페이징 상세
Page 객체가 담고 있는 것들
Page<Board> boardPage = boardRepository
    .findAllByOrderByCreatedAtDesc(pageable);

boardPage.getContent();         // 현재 페이지의 Board 리스트
boardPage.getTotalElements();   // 전체 게시글 수
boardPage.getTotalPages();      // 전체 페이지 수
boardPage.getNumber();          // 현재 페이지 번호 (0부터 시작)
boardPage.getSize();            // 한 페이지 크기
boardPage.isFirst();            // 첫 페이지 여부
boardPage.isLast();             // 마지막 페이지 여부
boardPage.hasNext();            // 다음 페이지 있는지
boardPage.hasPrevious();        // 이전 페이지 있는지

// Page<Board> → Page<BoardDto> 변환 (우리 프로젝트)
List<BoardDto> dtoList = boardPage.getContent()
    .stream().map(this::toDto)
    .collect(Collectors.toList());

return new PageImpl<>(dtoList, pageable, boardPage.getTotalElements());
📌 Pageable 만드는 법

PageRequest.of(페이지번호, 페이지크기) — 기본
PageRequest.of(페이지번호, 페이지크기, Sort.by("createdAt").descending()) — 정렬 포함

페이지 번호는 0부터 시작. 프론트에서 1페이지를 0으로 변환해서 보내거나, 서버에서 -1 처리 필요.

컬렉션 선택 가이드
상황쓸 컬렉션이유
게시글 목록, 유저 목록 — 순서 있는 데이터List (ArrayList)인덱스 접근, 순서 보장
API 응답에 여러 값 담기Map (Map.of)키-값으로 명확하게
중복 제거가 필요한 데이터Set (HashSet)자동 중복 제거
게시판 페이징 처리Page (Spring Data)전체 개수, 페이지 정보 자동 포함
순회 중 삭제Iterator 또는 removeIfConcurrentModificationException 방지
스프링 권한 목록List (List.of)GrantedAuthority 목록 — 순서 있고 불변
심화 1 AOP📚 전체 맵심화 3 예외처리