Day06: 메서드
안녕하세요! 여러분의 Java 가이드, 홍순구 튜터입니다.
Day 6에 오신 걸 환영합니다!
지난 시간에 우리는 배열이라는 도구로 같은 타입 데이터를 한꺼번에 묶어 보관하는 법을 배웠어요.
1차원 배열 선언부터 enhanced for 순회, 인덱스 안전 패턴, Arrays 유틸리티, 2차원 배열, 그리고 마지막에 살짝 맛본 가변 인자 (varargs) 까지 — 데이터를 묶는 법은 이제 익숙해지셨을 거예요.
그리고 지난 시간 마지막에 제가 두 가지 미끼를 던졌던 거 기억하시나요?
하나는 Step 6에서 살짝 등장한 static void printHashtags(String... tags)라는 낯선 형태였고,
다른 하나는 Step 7 종합 실습에서 평행 배열 세 개(postTitles, postLikes, postHashtags)를 다루면서 "한 사용자 출력에 네다섯 줄이 반복되는데, 이걸 어떻게 묶지?" 하는 답답함이었어요.
오늘은 그 두 가지를 한꺼번에 풀어드릴게요.
이름하여 메서드(method) — 같은 작업에 이름을 붙여두고 필요할 때마다 그 이름만 부르면 자동으로 실행되는 도구입니다.
오늘 이 도구 하나만 손에 넣으면, 지금까지 우리가 짠 길고 반복적인 main이 두세 줄짜리 로 깔끔해지는 마법을 직접 보게 될 거예요.
오늘의 주제는 "코드를 재사용하자 — 메서드" 입니다.
메서드를 이해하는 가장 쉬운 비유는 카페의 레시피 카드예요. "아메리카노 한 잔 만들기" 라는 동작을 손님이 올 때마다 "에스프레소 추출하고, 뜨거운 물 200ml 붓고, 종이컵에 옮기고, 뚜껑 닫고..." 라고 줄줄이 외워서 만든다고 생각해 보세요. 직원이 다섯 명이면 다섯 명이 각자 다르게 외우고, 누구는 한 단계 빠뜨리고, 어떤 분은 물의 양을 살짝 다르게 붓겠죠.
그래서 카페는 보통 레시피 카드를 만들어 둡니다. "아메리카노 한 잔 만들기" 라는 이름을 카드에 적어두고, 그 아래에 만드는 절차를 정리해 두는 거예요. 이제 손님이 "아메리카노 한 잔 주세요" 라고 하면, 직원은 "네, 아메리카노 한 잔!" 한 마디면 끝나요. 레시피 카드 안의 절차가 자동으로 실행되니까요.
자바의 메서드도 똑같아요. 한 번 정리해 둔 작업에 이름을 붙여두고, 그 이름만 부르면 안에 적힌 절차가 자동으로 실행됩니다. 영어로 method는 "방법, 절차"라는 뜻인데, "이 작업을 처리하는 방법"을 미리 적어두는 거죠. 어떤 분은 함수(function) 라고 부르기도 하는데, 자바에서는 메서드가 정식 이름이에요. 같은 개념인 줄 알고 계시면 OK.
메서드란? 반복되는 작업을 이름 붙여서 한 번만 작성해두고, 필요할 때마다 그 이름을 불러 실행하는 도구.
🎯 학습 목표
- 메서드를 정의하고 호출할 수 있다
- 매개변수 여러 개와 반환값을 활용해 데이터 흐름을 만들 수 있다
- 기본형과 배열을 메서드에 넘길 때의 차이(값 전달 vs 참조 전달)를 메모리 관점에서 이해할 수 있다
- 같은 이름의 메서드를 매개변수에 따라 여러 버전으로 만들 수 있다(메서드 오버로딩)
- 가변 인자(
varargs)로 0개부터 여러 개까지 유연하게 받는 메서드를 만들 수 있다 - 평행 배열에서 반복되는 출력 로직을 메서드로 추출해
main을 간결하게 만들 수 있다 - 재귀를 활용해 자기 자신을 부르는 메서드를 만들고, 반복문 버전과 비교할 수 있다
public static void main(String[] args)정식 형태의 의미를 단어별로 풀어볼 수 있다
Step 1: "같은 코드를 다섯 번 복붙하기 싫어요" — 메서드의 탄생
오프닝에서 카페 레시피 카드 비유로 메서드의 큰 그림을 잡아봤어요.
이번 Step에서는 그 비유가 자바 코드 위에서 어떤 답답함을 풀어주는지 직접 눈으로 확인하고, 오늘부터 main의 모양이 정식으로 바뀌는 이유까지 짚고 갑니다.
지난 시간 마지막 코드 다시 한 번
기억을 되살리기 위해 Day 5 Step 7의 마지막 코드를 머릿속에 떠올려 볼게요. 추천 사용자 5명을 화면에 뿌리는 평행 배열 코드였죠.
String[] usernames = {"jaehoon_dev", "minji_cafe", "seungwoo", "soyeon_art", "wooseok99"};
int[] followers = { 1240, 8500, 320, 4100, 15800 };
int[] posts = { 42, 150, 12, 88, 320 };
for (int i = 0; i < usernames.length; i++) {
System.out.print("@" + usernames[i]);
System.out.print(" 팔로워 ");
if (followers[i] >= 1_000) {
System.out.print((followers[i] / 1_000) + "." + ((followers[i] / 100) % 10) + "K");
} else {
System.out.print(followers[i]);
}
System.out.println(" 게시물 " + posts[i] + "개");
}
한 사용자를 출력하는 데에 네다섯 줄이 필요했어요. 그리고 이 출력 로직은 매번 똑같이 반복 됩니다. 지금은 사용자 5명이라 견딜 만한데, 만약 다른 화면에서도 같은 형태로 사용자 한 명을 출력해야 한다면 어떻게 될까요?
- 추천 사용자 목록 — 네다섯 줄
- 검색 결과 — 네다섯 줄 (똑같이)
- 프로필 페이지 상단 — 네다섯 줄 (역시 똑같이)
- 알림 화면의 보낸 사람 정보 — 또 네다섯 줄
화면 네 군데에 같은 출력 로직이 복붙으로 흩어져 있게 됩니다. 어느 날 디자이너가 "팔로워 표기를 K 대신 천 단위 콤마로 바꿔주세요" 라고 요청하면, 네 군데를 다 찾아가 똑같이 수정해야 해요. 하나라도 빠뜨리면 화면마다 표기가 달라지죠. 끔찍한 상황입니다.
오프닝의 "레시피 카드 없이 매번 절차를 외우는 직원" 비유가 바로 이런 상황을 가리킨 거였어요.
메서드를 손에 넣으면 어떻게 바뀌는가
오늘 우리가 만들 메서드 하나를 미리 보여드릴게요. 방금 봤던 다섯 줄짜리 출력 로직이 이렇게 변할 거예요.
// 변경 후 — main 안에서는 한 줄로 끝
printAllUsers(usernames, followers, posts);
다섯 줄짜리 반복 로직이 메서드 호출 한 줄 로 줄어요. 그리고 다른 화면에서 똑같이 한 사용자를 출력하고 싶을 때도 그 한 줄만 부르면 끝. 표기 방식을 바꿀 일이 생기면, 메서드 안의 코드 한 군데만 고치면 모든 호출이 자동으로 새 표기를 따라가요.
이 효율의 차이가 메서드의 진짜 매력입니다. "한 번 적어두고 여러 번 부른다" — 이 문장이 오늘 Day 6의 핵심이에요.
main의 모양이 오늘부터 바뀝니다
본격적인 첫 메서드를 만들기 전에 한 가지 더 짚고 갈 게 있어요. Day 1부터 Day 5까지 우리는 코드를 이렇게 시작했어요.
void main() {
System.out.println("안녕하세요");
}
void main() — 짧고 깔끔하죠. 이건 자바 25가 새로 제공한 간소화 형태 예요.
프로그래밍을 처음 시작하는 분들이 복잡한 키워드 줄에 막혀서 멈추지 않도록 자바가 배려해 둔 입구죠.
오늘부터는 정식 main 형태 로 바꿔서 갑니다. 이렇게 생겼어요.
public static void main(String[] args) {
System.out.println("안녕하세요");
}
처음 보면 "왜 갑자기 이렇게 길어졌지?" 싶으실 거예요.
이유는 단순합니다. 오늘 우리가 메서드를 본격적으로 배우는데, main 자체도 사실은 메서드였거든요.
그동안은 자바가 친절하게 숨겨줬던 부분이, 메서드를 배우는 오늘부터 원래 모양으로 드러나는 거예요.
각 단어의 의미를 한 줄씩만 짚고 갈게요. 자세한 풀이는 다음 Step들에서 차근차근 풀어갑니다.
public— "공개" 라는 뜻. 자바 실행기가 바깥에서 부를 수 있도록 열려 있어야 해서 붙어요.static— "클래스에 직접 붙어 있는" 메서드라는 표시. 우리는 Day 6 내내 모든 메서드 앞에static을 붙입니다. 진짜 의미는 Day 8 클래스에서 풀려요.void— "돌려주는 값이 없다" 는 표시. 이건 Step 2에서 바로 다뤄요.String[] args— "프로그램을 실행할 때 외부에서 받는 문자열 배열" 이에요. 지금은 거의 빈 배열로 들어와요. 자세한 활용은 나중에.
💡 튜터의 안내
지금은 "그냥 이런 모양이구나" 정도로 받아들이고 넘어가도 충분해요.
public,static,String[] args의 진짜 의미는 Day 6~Day 8 사이에 자연스럽게 풀려요. 오늘은 메서드 자체의 사용법에 집중해도 모든 코드가 잘 돌아갑니다.
🙋 학생 질문 — "튜터님, 그럼 지금까지 우리가 쓴 println 같은 것도 메서드였어요?"
네, 정답이에요! System.out.println(...)이 사실 메서드 호출이었습니다.
"화면에 한 줄을 출력하라" 라는 작업을 자바가 미리 만들어 둔 메서드고, 우리는 그 이름을 불러서 사용해 온 거죠.
Day 1 첫 줄부터 메서드를 쓰고는 있었던 셈이에요. 다만 직접 만들어 본 적이 없었던 것뿐.
오늘부터 우리가 직접 메서드를 만들기 시작합니다.
println의 친구를 한 명 더 만든다고 생각하시면 돼요.
큰 그림과 main의 형태 변화까지 짚었으니, 본격적인 첫 메서드를 만들러 Step 2로 가봅시다.
Step 2: "메서드 한 개를 직접 만들어 봅시다" — 정의와 호출
자, 이론은 충분히 짚었어요. 이번 Step에서는 진짜 메서드를 한 개 만들어 보고, 그걸 main에서 불러 봅시다.
"좋아요 수를 보기 좋게 포맷팅하는 메서드"를 만들어 볼 거예요.
인스타그램 화면에서 좋아요 수가 1,234일 때 "1.2K"라고 짧게 표시되는 거, 다들 보셨죠? 그 변환 로직을 메서드 한 개로 묶어보는 게 오늘의 첫 작품입니다.
메서드 없이 — 반복되는 코드의 답답함
먼저 메서드를 쓰지 않고 좋아요 수 두 개를 변환해 보겠습니다.
// day06/MethodBasics.java
public static void main(String[] args) {
System.out.println("=== 메서드 없이 — 같은 로직을 반복 ===");
int likesA = 1234;
String formattedA;
if (likesA >= 1_000_000) {
formattedA = (likesA / 1_000_000) + "." + ((likesA / 100_000) % 10) + "M";
} else if (likesA >= 1_000) {
formattedA = (likesA / 1_000) + "." + ((likesA / 100) % 10) + "K";
} else {
formattedA = String.valueOf(likesA);
}
System.out.println("게시물 A: " + formattedA);
int likesB = 45_678;
String formattedB;
if (likesB >= 1_000_000) {
formattedB = (likesB / 1_000_000) + "." + ((likesB / 100_000) % 10) + "M";
} else if (likesB >= 1_000) {
formattedB = (likesB / 1_000) + "." + ((likesB / 100) % 10) + "K";
} else {
formattedB = String.valueOf(likesB);
}
System.out.println("게시물 B: " + formattedB);
}
두 게시물의 좋아요 수를 바꾸는 데에 같은 if/else 구조가 두 번 반복됐어요.
숫자만 다를 뿐 로직은 100% 똑같죠.
지금은 두 개라서 견딜 만한데, 게시물 다섯 개·열 개를 처리하려면 이 if/else 덩어리를 그만큼 복사해야 합니다.
이게 오프닝에서 말씀드린 답답함이에요.
String.valueOf(...)는 자바가 미리 만들어 둔 메서드인데, 숫자를 문자열로 바꿔주는 역할을 해요.
String.valueOf(1234)을 부르면 "1234" 라는 문자열이 돌아옵니다.
그동안 + "" 같은 트릭으로 숫자를 문자열로 만들었었는데, 이 메서드를 쓰면 의도가 더 또렷해져요.
메서드 정의 — 작업에 이름을 붙이는 문법
이제 같은 로직을 formatLikes라는 이름의 메서드로 묶어 봅시다.
// 좋아요 수를 사람이 읽기 좋게 포맷팅
// 1234 -> "1.2K", 1234567 -> "1.2M", 999 -> "999"
static String formatLikes(int likes) {
if (likes >= 1_000_000) {
return (likes / 1_000_000) + "." + ((likes / 100_000) % 10) + "M";
}
if (likes >= 1_000) {
return (likes / 1_000) + "." + ((likes / 100) % 10) + "K";
}
return String.valueOf(likes);
}
한 줄씩 뜯어볼게요. 메서드 정의의 첫 줄에는 네 가지 정보가 들어 있어요.
static String formatLikes (int likes)
↑ ↑ ↑ ↑
① ② ③ ④
- ①
static— 오프닝에서 짚었던 그 키워드예요. 지금은 "모든 메서드 앞에 붙여 둔다" 정도로만 생각하시면 됩니다. 진짜 의미는 Day 8 클래스에서 풀려요. - ②
String— 반환 타입. 이 메서드가 결과로 돌려주는 값의 자료형이에요. 좋아요 포맷팅 결과는 문자열이니까String을 적어요. - ③
formatLikes— 메서드 이름. 우리가 이 작업에 붙이는 별명이에요. 영어 동사구가 자연스러워요 (formatLikes,printSeparator,calculateScore등). - ④
(int likes)— 매개변수(parameter). 메서드가 호출될 때 외부에서 받아오는 값이에요. 이 메서드는int타입의 숫자 하나를likes라는 이름으로 받아들이고, 그 값을 메서드 안에서 활용해요.
그리고 메서드 안쪽에서 return 키워드를 만나면, 그 자리에서 메서드가 끝나면서 값을 호출자에게 돌려줍니다.
return 다음에 오는 값이 ② 반환 타입과 일치해야 해요. 여기서는 문자열을 돌려주니까 String과 맞죠.
메서드 정의의 기본 모양
static 반환타입 메서드이름(매개변수타입 매개변수이름) { ... return 값; }
메서드 호출 — 부르면 그 안의 절차가 실행됨
이제 main에서 이 메서드를 부르면 됩니다.
System.out.println("게시물 A: " + formatLikes(1234));
System.out.println("게시물 B: " + formatLikes(45_678));
System.out.println("게시물 C: " + formatLikes(999));
System.out.println("게시물 D: " + formatLikes(1_234_567));
System.out.println("게시물 E: " + formatLikes(42));
다섯 번 호출했죠.
각 호출 안의 숫자 (1234, 45_678, ...)가 메서드의 매개변수 likes로 전달되고, 메서드 안의 if들이 차례대로 실행돼서 적절한 문자열이 return 으로 돌아옵니다.
호출자 (main) 입장에서는 메서드 안의 절차를 몰라도 돼요. formatLikes(1234) 하면 "1.2K" 가 돌아온다 는 약속만 알면 끝.
이게 메서드의 추상화 효과예요. 사용하는 쪽은 결과만 받으면 되고, 어떻게 그 결과가 만들어졌는지는 메서드 정의를 안 봐도 됩니다. 앞으로 다른 사람이 만든 메서드를 가져다 쓸 때도 똑같아요. 이름과 매개변수, 반환값만 알면 호출하면 끝.
실행하면 이렇게 출력돼요.
=== 메서드 없이 — 같은 로직을 반복 ===
게시물 A: 1.2K
게시물 B: 45.6K
=== 메서드로 묶어서 — 한 줄로 끝 ===
게시물 A: 1.2K
게시물 B: 45.6K
게시물 C: 999
게시물 D: 1.2M
게시물 E: 42
위 절반은 if/else 덩어리를 두 번 복붙해서 만든 결과, 아래 절반은 formatLikes 한 개를 다섯 번 호출해서 만든 결과예요.
두 결과는 동일하지만 코드 분량은 비교가 안 되죠.
void — 돌려주는 값이 없는 메서드
모든 메서드가 값을 돌려주는 건 아니에요.
"화면에 구분선 한 줄 출력하기" 같은 작업은 결과를 돌려줄 필요가 없죠. 화면에 글자를 찍기만 하고 끝.
이런 메서드는 반환 타입 자리에 void 라고 적어요. 영어로 void는 "비어 있다, 없다" 라는 뜻이에요.
// 화면에 구분선을 출력만 하고 돌려주는 값은 없음 (반환 타입 void)
static void printSeparator() {
System.out.println("--------------------");
}
호출하는 쪽은 이렇게 부릅니다.
printSeparator();
System.out.println("이 줄 위아래에 구분선이 찍혀요");
printSeparator();
호출 자체로 효과가 발생하고, 돌아오는 값은 없어요.
사실 우리가 그동안 가장 많이 부른 System.out.println(...)도 void 메서드예요. "글자를 찍는 부수 효과" 가 일어날 뿐, 돌려주는 값이 없죠.
void 메서드 안에서는 return 을 굳이 안 적어도 돼요. 메서드 본문이 끝나면 그대로 종료됩니다.
중간에 일찍 빠져나가고 싶을 때만 return; 한 줄을 (값 없이) 쓰면 돼요. 예를 들어 "조건이 안 맞으면 출력하지 말고 그냥 끝내자" 같은 경우.
💡 튜터의 안내
값을 돌려주는 메서드 (
String,int, ...) 와 안 돌려주는 메서드 (void) 를 구분하는 기준은 단순해요. "이 작업의 결과를 다른 곳에서 다시 쓸까?" 를 생각해 보세요.
- 좋아요 수 변환 → 결과 문자열을
println안에서 다시 써야 함 →String반환- 구분선 출력 → 화면에 찍히는 게 전부, 어디서 다시 쓸 일 없음 →
void
🙋 학생 질문 — "튜터님, return을 쓰면 그 뒤에 적어둔 코드는 실행 안 되나요?"
네, 정확해요! 메서드 안에서 return을 만나는 순간 메서드는 즉시 끝납니다.
그 줄 아래에 적힌 코드는 영원히 실행되지 않아요.
static String example(int n) {
if (n > 0) {
return "양수"; // 여기서 빠져나가면
}
System.out.println("이 줄은 n이 0 이하일 때만 실행됨");
return "0 이하";
}
if 안에서 return 으로 빠져나갔다면 그 아래 println 은 실행되지 않아요.
이걸 이른 반환(early return) 패턴이라고 부르는데, 조건이 충족되면 빨리 빠져나가는 깔끔한 코드 스타일이에요.
오늘 formatLikes 안에서도 세 줄의 return이 각자의 조건에서 일찍 빠져나가는 패턴을 쓰고 있어요.
자, 메서드 정의와 호출의 기본 골격을 손에 넣었어요. 다음 Step에서는 매개변수를 여러 개 받고, 반환값을 활용해 메서드끼리 데이터를 주고받는 방법을 익혀봅시다.
Step 3: "메서드 여러 개가 손을 잡고 일한다" — 매개변수와 반환값
Step 2에서 만든 formatLikes는 매개변수가 한 개였어요. 좋아요 수 하나를 넣으면 변환된 문자열이 돌아왔죠.
이번 Step에서는 매개변수 여러 개를 받는 메서드를 만들고, 한 메서드의 반환값을 다른 메서드의 매개변수로 흘려보내는 데이터 파이프라인을 익혀봅시다.
인스타그램의 추천 시스템을 단순화해서 흉내내볼게요. 사용자 한 명에게 다른 사용자를 "추천 친구"로 보여줄 때, 내부적으로 점수를 매기고 그 점수에 따라 "강력 추천 / 추천 / 보류" 같은 라벨을 붙이잖아요? 그 두 단계 (점수 계산 → 등급 분류)를 메서드 두 개로 만들고, 한 메서드의 결과를 다른 메서드가 받아먹는 흐름을 만들어 봅시다.
매개변수 여러 개 받기
추천 점수를 계산하는 메서드를 만들 거예요. 점수는 네 가지 정보를 종합해서 매깁니다.
- 서로 맞팔하는 친구가 몇 명인가? (
mutualFollows) - 관심 해시태그가 몇 개 겹치는가? (
commonHashtags) - 마지막 활동이 며칠 전인가? (
daysSinceActive) - 그 사용자의 게시물은 몇 개인가? (
postCount)
매개변수를 여러 개 받으려면, 괄호 안에 쉼표로 구분해서 차례대로 적으면 됩니다. 타입과 이름을 한 쌍씩.
// day06/ParameterAndReturn.java
// 추천 점수 계산
// - 서로 맞팔 수: 1명당 5점
// - 공통 해시태그 수: 1개당 3점
// - 마지막 활동 일수: 적을수록 좋음 → (30 - days) 점, 최대 30
// - 게시물 수: 1개당 1점 (단 100점 상한)
static int calculateRecommendScore(int mutualFollows, int commonHashtags, int daysSinceActive, int postCount) {
int score = 0;
score = score + mutualFollows * 5;
score = score + commonHashtags * 3;
int activityScore = 30 - daysSinceActive;
if (activityScore < 0) {
activityScore = 0;
}
score = score + activityScore;
int postScore = postCount;
if (postScore > 100) {
postScore = 100;
}
score = score + postScore;
return score;
}
메서드 시그니처 (첫 줄) 를 보면 매개변수가 네 개죠.
int mutualFollows, int commonHashtags, int daysSinceActive, int postCount — 각자 타입과 이름을 가지고 있어요.
호출하는 쪽에서는 이 순서대로 값을 넘겨주어야 합니다.
메서드 안쪽에서는 받은 값들로 점수를 계산하고, 마지막에 return score; 로 정수 한 개를 돌려줘요.
등급 분류 메서드 — 반환값을 다음 메서드가 받음
점수를 받았으니 이제 사람이 읽기 좋은 등급으로 분류해 봅시다.
// 점수를 사람이 읽는 등급으로 분류
static String classifyRecommendation(int score) {
if (score >= 150) {
return "강력 추천 (" + score + "점)";
}
if (score >= 80) {
return "추천 (" + score + "점)";
}
if (score >= 30) {
return "관심 있을 수도 (" + score + "점)";
}
return "추천 보류 (" + score + "점)";
}
이 메서드는 매개변수가 한 개 (int score) 이고, 결과로 String을 돌려줘요.
앞서 만든 calculateRecommendScore 의 결과 (int) 가 그대로 이 메서드의 매개변수 타입과 일치하죠.
이게 메서드 파이프라인 의 시작점이에요. 한 메서드의 출력이 다음 메서드의 입력으로 흘러가는 구조.
흐름 1 — 변수에 받아두고 단계별로 부르기
main에서 두 메서드를 차례로 부르면 이렇게 됩니다.
public static void main(String[] args) {
// (서로 맞팔 수, 공통 해시태그 수, 마지막 활동 일수, 게시물 수)
int scoreUserA = calculateRecommendScore(12, 5, 2, 30);
int scoreUserB = calculateRecommendScore(3, 0, 30, 5);
int scoreUserC = calculateRecommendScore(20, 8, 0, 100);
System.out.println("재훈님 추천 점수: " + scoreUserA);
System.out.println("민지님 추천 점수: " + scoreUserB);
System.out.println("승우님 추천 점수: " + scoreUserC);
// 메서드의 반환값을 다른 메서드의 매개변수로 — 흐름 연결
System.out.println("재훈님: " + classifyRecommendation(scoreUserA));
System.out.println("민지님: " + classifyRecommendation(scoreUserB));
System.out.println("승우님: " + classifyRecommendation(scoreUserC));
}
첫 단락에서 calculateRecommendScore(...) 의 결과를 scoreUserA, scoreUserB, scoreUserC에 받아뒀어요.
그리고 두 번째 단락에서 그 변수들을 classifyRecommendation(...) 의 인자로 넘기죠.
한 메서드의 결과가 변수를 거쳐 다음 메서드의 매개변수로 흘러가는 흐름이 한눈에 보여요.
실행 결과는 이렇습니다.
재훈님 추천 점수: 133
민지님 추천 점수: 20
승우님 추천 점수: 254
재훈님: 추천 (133점)
민지님: 추천 보류 (20점)
승우님: 강력 추천 (254점)
흐름 2 — 변수 없이 한 줄로 묶기
중간 변수가 굳이 필요 없을 때는 두 호출을 한 줄로 묶어버려도 돼요.
// 한 줄로 — 결과가 곧장 다음 메서드로 전달
System.out.println("새 사용자: " + classifyRecommendation(calculateRecommendScore(0, 0, 90, 1)));
이렇게 적으면 안쪽 호출 (calculateRecommendScore) 부터 먼저 실행되고, 그 결과 int 가 곧장 classifyRecommendation 의 매개변수로 들어가요. 그 결과 String 이 다시 + 로 이어져 println 으로 흘러가죠.
바깥 괄호를 풀려면 안쪽 호출의 결과부터 알아야 한다 는 게 핵심이에요. 자바가 안쪽부터 차근차근 평가해서 결과를 만들어요.
다만 너무 깊게 중첩하면 사람이 읽기 어려워요. 메서드 호출을 두세 단계 이상 한 줄로 묶을 거라면, 차라리 중간 변수를 둬서 단계마다 이름을 붙여주는 게 나을 때가 많아요.
💡 튜터의 안내
매개변수가 여러 개일 때 헷갈리는 자리는 순서예요.
calculateRecommendScore(12, 5, 2, 30)의 네 숫자가 각각 무엇인지, 호출하는 쪽에서 정의 시그니처를 안 보고 알아내기 어렵죠.그래서 실무 코드는 호출하는 줄 위에 짧은 주석을 두거나 (예:
// (mutualFollows, commonHashtags, daysSinceActive, postCount)), 매개변수 의미가 또렷한 자료형으로 묶는 패턴을 자주 씁니다. 후자는 Day 8 클래스에서 본격적으로 다뤄요.
🙋 학생 질문 — "튜터님, 매개변수 순서를 바꿔서 부르면 어떻게 돼요? 예: calculateRecommendScore(30, 2, 5, 12) 처럼."
좋은 질문이에요!
자바는 매개변수를 이름으로 매칭하지 않고 순서로만 매칭 해요.
그래서 (30, 2, 5, 12) 로 호출하면 자바는 묻지도 따지지도 않고:
- 첫 번째 자리
30→mutualFollows - 두 번째 자리
2→commonHashtags - 세 번째 자리
5→daysSinceActive - 네 번째 자리
12→postCount
이렇게 받아들여요. 컴파일 에러는 안 나지만, 의미가 완전히 어긋난 점수가 계산되겠죠. "맞팔 30명, 해시태그 2개, 5일 전 활동, 게시물 12개"의 점수가 나와요. 의도와 다른 결과.
타입이 같다면 (int 네 개) 자바는 막아주지 못해요. 그래서 매개변수 순서를 잘못 넘기는 실수는 초보자가 자주 만나는 버그 중 하나예요.
해결책은 두 가지예요.
- 호출하는 줄 위에 주석으로 의미를 적어두기 (방어 1)
- 여러 매개변수를 하나로 묶어주는 자료형에 담아 넘기기 — Day 8 클래스에서 배워요
매개변수가 여러 개여도 메서드의 본질은 같아요.
"받아서, 처리하고, 돌려준다."
다음 Step에서는 매개변수로 기본형 (int) 을 받을 때와 배열 (int[]) 을 받을 때 메모리 안에서 무엇이 달라지는지 들여다봅시다.
이번 Day에서 가장 중요한 개념 하나가 등장합니다.
Step 4: "넘긴 값이 바뀌나요, 안 바뀌나요?" — 값 전달 vs 참조 전달
오늘 Day 6에서 가장 까다로운 개념이 등장합니다. 천천히 가요.
지금까지 메서드에 넘긴 값은 모두 기본형 (int, String) 이었어요.
그런데 매개변수로 배열을 넘기면 살짝 신기한 일이 벌어집니다.
메서드 안에서 배열의 원소를 바꿨더니, 메서드 밖의 원본 배열도 같이 바뀌어 있더라 는 현상이에요.
같은 자바인데 기본형은 안 바뀌고, 배열은 바뀝니다. 왜일까요? 이 차이를 메모리 그림으로 풀어보면 명쾌해져요.
실험 1 — int 한 개를 넘기면
먼저 가장 단순한 경우. int 변수를 메서드에 넘기고, 메서드 안에서 그 값을 바꿔봅시다.
// day06/PassByValueVsReference.java
public static void main(String[] args) {
int followerCount = 100;
System.out.println("호출 전 followerCount: " + followerCount);
addOneFollower(followerCount);
System.out.println("호출 후 followerCount: " + followerCount);
}
// 기본형 int 를 받아 +1 한다 — 하지만 메서드 안의 n 만 바뀌고 원본은 안 바뀐다.
static void addOneFollower(int n) {
n = n + 1;
System.out.println(" (메서드 안) 받은 n + 1 = " + n);
}
실행하면 이렇게 나옵니다.
호출 전 followerCount: 100
(메서드 안) 받은 n + 1 = 101
호출 후 followerCount: 100
메서드 안에서는 n 이 101로 바뀐 게 분명히 보이는데, 메서드가 끝나고 main 으로 돌아오면 followerCount 는 그대로 100이에요.
메서드 안의 변경이 메서드 밖에 영향을 안 줍니다.
왜 이렇게 되는지 메모리 그림으로 봅시다.
[main 메서드의 메모리 칸]
┌──────────────────────────────┐
│ followerCount = 100 │ ← 호출 전
└──────────────────────────────┘
호출 시점 — addOneFollower(followerCount)
값 100 만 *복사* 돼서 새 칸으로 들어감
↓ 100 이라는 값만 복사 (원본과 연결 없음)
[addOneFollower 메서드의 메모리 칸]
┌──────────────────────────────┐
│ n = 100 │ ← 받은 값 (복사본)
└──────────────────────────────┘
↓ n = n + 1
┌──────────────────────────────┐
│ n = 101 │ ← 메서드 안에서만 바뀜
└──────────────────────────────┘
메서드가 끝나면 addOneFollower 의 메모리 칸은 사라지고
main 으로 돌아옴
[main 메서드의 메모리 칸]
┌──────────────────────────────┐
│ followerCount = 100 │ ← 그대로 100 (영향 받지 않음)
└──────────────────────────────┘
핵심은 화살표 부분이에요.
addOneFollower(followerCount) 라고 부르는 순간, 자바는 followerCount 의 값(100)을 복사해서 새로운 메모리 칸 (n) 에 담아 메서드에게 넘겨줘요.
이후 n 은 followerCount 와 완전히 별개의 변수예요. n 을 아무리 바꿔도 followerCount 와는 줄이 끊어진 상태죠.
이걸 값 전달 (pass by value) 이라고 불러요.
실험 2 — 배열을 넘기면
이번엔 배열을 넘겨봅시다.
int[] likesPerPost = {10, 20, 30, 40, 50};
System.out.println("호출 전 likesPerPost[0]: " + likesPerPost[0]);
doubleAllLikes(likesPerPost);
System.out.println("호출 후 likesPerPost[0]: " + likesPerPost[0]);
// 출력: [20, 40, 60, 80, 100]
// 배열을 받아 원소들을 두 배로 만든다 — 메서드 밖 원본도 같이 바뀐다.
static void doubleAllLikes(int[] arr) {
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i] * 2;
}
}
실행 결과는 이래요.
호출 전 likesPerPost[0]: 10
호출 후 likesPerPost[0]: 20
전체 배열: [20, 40, 60, 80, 100]
이번엔 메서드 밖의 원본 배열이 같이 바뀌어 있어요.
int 때와 정반대의 결과죠. 왜 그럴까요?
메모리 그림 — 배열은 "주소" 가 들어 있어요
배열 변수가 실제로 무엇을 담고 있는지가 핵심이에요.
int followerCount = 100; 같은 기본형은 변수 칸 안에 값 100 그 자체가 들어 있어요.
하지만 int[] likesPerPost = {10, 20, 30, 40, 50}; 같은 배열은 다릅니다. 변수 칸 안에는 배열이 보관된 위치 (주소) 만 들어 있고, 실제 다섯 개의 숫자는 다른 공간에 따로 보관돼요.
자바는 메모리를 크게 두 영역으로 나눠 써요.
- 스택 (stack) — 메서드별 지역 변수 칸이 모이는 영역. 메서드가 시작되면 생기고, 끝나면 사라져요.
- 힙 (heap) — 배열·객체 같은 "덩어리 데이터" 가 사는 영역.
new로 만든 것들은 다 여기에 자리잡아요.
배열 그림은 이렇게 됩니다.
[main 메서드의 스택 칸] [힙 메모리]
┌─────────────────────────┐ ┌──────────────────────────┐
│ likesPerPost → (주소) ┼──────────────→│ [10, 20, 30, 40, 50] │
└─────────────────────────┘ └──────────────────────────┘
스택에는 주소(화살표)만 보관 실제 다섯 개 숫자는 힙에 있음
호출 시점 — doubleAllLikes(likesPerPost)
*주소* 만 복사돼서 새 칸으로 들어감
↓ 주소(화살표) 복사 — 같은 배열을 가리키게 됨
[doubleAllLikes 메서드의 스택 칸] [힙 메모리]
┌─────────────────────────┐ ┌──────────────────────────┐
│ arr → (주소)┼──────────────→│ [10, 20, 30, 40, 50] │
└─────────────────────────┘ └──────────────────────────┘
main 이 가리키던 그 배열을 똑같이 가리킴
↓ arr[i] = arr[i] * 2 로 힙 안의 값을 직접 수정
[doubleAllLikes 메서드의 스택 칸] [힙 메모리]
┌─────────────────────────┐ ┌──────────────────────────┐
│ arr → (주소) ┼──────────────→│ [20, 40, 60, 80, 100] │
└─────────────────────────┘ └──────────────────────────┘
힙 안의 값이 변함!
메서드가 끝나고 main 으로 돌아오면
[main 메서드의 스택 칸] [힙 메모리]
┌─────────────────────────┐ ┌──────────────────────────┐
│ likesPerPost → (주소) ─┼──────────────→│ [20, 40, 60, 80, 100] │
└─────────────────────────┘ └──────────────────────────┘
같은 주소를 보고 있으니, 변경된 값을 똑같이 봄
핵심은 이거예요.
배열을 매개변수로 넘길 때 자바가 복사하는 건 주소 뿐이에요. 실제 배열 본체는 힙에 그대로 있고, 메서드 안의 arr 와 메서드 밖의 likesPerPost 가 같은 주소 를 가리킵니다.
그래서 arr[i] = arr[i] * 2 로 힙 안의 값을 바꾸면, 메서드 밖에서도 그 변경을 그대로 보게 되는 거예요.
이걸 자바에서는 참조 전달 (pass by reference) 이라고 부르는 게 보통이에요. 엄밀하게는 자바는 언제나 값 전달이고, 다만 배열의 경우 "주소라는 값" 을 복사해서 넘기는 거라고 설명하는 사람도 있어요. 이름이야 어떻든 학습 시점에서 중요한 건 현상입니다.
기억해 두실 한 줄 기본형 (
int,double,boolean등) 을 메서드에 넘기면 → 메서드 안의 변경이 밖에 안 보임. 배열 (또는 다른 객체) 을 메서드에 넘기면 → 메서드 안의 변경이 밖에서도 보임.
안전한 패턴 — 원본 보존하고 싶다면 새 배열로 돌려받기
배열을 매개변수로 넘기면 원본이 바뀐다는 동작이 항상 좋은 건 아니에요.
원본을 그대로 두고 변환된 결과만 받고 싶을 때는 메서드 안에서 새 배열을 만들어서 return 으로 돌려주는 패턴을 씁니다.
// 원본을 건드리지 않고 두 배 복사본을 새로 만들어 돌려준다 — 안전한 패턴.
static int[] makeDoubledCopy(int[] source) {
int[] result = new int[source.length];
for (int i = 0; i < source.length; i++) {
result[i] = source[i] * 2;
}
return result;
}
호출하는 쪽은 이렇게 써요.
int[] original = {1, 2, 3, 4, 5};
int[] doubled = makeDoubledCopy(original);
// original: [1, 2, 3, 4, 5] ← 변경 없음
// doubled : [2, 4, 6, 8, 10]
makeDoubledCopy 안쪽에서 new int[...] 로 새 배열을 힙에 만들어서 거기에 두 배 한 값을 채워넣고, 그 새 배열의 주소를 return 으로 돌려줘요.
원본 (source) 의 힙 공간은 손대지 않으니까 main 의 original 은 그대로 남아 있죠.
원본을 바꿀지 (doubleAllLikes 스타일) 새 배열을 돌려받을지 (makeDoubledCopy 스타일) 는 메서드 설계자의 선택이에요.
- 원본을 바꾸는 메서드 → 호출자가 원본을 그대로 두고 싶을 때 곤란. 호출 전후의 동작이 헷갈리기 쉬워요.
- 새 배열을 돌려주는 메서드 → 메모리를 한 번 더 잡아먹지만 호출자가 원본/변환본을 자유롭게 다룰 수 있음. 안전한 설계.
대체로 새 배열을 돌려주는 쪽이 안전하지만, 큰 배열을 자주 변환해야 할 때는 메모리 비용 때문에 원본 수정 스타일도 쓰여요. 상황에 맞게 선택 하는 거예요.
💡 튜터의 안내
자바를 처음 배울 때 이 차이를 헷갈리는 건 정말 자연스러워요. 지금은 두 가지만 기억해 두시면 충분합니다.
- 기본형은 메서드에 넘겨도 원본이 안전하다 (값이 복사돼서 넘어가니까).
- 배열·객체는 메서드에 넘기면 안쪽에서 한 수정이 원본에 그대로 반영된다 (주소가 복사돼서, 같은 데이터를 같이 보는 셈이니까).
Day 8 클래스에서 객체를 본격적으로 배울 때 이 차이가 한 번 더 등장해요. 그때 다시 만나면 익숙하실 거예요.
🙋 학생 질문 — "튜터님, String도 객체라고 들었는데, String을 메서드에 넘겨도 원본이 바뀌나요?"
좋은 질문이에요. 답은 살짝 까다로워요. String 은 객체가 맞지만, 한 번 만들어지면 내용을 못 바꾸도록 설계 되어 있어요. 이걸 불변(immutable) 이라고 불러요.
그래서 메서드 안에서 String 을 넘겨받아도, 그 안에 있는 글자를 마음대로 수정하는 방법이 애초에 없어요. 의도치 않게 원본이 바뀌는 사고는 일어나지 않습니다.
대신 자바는 "hello" 에서 "HELLO" 로 바꾸고 싶을 때 새 String 객체를 만들어서 돌려주는 방식을 써요. 예: "hello".toUpperCase() → 새로운 "HELLO" 가 생기고, 원본 "hello" 는 그대로.
Day 17 String API 단원에서 이 불변 설계를 본격적으로 다뤄요. 지금은 "String 은 객체지만 바뀌지 않게 설계돼 있다" 정도만 기억해두시면 충분합니다.
자, 메모리에서 무엇이 일어나는지까지 들여다봤어요. 다음 Step에서는 같은 이름의 메서드를 매개변수만 다르게 여러 개 만드는 오버로딩 (overloading) 을 배워봅시다. 새 메모리 개념은 등장하지 않으니 한숨 돌리고 가요.
Step 5: "이름은 같은데 받는 값이 달라요" — 메서드 오버로딩
Step 4에서 메모리까지 들여다봤으니 한숨 돌릴 시간입니다. 이번 Step은 가볍게 가요.
지금까지 우리가 만든 메서드들은 이름이 모두 달랐어요.
formatLikes, printSeparator, calculateRecommendScore, classifyRecommendation, doubleAllLikes ... 각자 다른 이름을 갖고 있죠.
그런데 자바에서는 같은 이름의 메서드를 매개변수만 다르게 여러 개 만들 수 있어요. 이걸 오버로딩(overloading) 이라고 부릅니다. 영어로 overload는 "많이 싣다, 한 이름에 여러 책임을 얹다" 라는 뜻이에요.
같은 이름, 다른 매개변수 — 인스타 사용자 소개
사용자 한 명을 한 줄 문자열로 소개하는 메서드를 만들어 봅시다.
그런데 어떤 화면에서는 사용자 이름만 알고 있고, 어떤 화면에서는 팔로워 수까지 알고 있고, 또 어떤 화면에서는 한 줄 소개까지 알고 있어요.
정보의 양에 따라 같은 이름 describeUser 의 세 가지 버전을 만들어 볼게요.
// day06/MethodOverloading.java
// 1. username 만 — 짧은 소개
static String describeUser(String username) {
return "@" + username;
}
// 2. username + 팔로워 수 — 팔로워까지 포함
static String describeUser(String username, int followers) {
return "@" + username + " (팔로워 " + followers + "명)";
}
// 3. username + 팔로워 + 한 줄 소개 — 풀 정보
static String describeUser(String username, int followers, String bio) {
return "@" + username + " (팔로워 " + followers + "명) — " + bio;
}
세 메서드 모두 이름이 describeUser 로 같아요.
그런데 받는 매개변수의 개수가 다르죠.
1번은 1개, 2번은 2개, 3번은 3개.
호출할 때는 이렇게 부릅니다.
public static void main(String[] args) {
System.out.println(describeUser("jaehoon_dev"));
System.out.println(describeUser("minji_cafe", 1240));
System.out.println(describeUser("seungwoo", 8500, "여행 좋아요"));
}
결과:
@jaehoon_dev
@minji_cafe (팔로워 1240명)
@seungwoo (팔로워 8500명) — 여행 좋아요
세 호출 모두 describeUser 라는 같은 이름을 쓰지만, 자바는 인자의 개수와 타입을 보고 어떤 버전을 부를지 자동으로 골라줍니다.
호출하는 쪽은 "describeUser 라는 작업을 하고 싶어요" 만 알면 되고, 정보가 한 개든 두 개든 세 개든 자연스럽게 같은 이름으로 부를 수 있어요.
매개변수 타입 이 달라도 오버로딩
매개변수의 개수만 달라도 되고, 타입만 달라도 오버로딩이 성립해요. 좋아요 수 포맷팅을 예로 들어볼게요.
// int 버전 — 일반 게시물
static String formatLikes(int likes) {
if (likes >= 1_000_000) {
return (likes / 1_000_000) + "." + ((likes / 100_000) % 10) + "M";
}
if (likes >= 1_000) {
return (likes / 1_000) + "." + ((likes / 100) % 10) + "K";
}
return String.valueOf(likes);
}
// long 버전 — 21억 넘는 초대형 게시물용 (이름은 같지만 매개변수 타입이 달라 따로 인식)
static String formatLikes(long likes) {
if (likes >= 1_000_000_000L) {
return (likes / 1_000_000_000L) + "." + ((likes / 100_000_000L) % 10) + "B";
}
if (likes >= 1_000_000L) {
return (likes / 1_000_000L) + "." + ((likes / 100_000L) % 10) + "M";
}
if (likes >= 1_000L) {
return (likes / 1_000L) + "." + ((likes / 100L) % 10) + "K";
}
return String.valueOf(likes);
}
int 의 최대값은 약 21억이에요. 인기 게시물 좋아요가 21억을 넘는 경우는 드물긴 하지만, 안전하게 long 버전을 별도로 두는 거예요.
System.out.println("작은 게시물: " + formatLikes(1234)); // int 버전 호출
System.out.println("초대형 게시물: " + formatLikes(5_000_000_000L)); // long 버전 호출
1234 는 그냥 정수 리터럴이라 int 로 인식되고, 5_000_000_000L 는 끝에 붙은 L 덕분에 long 으로 인식돼요.
자바는 인자의 타입을 보고 어떤 버전을 부를지 자동으로 판단합니다.
결과:
작은 게시물: 1.2K
초대형 게시물: 5.0B
오버로딩 규칙
자바가 오버로딩을 인정하려면 다음 중 하나라도 달라야 해요.
- 매개변수의 개수 가 다름
- 매개변수의 타입 이 다름
- 매개변수의 순서 가 다름 (예:
(int, String)과(String, int))
그리고 반환 타입만 달라서는 오버로딩이 안 돼요.
// 컴파일 에러! 매개변수 시그니처가 똑같으면 반환 타입이 달라도 구분 못 함
static int doSomething(int x) { ... }
static String doSomething(int x) { ... }
자바가 어떤 버전을 부를지는 호출하는 자리의 인자 만 보고 결정해요. 반환값은 호출 결과로 나오는 거지 호출 자체에는 영향을 안 주죠. 그래서 반환 타입만 다른 두 메서드를 두면 자바가 "어느 쪽을 불러야 할지" 판단 못 하게 됩니다.
어디서 오버로딩을 만나봤을까?
사실 우리는 Day 1부터 오버로딩을 모르고 써왔어요.
System.out.println(...) 를 떠올려 보세요.
System.out.println("문자열");
System.out.println(123);
System.out.println(3.14);
System.out.println(true);
System.out.println(); // 인자 없이
다섯 가지 다른 자료형을 같은 이름으로 호출하고 있어요. 자바 표준 라이브러리 안에 println(String), println(int), println(double), println(boolean), println() 등이 오버로딩으로 미리 정의 되어 있어서 가능한 거예요.
💡 튜터의 안내
오버로딩은 호출자 입장에서 인지 부하를 줄여주는 도구예요. 자료형마다 다른 이름 (
printlnString,printlnInt, ...) 을 외워야 한다면 끔찍할 텐데,println하나만 알면 어떤 자료형이든 같은 방식으로 출력할 수 있어요.다만 너무 많은 버전을 만들면 호출자가 헷갈릴 수 있어요. "이 인자 조합이면 어떤 버전이 불릴까?" 가 한눈에 안 보이는 오버로딩은 피하는 게 좋습니다.
🙋 학생 질문 — "튜터님, 그럼 매개변수만 정수 vs 실수로 다르게 한 오버로딩을 만들면, 자바가 정수를 실수로 자동 변환해서 호출해 버리지 않나요?"
날카로운 질문이에요! 자바는 오버로딩 후보가 여러 개일 때 가장 가까운 타입의 버전을 우선 호출해요.
예를 들어 formatLikes(int) 와 formatLikes(long) 두 개가 있고 formatLikes(1234) 라고 부르면, 1234 는 int 이니까 자바는 정확히 일치하는 int 버전을 부릅니다.
만약 formatLikes(int) 만 있고 formatLikes(long) 이 없는데 formatLikes(5_000_000_000L) 처럼 long 으로 호출하면? 컴파일 에러예요. 자바는 long 을 int 로 자동 변환해주지 않아요 (값이 잘릴 수 있으니까). 반대로 int 를 long 으로 자동 변환해주는 건 가능한데, 정확히 일치하는 int 버전이 있다면 그쪽이 우선이에요.
규칙이 살짝 복잡해 보이지만, 보통은 "자바는 가장 가까운 버전을 알아서 골라준다" 정도로 기억해두시면 충분합니다.
이름은 같지만 매개변수가 다른 메서드를 여러 버전으로 — 오버로딩의 첫인사였어요.
다음 Step에서는 매개변수의 개수를 미리 정해두지 않는 가변 인자 (varargs) 를 정식으로 다뤄봅시다.
지난 시간 살짝 본 점 세 개 (...) 의 정체를 풀어볼 시간이에요.
Step 6: "점 세 개의 정체" — varargs 정식 학습
지난 시간 마지막에 살짝 본 그 형태, 기억나시나요?
static void printHashtags(String... tags) {
...
}
String... — 타입 뒤에 붙은 점 세 개. 처음 본 분들은 "이게 뭐지?" 하고 멈칫하셨을 거예요.
지난 시간엔 "메서드를 다음에 본격적으로 배운다" 라며 맛만 보고 넘어갔는데, 오늘이 그 다음 시간입니다.
드디어 정식으로 다뤄봐요.
문제 — 매개변수 개수가 미리 안 정해질 때
지금까지 우리가 만든 메서드는 매개변수 개수가 미리 정해져 있었어요.
formatLikes(int) 는 정확히 1개, calculateRecommendScore(int, int, int, int) 는 정확히 4개.
그런데 인스타그램 게시물의 해시태그를 출력하는 메서드를 만든다고 생각해 봅시다. 어떤 게시물은 해시태그가 1개, 어떤 게시물은 3개, 어떤 게시물은 10개, 어떤 게시물은 아예 없을 수도 있어요. 호출하는 쪽이 매번 다른 개수의 인자를 넘기고 싶다 는 거죠.
오버로딩으로 풀어볼까요?
static void printHashtags(String t1) { ... }
static void printHashtags(String t1, String t2) { ... }
static void printHashtags(String t1, String t2, String t3) { ... }
static void printHashtags(String t1, String t2, String t3, String t4) { ... }
// ... 끝없이 ...
해시태그 개수가 5개·10개·100개까지 갈 수 있다면 오버로딩만으로는 못 풀어요. 그렇다고 호출자가 배열을 만들어서 넘기게 강요하는 것도 번거롭죠.
// 호출 때마다 배열을 만들어 넘기는 건 번거로워요
printHashtags(new String[]{"#카페", "#디저트"});
printHashtags(new String[]{"#OOTD"});
이 두 가지 답답함을 한꺼번에 풀어주는 도구가 varargs (가변 인자, variable arguments) 예요.
점 세 개의 의미
문법은 정말 간단해요. 매개변수의 타입 뒤에 점 세 개 를 붙이면 됩니다.
// day06/VarArgsBasics.java
// String... tags 는 0개 이상의 String 을 배열로 받음
// 메서드 안에서는 그냥 String[] 처럼 다룬다.
static void printHashtags(String... tags) {
System.out.print("해시태그 " + tags.length + "개: ");
if (tags.length == 0) {
System.out.println("(없음)");
return;
}
for (int i = 0; i < tags.length; i++) {
System.out.print(tags[i]);
if (i < tags.length - 1) {
System.out.print(" ");
}
}
System.out.println();
}
String... tags 를 풀어 읽으면 "0개 이상의 String 을 묶어서 배열로 받겠다" 라는 뜻이에요.
메서드 안쪽에서는 tags 가 평범한 String[] 배열처럼 동작합니다.
tags.length 로 개수도 알 수 있고, tags[0] 으로 인덱스 접근도 되고, for (String tag : tags) 같은 enhanced for 순회도 그대로예요.
Day 5에서 배운 배열 다루는 방법 그대로 쓰면 돼요.
호출자는 자유롭게
이제 호출하는 쪽이 편해져요.
printHashtags("#일상"); // 1개
printHashtags("#카페", "#디저트"); // 2개
printHashtags("#OOTD", "#패션", "#가을", "#스트릿"); // 4개
printHashtags(); // 0개도 OK
호출자는 그냥 쉼표로 인자를 넘기면 끝. 자바가 알아서 그 인자들을 배열로 묶어서 메서드에 전달해줘요.
실행 결과:
해시태그 1개: #일상
해시태그 2개: #카페 #디저트
해시태그 4개: #OOTD #패션 #가을 #스트릿
해시태그 0개: (없음)
같은 메서드 하나로 1개·2개·4개·0개를 모두 처리했어요. 이게 varargs의 핵심 매력입니다.
int... 도 똑같이
타입이 String 이어야만 되는 건 아니에요. int..., double..., boolean... 등 어떤 타입에도 점 세 개를 붙일 수 있어요.
// int... 도 같은 원리 — 0개 이상의 int 를 배열로 받음
static int sumLikes(int... likesArray) {
int total = 0;
for (int likes : likesArray) {
total = total + likes;
}
return total;
}
System.out.println("게시물 2개 합계: " + sumLikes(120, 340)); // 460
System.out.println("게시물 5개 합계: " + sumLikes(120, 340, 50, 700, 900)); // 2110
System.out.println("게시물 0개 합계: " + sumLikes()); // 0
빈 호출 (sumLikes()) 도 가능해요. 메서드 안에서는 likesArray.length == 0 이라 합계가 0이 됩니다.
고정 매개변수 + varargs 혼합
매개변수 중 일부는 고정으로 두고, 마지막에만 가변 인자를 받는 형태도 가능해요.
// 고정 매개변수가 앞에 오고, varargs 는 *반드시 맨 뒤* 에만 올 수 있음.
// introduce(String name, String... titles) — name 은 1개 고정, titles 는 0개 이상.
static void introduce(String name, String... titles) {
System.out.print(name + " 님");
if (titles.length == 0) {
System.out.println(" (소개 없음)");
return;
}
System.out.print(" — ");
for (int i = 0; i < titles.length; i++) {
System.out.print(titles[i]);
if (i < titles.length - 1) {
System.out.print(", ");
}
}
System.out.println();
}
introduce("재훈", "개발자", "사진가", "커피러버"); // 직함 3개
introduce("민지", "디자이너"); // 직함 1개
introduce("승우"); // 직함 0개
결과:
재훈 님 — 개발자, 사진가, 커피러버
민지 님 — 디자이너
승우 님 (소개 없음)
여기서 중요한 규칙 하나. varargs 매개변수는 반드시 매개변수 목록의 맨 뒤에만 올 수 있어요.
// OK
static void f(String name, String... titles) { ... }
// 컴파일 에러 — varargs 가 중간에 오면 자바가 어디까지가 varargs 인지 알 수 없음
static void f(String... titles, String name) { ... }
생각해 보면 당연한 규칙이에요.
introduce("재훈", "개발자", "사진가", "커피러버") 라고 부르면 자바가 "재훈은 name, 그 뒤는 모두 titles" 라고 자연스럽게 갈라낼 수 있죠. 그런데 varargs가 앞에 있다면 어디까지가 가변 인자인지 자바가 판단할 수 없어요.
배열도 그대로 넘길 수 있음
이미 배열을 들고 있는데 varargs 메서드에 넘기고 싶다면, 배열을 그대로 전달해도 돼요.
String[] preset = {"#운동", "#오운완", "#홈트"};
printHashtags(preset); // 배열을 그대로 넘겨도 OK
자바는 String... 와 String[] 를 메서드 시그니처상 거의 같게 봐줘요.
이미 다른 메서드에서 만든 배열을 그대로 흘려보낼 때 편하게 쓸 수 있어요.
💡 튜터의 안내
varargs 는 "호출자에게 자유를 주는" 도구예요. 메서드를 만드는 사람 입장에서 "이 메서드를 부르는 사람이 1개를 넘길지 5개를 넘길지 모를 때" 망설임 없이
...을 쓰면 돼요.대표적인 예가
String.format(...),List.of(...),Arrays.asList(...)같은 자바 표준 라이브러리 메서드들입니다. 모두 varargs 로 설계돼서 "0개부터 N개까지 넘기는 자유" 를 호출자에게 주죠.
🙋 학생 질문 — "튜터님, 그럼 한 메서드에 varargs 를 두 개 넣을 순 없나요? 예: f(int... nums, String... tags)"
좋은 추측이에요! 답은 안 됩니다.
이유는 단순해요. varargs가 두 개 있으면 자바가 호출 인자를 어떻게 나눌지 정할 수 없어요.
// 컴파일 에러 — 두 varargs 의 경계를 자바가 판단 못 함
static void f(int... nums, String... tags) { ... }
f(1, 2, 3, "#카페", "#책");
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
// int 가 어디서 끝나고 String 이 어디서 시작하는지 불명
타입이 다르면 자바가 자동 판단할 수 있을 것 같지만, 자바의 문법 설계상 "맨 뒤에 단 한 개" 만 허용해요.
여러 종류의 가변 인자를 한 메서드에서 받고 싶다면, 한쪽을 배열로 명시적으로 묶어서 받는 식으로 풀어요.
static void f(int[] nums, String... tags) { ... }
f(new int[]{1, 2, 3}, "#카페", "#책"); // 정수 배열 + 가변 해시태그
자, varargs 까지 손에 넣었어요. 지난 시간의 미끼 하나가 풀렸습니다.
다음 Step에서는 또 다른 미끼였던 평행 배열 출력 로직 을 메서드로 추출해서 main 을 깔끔하게 정리해봅시다.
오늘 Day 6 의 절반이 지난 시간 두 미끼를 회수하는 자리였네요.
Step 7: "main 이 두 줄짜리로 깔끔해진다" — 평행 배열을 메서드로 추출
지난 시간 두 번째 미끼를 풀 시간이에요.
Day 5 Step 7 종합 실습에서 평행 배열 세 개 (postTitles, postLikes, postHashtags) 를 다뤘었죠.
한 게시물을 출력하는 데에 네다섯 줄이 반복됐던 그 답답함, 지금까지 익힌 메서드로 풀어봅시다.
이번 Step의 목적은 두 가지예요.
첫째, 반복되는 출력 로직을 메서드로 추출 해서 main 을 깔끔하게 만드는 실전 패턴 익히기.
둘째, 이렇게 메서드로 묶어도 여전히 남는 불편함 을 짚어서, 다음 시간 클래스 단원의 동기를 미리 심어두기.
Before — 출력 로직이 main 안에 가득
먼저 Day 5 식 코드를 다시 한 번 봅시다. 추천 사용자 5명을 출력해요.
// day06/ParallelArrayMethods.java
// 추천 사용자 5명 — 평행 배열 패턴 (같은 인덱스가 한 사람을 가리킴)
String[] usernames = {"jaehoon_dev", "minji_cafe", "seungwoo", "soyeon_art", "wooseok99"};
int[] followers = { 1240, 8500, 320, 4100, 15800 };
int[] posts = { 42, 150, 12, 88, 320 };
for (int i = 0; i < usernames.length; i++) {
// 한 사용자를 출력하는 데에만 4~5 줄이 필요
System.out.print("@" + usernames[i]);
System.out.print(" 팔로워 ");
if (followers[i] >= 1_000) {
System.out.print((followers[i] / 1_000) + "." + ((followers[i] / 100) % 10) + "K");
} else {
System.out.print(followers[i]);
}
System.out.println(" 게시물 " + posts[i] + "개");
}
for 루프 안에 한 사용자 출력 로직이 다섯 줄짜리로 들어가 있어요.
이 다섯 줄을 똑같이 검색 결과 화면이나 알림 화면에서도 써야 한다면 복붙이 시작되겠죠.
메서드 추출 — printOneUser
가장 작은 단위부터 메서드로 묶어볼게요. 한 사용자를 한 줄로 출력하는 메서드.
// 한 사용자의 한 줄 출력 — 추출 핵심
static void printOneUser(String username, int followerCount, int postCount) {
System.out.print("@" + username);
System.out.print(" 팔로워 " + formatCount(followerCount));
System.out.println(" 게시물 " + postCount + "개");
}
// 1000 이상이면 K 표기로 — 통계 출력에서 재사용
static String formatCount(int count) {
if (count >= 1_000) {
return (count / 1_000) + "." + ((count / 100) % 10) + "K";
}
return String.valueOf(count);
}
printOneUser 는 매개변수 세 개 (username, followerCount, postCount) 를 받아서 한 줄을 화면에 출력해요. 반환값은 없으니까 void.
그리고 그 안에서 또 다른 메서드 formatCount 를 부르고 있죠. 메서드 안에서 메서드를 부르는 자연스러운 합성이에요.
formatCount 는 Step 2의 formatLikes 와 거의 같은 로직인데, 이름을 "좋아요" 가 아닌 "개수" 로 일반화했어요. 팔로워든 게시물이든 어떤 숫자든 보기 좋게 K 표기로 바꿔주는 도구죠.
한 단계 더 — printAllUsers
printOneUser 가 있으니, 평행 배열 세 개를 받아 전체를 한 번에 출력하는 메서드 도 쉽게 만들 수 있어요.
// 전체 순회 — 평행 배열 3개를 받아 한 사람씩 출력에 위임
static void printAllUsers(String[] usernames, int[] followers, int[] posts) {
for (int i = 0; i < usernames.length; i++) {
printOneUser(usernames[i], followers[i], posts[i]);
}
}
이 메서드는 for 루프만 갖고 있어요. 한 줄 출력은 printOneUser 에게 위임하죠.
자기가 잘하는 일 (전체 순회) 만 책임지고, 나머지 (한 줄 출력) 는 다른 메서드에게 맡깁니다. 메서드 설계의 좋은 패턴이에요.
After — main 이 두 줄로
이제 main 은 이렇게 변합니다.
public static void main(String[] args) {
String[] usernames = {"jaehoon_dev", "minji_cafe", "seungwoo", "soyeon_art", "wooseok99"};
int[] followers = { 1240, 8500, 320, 4100, 15800 };
int[] posts = { 42, 150, 12, 88, 320 };
System.out.println("=== 메서드로 묶고 나면 — main 이 두 줄짜리로 깨끗해짐 ===");
printAllUsers(usernames, followers, posts);
System.out.println();
System.out.println("=== 한 사람만 따로 출력하고 싶을 때도 재사용 ===");
printOneUser(usernames[2], followers[2], posts[2]);
}
for 루프와 다섯 줄짜리 출력 로직이 모두 사라졌어요.
main 은 데이터 정의하고 printAllUsers 한 줄 부르는 것만 남았죠.
다른 화면에서 한 사람만 따로 출력하고 싶을 때도 printOneUser 한 줄로 끝.
표기 방식을 바꿀 일이 생기면? formatCount 안의 코드 한 줄만 고치면 모든 호출 자리에 자동으로 반영돼요.
오프닝에서 약속드린 "한 군데만 고치면 다 따라온다" 가 드디어 실현됐어요.
평행 배열 → 통계 메서드들
같은 평행 배열을 가지고 통계를 내는 메서드도 추가해 봅시다. 합계와 평균 같은 거요.
// 팔로워 합계
static int sumFollowers(int[] followers) {
int total = 0;
for (int f : followers) {
total = total + f;
}
return total;
}
// 평균 게시물 수 (소수점 한 자리 정수 근사)
static int averagePosts(int[] posts) {
if (posts.length == 0) {
return 0;
}
int total = 0;
for (int p : posts) {
total = total + p;
}
return total / posts.length;
}
main 에서 부르면 이렇게 됩니다.
System.out.println("팔로워 합계: " + sumFollowers(followers));
System.out.println("평균 게시물 수: " + averagePosts(posts));
데이터는 평행 배열에 있고, 그 배열을 받아서 의미 있는 숫자를 돌려주는 메서드들이 각자 책임을 나눠 가지고 있어요.
실행 결과:
@jaehoon_dev 팔로워 1.2K 게시물 42개
@minji_cafe 팔로워 8.5K 게시물 150개
@seungwoo 팔로워 320 게시물 12개
@soyeon_art 팔로워 4.1K 게시물 88개
@wooseok99 팔로워 15.8K 게시물 320개
팔로워 합계: 29960
평균 게시물 수: 122
그래도 남는 불편함 — 다음 시간으로 가는 다리
자, 메서드 추출로 main 이 깔끔해진 건 분명해요. 그런데 한 가지 불편함이 남아 있는 거 느껴지시나요?
printOneUser, printAllUsers, sumFollowers, averagePosts 같은 메서드들 모두 매개변수가 세 개씩 따로 다녀요.
printOneUser(usernames[i], followers[i], posts[i]); // 인자 3개
printAllUsers(usernames, followers, posts); // 배열 3개
"같은 인덱스가 한 사용자를 가리킨다" 는 약속만 유지되면 동작은 하지만, 메서드를 부를 때마다 세 배열을 일일이 챙겨 넘겨야 해요. 새 정보 (예: 가입 날짜, 비공개 여부) 가 추가되면 모든 메서드 시그니처에 매개변수를 추가하는 대공사 가 벌어집니다. "사용자 한 명을 통째로 하나의 자료형에 담을 수 있다면" 이 답답함이 풀릴 텐데... 라는 동기가 자연스럽게 생기죠.
다음 시간으로 가는 다리 이 답답함은 다음 시간 즈음에 만날 클래스(class) 가 풀어줍니다. "사용자 한 명" 을 하나의 자료형으로 묶어서 평행 배열 대신 그 자료형의 배열을 쓰는 방향으로 진화하게 돼요. 지금은 "메서드만으로는 평행 배열의 한계를 완전히 풀지는 못한다" 는 감각만 잡고 가시면 충분합니다.
💡 튜터의 안내
메서드 추출은 나중에 큰 변경을 쉽게 만들어주는 투자 예요. 지금 다섯 줄 로직을 메서드로 묶어두면, 표기 방식을 바꿀 때 한 군데만 고치면 끝나죠. 반대로 메서드 없이 흩어두면, 변경 시점에 모든 자리를 일일이 찾아야 해요.
코드를 처음 짤 때 "이 로직이 한 번만 쓸 건지, 여러 번 쓸 건지" 를 잠깐 떠올려 보세요. 여러 번 쓸 가능성이 보인다면 메서드로 묶어두는 게 거의 항상 이득이에요.
🙋 학생 질문 — "튜터님, 평행 배열의 한계를 풀려고 메서드 추출을 한 거잖아요. 그런데 또 평행 배열을 메서드로 넘기게 되니, 결국 평행 배열을 진짜 풀어준 건 아닌 거 같아요."
정확한 지적이에요! 오늘 Step 7의 메서드 추출은 출력 로직의 반복 을 푼 거지, 평행 배열 그 자체의 구조적 한계 를 푼 건 아니에요.
평행 배열은 "같은 인덱스를 약속" 으로 유지하는 구조라서, 메서드로 묶는다고 그 약속이 사라지진 않아요. 메서드의 매개변수로 세 배열이 항상 같이 다녀야 한다는 부담도 그대로 남고요.
이 본질적인 한계를 푸는 건 객체(object) 의 개념이에요. 사용자 한 명을 하나의 묶음 자료형 에 담는 거죠.
// 미리 보기 (Day 8 에서 본격 학습)
User u1 = new User("jaehoon_dev", 1240, 42);
printOneUser(u1); // 인자 3개 → 인자 1개로
다음 시간 (Day 7 종합 실습) 까지 메서드만으로 갈 수 있는 데까지 가본 뒤, Day 8 에서 클래스를 만나면 "아, 이 답답함을 이렇게 풀어주는 도구가 있었구나" 하는 후련함을 느끼게 될 거예요. 지금은 그 답답함을 살짝 묻어두시면 됩니다.
자, 평행 배열 출력 로직을 메서드로 묶어서 main 을 정리했어요.
마지막 Step에서는 메서드가 자기 자신을 부르는 신기한 패턴, 재귀 (recursion) 를 가볍게 만나봅시다.
Step 8: "자기 자신을 부르는 메서드?" — 재귀 기초
Day 6의 마지막 Step입니다. 좀 신기한 패턴 하나를 만나봐요.
지금까지 우리가 본 메서드 호출은 늘 다른 메서드를 부르는 형태였어요.
main 이 formatLikes 를 부르고, printAllUsers 가 printOneUser 를 부르고, 그런 식이죠.
그런데 메서드가 자기 자신을 부르는 경우도 있어요. 이걸 재귀(recursion) 라고 불러요. 영어로 recursion은 "다시 돌아옴" 이라는 뜻이에요.
처음 들으면 "무한히 자기 호출하면 끝나지 않을 텐데?" 싶으실 거예요. 정확한 직관이에요. 재귀가 무한 호출에 빠지지 않으려면 종료 조건 (base case) 을 반드시 둬야 합니다. "이 조건에서는 자기 자신을 안 부르고 그냥 값을 돌려준다" 라는 출구를 만들어두는 거죠.
가장 익숙한 예 — 팩토리얼
수학에서 배운 팩토리얼 (n!) 을 기억하시나요?
5! = 5 × 4 × 3 × 2 × 1 = 120
3! = 3 × 2 × 1 = 6
1! = 1
0! = 1 (수학적 정의)
5! 을 다시 보면 5 × 4! 이에요. 4! 은 4 × 3! 이고요. 이런 식으로 자기보다 1 작은 팩토리얼을 안에 담고 있죠. 재귀가 자연스럽게 잘 어울리는 패턴이에요.
자바 코드로 옮기면 이렇게 됩니다.
// day06/RecursionBasics.java
// 팩토리얼 — n! = n * (n-1)!
// 종료 조건 (base case): n <= 1 이면 1 을 그대로 돌려준다 (더 이상 자기 호출 안 함).
static long factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
여섯 줄 안에 재귀의 본질이 다 들어 있어요.
- 첫 줄
if (n <= 1) return 1;— 종료 조건. n이 1 이하면 그냥 1을 돌려주고 끝. - 마지막 줄
return n * factorial(n - 1);— 자기 호출. 자기보다 1 작은 값으로 자기 자신을 다시 부르고, 그 결과에 n을 곱해서 돌려줘요.
호출 흐름을 손으로 따라가 봅시다. factorial(5) 를 부르면:
factorial(5) = 5 * factorial(4)
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 ← 종료 조건, 자기 호출 안 함
여기서부터 거꾸로 돌아오면서 곱셈이 이루어져요.
factorial(1) = 1
factorial(2) = 2 * 1 = 2
factorial(3) = 3 * 2 = 6
factorial(4) = 4 * 6 = 24
factorial(5) = 5 * 24 = 120
호출이 깊이 들어갔다가, 종료 조건에 도달한 뒤 차곡차곡 곱하면서 결과를 만들어 돌아오는 흐름이에요.
같은 일을 반복문으로도 — 비교
팩토리얼은 반복문으로도 똑같이 만들 수 있어요.
// 반복문 버전 — 비교용
static long factorialLoop(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result = result * i;
}
return result;
}
factorial(5) 든 factorialLoop(5) 든 결과는 120 으로 같아요.
재귀 버전은 더 짧고 수학 정의를 그대로 옮긴 듯하고, 반복문 버전은 좀 더 흔한 절차적 풀이로 보여요.
어느 쪽을 쓸지는 상황에 따라 달라요. 종료 조건이 또렷하고 재귀 구조가 자연스러운 문제 (예: 트리 탐색) 는 재귀가 깔끔하고, 단순 누적 계산은 반복문이 더 효율적일 때가 많아요. 어느 쪽이든 동작은 같다 는 게 핵심이에요.
두 번 부르는 재귀 — 피보나치
피보나치 수열은 한 함수 안에서 자기 자신을 두 번 부르는 재귀의 대표 예시예요.
수열의 정의는 단순해요.
fib(0) = 0
fib(1) = 1
fib(n) = fib(n-1) + fib(n-2) (n >= 2)
앞 두 수를 더해서 다음 수가 만들어지는 패턴이죠. 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
자바 코드:
// 피보나치 — fib(n) = fib(n-1) + fib(n-2)
// 종료 조건: n <= 1 이면 n 자체를 돌려준다 (0 또는 1).
// 자기 자신을 *두 번* 부르기 때문에 같은 계산을 중복하게 됨 → 큰 n 은 매우 느려요.
static int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(5) 면 결과는 5, fibonacci(10) 면 55, fibonacci(20) 면 6765 가 나와요.
그런데 한 가지 함정이 있어요.
fibonacci(40) 부터는 수 초가 걸려요. 같은 계산을 엄청나게 중복하기 때문에 호출 횟수가 폭발적으로 늘어나거든요.
fibonacci(5) 의 호출 트리를 그려보면:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
/ \
fib(1) fib(0)
fib(2) 가 세 번, fib(1) 이 다섯 번, fib(0) 이 세 번 계산돼요.
같은 값을 매번 다시 만드는 거죠. n이 커질수록 이 낭비가 기하급수적으로 늘어요.
이런 낭비를 피하려면 반복문 버전이 훨씬 빨라요.
// 피보나치 반복문 버전 — O(n) 으로 훨씬 빠름
static long fibonacciLoop(int n) {
if (n <= 1) {
return n;
}
long prev = 0;
long curr = 1;
for (int i = 2; i <= n; i++) {
long next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
fibonacciLoop(40) 은 눈 깜짝할 사이에 결과 (102334155) 가 나옵니다.
재귀가 항상 좋은 선택은 아니에요. 호출 트리에 같은 계산이 반복되는 구조라면 반복문이 훨씬 빠르고, 자기 호출이 깊어지면 스택 오버플로(StackOverflowError) 라는 에러를 만날 수도 있어요. 자바는 메서드 호출마다 스택 메모리를 잡아두는데, 재귀가 너무 깊어지면 그 공간이 바닥나요.
재귀를 쓸지 말지 판단할 때 종료 조건이 또렷하고, 호출 트리에 중복이 적고, 깊이도 합리적이라면 → 재귀가 깔끔. 같은 계산을 자꾸 다시 하거나 깊이가 수만 단계로 갈 위험이 있다면 → 반복문이 안전.
흐름을 눈으로 — 카운트다운
마지막으로 재귀의 흐름을 출력으로 확인하는 간단한 예제.
// 카운트다운 — 재귀가 *위에서 아래로* 흘러가는 모습을 출력으로 보여줌
static void countdown(int n) {
if (n <= 0) {
return; // 종료 조건
}
System.out.println(" 카운트: " + n);
countdown(n - 1); // 자기 호출 — n 이 하나씩 줄어듦
}
countdown(5) 를 부르면:
카운트: 5
카운트: 4
카운트: 3
카운트: 2
카운트: 1
각 호출이 자기보다 1 작은 값으로 자기 자신을 부르고, n이 0이 되는 순간 종료 조건이 작동해서 자기 호출이 멈춰요. 다섯 번의 호출이 위에서 아래로 펼쳐지는 흐름이 한눈에 보이죠.
💡 튜터의 안내
재귀는 처음 만나면 머리가 빙빙 도는 개념이에요. 너무 깊이 파고들지 않으셔도 돼요. 지금은 종료 조건 + 자기 호출 두 가지 요소만 기억해두시면 충분합니다.
재귀가 본격적으로 빛나는 자리는 트리·그래프 탐색 이나 분할 정복 알고리즘 같은 데인데, 자료구조와 알고리즘을 본격 다루는 단원에서 다시 만나게 돼요. 오늘은 "자바 메서드는 자기 자신도 부를 수 있구나" 와 "종료 조건이 없으면 무한 호출에 빠진다" 두 가지만 챙기시면 됩니다.
🙋 학생 질문 — "튜터님, 재귀의 호출이 끝나는 시점에 자바는 어떻게 결과를 차곡차곡 곱해서 돌려보내나요?"
좋은 질문이에요!
자바는 메서드를 호출할 때마다 스택 (stack) 이라는 메모리 공간에 호출 기록을 쌓아둬요.
factorial(5) 가 factorial(4) 를 부르면 스택에 factorial(5) 의 상태 (n=5, 계산 중) 가 쌓이고, 그 위에 factorial(4) 가 올라가요.
[스택 — 호출 기록]
factorial(1) ← 현재 실행 중 (1 반환)
factorial(2) ← n=2, factorial(1) 기다리는 중
factorial(3) ← n=3, factorial(2) 기다리는 중
factorial(4) ← n=4, factorial(3) 기다리는 중
factorial(5) ← n=5, factorial(4) 기다리는 중
factorial(1) 이 1을 돌려주고 끝나면 스택에서 사라지고, 그 결과가 바로 위 factorial(2) 의 n * factorial(n-1) 식에서 factorial(1) 자리에 들어가요. 그래서 2 * 1 = 2 가 돼서 factorial(2) 가 끝나고... 이런 식으로 스택이 위에서부터 차곡차곡 비워지면서 결과가 위로 흘러가요.
재귀의 깊이가 너무 깊으면 이 스택 공간이 모자라서 StackOverflowError 가 발생하는 거예요.
자바의 기본 스택 크기로 보통 수천~수만 단계의 재귀까지는 견디지만, 그 이상은 위험해요.
자, Day 6의 마지막 Step도 마무리됐어요. 메서드를 한 바퀴 돌아봤죠. 오늘 새로 손에 넣은 도구들을 정리하고, 다음 시간을 가볍게 예고하면서 마칠 시간이에요.
마무리
오늘 배운 내용을 정리해 볼게요.
- 왜 메서드가 필요한가 — 반복되는 출력 로직 다섯 줄이 네 화면에 흩어지는 답답함을 직접 확인
- 메서드 기본 문법 —
static 반환타입 이름(매개변수)시그니처 +return키워드, 그리고void - 매개변수와 반환값 — 매개변수 여러 개를 받아 결과를 만들고, 그 결과를 다음 메서드의 인자로 흘려보내는 파이프라인
- 값 전달 vs 참조 전달 — 기본형은 메서드 안의 변경이 밖에 안 보이고, 배열은 그대로 보임. 스택과 힙의 메모리 그림으로 짚어봄
- 메서드 오버로딩 — 같은 이름으로 매개변수만 다르게 여러 버전.
System.out.println(...)도 사실 오버로딩의 결과 - 가변 인자 (
varargs) — 점 세 개로 0개부터 여러 개까지 유연하게 받기. 지난 시간 미끼 회수 - 평행 배열 메서드 추출 — Day 5 식 출력 로직 다섯 줄을
printOneUser+printAllUsers두 메서드로 묶어main을 정리 - 재귀 기초 — 팩토리얼·피보나치·카운트다운으로 자기 호출 흐름과 종료 조건 익히기
main 의 정식 형태 public static void main(String[] args) 도 오늘부터 자리잡았어요.
public, static, String[] args 각 키워드의 의미는 Day 6~Day 8 사이에 차근차근 풀어갑니다.
Day 2 에서 기억, Day 3 에서 판단, Day 4 에서 반복, Day 5 에서 묶음 보관, 그리고 오늘 Day 6 에서는 재사용 도구를 손에 넣었습니다. 기억 + 판단 + 반복 + 묶음 + 재사용. 이제 작은 인스타 분석 프로그램의 핵심 기능을 메서드 단위로 깔끔하게 설계할 수 있게 됐어요.
그런데 Step 7 의 마지막에서 본 남은 답답함, 기억나시죠? 평행 배열 세 개가 메서드를 부를 때마다 같이 다녀야 하는 부담. "사용자 한 명을 통째로 하나의 자료형에 담을 수 있다면" 하는 갈증.
다음 시간에는 오늘 배운 메서드를 가지고 인스타 사용자 관리 미니 시스템을 종합 실습으로 만들어 봅니다. Phase 1 의 마지막 Day이기도 해서, 변수 → 조건 → 반복 → 배열 → 메서드까지 다 끌어와 한 번에 활용할 거예요. 그리고 그 다음 Day 8 에서 드디어 클래스 (class) 가 등장합니다. 오늘 살짝 묻어둔 평행 배열의 답답함이 거기서 풀려요.
과제
오늘 배운 메서드를 직접 써보면서 익혀볼 시간이에요. 난이도별로 세 개 준비했습니다.
과제 1: [기초] 인스타 댓글 점검 도우미
오늘 Step 2~3 에서 배운 메서드 정의·호출, 매개변수, 반환값을 직접 써보는 과제예요. 인스타 댓글에 자동으로 라벨을 붙여주는 두 메서드를 만들어 봅시다.
시작 데이터:
String[] comments = {
"오늘도 좋은 하루 보내세요!",
"ㄴㄴ",
"이 사진 너무 예쁘네요. 어디서 찍으셨어요? 저도 가보고 싶어요.",
"ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"
};
만들어야 할 메서드 두 개:
static String judgeCommentLength(String comment)— 댓글 길이를 기준으로 라벨을 돌려준다- 5자 미만 →
"너무 짧음" - 5~30자 →
"적당함" - 31자 이상 →
"긴 댓글"
- 5자 미만 →
static void printCommentReport(String comment)— 위 메서드를 사용해서 한 줄 리포트 출력- 예:
[적당함] 오늘도 좋은 하루 보내세요! void반환 (출력만 함)
- 예:
동작 규칙:
main에서 네 댓글을 모두printCommentReport로 출력하세요.- 메서드 두 개를 분리해서 만들고,
printCommentReport가judgeCommentLength를 부르도록 해보세요. 한 메서드 안에서 다른 메서드를 부르는 합성 패턴 연습이에요.
힌트:
- 문자열 길이는
comment.length()로 알 수 있어요. 이 자체도 메서드 호출이에요. - 라벨을
[...]로 감싸는 건+로 문자열을 이어붙이면 돼요.
과제 2: [응용] 인스타 게시물 출력 — 오버로딩 + varargs 종합
오늘 Step 5 (오버로딩) 와 Step 6 (varargs) 를 함께 활용하는 과제예요. 같은 이름의 메서드를 매개변수만 다르게 여러 버전 만들어 봅시다.
만들어야 할 메서드들:
static void printPost(String title) — 제목만 출력
static void printPost(String title, int likes) — 제목 + 좋아요 출력
static void printPost(String title, int likes, String... hashtags) — 제목 + 좋아요 + 해시태그들 출력 (varargs)
출력 형식 예시:
[게시물] 오늘 점심
[게시물] 오늘 점심 (좋아요 42개)
[게시물] 오늘 점심 (좋아요 42개) — #카페 #디저트 #힐링
main 에서 호출:
printPost("오늘 점심");
printPost("주말 등산", 120);
printPost("강아지 산책", 88, "#강아지", "#산책", "#일상");
printPost("OOTD", 250, "#OOTD"); // 해시태그 1개
printPost("아침 커피", 33); // 해시태그 없음
동작 규칙:
- 매개변수 개수에 따라 어떤 오버로딩 버전이 불릴지 자바가 자동으로 결정해요. 호출자는 같은 이름만 부르면 끝.
- varargs 버전을 호출할 때 해시태그를 0개 넘기는 경우 (
printPost("아침 커피", 33)) 는 오버로딩의 2번째 버전 이 우선 호출돼요. 자바가 더 가까운 매칭을 우선하기 때문이에요.
도전 (선택): printPost 가 출력만 하는 게 아니라, formatLikes(int) 같은 좋아요 포맷팅 메서드를 안에서 부르도록 확장해 보세요. 1234 → "1.2K" 표기로 더 예쁘게 출력되게요. 메서드 합성 연습이에요.
과제 3: [심화] 인스타 추천 친구 종합 시스템
Step 3 (매개변수와 반환값), Step 7 (평행 배열 메서드 추출) 을 종합하는 과제예요. 오늘의 마지막 미션입니다.
시작 데이터:
String[] usernames = {"jaehoon", "minji", "seungwoo", "soyeon", "wooseok"};
int[] mutualFollows = { 12, 3, 20, 8, 5 };
int[] commonHashtags = { 5, 0, 8, 3, 2 };
int[] daysSinceActive = { 2, 30, 0, 7, 15 };
int[] postCounts = { 30, 5, 100, 45, 20 };
만들어야 할 메서드들:
static int calculateScore(int mutualFollows, int commonHashtags, int daysSinceActive, int postCount)— Step 3 처럼 점수 계산static String classify(int score)— 점수를 등급 문자열로static void printRecommendation(String username, int score, String classification)— 한 사용자의 추천 정보 한 줄 출력static void printAllRecommendations(String[] usernames, int[] mutualFollows, int[] commonHashtags, int[] daysSinceActive, int[] postCounts)— 전체 순회
main 은 이렇게 단 두 줄이어야 해요:
public static void main(String[] args) {
String[] usernames = {...};
int[] mutualFollows = {...};
int[] commonHashtags = {...};
int[] daysSinceActive = {...};
int[] postCounts = {...};
printAllRecommendations(usernames, mutualFollows, commonHashtags, daysSinceActive, postCounts);
}
출력 예시 (구체적인 점수·등급은 본인 채점 기준에 따라 달라요):
@jaehoon — 점수 133 → 추천
@minji — 점수 20 → 추천 보류
@seungwoo — 점수 254 → 강력 추천
@soyeon — 점수 117 → 추천
@wooseok — 점수 66 → 관심 있을 수도
힌트:
printAllRecommendations는for루프 하나만 가져야 해요. 한 사람 출력은printRecommendation에게 위임.printRecommendation안에서calculateScore와classify두 메서드의 결과를 받아서 한 줄을 출력하면 됩니다.- 점수 계산 가중치는 본인이 자유롭게 잡으세요. Step 3 의 (맞팔 × 5점, 해시태그 × 3점, 활동성, 게시물 상한) 패턴을 그대로 가져와도 OK.
도전 (선택): 추천 등급이 "강력 추천" 인 사용자만 다시 한 번 모아서 출력해 보세요. 새 메서드 static void printOnlyStrongRecommendations(...) 를 추가하고, 그 안에서 classify 의 결과를 보고 필터링하면 돼요.
생각해볼 주제
오늘 배운 메서드 너머의 이야기를 세 가지 던져드릴게요. 정답이 정해진 질문이 아니라, 직접 생각해보고 본인의 답을 만들어보세요.
1. "메서드 이름을 잘 짓는다는 건 뭘까?"
오늘 우리가 만든 메서드 이름들을 떠올려 보세요.
formatLikes, calculateRecommendScore, printOneUser, factorial, countdown...
모두 "이 메서드가 뭐 하는 놈인지" 한눈에 보이는 동사구로 지어졌어요.
만약 같은 메서드를 f1, doStuff, process, handle 같은 이름으로 지었다면 어땠을까요?
컴파일은 똑같이 되겠지만, 한 달 뒤 코드를 다시 봤을 때 "내가 이 메서드를 왜 만들었지?" 라고 헤매게 될 거예요.
좋은 메서드 이름의 조건을 본인 나름의 기준으로 세 가지만 정리해 보세요. 힌트로 몇 가지 던져드리면 — 동사로 시작? / 반환값을 짐작할 수 있나? / 부수 효과 (출력·저장) 가 있는지 이름에서 보이나? / 얼마나 길어야 적당한가?
(인스타그램 같은 대규모 코드베이스에서 이름 짓기는 진짜로 중요해요. 코드를 짜는 시간보다 읽는 시간이 압도적으로 길거든요.)
2. "재귀 vs 반복문 — 어느 쪽을 골라야 할까?"
Step 8 에서 팩토리얼과 피보나치 두 예제를 봤어요. 팩토리얼은 재귀 버전이 더 깔끔했고, 피보나치는 재귀 버전이 같은 계산을 너무 많이 반복해서 느렸죠.
같은 일을 두 가지 방식으로 풀 수 있을 때, 본인이라면 어떤 기준으로 한 쪽을 고를까요?
세 가지 관점에서 생각해 보세요.
- 코드의 가독성 — 어느 쪽이 "수학 정의를 그대로 옮긴 듯한" 느낌인가
- 실행 효율 — 같은 계산이 중복되는가, 호출 깊이가 위험할 정도로 깊어지는가
- 유지보수 — 종료 조건을 빠뜨릴 위험 vs 반복 조건을 헷갈릴 위험, 어느 쪽이 더 무섭나
힌트로 한 가지 던지면 — 대부분의 실무 코드는 반복문이 기본, 재귀는 트리·그래프 탐색 같은 특정 자리에서만 골라 쓰는 도구 예요.
3. "매개변수가 다섯 개를 넘어가면?"
오늘 만든 메서드 중에 calculateRecommendScore(int mutualFollows, int commonHashtags, int daysSinceActive, int postCount) 가 매개변수 네 개였어요.
이미 호출하는 자리에서 "이 숫자가 뭐였더라?" 헷갈리기 시작했죠.
만약 추천 점수 계산에 추가 정보 (지역 일치 여부, 직업 일치 여부, 공통 친구의 친구 수, 마지막 메시지 일수, ...) 가 더 들어가서 매개변수가 8개·10개·20개 까지 늘어난다면 어떨까요?
호출 자리는 어떻게 보일까? 매개변수 순서를 외우는 건 가능할까? 이런 상황을 풀어주는 도구를 자바가 미리 준비해뒀을 거예요. 어떤 도구일지 추측해 보세요.
(힌트: 다음 시간 Day 8 즈음에 그 답을 만나게 됩니다. 오늘 Step 7 의 마지막에서 살짝 묻어둔 답답함과 같은 주제예요.)
✅ 예시 답안정답 보기
본인 답안을 먼저 작성한 뒤 비교해보세요. 정답이 하나만 있는 건 아니에요. 여기 답안은 모범 사례 중 하나일 뿐, 본인만의 더 깔끔한 풀이가 있다면 그게 답입니다.
🎯 [과제 1 예시 답안] 인스타 댓글 점검 도우미
댓글 길이를 기준으로 라벨을 돌려주는 judgeCommentLength 와, 그 라벨을 사용해 한 줄 리포트를 출력하는 printCommentReport 두 메서드를 만들고, main 에서 네 댓글을 모두 출력하는 과제입니다.
핵심 접근
세 가지를 챙기면 됩니다.
첫째, judgeCommentLength 는 반환값이 있는 메서드 (String) 로 만들어 라벨을 돌려주게 합니다. 둘째, printCommentReport 는 출력 전용 메서드 (void) 로 만들어 안에서 judgeCommentLength 를 호출하는 합성 패턴을 보여줍니다. 셋째, main 은 데이터 정의 + for 루프 + 메서드 호출 한 줄로 깔끔하게 유지합니다.
예시 코드
// day06/CommentInspector.java (학습 실습용 — 코드베이스 외부에서 작성 OK)
public static void main(String[] args) {
String[] comments = {
"오늘도 좋은 하루 보내세요!",
"ㄴㄴ",
"이 사진 너무 예쁘네요. 어디서 찍으셨어요? 저도 가보고 싶어요.",
"ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"
};
for (String comment : comments) {
printCommentReport(comment);
}
}
// 댓글 길이를 기준으로 라벨을 돌려주는 메서드 (반환값 있음)
static String judgeCommentLength(String comment) {
int length = comment.length();
if (length < 5) {
return "너무 짧음";
}
if (length <= 30) {
return "적당함";
}
return "긴 댓글";
}
// 한 줄 리포트를 출력만 하는 메서드 (void, 안에서 judgeCommentLength 호출)
static void printCommentReport(String comment) {
String label = judgeCommentLength(comment);
System.out.println("[" + label + "] " + comment);
}
실행 결과:
[적당함] 오늘도 좋은 하루 보내세요!
[너무 짧음] ㄴㄴ
[긴 댓글] 이 사진 너무 예쁘네요. 어디서 찍으셨어요? 저도 가보고 싶어요.
[긴 댓글] ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 메서드 두 개 분리 | judgeCommentLength (반환) 와 printCommentReport (void) 가 독립된 책임을 가짐 |
상 |
| 합성 호출 | printCommentReport 안에서 judgeCommentLength 를 부르고, 그 결과를 활용 |
상 |
| 조건 경계 처리 | < 5, <= 30, > 30 세 구간이 겹치거나 빠지지 않도록 정확히 설정 |
상 |
main 의 간결성 |
for 루프 한 개 + 메서드 호출 한 줄로 정리 |
중 |
| 출력 포맷 | [라벨] 본문 형식을 일관되게 유지 |
중 |
흔한 실수
- 경계값을 양쪽 부등호로 처리 —
length < 5와length > 30사이에length >= 5 && length <= 30같이 and 두 조건을 적으면 동작은 같지만 가독성이 떨어져요. 이른 반환 패턴 (if (length < 5) return ...) 으로 빠져나가면서 처리하면 깔끔합니다. printCommentReport안에서 길이 판정을 또 하기 — 같은 로직이 두 메서드에 중복되면 메서드 분리의 의미가 사라져요. 길이 판정은judgeCommentLength에서만, 출력은printCommentReport에서만.void메서드에return label;처럼 값을 돌려주려 시도 — 컴파일 에러가 납니다.void는 값을 안 돌려주는 표시예요.main안에 라벨링 로직을 직접 박기 — 그러면 메서드 분리의 학습 목표가 사라져요. 반드시 메서드 호출 형태로 정리하세요.
실무 개선 포인트 (심화)
- 상수 추출 —
5,30같은 매직 넘버 대신static final int SHORT_LIMIT = 5;처럼 이름 붙은 상수로 빼두면 의도가 또렷해져요. 다음 시간 이후로 자주 쓰게 될 패턴이에요. - 빈 문자열·null 처리 — 댓글이 빈 문자열 (
"") 이거나null일 때judgeCommentLength가 어떻게 동작해야 할지 생각해 보세요. 빈 문자열은 길이 0이라 "너무 짧음" 으로 자연스럽게 분류되지만,null은length()호출에서 예외가 터집니다. "비어있음" 같은 별도 라벨을 추가하면 더 완전해져요. - 라벨 종류 확장 — "너무 짧음 / 적당함 / 긴 댓글 / 도배 의심" 처럼 라벨이 늘어나면
if/else if사슬이 길어져요. Day 3 에서 배운switch표현식으로 정리하거나, 다음 시간 클래스 단원에서 배울enum으로 바꿔 두면 깔끔합니다.
🎯 [과제 2 예시 답안] printPost — 오버로딩 + varargs 종합
printPost 라는 같은 이름의 메서드를 매개변수만 다르게 세 가지 버전 만드는 과제입니다. 호출자가 정보의 양에 따라 자연스럽게 같은 이름을 부를 수 있도록 설계하는 연습이에요.
핵심 접근
세 가지를 챙기면 됩니다.
첫째, 세 오버로딩 버전이 명확하게 분기되도록 매개변수 시그니처를 다르게 작성합니다. 둘째, varargs 버전의 출력에서 해시태그가 0개일 때는 해시태그 없는 형식 으로 떨어지도록 처리합니다. 셋째, printPost("아침 커피", 33) 처럼 해시태그가 0개인 경우는 자바가 더 가까운 오버로딩 (2번째 버전) 을 우선 호출한다는 사실을 의식하고 코드를 작성합니다.
예시 코드
// day06/PostPrinter.java
public static void main(String[] args) {
printPost("오늘 점심");
printPost("주말 등산", 120);
printPost("강아지 산책", 88, "#강아지", "#산책", "#일상");
printPost("OOTD", 250, "#OOTD"); // 해시태그 1개
printPost("아침 커피", 33); // 해시태그 없음
}
// 버전 1 — 제목만
static void printPost(String title) {
System.out.println("[게시물] " + title);
}
// 버전 2 — 제목 + 좋아요
static void printPost(String title, int likes) {
System.out.println("[게시물] " + title + " (좋아요 " + likes + "개)");
}
// 버전 3 — 제목 + 좋아요 + 해시태그들 (varargs)
static void printPost(String title, int likes, String... hashtags) {
System.out.print("[게시물] " + title + " (좋아요 " + likes + "개)");
if (hashtags.length > 0) {
System.out.print(" — ");
for (int i = 0; i < hashtags.length; i++) {
System.out.print(hashtags[i]);
if (i < hashtags.length - 1) {
System.out.print(" ");
}
}
}
System.out.println();
}
실행 결과:
[게시물] 오늘 점심
[게시물] 주말 등산 (좋아요 120개)
[게시물] 강아지 산책 (좋아요 88개) — #강아지 #산책 #일상
[게시물] OOTD (좋아요 250개) — #OOTD
[게시물] 아침 커피 (좋아요 33개)
printPost("아침 커피", 33) 호출이 버전 2 로 분기된 결과예요. 자바가 더 정확히 일치하는 오버로딩을 우선 호출하기 때문에 버전 3 의 varargs 가 0개 매칭으로 가지 않습니다.
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 세 오버로딩 시그니처 분리 | 매개변수 개수·타입이 자바가 구분 가능한 형태 | 상 |
| varargs 자리 | String... hashtags 가 매개변수 목록의 맨 뒤 에 위치 |
상 |
| 0개 케이스 처리 | 해시태그 0개일 때 "— " 같은 빈 구분자가 출력되지 않음 | 상 |
| 일관된 출력 포맷 | [게시물] 제목 (좋아요 N개) — 태그들 흐름이 모든 버전에서 자연스러움 |
중 |
| 매개변수 이름의 가독성 | title, likes, hashtags 처럼 영문 동사구 또는 명사구 로 의도가 보임 |
중 |
흔한 실수
- varargs 자리에 빈 분기를 빠뜨림 —
String... hashtags의 길이가 0인 경우를 처리하지 않으면 "— " 같은 빈 구분자가 출력에 남아요. 항상if (hashtags.length > 0)가드를 한 번 거치는 습관을 들이세요. - 오버로딩 시그니처 충돌 —
printPost(String, int, String...)과printPost(String, int, String)두 개를 같이 두려고 하면, 인자 한 개짜리 String 호출에서 모호한 호출 컴파일 에러가 나요. 첫 번째가 0개 이상 을 받을 수 있어서 두 시그니처를 자바가 구분 못 합니다. varargs버전 안에서 마지막 항목 뒤에도 구분자를 찍기 — 출력이 "#OOTD " 처럼 뒤에 공백이 남아요.if (i < hashtags.length - 1)으로 마지막 직전까지만 구분자를 찍는 패턴이 깔끔합니다.println과print혼동 — 한 줄에 여러 출력을 이어붙일 때는print로 모은 뒤 마지막에println()한 번으로 줄바꿈.println을 중간에 섞으면 한 게시물이 두 줄로 갈라져요.
실무 개선 포인트 (심화)
String.join활용 — 해시태그 출력에서for루프 + 구분자 분기 대신String.join(" ", hashtags)한 줄이면 깔끔하게 처리돼요. Day 17 String API 단원에서 본격적으로 다룰 도구지만, 미리 한 번 검색해 보면 "이렇게 짧게도 되는구나" 하는 재미가 있어요.- 포맷팅을 위한 헬퍼 메서드 추가 — 좋아요 수가 1234일 때 "1.2K" 로 보이게 하려면 Step 2 에서 만든
formatLikes를 안에서 부르면 끝. 메서드 합성의 진가가 한눈에 드러나요. Optional같은 모던 자바 도구 — 좋아요 정보가 "있을 수도 없을 수도 있는" 데이터일 때,int likes대신Optional<Integer> likes로 받는 게 더 의도가 또렷해요. Day 24 모던 Java 단원에서 다뤄요.
🎯 [과제 3 예시 답안] 인스타 추천 친구 종합 시스템
오늘 배운 거의 모든 도구 (매개변수 여러 개, 반환값, 평행 배열 메서드 추출, 메서드 합성) 를 종합하는 마지막 과제입니다.
핵심 접근
네 단계로 책임을 나눕니다.
(1) calculateScore 는 매개변수 네 개를 받아 점수만 돌려줍니다 — 출력은 절대 안 함.
(2) classify 는 점수를 받아 등급 문자열만 돌려줍니다 — 역시 출력 안 함.
(3) printRecommendation 은 한 사용자의 정보를 받아 한 줄을 출력합니다 (void). 안에서 classify 의 결과를 활용해도 OK.
(4) printAllRecommendations 는 평행 배열을 받아 전체 순회 만 책임집니다 — 한 사람 출력은 printRecommendation 에 위임.
이렇게 책임이 또렷이 나뉘면 main 은 데이터 정의 + 메서드 한 줄 호출로 끝납니다.
예시 코드
// day06/RecommendSystem.java
public static void main(String[] args) {
String[] usernames = {"jaehoon", "minji", "seungwoo", "soyeon", "wooseok"};
int[] mutualFollows = { 12, 3, 20, 8, 5 };
int[] commonHashtags = { 5, 0, 8, 3, 2 };
int[] daysSinceActive = { 2, 30, 0, 7, 15 };
int[] postCounts = { 30, 5, 100, 45, 20 };
printAllRecommendations(usernames, mutualFollows, commonHashtags, daysSinceActive, postCounts);
}
// (1) 점수 계산 — 출력은 절대 안 함
static int calculateScore(int mutualFollows, int commonHashtags, int daysSinceActive, int postCount) {
int score = 0;
score = score + mutualFollows * 5;
score = score + commonHashtags * 3;
int activityScore = 30 - daysSinceActive;
if (activityScore < 0) {
activityScore = 0;
}
score = score + activityScore;
int postScore = postCount;
if (postScore > 100) {
postScore = 100;
}
score = score + postScore;
return score;
}
// (2) 등급 분류 — 출력은 절대 안 함
static String classify(int score) {
if (score >= 150) {
return "강력 추천";
}
if (score >= 80) {
return "추천";
}
if (score >= 30) {
return "관심 있을 수도";
}
return "추천 보류";
}
// (3) 한 사용자 출력 — void
static void printRecommendation(String username, int score, String classification) {
System.out.println("@" + username + " — 점수 " + score + " → " + classification);
}
// (4) 전체 순회 — for 루프와 위임만 책임
static void printAllRecommendations(String[] usernames,
int[] mutualFollows,
int[] commonHashtags,
int[] daysSinceActive,
int[] postCounts) {
for (int i = 0; i < usernames.length; i++) {
int score = calculateScore(mutualFollows[i], commonHashtags[i], daysSinceActive[i], postCounts[i]);
String classification = classify(score);
printRecommendation(usernames[i], score, classification);
}
}
실행 결과 (가중치 본 답안 기준):
@jaehoon — 점수 133 → 추천
@minji — 점수 20 → 추천 보류
@seungwoo — 점수 254 → 강력 추천
@soyeon — 점수 117 → 추천
@wooseok — 점수 66 → 관심 있을 수도
채점 포인트
| 포인트 | 설명 | 배점 가중 |
|---|---|---|
| 책임 분리 | 점수 계산 / 등급 분류 / 한 사람 출력 / 전체 순회가 네 메서드로 나뉨 | 상 |
| 출력 없는 메서드 | calculateScore, classify 는 return 만 하고 절대 출력 안 함 |
상 |
| 평행 배열 처리 | printAllRecommendations 가 for 와 인덱스 위임만으로 동작 |
상 |
main 의 간결성 |
데이터 정의 + 메서드 한 줄 호출로 정리 | 상 |
| 매개변수 가중치 일관 | 활동성 (30 - days) 과 게시물 상한 (100) 가드 처리 |
중 |
흔한 실수
calculateScore안에서println으로 디버깅 출력 — 디버깅 중에는 OK 지만, 최종 코드에서 제거하지 않으면 반환값을 받아 출력하는 메서드 와 책임이 섞여요. 메서드는 한 가지만 하게 두세요.printRecommendation안에서 점수를 다시 계산 — 호출자 (printAllRecommendations) 가 이미 계산해서 넘겨주는데 또 계산하면 일이 두 배. 매개변수로 받은 값을 그대로 활용 하세요.activityScore가드 빠뜨림 —daysSinceActive가 30 을 넘으면activityScore가 음수가 돼서 점수가 깎이는 사고가 발생해요. 0 이하면 0 으로 클램프 한 줄이 안전장치.- 메서드 시그니처의 매개변수 순서를 호출 자리마다 다르게 —
calculateScore(맞팔, 해시태그, 활동일수, 게시물수)순서를 한 번 정했으면 어디서든 같은 순서로 부르세요. 순서를 섞으면 컴파일은 통과해도 의미가 어긋난 점수가 나옵니다.
실무 개선 포인트 (심화)
- 가중치를 외부에서 받기 — 지금은
* 5,* 3같은 가중치가 메서드 안에 그대로 적혀 있어요. "맞팔 가중치를 6점으로 조정해 보자" 같은 실험을 자주 한다면 가중치를 매개변수로 받거나 상수로 빼두면 편해요. - 점수와 등급을 한 데이터로 — 지금은
int score와String classification을 두 번 따로 다루고 있어요. 다음 시간 클래스에서 "추천 결과" 라는 자료형 하나로 묶어내면 호출 자리가 더 깔끔해집니다. - 테스트 가능한 설계 —
calculateScore처럼 출력 없이 값만 돌려주는 메서드는 테스트하기 쉽다 는 장점이 있어요. "맞팔 12명 + 해시태그 5개 → 점수 133" 같이 결과를 정확히 검증할 수 있죠. 출력이 섞인 메서드는 테스트가 어려워요. 이 차이는 실무에서 굉장히 중요합니다.
🎯 [생각해볼 주제 1 답안] 메서드 이름을 잘 짓는다는 건 뭘까?
문제 상황 요약
같은 동작을 하는 메서드라도 formatLikes vs f1 vs process 처럼 이름에 따라 코드의 이해도가 크게 달라집니다. 좋은 메서드 이름의 조건을 본인 나름의 기준으로 정리해보는 주제예요.
튜터의 가이드 및 해설
좋은 메서드 이름의 조건을 다섯 가지로 정리해봤어요. 본인이 세운 기준과 비교해보세요.
1. 동사 또는 동사구로 시작
메서드는 작업 을 나타내니까 동사로 시작하는 게 자연스러워요.
formatLikes,calculateScore,printOneUser,findRecommendations— 동사형으로 의도가 또렷score,users,recommendation— 명사형은 변수 이름 에 가까워서 메서드 이름으로는 어색
예외는 boolean 을 돌려주는 메서드 예요. 이때는 isXxx, hasXxx, canXxx, shouldXxx 같이 be 동사 또는 조동사 로 시작해서 "~인가?" 라는 질문 형태로 짓는 게 관습이에요.
2. 반환값을 짐작할 수 있어야 함
이름만 봐도 "이 메서드는 무엇을 돌려주겠다" 가 보여야 해요.
calculateScore→ 점수를 돌려줄 것 같음 ✓classify→ 분류 결과 (문자열·enum) 을 돌려줄 것 같음 ✓doSomething→ 도무지 모름 ✗processUser→ "처리한다는데 결과가 뭐지?" 불명확
3. 부수 효과가 있다면 이름에 드러남
화면 출력·파일 저장·DB 변경 같은 부수 효과 가 있는 메서드라면 이름에 그게 보이는 편이 안전해요.
printOneUser→ "출력만 한다" 가 명확saveToFile,notifyUsers,deletePost→ 무엇이 변경되는지 한눈에
반대로 값만 돌려주는 순수한 메서드는 동사+명사만으로 충분해요.
formatLikes→ 포맷팅된 결과만 돌려줌, 다른 건 안 건드림
4. 길이는 읽힐 정도면 충분
너무 짧으면 의미가 안 보이고, 너무 길면 호출 자리가 지저분해져요.
getRecommendationScoreForUserBasedOnFollowsAndActivity→ 너무 김calc→ 너무 짧음calculateScore→ 적정
"메서드 이름이 길어지는 건 보통 메서드가 너무 많은 일을 하고 있다는 신호" 라는 격언이 있어요. 이름이 길어지면 메서드 자체를 두 개로 나누는 걸 먼저 검토해 보세요.
5. 같은 코드베이스 안에서 일관성
fetch vs get vs load, delete vs remove, create vs make 같은 동의어는 프로젝트 전체에서 한 가지 로 통일하는 게 좋아요. "get은 메모리에서, load는 DB에서, fetch는 외부 API에서" 같이 의미를 분리해서 약속하기도 해요.
작은 프로젝트에선 본인이 일관적이기만 하면 됩니다. 큰 프로젝트에선 팀 차원의 명명 규칙 가이드 를 따로 두기도 해요.
🎯 면접관을 홀리는 핵심 멘트
"메서드 이름은 그 메서드의 외부 API 입니다. 호출자가 그 이름만 보고 무엇을 받아 무엇을 돌려주는지 정확히 짐작할 수 있어야 잘 지은 이름이라고 생각해요. 동사로 시작하고, 반환값과 부수 효과가 이름에 드러나며, 같은 코드베이스 안에서 일관된 동의어 규칙을 따르는 것 — 이 세 가지를 우선 챙기는 편입니다."
🎯 [생각해볼 주제 2 답안] 재귀 vs 반복문 — 어느 쪽을 골라야 할까?
문제 상황 요약
팩토리얼과 피보나치 두 예제에서 같은 문제를 재귀와 반복문 두 가지로 풀 수 있다는 걸 봤어요. 어떤 기준으로 한 쪽을 골라야 할지 정리해보는 주제입니다.
튜터의 가이드 및 해설
세 가지 관점에서 정리해봅니다.
1. 코드의 가독성
문제 자체가 재귀적인 구조 를 갖고 있다면 재귀가 자연스러워요.
- 팩토리얼
n! = n × (n-1)!— 정의 자체가 자기 호출 - 트리 / 디렉토리 탐색 — "폴더 안의 폴더 안의 폴더..." 가 본질적으로 재귀
- 분할 정복 (merge sort, quick sort) — "절반을 정렬하고 합친다" 의 재귀 구조
이런 문제에 반복문을 억지로 끼우면 스택을 직접 관리하는 지저분한 코드가 됩니다.
반대로 선형 누적 문제는 반복문이 더 직관적이에요.
- 배열 합계 —
for한 줄 - 문자열의 각 글자 처리 —
for한 줄
2. 실행 효율
재귀의 함정 두 가지를 의식하세요.
먼저 중복 계산 입니다. 피보나치 재귀 버전은 fib(2), fib(3) 같은 작은 값들을 수십 번 반복 계산 해서 n=40 부터 수 초가 걸렸어요. 같은 입력에 같은 결과가 나오는데 매번 다시 계산하는 건 낭비죠.
이걸 푸는 방법으로 메모이제이션 (memoization) — 이전 결과를 저장해두고 재사용 — 이 있는데, 자료구조 (해시맵) 가 필요해서 Day 19 즈음에 본격 다뤄요.
또 하나는 호출 스택 깊이. 재귀가 1만 단계, 10만 단계로 깊어지면 자바의 스택 메모리가 바닥나서 StackOverflowError 를 만나요. 반복문은 이런 문제가 원천적으로 없어요.
3. 유지보수
재귀를 잘못 짜면 무한 호출 에 빠집니다. 종료 조건이 빠지거나, 자기 호출이 조건을 좁히지 못하면 (recursion(n) → recursion(n) 같은 자기 호출) 스택 오버플로가 터져요.
반복문도 무한 루프 위험이 있지만, 루프 조건 한 군데만 확인하면 되어서 디버깅이 비교적 쉬워요.
실무의 디폴트는 반복문
대부분의 실무 자바 코드는 반복문이 기본입니다. 재귀는 문제 구조가 분명히 재귀적일 때만 선택적으로 씁니다. 그리고 깊이가 합리적인 한도 안에 있는지 (< 1000 정도) 항상 확인합니다.
함수형 언어 (Haskell, Scheme) 에서는 재귀가 더 흔하게 쓰이는데, 그쪽 언어는 꼬리 재귀 최적화 (tail call optimization) 라는 도구로 깊은 재귀를 반복문 수준으로 변환해주거든요. 자바는 (현재까진) 이 최적화가 없어서 깊은 재귀는 여전히 위험합니다.
🎯 면접관을 홀리는 핵심 멘트
"기본은 반복문, 재귀는 문제 구조가 본질적으로 재귀적일 때만 선택합니다. 트리 탐색, 분할 정복, 종료 조건이 또렷한 수학적 정의 같은 자리에서는 재귀가 코드를 절반 길이로 줄여주죠. 반대로 같은 계산이 중복되거나 호출 깊이가 위험할 가능성이 있다면 반복문 + 누적 변수가 안전합니다. 자바는 꼬리 재귀 최적화가 없어서, 깊은 재귀는 항상 한 번 더 의심하는 습관을 들이고 있어요."
🎯 [생각해볼 주제 3 답안] 매개변수가 다섯 개를 넘어가면?
문제 상황 요약
calculateRecommendScore(int, int, int, int) 의 네 매개변수도 이미 호출 자리에서 "이 숫자가 뭐였더라" 헷갈리기 시작했어요. 정보가 더 늘어나 매개변수가 8개·10개·20개까지 가면 어떻게 다뤄야 할까요?
튜터의 가이드 및 해설
세 가지 단계의 해결책을 소개해 드릴게요.
1. 첫 번째 방어 — 주석으로 의미 적기
가장 단순한 방어는 호출 자리 바로 위에 주석으로 매개변수의 의미를 적어두는 거예요.
// (mutualFollows, commonHashtags, daysSinceActive, postCount)
int score = calculateRecommendScore(12, 5, 2, 30);
코드가 컴파일되는 데에는 영향이 없고, 사람이 읽을 때 한 번 더 의미를 확인할 수 있어요. 다만 주석은 코드와 함께 갱신되지 않을 위험 이 있어요. 메서드 시그니처가 바뀌었는데 주석이 옛것으로 남아 있으면 더 큰 혼란이 생기죠.
2. 두 번째 방어 — 메서드를 더 작게 쪼개기
매개변수가 너무 많다는 건 보통 메서드가 한 번에 너무 많은 일을 하고 있다는 신호 예요. 메서드를 더 작은 단위로 나누면 자연스럽게 매개변수도 줄어듭니다.
예를 들어 calculateRecommendScore(맞팔, 해시태그, 활동성, 게시물수) 를 점수의 각 부분 을 따로 계산하는 메서드들로 쪼개면:
int socialScore = calculateSocialScore(mutualFollows, commonHashtags); // 2개
int activityScore = calculateActivityScore(daysSinceActive, postCount); // 2개
int totalScore = socialScore + activityScore;
각 작은 메서드는 매개변수가 2~3개로 줄고, 호출 자리도 의미가 더 또렷해져요.
3. 세 번째 방어 — 매개변수를 묶어주는 자료형
진짜 해결책은 여러 매개변수를 한 자료형 으로 묶는 것 입니다. 자바에선 두 가지 도구가 준비되어 있어요.
먼저 클래스 (class) — Day 8 에서 본격적으로 배웁니다.
// 미리 보기 (Day 8 학습 이후 가능)
class UserSignal {
int mutualFollows;
int commonHashtags;
int daysSinceActive;
int postCount;
}
int score = calculateRecommendScore(signal); // 인자 4개 → 1개
호출 자리에서 매개변수 4개가 의미를 가진 하나의 묶음 이 돼요. 새 정보 (지역 일치, 직업 일치 등) 가 추가될 때 클래스 안의 필드만 늘리면 되고, 메서드 시그니처는 그대로 유지됩니다.
더 가볍게는 레코드 (record) — Day 24 에서 다룹니다. 클래스의 데이터 묶음 전용 단순화 버전이에요.
// Day 24 record 미리 보기
record UserSignal(int mutualFollows, int commonHashtags, int daysSinceActive, int postCount) {}
대규모 자바 코드베이스에서 이 묶음 자료형은 거의 모든 메서드 시그니처에 등장해요. DTO (Data Transfer Object) 라는 표현으로도 자주 부르죠.
경험 법칙
매개변수가 3개를 넘기 시작하면 묶음 자료형을 검토할 시간이에요. 5개를 넘기면 거의 반드시 묶어야 한다고 봐도 됩니다.
🎯 면접관을 홀리는 핵심 멘트
"매개변수가 많아진다는 건 보통 메서드가 너무 많은 책임을 지고 있다는 신호로 받아들입니다. 우선 메서드를 더 작은 단위로 쪼개서 자연스럽게 매개변수가 2~3개로 줄어들지 살펴보고, 그래도 안 되면 의미가 있는 묶음을 별도 자료형 — 자바라면 class, record, 또는 DTO — 으로 추출합니다. 매개변수 4개부터는 순서 의존성 으로 인한 버그 위험이 급격히 올라가서, 가능하면 이름을 가진 묶음 자료형 을 통해 호출하는 게 안전합니다."