Day 18 — 컬렉션 (1): List와 ArrayList (배열 졸업)
지난 시간 마지막에 약속 하나를 남겼었죠. "다음 시간엔 드디어 배열을 졸업한다" 고요. 오늘이 바로 그날이에요.
돌아보면 Phase 2 내내 우리를 따라다닌 답답함이 있었어요. 회원의 글 목록도, 팔로잉 목록도 전부 배열 + 카운터로 담았잖아요. Member[] following 을 만들 때 "팔로잉이 몇 명까지 늘어날지 모르는데 크기를 미리 정하라니…" 하고 한 번쯤 갸웃했을 거예요. 그 갸웃함을 오늘 시원하게 풀어요.
오늘 배울 도구의 이름은 컬렉션(collection, 모음)이에요. 그중에서도 가장 많이 쓰는 List(리스트, 목록)와 그 대표 구현인 ArrayList 를 만나요. 크기를 미리 정할 필요 없이 알아서 늘어나고, 지금 몇 개인지 스스로 세고, 중간 항목을 빼면 빈자리도 알아서 메우는 그릇이에요.
그리고 지난 시간 끝에 흘려둔 그 한마디 — "컬렉션은 원시형을 못 담는다" 던 말, 그래서 래퍼 클래스가 왜 필요했는지도 오늘 정확히 확인하게 될 거예요. 자, Day 18 시작해봐요!
🎯 학습 목표
- 배열의 한계 세 가지(고정 크기·수동 카운터·중간 삭제)를 설명하고, 왜 컬렉션이 필요한지 말할 수 있어요.
ArrayList를 만들어add·get·size로 데이터를 담고 꺼낼 수 있어요.set·remove·contains·indexOf·clear로 리스트를 자유자재로 다루고,remove(int)vsremove(Object)함정을 피할 수 있어요.- 제네릭
List<String>의 꺾쇠가 무슨 약속인지 이해하고, "컬렉션은 원시형을 못 담는다"(List<int>불가)는 의미를 설명할 수 있어요. List인터페이스와ArrayList·LinkedList구현의 관계를 알고, 둘의 트레이드오프를 말할 수 있어요.Comparable과Comparator로 명단을 원하는 기준으로 정렬하고,Collections도구상자를 쓸 수 있어요.
Step 1. 배열은 왜 답답했을까 — 한계 세 가지
먼저 우리가 그동안 어떻게 명단을 담아왔는지 떠올려봐요. Day 16에서 설계한 Member 는 팔로잉 명단을 이렇게 들고 있었어요.
// com/instagram/javabasic/domain/member/Member.java (지난 시간 설계)
private Member[] following = new Member[CAPACITY]; // 크기를 미리 정한 배열
private int followingCount = 0; // 지금 몇 명인지 직접 세는 카운터
배열 + 카운터 이 한 쌍이 늘 함께 다녔죠. 이 방식의 답답함을 압축해서 보여주는 작은 데모를 준비했어요. 팔로잉 명단을 크기 3짜리 배열로 직접 관리해봐요.
// com/instagram/javabasic/collection/ArrayLimitDemo.java
public class ArrayLimitDemo {
private final String[] following = new String[3]; // 한 번 정하면 늘릴 수 없는 크기
private int count = 0; // 우리가 손으로 세는 카운터
public boolean add(String username) {
if (count >= following.length) {
return false; // 가득 참 — 더 못 담아요
}
following[count] = username;
count++;
return true;
}
public void removeWithShift(int index) {
for (int i = index; i < count - 1; i++) {
following[i] = following[i + 1]; // 뒷사람을 한 칸씩 직접 당겨와요
}
following[count - 1] = null; // 마지막 자리를 비워요
count--;
}
}
이 코드를 실행하면 어떤 답답함이 드러나는지 볼게요.
ArrayLimitDemo demo = new ArrayLimitDemo();
demo.add("minji"); // true
demo.add("seungwoo"); // true
demo.add("jaehoon"); // true
demo.add("yuna"); // false ← 크기 3을 넘어 더 못 담아요!
세 명까지는 들어가는데, 네 번째 yuna 는 거절당해요. 그릇 크기를 처음에 3으로 못박았기 때문이에요. 한계를 그림으로 보면 이래요.
배열 following (크기 3, 고정)
┌──────────┬──────────┬──────────┐
│ minji │ seungwoo │ jaehoon │ ← 가득 참
└──────────┴──────────┴──────────┘
"yuna 도 넣어줘" → ✗ 자리가 없어요 (크기를 늘릴 수 없음)
중간에서 minji(0번)를 빼면?
┌──────────┬──────────┬──────────┐
│ (빈칸) │ seungwoo │ jaehoon │ ← 0번에 구멍
└──────────┴──────────┴──────────┘
빈칸을 없애려면 뒷사람을 한 칸씩 직접 당겨와야 해요
┌──────────┬──────────┬──────────┐
│ seungwoo │ jaehoon │ (null) │
└──────────┴──────────┴──────────┘
정리하면 배열의 답답함은 세 가지예요.
- 크기를 미리 정해야 한다 — 넘으면 더 못 담아요.
- 카운터를 손으로 챙겨야 한다 —
count++를 깜빡하면 바로 버그예요. - 중간 삭제가 번거롭다 — 빈칸을 메우려고 뒷사람을 직접 당겨와야 해요.
이 세 가지를 한 번에 풀어주는 도구가 오늘의 주인공, ArrayList 예요. 바로 만나볼게요.
Step 2. ArrayList 첫 만남 — add · get · size
ArrayList(어레이리스트) 를 한 문장으로 표현하면 "스스로 크기를 늘리는 배열" 이에요. 배열처럼 순서대로 담지만, 크기를 미리 정하지 않아도 되고 카운터도 필요 없어요.
먼저 코드부터 보고 한 줄씩 뜯어봐요.
// com/instagram/javabasic/collection/ArrayListBasics.java
import java.util.ArrayList;
import java.util.List;
public class ArrayListBasics {
public List<String> buildTags() {
List<String> tags = new ArrayList<>();
tags.add("#sunset");
tags.add("#jeju");
tags.add("#travel");
return tags;
}
}
낯선 줄이 두 개 있죠. 하나씩 짚어요.
먼저 맨 위 import 두 줄. ArrayList 와 List 는 자바가 java.util 패키지에 미리 만들어둔 도구라, 쓰기 전에 "이 도구를 가져다 쓸게요" 하고 불러오는 거예요. IntelliJ에서는 ArrayList 를 입력하면 자동으로 import 가 붙으니 손으로 칠 일은 거의 없어요.
다음은 핵심인 이 한 줄이에요.
List<String> tags = new ArrayList<>();
List<String>— "문자열(String)만 담는 목록" 이라는 뜻이에요. 꺾쇠<String>이 "이 그릇엔 String 만 넣어요" 라는 약속이에요. 이걸 제네릭(generic)이라고 부르는데, 자세한 건 Step 4와 Day 20에서 다뤄요. 지금은 "담을 타입을 꺾쇠 안에 적는다" 정도만 기억해요.new ArrayList<>()— 실제 그릇을 만드는 부분이에요. 오른쪽 꺾쇠<>는 비어 있는데, 왼쪽에서 이미<String>이라고 했으니 자바가 알아서 채워줘요(다이아몬드 연산자).
변수 타입은
List로 적고 실제로는ArrayList로 만드는 게 살짝 의아할 수 있어요. 이건 다음 시간에 배우는 다형성의 맛보기인데, Step 5에서 왜 이렇게 쓰는지 정확히 설명할게요. 지금은 관용구처럼 받아들여요.
이제 담고 꺼내봐요.
ArrayListBasics demo = new ArrayListBasics();
List<String> tags = demo.buildTags();
System.out.println("담긴 개수: " + tags.size()); // 3 ← size()가 알아서 세요
System.out.println("첫 번째: " + tags.get(0)); // #sunset ← get(index)로 꺼내요
System.out.println("전체: " + tags); // [#sunset, #jeju, #travel]
배열과 비교하면 두 가지가 확 달라졌어요.
add()만 하면 알아서 늘어나요. 크기를 미리 정한 적이 없는데 다섯 개, 열 개도 그냥 들어가요.size()가 지금 개수를 스스로 알려줘요.count++같은 카운터를 우리가 들고 다닐 필요가 없어요.
Step 1의 답답함 세 가지 중 벌써 두 개(고정 크기·수동 카운터)가 사라졌죠. 동적으로 늘어나는 걸 직접 확인해봐요.
List<String> many = new ArrayList<>();
many.add("#food");
many.add("#cafe");
many.add("#daily");
many.add("#ootd");
many.add("#seoul");
System.out.println("다섯 개도 OK: " + many.size()); // 5
크기 5를 어디에도 적지 않았는데 다섯 개가 멀쩡히 담겼어요. 이게 컬렉션의 첫 번째 매력이에요.
Step 3. 자유자재로 — 수정·삭제·검색 + 향상된 for
담는 법을 배웠으니, 이제 바꾸고 빼고 찾는 법을 익혀요. 자주 쓰는 동작을 한자리에 모았어요.
// com/instagram/javabasic/collection/ArrayListCrud.java
List<String> tags = new ArrayList<>();
tags.add("#sunset");
tags.add("#jeju");
tags.add("#travel");
tags.set(0, "#first"); // 0번 자리의 값을 바꿔요 → [#first, #jeju, #travel]
boolean has = tags.contains("#jeju"); // 들어있나? → true
int where = tags.indexOf("#travel"); // 몇 번째? → 2
tags.clear(); // 전부 비우기
boolean empty = tags.isEmpty(); // 비었나? → true
이름만 봐도 뜻이 보이죠. set 은 교체, contains 는 포함 여부, indexOf 는 위치, clear 는 비우기, isEmpty 는 비었는지예요. 배열이었다면 직접 반복문을 돌려야 했을 일들을 메서드 하나로 끝내요.
순회도 훨씬 깔끔해요. Day 5에서 배운 향상된 for(for-each)를 그대로 쓸 수 있어요.
public String joinAll() {
List<String> tags = sampleTags(); // [#sunset, #jeju, #travel]
String result = "";
for (String tag : tags) { // 리스트도 향상된 for로 훑어요
result = result + tag + " ";
}
return result.trim(); // "#sunset #jeju #travel"
}
배열에 쓰던 for (String tag : 배열) 문법이 리스트에도 똑같이 통해요. 인덱스를 셀 필요 없이 "안에 든 것을 하나씩" 꺼내주죠.
⚠️ remove 의 함정 — 위치냐 값이냐
리스트에서 꼭 짚고 갈 함정이 하나 있어요. 바로 remove 예요. 이 메서드는 이름은 하나인데 두 가지로 동작해요.
remove(int index)— 정수를 주면 그 위치를 빼요.remove(Object 값)— 값 객체를 주면 그 값을 찾아 빼요.
문자열 리스트에선 헷갈릴 일이 없어요. tags.remove("#jeju") 는 누가 봐도 값이니까요. 그런데 숫자 리스트에선 사고가 나기 쉬워요.
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.remove(2); // "2번 위치" → 값 30이 빠져요! [10, 20]
numbers.remove(Integer.valueOf(20)); // "값 20" 을 찾아 빼요 [10, 30]
numbers.remove(2) 는 "값 2를 빼라" 가 아니라 "2번 자리를 빼라" 로 해석돼요. 정수를 직접 주면 위치로 동작하기 때문이에요. 값으로 빼고 싶으면 Integer.valueOf(20) 처럼 객체로 감싸서 줘야 해요. 이건 자바를 오래 쓴 개발자도 한 번씩 빠지는 함정이라, 지금 확실히 기억해두면 나중에 디버깅 시간을 크게 아껴요.
Step 4. 컬렉션은 원시형을 못 담는다 — 제네릭과 래퍼
여기서 지난 시간의 약속을 회수해요. Day 17 끝에 "컬렉션은 원시형을 못 담아서 래퍼 클래스가 필요하다" 고 했었죠. 이제 그게 무슨 뜻인지 직접 봐요.
좋아요 수(정수)를 리스트에 담아 합을 구해봐요.
// com/instagram/javabasic/collection/GenericTypeSafety.java
public int sumLikes() {
List<Integer> likes = new ArrayList<>();
likes.add(120); // int 120 → Integer 로 자동 박싱
likes.add(85);
likes.add(300);
int total = 0;
for (int like : likes) { // Integer → int 자동 언박싱
total = total + like;
}
return total; // 505
}
꺾쇠 안을 보세요. List<int> 가 아니라 List<Integer> 예요. 여기에 두 가지 핵심이 숨어 있어요.
첫째, 컬렉션의 꺾쇠 <> 안에는 클래스 타입만 올 수 있어요. int·double 같은 원시형(기본형)은 클래스가 아니라서 못 들어가요. 그래서 정수를 담으려면 int 를 객체로 감싼 래퍼 클래스 Integer 를 써요. 지난 시간에 "래퍼 클래스가 왜 필요하냐" 던 답이 바로 이거예요 — 컬렉션에 담으려고요.
List<int> ✗ 원시형은 못 담아요 (int 는 클래스가 아님)
List<Integer> ✓ 래퍼 클래스로 감싸면 OK
│
└── int ──[감싸기/오토박싱]──> Integer ──[담김]──> 리스트
둘째, 그런데 코드를 보면 likes.add(120) 처럼 그냥 int 를 넣고 있죠? 이게 가능한 건 지난 시간에 배운 오토박싱 덕분이에요. int 120 을 넣으면 자바가 자동으로 Integer 로 감싸서 담아요. 꺼낼 때 int like 변수로 받으면 다시 int 로 풀어주고요(언박싱). 우리는 편하게 int 만 다루는 것 같지만, 리스트 안에서는 Integer 객체로 살고 있는 거예요.
제네릭이 막아주는 사고
꺾쇠 <String>·<Integer> 가 단순히 "이런 걸 담아요" 라는 메모가 아니에요. 잘못된 타입이 섞이는 걸 실행 전에 막아주는 안전장치예요.
List<Integer> likes = new ArrayList<>();
likes.add(7); // OK
// likes.add("백이십"); // ✗ 컴파일 에러: List<Integer>에는 Integer만 담을 수 있어요
만약 제네릭이 없다면 정수 리스트에 실수로 문자열이 섞여 들어가도 모르고 있다가, 한참 뒤 꺼내 쓸 때 프로그램이 터졌을 거예요. 제네릭은 그 사고를 컴파일 단계에서 빨간 줄로 미리 잡아줘요. "엉뚱한 게 들어오기 전에 문 앞에서 막는" 셈이죠. 제네릭을 직접 만드는 방법은 Day 20에서 본격적으로 다뤄요. 오늘은 "꺾쇠가 타입 안전을 지켜준다" 는 감만 잡아요.
Step 5. Collection 가계도 + ArrayList vs LinkedList
지금까지 List 와 ArrayList 를 거의 같은 것처럼 썼는데, 사실 둘은 역할이 달라요. 이 관계를 가계도로 정리하면 컬렉션 전체 그림이 잡혀요.
Iterable "하나씩 꺼낼 수 있다" (향상된 for 가능)
│
Collection "여러 개를 담는다" 의 큰 약속
│
┌─┴─────────┬──────────┐
List Set Queue ← 약속(인터페이스)들
│
┌────┴─────┐
ArrayList LinkedList ← 실제 일꾼(구현 클래스)
위쪽 List·Collection 은 "이런 걸 할 수 있어야 한다" 는 약속(인터페이스)이에요. Day 13에서 배운 그 인터페이스 맞아요. 아래쪽 ArrayList·LinkedList 는 그 약속을 실제로 지키는 일꾼(구현 클래스)이고요.
List 약속을 지키는 일꾼은 둘이에요. 속이 어떻게 생겼는지가 달라요.
ArrayList (속이 배열)
┌────┬────┬────┬────┐
│ 0 │ 1 │ 2 │ 3 │ 번호표로 바로 찾기 → get(2) 빠름
└────┴────┴────┴────┘
LinkedList (칸들이 손을 잡음)
[minji] ⇄ [seungwoo] ⇄ [jaehoon]
앞/중간에 끼워넣기 가벼움 → add(0, ...) 빠름
둘 다 List 약속을 지키니까, 변수는 List 로 받아두고 일꾼만 바꿔 끼울 수 있어요. 이게 다형성이에요. Day 11에서 배운 그 개념이 여기서 빛을 발해요.
// com/instagram/javabasic/collection/ListImplementations.java
public List<String> withArrayList() {
List<String> names = new ArrayList<>(); // 일꾼: ArrayList
names.add("minji");
names.add("seungwoo");
return names;
}
public List<String> withLinkedList() {
List<String> names = new LinkedList<>(); // 일꾼만 LinkedList 로 교체
names.add("minji");
names.add("seungwoo");
return names;
}
new 뒤에 적은 일꾼만 다를 뿐, add·get 을 쓰는 코드는 글자 하나 안 바뀌었죠. 변수 타입을 List 로 두면 이렇게 구현을 자유롭게 갈아끼울 수 있어요. 그래서 "변수는 약속(List)으로 받고, 일꾼(ArrayList)은 new 에서만 고른다" 가 자바의 관용구예요.
그럼 둘 중 뭘 쓰냐고요? 트레이드오프를 표로 정리했어요.
| ArrayList | LinkedList | |
|---|---|---|
| 속 구조 | 배열 | 칸들이 손잡고 연결 |
get(index) 조회 |
빠름 (번호표로 바로) | 느림 (앞에서부터 세어감) |
| 앞·중간 삽입/삭제 | 느림 (뒤 당겨오기) | 빠름 (손만 다시 잡으면 됨) |
| 실무 기본 선택 | 대부분 이걸 써요 | 앞쪽 삽입이 잦을 때만 |
결론부터 말하면 실무에서는 거의 항상 ArrayList 예요. 우리가 리스트로 하는 일은 대부분 "끝에 담고, 번호로 꺼내고, 전체를 훑는" 동작이라 ArrayList 가 잘 맞아요. LinkedList 는 "맨 앞에 자주 끼워넣는" 특수한 상황에서만 꺼내요. 그래서 앞으로 우리도 기본은 ArrayList 로 갈 거예요.
Step 6. 정렬 (1) — Comparable로 "기본 순서" 정하기
명단을 담았으면 다음은 줄 세우기예요. 팔로워가 많은 순서, 이름 가나다 순서처럼요. 정렬에는 두 가지 길이 있는데, 먼저 "이 클래스의 기본 순서" 를 정하는 Comparable(컴패러블) 부터 봐요.
회원을 팔로워 수로 정렬할 수 있게 만들어볼게요.
// com/instagram/javabasic/collection/SortableMember.java
public class SortableMember implements Comparable<SortableMember> {
private final String username;
private final int followers;
// ... 생성자와 getter 생략 ...
@Override
public int compareTo(SortableMember other) {
return this.followers - other.followers; // 팔로워 적은 사람부터(오름차순)
}
}
implements Comparable<SortableMember> — "나는 정렬 가능한 회원이에요" 라는 약속을 지키겠다는 선언이에요. 그 약속의 핵심이 compareTo 한 메서드고요. 이 메서드는 "나를 상대(other)와 견주면 누가 앞이냐" 를 숫자로 답해요.
- 음수를 돌려주면 → 내가 앞
- 0을 돌려주면 → 같은 자리
- 양수를 돌려주면 → 상대가 앞
this.followers - other.followers 는 내 팔로워에서 상대 팔로워를 뺀 값이에요. 내가 적으면 음수가 나와 내가 앞으로 가니, 팔로워 적은 사람부터 줄 서는 오름차순이 돼요.
이제 약속을 지킨 회원들은 Collections.sort 한 줄로 정렬돼요.
List<SortableMember> members = new ArrayList<>();
members.add(new SortableMember("minji", 8500));
members.add(new SortableMember("jaehoon", 1240));
members.add(new SortableMember("seungwoo", 320));
Collections.sort(members); // compareTo 기준으로 자동 정렬
System.out.println(members);
// [@seungwoo(320), @jaehoon(1240), @minji(8500)] ← 팔로워 오름차순
우리가 정렬 알고리즘을 한 줄도 짜지 않았는데 자동으로 줄을 섰죠. Collections.sort 가 각 회원의 compareTo 를 호출해 누가 앞인지 물어보면서 정렬해준 거예요. compareTo 안의 기준만 바꾸면 정렬 방향도 바뀌고요.
Step 7. 정렬 (2) — Comparator + Collections 도구상자
Comparable 은 클래스가 "타고난 기본 순서" 한 가지를 정해요. 그런데 같은 명단을 상황에 따라 다른 기준으로 정렬하고 싶을 때가 있죠. 어떤 화면에선 팔로워 순, 어떤 화면에선 이름 순처럼요. 이때 쓰는 게 Comparator(컴패레이터) 예요.
Comparator 는 "정렬 기준" 자체를 별도 클래스로 떼어내요. 팔로워 많은 순(내림차순) 비교기를 만들어볼게요.
// com/instagram/javabasic/collection/FollowerComparator.java
import java.util.Comparator;
public class FollowerComparator implements Comparator<SortableMember> {
@Override
public int compare(SortableMember a, SortableMember b) {
return b.getFollowers() - a.getFollowers(); // 많은 사람부터(내림차순)
}
}
compare(a, b) 도 compareTo 와 똑같은 규칙이에요. 음수면 a 가 앞, 양수면 b 가 앞이죠. 이번엔 b - a 로 순서를 뒤집어 팔로워 많은 사람이 앞에 오게 했어요. 이름 순 비교기도 같은 방식으로 하나 더 만들 수 있어요.
// com/instagram/javabasic/collection/UsernameComparator.java
public class UsernameComparator implements Comparator<SortableMember> {
@Override
public int compare(SortableMember a, SortableMember b) {
return a.getUsername().compareTo(b.getUsername()); // 이름 사전 순
}
}
문자열 비교는 String 이 이미 가진 compareTo 에 맡겼어요. 사전 순서를 알아서 계산해주거든요. 이제 정렬할 때 어떤 비교기를 넘기느냐에 따라 순서가 달라져요.
List<SortableMember> members = sample(); // minji(8500), jaehoon(1240), seungwoo(320)
Collections.sort(members, new FollowerComparator());
// [@minji(8500), @jaehoon(1240), @seungwoo(320)] ← 팔로워 내림차순
Collections.sort(members, new UsernameComparator());
// [@jaehoon(1240), @minji(8500), @seungwoo(320)] ← 이름 순
같은 명단인데 넘기는 비교기만 바꿔 다른 순서로 줄 세웠죠. 기본 순서는 Comparable(클래스에 박아둠), 그때그때 다른 순서는 Comparator(따로 만들어 넘김) — 이 둘의 역할 분담을 기억해요.
그런데 정렬 기준 하나 쓰자고 클래스를 통째로 만드는 게 좀 번거롭게 느껴지지 않나요?
compare메서드 한 줄을 위해public class ... implements ...를 다 적어야 하니까요. 사실 이건 자바도 인정한 불편함이에요. 그래서 나중에 이 비교기를 단 한 줄로 줄이는 문법이 등장하는데, 그게 바로 람다예요. Day 24에서 이FollowerComparator가 한 줄로 변신하는 걸 보게 될 거예요. 지금은 "기준을 클래스로 떼어낸다" 는 원리를 확실히 잡아둬요.
Collections 도구상자
Collections(컬렉션즈, 끝에 s) 는 명단을 다루는 편리한 도구들을 모아둔 상자예요. 방금 쓴 sort 말고도 자주 쓰는 게 몇 개 더 있어요.
// com/instagram/javabasic/collection/CollectionsUtilDemo.java
Collections.sort(members); // Comparable 기본 순서로 정렬
Collections.reverse(members); // 순서를 통째로 뒤집기
SortableMember top = Collections.max(sample()); // 기본 기준으로 최댓값
SortableMember low = Collections.min(sample()); // 기본 기준으로 최솟값
Collections.shuffle(members, new Random(42)); // 무작위로 섞기
reverse— 현재 순서를 그대로 뒤집어요. 오름차순으로 정렬한 뒤reverse하면 내림차순이 되죠.max·min—Comparable기본 기준으로 가장 크고 작은 원소를 찾아줘요.shuffle— 무작위로 섞어요.new Random(42)처럼 씨앗 숫자를 주면 매번 같은 순서로 섞여서, 결과를 확인하거나 테스트할 때 좋아요.
정렬 알고리즘, 최댓값 찾기 반복문… 예전 같으면 직접 짰을 일들을 Collections 도구상자가 한 줄로 대신해줘요. "바퀴를 다시 발명하지 말라" 는 말이 딱 어울리는 도구예요.
Step 8. 배열을 졸업하다 — 도메인 리팩토링 + 정리
오늘의 마지막은 약속한 졸업식이에요. Step 1에서 답답해하던 그 배열 + 카운터 팔로잉 명단을, 이제 List 로 다시 써봐요.
먼저 Before를 떠올려요. Step 1의 ArrayLimitDemo 는 이랬죠.
// Before — 배열 + 카운터 (Step 1)
private final String[] following = new String[3]; // 크기 고정
private int count = 0; // 수동 카운터
// add 할 때마다 가득 찼는지 검사, remove 할 때마다 직접 당겨오기...
이걸 List 로 졸업하면 이렇게 가벼워져요.
// com/instagram/javabasic/collection/FollowingList.java
public class FollowingList {
private final List<String> following = new ArrayList<>(); // 카운터 없이 이것 하나면 끝
public void addFollowing(String username) {
following.add(username); // 가득 찰 걱정 없이 그냥 add
}
public void removeFollowing(String username) {
following.remove(username); // 값으로 빼면 빈칸 없이 알아서 메워짐
}
public int getFollowingCount() {
return following.size(); // 우리가 안 세도 size()가 알려줘요
}
public boolean isFollowing(String username) {
return following.contains(username); // contains 한 줄
}
}
Step 1의 한계 세 가지가 어떻게 사라졌는지 하나씩 짚어봐요.
- 크기 고정 → 무제한.
new String[3]이 사라졌어요. 네 명이든 백 명이든add만 하면 들어가요. - 수동 카운터 → 자동.
int count가 사라졌어요.size()가 알아서 세주니까요. - 중간 삭제 번거로움 → 자동. 뒷사람 당겨오는 반복문이 사라졌어요.
remove가 빈칸 없이 메워줘요.
실행해보면 예전 배열이라면 막혔을 네 번째 팔로우도 문제없이 들어가요.
FollowingList me = new FollowingList();
me.addFollowing("minji");
me.addFollowing("seungwoo");
me.addFollowing("jaehoon");
me.addFollowing("yuna"); // 크기 3 배열이라면 막혔을 자리 — 이젠 그냥 들어가요
System.out.println("팔로잉 수: " + me.getFollowingCount()); // 4
me.removeFollowing("seungwoo"); // 중간 삭제 — 빈칸 없이 알아서 메워짐
System.out.println("seungwoo 언팔 후: " + me.getFollowingCount()); // 3
코드는 절반으로 줄었는데 할 수 있는 일은 오히려 늘었어요. 이게 컬렉션을 배우는 진짜 이유예요. 우리가 Phase 2 내내 배열로 끙끙대던 일을, 자바가 이미 잘 만들어둔 도구에 맡기는 거죠.
오늘 배운 것 — 언제 무엇을 쓸까
| 도구 | 언제 쓰나 | 핵심 |
|---|---|---|
ArrayList |
순서 있는 여러 개를 담을 때 | 크기 자동·카운터 불필요. 실무 리스트 기본 |
List<타입> |
변수를 선언할 때 | 약속은 List, 일꾼은 ArrayList(다형성) |
제네릭 <> |
담을 타입을 정할 때 | 클래스 타입만(원시형 ✗). 타입 안전 보장 |
Comparable |
클래스의 기본 순서를 정할 때 | compareTo 를 클래스 안에 박음 |
Comparator |
다른 순서로 정렬할 때 | 비교기를 따로 만들어 sort 에 넘김 |
Collections |
정렬·뒤집기·최댓값 등 | 도구상자. 바퀴를 다시 만들지 말 것 |
마무리 — 오늘 배운 것 압축 요약
- Step 1: 배열은 크기 고정·수동 카운터·중간 삭제 번거로움이라는 한계가 있어요.
- Step 2:
ArrayList는add·get·size로 다루고, 크기를 미리 안 정해도 알아서 늘어나요. - Step 3:
set·remove·contains로 자유자재로 다뤄요.remove(int)는 위치,remove(Object)는 값. - Step 4: 컬렉션은 원시형을 못 담아 래퍼(
Integer)를 써요. 제네릭<>가 타입 안전을 지켜줘요. - Step 5:
List는 약속(인터페이스),ArrayList·LinkedList는 일꾼(구현). 실무 기본은ArrayList. - Step 6~7: 기본 순서는
Comparable, 다른 순서는Comparator.Collections도구상자로 정렬·뒤집기·최댓값.
다음 시간엔 — 중복 없는 명단, 이름표 붙은 저장소
오늘 배운 List 는 순서가 있고 중복도 허용해요. 같은 #travel 을 두 번 add 하면 두 번 다 들어가죠. 그런데 이게 곤란할 때가 있어요. 예를 들어 "이 게시물에 좋아요 누른 사람 목록" 에 같은 사람이 두 번 들어가면 안 되잖아요. 중복을 자동으로 걸러주는 그릇이 필요해요.
또 다른 경우도 있어요. 지금 우리는 회원을 찾을 때 리스트를 처음부터 훑어야 해요. 그런데 "username 으로 회원을 바로 찾기" 처럼, 이름표(키)를 대면 값이 툭 튀어나오는 저장소가 있으면 훨씬 빠르겠죠.
다음 시간엔 이 두 가지를 풀어주는 컬렉션을 배워요. 중복을 자동으로 없애는 Set(세트), 그리고 이름표(키)와 값을 짝지어 저장하는 Map(맵)이에요. 오늘 배운 List 와 형제 같은 도구들이라, 금방 익숙해질 거예요. 그리고 "두 회원이 같은 사람인지" 를 판단하는 equals 와 hashCode 가 왜 짝꿍인지도 그때 정확히 만나요.
배열을 졸업한 오늘, 정말 큰 한 걸음을 내디뎠어요. 수고 많으셨어요!
과제
오늘 배운 컬렉션은 손에 익혀야 진짜 내 것이 돼요. 세 가지 과제를 준비했어요. 모두 오늘까지 배운 문법(클래스·메서드·List/ArrayList·제네릭 소비·Comparable/Comparator·향상된 for·Collections)만으로 풀 수 있어요. 람다·Stream은 아직 안 배웠으니, 정렬 기준은 이름을 가진 비교기 클래스로 만들어주세요.
[기초] 과제 1 — 해시태그 보드 만들기
해야 할 일
게시물에 붙은 해시태그를 관리하는 TagBoard 클래스를 작성해보세요.
요구사항
private final List<String> tags = new ArrayList<>();필드를 둬요.void add(String tag)— 단, 이미 있는 태그면 또 넣지 않아요(contains로 검사). 중복 방지가 핵심이에요.boolean remove(String tag)— 태그를 빼고, 실제로 뺐으면true를 돌려줘요.int size()— 지금 태그 개수.String render()— 향상된 for로 전체 태그를"#a #b #c"한 줄 문자열로 이어줘요(없으면 빈 문자열).
힌트
- 중복 방지는
add안에서if (tags.contains(tag)) return;한 줄이면 돼요. render는 Step 3의joinAll패턴을 그대로 응용해요. 마지막 공백은trim()으로 정리하고요.
[기초~중] 과제 2 — 팔로워 랭킹 정렬
해야 할 일
회원들을 팔로워 수로 정렬해 상위 랭킹을 뽑는 프로그램을 작성해보세요.
요구사항
RankMember클래스를 만들고implements Comparable<RankMember>로 팔로워 내림차순(많은 사람부터) 기본 정렬을 정의해요.- 필드는
username(String)·followers(int), getter와toString(@이름(팔로워수)형식)을 둬요. RankBoard클래스에List<RankMember>를 담고,Collections.sort로 정렬한 뒤 향상된 for로 상위 N명만 출력하는printTop(int n)을 작성해요.main에서 회원 4~5명을 담아 상위 3명을 출력해보세요.
힌트
- 내림차순은
compareTo에서other.followers - this.followers처럼 순서를 뒤집어요(Step 6은 오름차순이었죠). - 상위 N명은 정렬 후
for로n번만 돌면 돼요. 리스트가 N보다 짧을 수 있으니Math.min(n, list.size())로 한도를 잡아요.
[심화] 과제 3 — 두 가지 기준으로 정렬하기
해야 할 일
같은 팔로잉 명단을 상황에 따라 다른 기준으로 정렬하는 비교기 두 개를 만들어보세요.
상황
기획자가 이렇게 요청했어요. "팔로잉 목록을 화면에 보여줄 때, 어떤 화면에선 이름 가나다 순으로, 어떤 화면에선 최근 활동일 순으로 정렬하고 싶어요."
요구사항
FollowUser클래스 —username(String)·activeDays(int, 작을수록 최근 활동) 필드와 getter·toString.UsernameOrder implements Comparator<FollowUser>— 이름 사전 순 비교기.ActiveOrder implements Comparator<FollowUser>—activeDays오름차순(최근 활동이 위) 비교기.main에서 같은 리스트를 두 비교기로 각각 정렬해, 순서가 어떻게 달라지는지 출력으로 비교해요.
힌트
- 이름 비교는
a.getUsername().compareTo(b.getUsername())에 맡겨요(Step 7의UsernameComparator참고). - 두 비교기 모두 람다 없이
implements Comparator<FollowUser>명명 클래스로 만들어요. 이게 왜 번거로운지 직접 느껴보는 게 이 과제의 숨은 목표예요 — 그 불편함이 Day 24 람다로 이어져요.
생각해볼 주제
오늘 배운 컬렉션 뒤에는 "왜 자바를 이렇게 설계했을까?" 하는 질문이 숨어 있어요. 정답을 외우기보다 한번 곰곰이 생각해보면, 도구를 보는 눈이 한층 깊어질 거예요.
주제 1 — ArrayList 가 "스스로 늘어난다" 는 건 공짜일까?
ArrayList 는 크기를 미리 정하지 않아도 알아서 늘어나죠. 마법처럼 느껴지지만, 속은 결국 고정 크기 배열이에요. 그렇다면 배열이 가득 찼을 때 add 를 하면 내부에서 무슨 일이 벌어질까요? 더 큰 배열을 새로 만들어 기존 내용을 옮겨 담는 과정이 숨어 있다면, 그 비용은 언제 치러지고 평소엔 왜 느껴지지 않는지 생각해보세요.
주제 2 — 변수는 왜 ArrayList 가 아니라 List 로 받을까?
ArrayList<String> list = new ArrayList<>(); 라고 써도 똑같이 동작해요. 그런데 실무에서는 굳이 List<String> list = new ArrayList<>(); 처럼 왼쪽을 List 로 적죠. 양쪽 다 ArrayList 로 맞추면 더 직관적일 것 같은데, 왜 일부러 약속(List)으로 받을까요? 나중에 일꾼을 LinkedList 로 바꿔야 하는 상황을 떠올리면 그 이유가 보일 거예요.
주제 3 — Comparable 과 Comparator, 둘 다 있는 이유는?
정렬 기준을 정하는 방법이 두 개나 있어요. 클래스 안에 박는 Comparable, 따로 떼어내는 Comparator. 하나로 합쳤으면 더 단순했을 텐데 왜 둘을 나눴을까요? "이 클래스에 가장 자연스러운 순서가 하나 있다" 는 경우와 "정렬 기준이 화면마다 다르다" 는 경우를 떠올리며, 각각 어느 쪽이 어울리는지 나만의 기준을 세워보세요.
✅ 예시 답안정답 보기
오늘 과제는 "배열을 졸업하고 List 로 갈아타기" 가 핵심이에요. 세 과제 모두 정답은 하나가 아니에요. 아래 예시와 다르게 풀었더라도, 요구사항을 만족하고 오늘 배운 컬렉션 도구를 제대로 썼다면 훌륭한 답이에요.
과제 1 예시답안 — 해시태그 보드 만들기
핵심 접근
이 과제의 진짜 주제는 "중복 방지" 예요. List 는 원래 같은 값을 두 번 담을 수 있으니, add 안에서 contains 로 한 번 걸러주는 게 포인트예요. 나머지 remove·size·render 는 Step 2~3에서 배운 메서드를 그대로 쓰면 돼요.
예시 구현
// com/instagram/javabasic/solution/day18/TagBoard.java
class TagBoard {
private final List<String> tags = new ArrayList<>();
// 이미 들어 있는 태그면 그냥 무시하고, 새 태그만 추가한다.
void add(String tag) {
if (tags.contains(tag)) {
return;
}
tags.add(tag);
}
// 태그를 빼고, 실제로 빠졌으면 true 를 돌려준다.
boolean remove(String tag) {
return tags.remove(tag);
}
int size() {
return tags.size();
}
// "#a #b #c" 처럼 한 줄로 그려준다. 비어 있으면 빈 문자열.
String render() {
String line = "";
for (String tag : tags) {
line = line + "#" + tag + " ";
}
return line.trim();
}
}
add 의 if (tags.contains(tag)) return; 한 줄이 중복을 막아요. remove 는 List.remove 가 이미 "뺐으면 true, 없으면 false" 를 돌려주니, 그 값을 그대로 반환하면 끝이에요. render 는 향상된 for로 한 줄씩 이어 붙인 뒤 마지막 공백을 trim() 으로 정리했어요.
채점 포인트
| 항목 | 배점 기준 |
|---|---|
| 중복 방지 | add 에서 contains 로 이미 있는 태그를 거르는가 |
remove 반환값 |
List.remove 의 boolean 결과를 그대로 돌려주는가 |
size 위임 |
직접 카운터를 두지 않고 tags.size() 에 맡기는가 |
render 향상된 for |
인덱스 없이 for (String tag : tags) 로 순회하는가 |
| 빈 보드 처리 | 태그가 없을 때 빈 문자열이 나오는가 |
흔한 실수
- 카운터 필드를 또 두기 —
int count를 만들어 직접 세는 경우. 배열을 졸업했으니size()에 맡겨요. 카운터를 두면 오늘 배운 의미가 사라져요. render끝 공백 —trim()을 빼먹어"#a #b #c "처럼 끝에 공백이 남는 경우. 마지막에 한 번 정리해줘요.- 중복 검사 위치 —
contains를render나size에서 하려는 경우. 중복은 "담을 때" 막아야지, 꺼낼 때 거르면 이미 늦어요.
실무 개선 포인트 (심화)
"중복을 자동으로 막는다" 는 건 사실 Set(세트) 이라는 컬렉션의 전문 분야예요. 다음 시간에 배우는 Set 을 쓰면 contains 검사 없이도 중복이 자동으로 걸러져요. 지금은 List + contains 로 직접 막았지만, "중복 방지가 목적이라면 Set 이 더 어울린다" 는 감을 잡아두면 다음 시간이 한결 쉬워요.
과제 2 예시답안 — 팔로워 랭킹 정렬
핵심 접근
Comparable 로 "팔로워 내림차순" 기본 정렬을 클래스에 박아두는 게 1단계예요. Step 6은 오름차순(this - other)이었으니, 내림차순은 순서를 뒤집어 other - this 로 쓰면 돼요. 그다음 Collections.sort 로 정렬하고, 상위 N명만 끊어 보여줄 때 리스트가 N보다 짧을 수 있으니 Math.min 으로 한도를 잡는 게 2단계예요.
예시 구현
먼저 정렬 기준을 품은 회원 클래스예요.
// com/instagram/javabasic/solution/day18/RankMember.java
class RankMember implements Comparable<RankMember> {
private final String username;
private final int followers;
// ... 생성자·getter 생략 ...
// 팔로워가 많은 쪽이 앞으로 오도록 (내림차순) 비교한다.
@Override
public int compareTo(RankMember other) {
return other.followers - this.followers;
}
@Override
public String toString() {
return "@" + username + "(" + followers + ")";
}
}
그리고 회원들을 담아 순위를 뽑는 보드예요.
// com/instagram/javabasic/solution/day18/RankBoard.java
class RankBoard {
private final List<RankMember> members = new ArrayList<>();
void add(RankMember member) {
members.add(member);
}
// 팔로워 많은 순으로 정렬한 뒤, 위에서 n 명을 출력한다.
void printTop(int n) {
Collections.sort(members);
int limit = Math.min(n, members.size());
for (int i = 0; i < limit; i++) {
System.out.println((i + 1) + "위 " + members.get(i));
}
}
}
compareTo 에서 other.followers - this.followers 로 순서를 뒤집은 게 내림차순의 핵심이에요. RankMember 가 Comparable 이라 Collections.sort(members) 한 줄이면 비교기를 따로 안 넘겨도 정렬돼요. Math.min(n, members.size()) 는 "3명을 보여달라" 는데 2명뿐일 때 터지지 않게 막아주는 안전선이에요.
채점 포인트
| 항목 | 배점 기준 |
|---|---|
Comparable 구현 |
implements Comparable<RankMember> + compareTo 작성 |
| 내림차순 방향 | other.followers - this.followers 로 많은 사람이 앞에 오는가 |
| 기본 정렬 활용 | Collections.sort(members) 한 줄로 정렬하는가 |
| 상위 N 한도 | Math.min(n, size()) 로 리스트보다 큰 N을 막는가 |
toString |
@이름(수) 형식으로 보기 좋게 출력되는가 |
흔한 실수
- 정렬 방향 헷갈림 —
this - other(오름차순)로 써서 팔로워 적은 사람이 위로 오는 경우. 랭킹은 많은 사람이 위니other - this예요. - N 한도 안 잡기 — 회원이 2명인데
printTop(3)을 호출해get(2)에서IndexOutOfBoundsException이 나는 경우.Math.min으로 막아요. - 정렬을 직접 짜기 — 버블 정렬 등을 손으로 구현하는 경우. 틀린 건 아니지만 오늘의 목표는
Comparable+Collections.sort에 맡기는 거예요.
실무 개선 포인트 (심화)
지금은 printTop 이 정렬과 출력을 한꺼번에 해요. 그런데 실무에서는 "정렬해서 상위 N명을 돌려주는 메서드(List<RankMember> top(int n))" 와 "그걸 출력하는 코드" 를 나누는 게 좋아요. 데이터를 만드는 일과 보여주는 일을 분리하면, 같은 상위 N명을 화면에도 쓰고 통계에도 쓸 수 있거든요. 반환형을 List 로 두면 호출한 쪽이 자유롭게 활용할 수 있어요.
과제 3 예시답안 — 두 가지 기준으로 정렬하기
핵심 접근
같은 명단을 "이름 순" 과 "최근 활동 순" 두 가지로 정렬하는 게 목표예요. 정렬 기준이 둘이니 Comparable(하나의 기본 순서) 로는 부족하고, Comparator 를 두 개 만들어요. 각 비교기는 람다 없이 implements Comparator<FollowUser> 명명 클래스로 작성해요. 이 "번거로움" 을 직접 느껴보는 게 숨은 목표예요.
예시 구현
정렬 대상 클래스부터 봐요.
// com/instagram/javabasic/solution/day18/FollowUser.java
// activeDays 는 "며칠 전에 활동했나" 라서, 작을수록 최근에 활동한 사람이다.
class FollowUser {
private final String username;
private final int activeDays;
// ... 생성자·getter 생략 ...
@Override
public String toString() {
return "@" + username + "(" + activeDays + "일 전)";
}
}
이제 정렬 기준 두 개를 각각 명명 클래스로 떼어내요.
// com/instagram/javabasic/solution/day18/UsernameOrder.java
// 이름을 가나다(알파벳) 순으로 줄 세우는 기준.
class UsernameOrder implements Comparator<FollowUser> {
@Override
public int compare(FollowUser a, FollowUser b) {
return a.getUsername().compareTo(b.getUsername());
}
}
// com/instagram/javabasic/solution/day18/ActiveOrder.java
// activeDays 가 작은 사람(=최근 활동) 이 앞으로 오도록 줄 세우는 기준.
class ActiveOrder implements Comparator<FollowUser> {
@Override
public int compare(FollowUser a, FollowUser b) {
return a.getActiveDays() - b.getActiveDays();
}
}
이름 비교는 String 이 가진 compareTo 에 맡겨 사전 순서를 자동으로 계산했어요. 활동일 비교는 a - b 로 작은 값(최근 활동)이 앞에 오는 오름차순이에요. 같은 리스트에 Collections.sort(list, new UsernameOrder()) 와 Collections.sort(list, new ActiveOrder()) 를 번갈아 적용하면 순서가 달라지는 걸 볼 수 있어요.
채점 포인트
| 항목 | 배점 기준 |
|---|---|
| 명명 Comparator | 람다·익명클래스 없이 implements Comparator<FollowUser> 클래스로 만드는가 |
| 두 기준 분리 | 이름 순·활동 순을 각각 별도 클래스로 떼어냈는가 |
| 이름 비교 위임 | String.compareTo 에 맡겨 직접 글자 비교를 안 하는가 |
| 활동일 오름차순 | a - b 로 작은 값(최근)이 앞에 오는가 |
| 동일 리스트 재정렬 | 같은 명단을 두 비교기로 각각 정렬해 비교하는가 |
흔한 실수
Comparable로 풀기 —FollowUser에compareTo하나만 두는 경우. 기준이 둘이라 하나의 기본 순서로는 표현이 안 돼요.Comparator두 개가 정답이에요.- 이름 비교를 손으로 — 글자를 하나씩 비교하려는 경우.
String.compareTo가 사전 순서를 이미 계산해줘요. - 활동일 방향 —
b - a로 써서 오래된 사람이 위로 오는 경우. "최근 활동이 위" 면 작은 값이 앞이니a - b예요.
실무 개선 포인트 (심화)
비교기 하나를 위해 class ... implements Comparator<...> { ... } 를 통째로 적는 게 번거롭게 느껴졌을 거예요. 바로 그 불편함이 자바에 람다가 도입된 이유예요. Day 24에서 이 UsernameOrder 가 Comparator.comparing(FollowUser::getUsername) 한 줄로 줄어드는 걸 보게 될 거예요. 지금은 "기준을 클래스로 떼어낸다" 는 원리를 확실히 익혀두면, 그때 람다가 무엇을 줄여주는지 또렷이 보여요.
생각해볼 주제 1 예시답안 — ArrayList 가 "스스로 늘어난다" 는 건 공짜일까?
[문제 상황 요약]
ArrayList 는 크기를 미리 정하지 않아도 알아서 늘어나죠. 마법처럼 보이지만 속은 결국 고정 크기 배열이에요. 가득 찬 배열에 add 를 하면 내부에서 무슨 일이 벌어지는지, 그 비용은 언제 치러지는지 생각해보는 주제예요.
[튜터의 가이드 및 해설]
핵심은 "ArrayList 도 속은 배열이라, 가득 차면 더 큰 배열로 이사를 간다" 는 거예요. 보통 기존 크기의 1.5배쯤 되는 새 배열을 만들고, 기존 원소를 전부 새 배열로 복사한 뒤, 헌 배열은 버려요. 이 "이사" 가 늘어남의 정체예요. 공짜처럼 보였지만 사실 가끔 한 번씩 복사 비용을 치르고 있던 거죠.
그런데 왜 평소엔 안 느껴질까요? 이사가 매번 일어나는 게 아니라 가끔만 일어나기 때문이에요. 한 번 1.5배로 늘려두면 그다음 add 들은 한동안 빈자리에 그냥 들어가요. 큰 비용(복사)을 여러 번의 싼 add 에 나눠 부담하는 셈이라, 평균을 내면 한 번의 add 는 여전히 가벼워요.
여기서 실무 감각이 하나 나와요. 담을 개수를 미리 안다면 new ArrayList<>(1000) 처럼 초기 크기를 귀띔해줄 수 있어요. 그러면 이사를 여러 번 할 일이 줄어 더 효율적이에요. "알아서 늘어남" 에 기대되, 규모를 알면 미리 알려주는 게 프로의 습관이에요.
🎯 면접관을 홀리는 핵심 멘트
"
ArrayList의 동적 크기는 공짜가 아니라, 가득 차면 더 큰 배열로 복사·이사하는 비용을 가끔 치르는 구조입니다. 다만 그 비용을 여러 번의add에 분산하기 때문에 평균적으로는 상수 시간이고요. 그래서 담을 규모를 알면 초기 용량을 지정해 재할당 횟수를 줄이는 걸 선호합니다."
생각해볼 주제 2 예시답안 — 변수는 왜 ArrayList 가 아니라 List 로 받을까?
[문제 상황 요약]
ArrayList<String> list = new ArrayList<>(); 도 똑같이 동작하는데, 실무에서는 왼쪽을 List<String> 으로 적죠. 양쪽을 ArrayList 로 맞추면 더 직관적일 것 같은데, 왜 일부러 약속(List)으로 받는지 생각해보는 주제예요.
[튜터의 가이드 및 해설]
답의 실마리는 "나중에 일꾼을 바꿔야 할 때" 예요. 변수를 List 로 받아두면, 나중에 new ArrayList<>() 를 new LinkedList<>() 로 바꿔도 그 한 줄만 고치면 끝이에요. 변수 타입도 ArrayList 로 박아뒀다면, 그 변수를 쓰는 모든 코드를 LinkedList 로 바꿔야 할 수도 있어요.
조금 더 깊이 보면, 이건 "구체적인 것보다 약속에 기대라" 는 설계 원칙이에요. 우리가 리스트로 하고 싶은 일(add·get·순회)은 전부 List 약속에 들어 있어요. 그러니 코드는 List 라는 약속만 알면 충분하고, 그 약속을 누가 지키는지(ArrayList 냐 LinkedList 냐)는 new 하는 순간에만 정하면 돼요. 약속에 기대면 일꾼을 갈아끼울 자유가 생겨요.
이건 Day 11에서 배운 다형성의 실전 활용이에요. 그리고 앞으로 메서드의 매개변수 타입을 정할 때도 똑같이 적용돼요. void print(ArrayList<String> list) 보다 void print(List<String> list) 가 나아요. 후자는 ArrayList 든 LinkedList 든 다 받을 수 있으니까요.
🎯 면접관을 홀리는 핵심 멘트
"변수와 매개변수는 구현 클래스가 아니라 인터페이스(
List)로 선언하는 걸 원칙으로 합니다. 우리가 필요한 동작은 전부List약속에 있으니, 구현은 생성 시점에만 정하면 되고, 나중에ArrayList를LinkedList로 교체해도 한 줄만 바뀝니다. 구체 타입이 아니라 추상에 의존하라는 다형성 원칙의 실전 적용이죠."
생각해볼 주제 3 예시답안 — Comparable 과 Comparator, 둘 다 있는 이유는?
[문제 상황 요약]
정렬 기준을 정하는 방법이 Comparable·Comparator 두 개나 있어요. 하나로 합쳤으면 단순했을 텐데 왜 나눴을까요? 각각이 어울리는 상황을 떠올려보는 주제예요.
[튜터의 가이드 및 해설]
두 도구의 차이는 "정렬 기준을 어디에 두느냐" 예요. Comparable 은 기준을 클래스 안에 박아요. 그래서 "이 클래스에 가장 자연스러운 순서 하나" 를 정할 때 어울려요. 예를 들어 숫자는 크기 순, 날짜는 시간 순처럼 누가 봐도 당연한 기본 순서가 있을 때죠.
Comparator 는 기준을 클래스 밖으로 떼어내요. 그래서 "기준이 상황마다 다를 때" 어울려요. 회원 명단을 어떤 화면에선 이름 순으로, 어떤 화면에선 팔로워 순으로 보여주고 싶다면, 기본 순서 하나로는 표현이 안 되잖아요. 이럴 때 비교기를 여러 개 만들어 그때그때 골라 넘겨요.
둘을 나눈 진짜 이유가 여기 있어요. 만약 Comparable 만 있었다면 클래스는 평생 한 가지 순서밖에 못 가져요. 게다가 내가 만들지 않은 남의 클래스(예: 라이브러리가 준 클래스)는 compareTo 를 고칠 수도 없고요. Comparator 는 클래스를 건드리지 않고도 바깥에서 새 기준을 더할 수 있어요. "기본 순서는 안에, 상황별 순서는 밖에" — 이 역할 분담이 두 도구를 모두 둔 이유예요.
🎯 면접관을 홀리는 핵심 멘트
"
Comparable은 클래스의 자연스러운 기본 순서를 내부에 정의하고,Comparator는 상황별 정렬 기준을 외부에서 주입합니다. 기준이 하나로 고정되면Comparable, 화면이나 맥락에 따라 달라지면Comparator를 쓰죠. 특히 수정할 수 없는 외부 클래스도Comparator로는 원하는 순서로 정렬할 수 있다는 점이 둘을 분리한 핵심 이유라고 봅니다."