List · Map · Set · Iterator · 정렬 · Collections 유틸 — 우리 프로젝트에서 이미 쓰고 있었지만 이론 정리가 안 됐던 것들을 한 번에 정복
사실 컬렉션은 1차~3차 프로젝트 전체에서 이미 사용하고 있었다. List<BoardDto>, Map.of("token", token), Page<Board> — 이것들이 전부 컬렉션이다. 의식하지 못하고 쓴 것들을 이번에 이론부터 체계적으로 정리한다. STEP 2는 새 코드를 추가하는 단계가 아니라, 이미 쓴 코드를 이해하는 단계다.
List<BoardDto> — 게시글 목록 (BoardService)List<GrantedAuthority> — 권한 목록 (CustomUserDetailsService)Map.of("token", token) — API 응답 (UserController)Page<Board> — 페이징 목록 (BoardService)우리 프로젝트에서 이미 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 방지용) |
Collection 인터페이스 ← List, Set, Queue 가 구현
Map 인터페이스 ← HashMap, LinkedHashMap, TreeMap 등이 구현 (Collection과 별도 계층)
실무에서 변수 선언은 인터페이스 타입으로, 생성은 구현체로:
List<String> list = new ArrayList<>();
→ 나중에 구현체를 LinkedList로 바꿔도 사용 코드 변경 불필요
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<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<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);
// 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());
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
remove()를 직접 호출하면 ConcurrentModificationException 발생!// ❌ 잘못된 방식 — 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() → 중간 연산들 → 최종 연산 순서로 연결한다. 우리 프로젝트의 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 | — |
// 패턴 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());
.map(board -> this.toDto(board)) 와 .map(this::toDto) 는 완전히 같다.
this::toDto는 "this 객체의 toDto 메서드를 사용해라"는 뜻의 메서드 레퍼런스 표현식.
Board::getCreatedAt도 마찬가지 — b -> b.getCreatedAt()과 동일.
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());
PageRequest.of(페이지번호, 페이지크기) — 기본
PageRequest.of(페이지번호, 페이지크기, Sort.by("createdAt").descending()) — 정렬 포함
페이지 번호는 0부터 시작. 프론트에서 1페이지를 0으로 변환해서 보내거나, 서버에서 -1 처리 필요.
| 상황 | 쓸 컬렉션 | 이유 |
|---|---|---|
| 게시글 목록, 유저 목록 — 순서 있는 데이터 | List (ArrayList) | 인덱스 접근, 순서 보장 |
| API 응답에 여러 값 담기 | Map (Map.of) | 키-값으로 명확하게 |
| 중복 제거가 필요한 데이터 | Set (HashSet) | 자동 중복 제거 |
| 게시판 페이징 처리 | Page (Spring Data) | 전체 개수, 페이지 정보 자동 포함 |
| 순회 중 삭제 | Iterator 또는 removeIf | ConcurrentModificationException 방지 |
| 스프링 권한 목록 | List (List.of) | GrantedAuthority 목록 — 순서 있고 불변 |