F-lab Java 1주차 / Phase 2 / Unit 2.2 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 2.1 (메서드의 구조)
다음 Unit: 2.3 — 상속과 생성자 체이닝
마트 계산대에서 결제할 때를 생각해보세요. 손님마다 사는 물건의 개수가 다릅니다:
계산원이 일하는 방식:
만약 계산원이 이런 식이라면 끔찍할 것:
→ 가변인자는 "개수에 상관없이 받아주는 계산대" 의 정신.
System.out.printf자바를 써본 사람이라면 이미 가변인자를 매일 쓰고 있습니다:
System.out.printf("이름: %s%n", "Alice");
System.out.printf("이름: %s, 나이: %d%n", "Alice", 25);
System.out.printf("좌표: (%d, %d, %d)%n", 1, 2, 3);
같은 printf 메서드인데 인자 개수가 다 다름. 어떻게 가능할까요?
printf 의 시그니처:
public PrintStream printf(String format, Object... args) { ... }
// ↑ 가변인자
→ Object... args 가 "몇 개든 받아주겠다" 는 선언.
이게 가변인자(varargs).
| 비유 요소 | 자바 가변인자 |
|---|---|
| 마트 계산대 | 가변인자 메서드 |
| 카트의 물건들 | 가변인자로 전달되는 인자들 |
| 1개든 100개든 OK | 0개부터 N개까지 자유 |
| 한 줄에 모든 손님 처리 | 한 메서드로 모든 시나리오 처리 |
자바 1.4 이전 (2004년 이전), 가변인자가 없었습니다. 인자 개수가 다양한 메서드를 만들려면?
방법 1 — 오버로딩 폭증:
public class Logger {
public void log(String msg1) {
System.out.println(msg1);
}
public void log(String msg1, String msg2) {
System.out.println(msg1 + " " + msg2);
}
public void log(String msg1, String msg2, String msg3) {
System.out.println(msg1 + " " + msg2 + " " + msg3);
}
public void log(String msg1, String msg2, String msg3, String msg4) {
System.out.println(...);
}
// ... 5개, 6개, 7개... 끝없이 ❌
}
문제:
방법 2 — 배열로 받기:
public class Logger {
public void log(String[] messages) {
for (String msg : messages) {
System.out.println(msg);
}
}
}
// 사용 — 매번 배열을 만들어야 함 ❌
logger.log(new String[]{"hello"});
logger.log(new String[]{"hello", "world"});
logger.log(new String[]{"a", "b", "c", "d"});
문제:
new String[]{...} 작성 — 번거로움Java 5 에서 ... 문법 이 등장:
public class Logger {
public void log(String... messages) { // ← 가변인자
for (String msg : messages) {
System.out.println(msg);
}
}
}
// 사용 — 자연스러움 ✅
logger.log("hello");
logger.log("hello", "world");
logger.log("a", "b", "c", "d");
logger.log(); // 0개도 OK!
효과:
→ Java 5는 가변인자 외에도 제네릭, 어노테이션, enum 등 자바 역사의 분수령.
"가변인자는 '메서드의 입력 유연성' 을 극대화하기 위한 문법적 설탕(syntactic sugar)이다."
내부적으로는 여전히 배열 입니다. 단지 호출하는 사용자가 편하게 쓰도록 해준 것뿐.
가변인자가 없을 때 어떤 불편함이 있는지 실제 시나리오 로 보겠습니다.
운임 견적 검색에서 다양한 조건을 받고 싶다고 합시다:
public class FareSearchService {
public List<Fare> search(int minPrice) { ... }
public List<Fare> search(int minPrice, int maxPrice) { ... }
public List<Fare> search(int minPrice, int maxPrice, String currency) { ... }
public List<Fare> search(int minPrice, int maxPrice, String currency, String origin) { ... }
public List<Fare> search(int minPrice, int maxPrice, String currency, String origin, String dest) { ... }
// ... 시나리오 추가될 때마다 메서드 추가 ❌
public List<Fare> search(int maxPrice, boolean isMaxOnly) {
// "최대 가격만으로 검색" 시나리오 — 매개변수가 같은 타입이라 별도 처리 ❌
}
}
문제:
1. 메서드 폭증 — 조합마다 새 메서드
2. 타입이 같으면 모호함 (위 코드의 마지막 메서드 참고)
3. 유지보수 지옥 — 검색 로직 변경 시 모든 메서드 수정
public class FareSearchService {
public List<Fare> search(SearchCriteria[] criteria) { ... }
}
// 사용 — 배열 매번 생성 ❌
service.search(new SearchCriteria[]{
new SearchCriteria("price", ">=", 1000)
});
service.search(new SearchCriteria[]{
new SearchCriteria("price", ">=", 1000),
new SearchCriteria("price", "<=", 5000)
});
// "조건 없이 전체 검색"
service.search(new SearchCriteria[0]); // 빈 배열 ❌ 어색함
문제:
1. 호출 시 노이즈 — new SearchCriteria[]{...} 매번
2. 빈 인자 표현 어색 — new SearchCriteria[0]
3. 가독성 ↓
public class FareSearchService {
public List<Fare> search(SearchCriteria... criteria) { ... }
}
// 사용 — 자연스러움 ✅
service.search(); // 조건 없음 → 전체 검색
service.search(priceMin);
service.search(priceMin, priceMax);
service.search(priceMin, priceMax, currency, origin, dest);
효과:
가변인자가 없으면 자바 자체가 매우 불편할 것입니다:
// 1. printf 계열
System.out.printf("이름: %s, 나이: %d", name, age);
// 2. String.format
String.format("Hello, %s!", "Alice");
// 3. List.of, Set.of, Map.of (Java 9+)
List.of("a", "b", "c");
Set.of(1, 2, 3, 4, 5);
// 4. Arrays.asList
Arrays.asList("x", "y", "z");
// 5. String.join
String.join(", ", "a", "b", "c"); // "a, b, c"
// 6. Stream.of
Stream.of(1, 2, 3, 4, 5);
→ 모두 가변인자 덕분. 없었다면 이 모든 게 배열을 매번 만들어야 했을 것.
[접근제어자] 반환타입 메서드명(타입... 변수명) {
// 변수명은 배열처럼 다룸
}
핵심 표시: ... (점 3개)
public class Logger {
public void log(String... messages) {
// messages는 String[] 배열로 다룸
System.out.println("받은 메시지 개수: " + messages.length);
for (String msg : messages) {
System.out.println("- " + msg);
}
}
}
Logger logger = new Logger();
logger.log(); // 0개
logger.log("hello"); // 1개
logger.log("hello", "world"); // 2개
logger.log("a", "b", "c", "d", "e"); // 5개
출력:
받은 메시지 개수: 0
받은 메시지 개수: 1
- hello
받은 메시지 개수: 2
- hello
- world
받은 메시지 개수: 5
- a
- b
- c
- d
- e
public void process(int... numbers) {
// numbers는 int[]
// 1. length 사용 가능
System.out.println("개수: " + numbers.length);
// 2. 인덱스 접근 가능
if (numbers.length > 0) {
System.out.println("첫 번째: " + numbers[0]);
}
// 3. for-each 가능
for (int n : numbers) {
System.out.println(n);
}
// 4. for 루프 가능
for (int i = 0; i < numbers.length; i++) {
System.out.println(i + ": " + numbers[i]);
}
// 5. Arrays 유틸 사용 가능
int sum = Arrays.stream(numbers).sum();
}
→ 호출하는 쪽은 인자 나열, 받는 쪽은 배열 처리.
가변인자는 다른 매개변수와 함께 사용 가능:
public class Logger {
public void log(String level, String... messages) {
for (String msg : messages) {
System.out.println("[" + level + "] " + msg);
}
}
}
logger.log("INFO"); // 0개 메시지
logger.log("INFO", "시작"); // 1개
logger.log("ERROR", "DB 연결 실패", "재시도 중"); // 2개
logger.log("DEBUG", "a", "b", "c"); // 3개
public void log(String level, String... messages) { ... } // ✅ 마지막
public void log(String... messages, String level) { ... } // ❌ 컴파일 에러
왜?: 자바가 어디까지가 가변인자인지 구별 불가.
logger.log("INFO", "msg1", "msg2", "ERROR");
// "INFO"가 첫 인자? "ERROR"가 level? 자바가 모름
public void method(String... s, int... i) { ... } // ❌ 컴파일 에러
위 규칙 1과 같은 이유 — 어디까지가 첫 가변인자, 어디부터 두 번째인지 모름.
public void log(String... messages) { ... }
logger.log(); // 0개 OK
logger.log("a"); // 1개 OK
logger.log("a", "b"); // 2개 OK
// 100개도 OK
"가변인자는 컴파일러가 자동으로 배열로 변환해주는 문법적 설탕(syntactic sugar)"
실제 동작:
// 우리가 작성한 코드
public void log(String... messages) {
System.out.println(messages.length);
}
logger.log("a", "b", "c");
컴파일러가 변환한 코드:
public void log(String[] messages) { // ← 사실은 배열
System.out.println(messages.length);
}
logger.log(new String[]{"a", "b", "c"}); // ← 호출 시 자동으로 배열 생성
→ JVM 입장에서는 가변인자와 배열이 완전히 동일.
logger.log("a", "b", "c");
JVM 내부 흐름:
1. 호출 시점:
- JVM이 "a", "b", "c"를 담은 String[] 배열을 Heap에 생성
- 그 배열의 참조를 log 메서드의 messages 매개변수로 전달
2. 메서드 안에서:
- messages는 그 배열을 가리키는 참조
- 배열의 모든 메서드/속성 사용 가능
3. 메서드 종료 후:
- 배열은 더 이상 참조되지 않음
- 다음 GC에서 수거됨
→ 가변인자 호출은 매번 새 배열 생성 (성능 영향 가능 — 7번 섹션에서 자세히)
public void log(String... messages) { ... }
// 1. 일반적인 호출 — 인자 나열
logger.log("a", "b", "c");
// 2. 배열을 직접 전달
String[] arr = {"a", "b", "c"};
logger.log(arr); // ✅ 가능
// 3. new로 만든 배열 직접 전달
logger.log(new String[]{"a", "b", "c"}); // ✅ 가능 (가변인자 없을 때 방식)
왜?: 어차피 내부적으로 배열이라서.
null 전달 시 함정 ⚠️public void log(String... messages) {
System.out.println(messages.length); // ⚠️
}
// 1. null 직접 전달
logger.log(null); // → messages가 null
// → messages.length → NullPointerException!
// 2. null 인자
logger.log("a", null, "b"); // → messages = ["a", null, "b"]
// → messages.length = 3 (OK)
// → messages[1] 사용 시 NullPointerException 가능
// 3. 빈 배열과 다름
logger.log(new String[0]); // → messages = [] (빈 배열)
// → messages.length = 0
안전한 코드:
public void log(String... messages) {
if (messages == null) {
return; // 또는 빈 배열로 처리
}
for (String msg : messages) {
if (msg != null) {
System.out.println(msg);
}
}
}
public void process(Integer... nums) { ... }
process(1, 2, 3); // int → Integer 자동 박싱
// → new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)}
// → 3번의 박싱 발생 (성능 영향)
대안 — 기본형 가변인자:
public void process(int... nums) { ... }
process(1, 2, 3); // → new int[]{1, 2, 3}
// → 박싱 없음 ✅
→ 기본형이 가능한 시나리오는 기본형 가변인자 권장.
ILIC 운임 시스템에서 가변인자를 다양하게 활용하는 예시들.
public class IlicLogger {
public void info(String message, Object... args) {
String formatted = String.format(message, args);
System.out.println("[INFO] " + formatted);
}
public void error(String message, Throwable error, Object... args) {
String formatted = String.format(message, args);
System.err.println("[ERROR] " + formatted);
error.printStackTrace();
}
}
// 사용
logger.info("운임 생성 완료");
logger.info("운임 ID: %d", 100);
logger.info("고객 %s, 금액 %d원, 통화 %s", "Alice", 50000, "KRW");
logger.error("DB 연결 실패", exception);
logger.error("운임 %d 처리 실패: %s", exception, fareId, reason);
→ SLF4J, Log4j 모두 이런 패턴 사용.
public class FareValidator {
public static void validateNotNull(String fieldName, Object... values) {
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
throw new IllegalArgumentException(
fieldName + "[" + i + "] 는 null일 수 없습니다"
);
}
}
}
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
public static int max(int first, int... rest) {
// 최소 1개는 필수, 나머지는 선택
int max = first;
for (int n : rest) {
if (n > max) max = n;
}
return max;
}
}
// 사용
FareValidator.validateNotNull("고객 정보", customerId, customerName, email);
int total = FareValidator.sum(1000, 2000, 3000); // 6000
int largest = FareValidator.max(10, 5, 8, 12, 3); // 12
FareValidator.max(10); // 10 (rest는 빈 배열)
max(int first, int... rest) 패턴 ⭐ :
public class FareQuery {
private final List<String> conditions = new ArrayList<>();
public FareQuery in(String field, Object... values) {
if (values.length == 0) {
return this; // 무시
}
String inClause = field + " IN (" +
Arrays.stream(values).map(v -> "?").collect(Collectors.joining(",")) +
")";
conditions.add(inClause);
return this;
}
public FareQuery between(String field, Object min, Object max) {
conditions.add(field + " BETWEEN ? AND ?");
return this;
}
public String build() {
return "WHERE " + String.join(" AND ", conditions);
}
}
// 사용
FareQuery query = new FareQuery()
.in("status", "DRAFT", "SUBMITTED", "PAID") // 가변인자
.in("currency", "KRW") // 1개도 OK
.between("amount", 1000, 100000);
String sql = query.build();
// "WHERE status IN (?,?,?) AND currency IN (?) AND amount BETWEEN ? AND ?"
public class NotificationService {
public void sendToCustomers(String template, Customer... customers) {
for (Customer customer : customers) {
String message = template.replace("{name}", customer.getName());
send(customer, message);
}
}
private void send(Customer customer, String message) {
// 발송 로직
}
}
// 사용
service.sendToCustomers("안녕하세요 {name}님", alice);
service.sendToCustomers("이벤트 안내 {name}", alice, bob, charlie);
// 또는 Customer 배열을 직접
Customer[] vips = customerRepository.findVips();
service.sendToCustomers("VIP {name}님께", vips); // 배열 직접 전달 OK
// String.format
String msg = String.format("이름: %s, 나이: %d, 직업: %s", "Alice", 25, "개발자");
// List.of (Java 9+) — 불변 리스트
List<String> tags = List.of("urgent", "shipping", "international");
// Arrays.asList
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Stream.of
Stream<String> stream = Stream.of("a", "b", "c");
// String.join
String joined = String.join(", ", "apple", "banana", "cherry");
// "apple, banana, cherry"
// Path.of (Java 11+)
Path p = Path.of("home", "user", "documents", "file.txt");
// Collections.addAll
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d");
→ 자바를 잘 쓰려면 가변인자는 필수.
// ❌ 컴파일 에러
public void log(String... messages, String level) { ... }
// ✅ 마지막에 위치
public void log(String level, String... messages) { ... }
규칙: 가변인자는 무조건 마지막.
// ❌ 컴파일 에러
public void method(String... names, int... ages) { ... }
// ✅ 다른 방법으로 해결
public void method(String[] names, int[] ages) { ... }
public void method(List<String> names, List<Integer> ages) { ... }
public class Service {
public void process(int x) { System.out.println("int"); }
public void process(int... nums) { System.out.println("varargs"); }
}
Service s = new Service();
s.process(1); // ⚠️ "int" 출력 — 정확한 매칭이 우선
s.process(1, 2); // "varargs"
s.process(); // "varargs"
규칙 ⭐ :
권장: 가변인자와 일반 메서드를 같은 이름으로 오버로딩하지 말 것.
public void log(Object... args) {
System.out.println("개수: " + args.length);
}
String[] arr = {"a", "b", "c"};
logger.log(arr); // ⚠️ args.length = 3 (배열의 요소들)
왜?: String[] 은 Object[] 호환 → 배열 자체가 가변인자로 풀림.
해결 — 배열을 한 인자로 명시적 전달:
logger.log((Object) arr); // 캐스팅으로 배열 자체를 한 인자로
// → args.length = 1, args[0] = arr
→ 헷갈리는 케이스. 면접에서도 가끔 출제.
public void log(String... messages) {
for (String msg : messages) { // messages가 null이면 NPE!
System.out.println(msg);
}
}
logger.log(null); // 💥 NullPointerException
해결:
public void log(String... messages) {
if (messages == null || messages.length == 0) {
return; // 또는 기본 동작
}
for (String msg : messages) {
if (msg != null) {
System.out.println(msg);
}
}
}
가변인자는 호출할 때마다 새 배열 생성:
public void log(Object... args) { ... }
// 매 호출마다 new Object[]{...} 생성 (Heap 사용)
for (int i = 0; i < 1000000; i++) {
logger.log("msg", i, "data"); // 100만 번의 배열 생성
}
고성능이 필요한 핫 패스(hot path)에서는:
// 자주 쓰는 케이스 오버로딩
public void log(String message) { ... } // 1개 — 배열 생성 X
public void log(String m1, String m2) { ... } // 2개 — 배열 생성 X
public void log(String.<.. messages) { ... } // N개 — 배열 생성
→ 로깅 라이브러리 (SLF4J 등) 가 정확히 이 패턴을 사용.
// ⚠️ 의도가 불명확
public Fare create(String... params) {
// params[0] = customerId
// params[1] = amount
// params[2] = currency
// ... 호출자가 순서를 외워야 함
}
fareService.create("1", "50000", "KRW"); // ❌ 의도 불명확
해결 — 객체로 명확히:
public Fare create(FareCreateRequest request) {
// 명확한 필드명
}
fareService.create(new FareCreateRequest(1L, 50000, "KRW"));
→ 가변인자는 "동질적 데이터" 일 때만. 의미가 다른 값들은 객체로.
[Unit 2.1: 메서드의 구조]
↓
[Unit 2.2: 가변인자] ← 지금 여기
↓
[Unit 2.3: 상속과 생성자 체이닝]
↓
[Unit 2.4: 다형성]
1주차 내:
List.of(...), Set.of(...), Arrays.asList(...) 모두 가변인자Path.of("home", "user", "file") 같은 패턴미래 주차:
<T> T method(T... values) 같은 제네릭 가변인자Stream.of(1, 2, 3), Collectors.toMap(...)@RequestMapping(value = {...}, method = {...}) 같은 어노테이션 가변값assertThat(list).contains("a", "b", "c") 같은 assertion@PathVariable, @RequestParam 의 다중 값 처리언제 무엇을?
| 상황 | 추천 |
|---|---|
| 호출 시 인자를 직접 나열 | 가변인자 (method("a", "b", "c")) |
| 이미 List/배열이 있음 | 컬렉션 매개변수 (method(List<String>)) |
| 둘 다 자주 발생 | 가변인자 (배열 직접 전달도 가능) |
| 매우 빈번한 호출 (성능 중요) | 일반 매개변수 또는 오버로딩 |
유연한 패턴 — 가변인자가 둘 다 받음:
public void process(String... items) { ... }
process("a", "b", "c"); // 직접 나열
process(myList.toArray(new String[0])); // 배열로 변환 후 전달
| 질문 | 이 Unit에서의 답 |
|---|---|
| "가변인자란?" | 타입... 변수명 형식, 메서드 안에서는 배열로 다룸 |
| "가변인자 위치 규칙?" | 마지막에 1개만 |
| "내부적으로는?" | 컴파일 시 배열로 변환되는 syntactic sugar |
| "배열 매개변수와 차이?" | 호출 편의성 (호출자가 배열 안 만들어도 됨) |
| "0개 인자도 가능?" | YES — 빈 배열로 처리됨 |
1️⃣ 가변인자는 "메서드의 입력 유연성을 위한 문법적 설탕" 이다.
타입... 변수명으로 선언하면 0개부터 N개까지 자유롭게 인자를 받을 수 있다. 내부적으로는 배열로 변환 되므로, JVM 입장에서는 가변인자와 배열이 완전히 동일하다. 호출자의 편의를 위한 문법.2️⃣ 2가지 핵심 규칙: 마지막에, 1개만.
가변인자는 매개변수 목록의 마지막 에만 올 수 있고, 메서드당 1개만 가능하다. 이 두 규칙은 자바 컴파일러가 어디까지가 가변인자인지 구별하기 위함이다. 이걸 어기면 컴파일 에러.
3️⃣ 편하지만 함정도 있다 — null, 성능, 모호함.
null직접 전달 시 NPE, 매 호출마다 배열 생성으로 성능 영향, 오버로딩과 섞으면 매칭 우선순위 헷갈림. 의미가 다른 값들은 객체로 묶는 게 좋고, 동질적 데이터 일 때만 가변인자가 자연스럽다.
타입... 변수명) 을 작성할 수 있다String.format, List.of 등 자바 표준 API의 가변인자를 활용할 수 있다Q1: void log(String... args) 와 void log(String[] args) 의 차이는?
호출 측면 (가장 큰 차이):
// 가변인자 — 자연스러움
log("a", "b", "c");
// 배열 매개변수 — 매번 배열 생성 필요
log(new String[]{"a", "b", "c"});
메서드 안에서는 동일 — 둘 다 String[] 로 다룸:
public void log(String... args) {
System.out.println(args.length); // OK
for (String s : args) { ... } // OK
}
public void log(String[] args) {
System.out.println(args.length); // 동일
for (String s : args) { ... } // 동일
}
JVM 관점:
String[] 으로 변환됨ACC_VARARGS 플래그가 추가될 뿐)// ❌ 컴파일 에러 — 같은 시그니처로 인식
public void log(String... args) { ... }
public void log(String[] args) { ... }
결론:
Q2: 가변인자는 매개변수 목록의 어느 위치에 와야 하는가?
반드시 마지막 위치.
// ✅ OK
public void method(int a, String... rest) { ... }
public void method(int a, double b, Object... rest) { ... }
// ❌ 컴파일 에러
public void method(String... rest, int a) { ... }
public void method(int a, String... rest, int b) { ... }
왜?:
method("a", "b", 1) — "a", "b" 가 가변인자? 아니면 "a" 만?또한 — 메서드당 가변인자는 1개만:
// ❌ 컴파일 에러
public void method(String... s, int... i) { ... }
같은 이유.
extends, super() 같은 키워드의 의미가 궁금하다