코드 덩어리에 이름 붙이기
같은 피해량 공식을 여러 전투 장면에 복사하면 공식을 바꿀 때 모든 위치를 수정해야 합니다. 메서드는 입력을 받고 한 가지 작업을 수행한 뒤 필요하면 결과를 돌려주는 재사용 단위입니다.
반환형 메서드이름(자료형 매개변수) {
실행 코드
return 결과;
}
피해량 계산을 메서드로 분리하기
public class Main {
public static void main(String[] args) {
int firstDamage = calculateDamage(12, 3);
int secondDamage = calculateDamage(7, 10);
printDamage("첫 번째 공격", firstDamage);
printDamage("두 번째 공격", secondDamage);
}
static int calculateDamage(int attack, int defense) {
return Math.max(attack - defense, 0);
}
static void printDamage(String label, int damage) {
System.out.println(label + ": " + damage);
}
}
예상 출력:
첫 번째 공격: 9
두 번째 공격: 0
calculateDamage는 공격력과 방어력을 입력받아 정수를 반환합니다. Math.max(..., 0)은 방어력이 더 높아도 음수 피해가 나오지 않게 합니다. printDamage는 출력만 하고 돌려줄 값이 없으므로 반환형이 void입니다.
선언과 호출을 구분하기
static int calculateDamage(int attack, int defense)는 메서드를 선언합니다. calculateDamage(12, 3)은 실제 값으로 메서드를 호출합니다. 선언의 attack, defense가 매개변수이고 호출의 12, 3이 인수입니다.
반환값은 변수에 저장할 수도 있고 다른 메서드의 인수로 바로 전달할 수도 있습니다.
System.out.println(calculateDamage(20, 6));
오버로딩
입력 목록이 다르면 같은 이름의 메서드를 여러 개 선언할 수 있습니다.
static int heal(int health, int amount) {
return health + amount;
}
static int heal(int health, int amount, int maxHealth) {
return Math.min(health + amount, maxHealth);
}
호출할 때 인수 개수에 맞는 메서드가 선택됩니다. 반환형만 다르게 만드는 것은 오버로딩이 아닙니다.
좋은 메서드 이름
doIt보다 calculateDamage, printPlayerStatus, hasEnoughBlocks처럼 동작이나 질문이 드러나는 이름이 좋습니다. 메서드가 계산과 출력과 저장을 모두 맡고 있다면 작은 역할로 나눌 신호입니다.
직접 해보기
- 최대 피해를 20으로 제한하도록
Math.min을 추가합니다. - 두 수 중 큰 값을 반환하는
findLarger(int first, int second)를 만듭니다. - 플레이어 이름과 체력을 받아 한 줄로 출력하는 void 메서드를 만듭니다.
최대 피해 힌트
int damage = Math.max(attack - defense, 0);을 만든 뒤 return Math.min(damage, 20);으로 반환할 수 있습니다.
static은 왜 붙였나요?
아직 객체를 만들지 않고 main에서 바로 호출하기 위해 static 메서드를 사용했습니다. 다음 Chapter에서 객체를 배우면 플레이어마다 다른 상태를 사용하는 인스턴스 메서드와 차이를 확인합니다.
메서드로 생각을 나누는 기준
메서드는 단순히 코드를 짧게 만드는 문법이 아닙니다. 사람이 문제를 이해하는 단위에 이름을 붙이는 도구입니다. 예를 들어 전투 결과를 출력하는 코드 안에 피해량 계산, 치명타 판정, 체력 감소, 메시지 출력이 한꺼번에 들어 있으면 어느 부분이 잘못됐는지 찾기 어렵습니다. 반대로 calculateDamage, isCriticalHit, applyDamage, printBattleLog처럼 역할을 나누면 코드를 읽는 사람이 흐름을 따라가기 쉽습니다.
좋은 기준은 “이 부분을 한 문장으로 설명할 수 있는가”입니다. 설명이 “공격력을 받아 방어력을 뺀 피해량을 계산한다”처럼 분명하면 메서드로 떼어내기 좋습니다. 설명이 “공격도 하고 출력도 하고 저장도 하고 예외도 처리한다”처럼 여러 동사가 섞이면 아직 책임이 큰 상태입니다. 처음부터 완벽하게 나누려고 애쓸 필요는 없지만, 같은 코드가 두 번 보이거나 주석 없이는 의도를 알기 어려운 덩어리가 보이면 메서드 후보로 표시해 두세요.
입문 단계에서 가장 흔한 실수는 메서드를 너무 늦게 만드는 것입니다. “일단 main에 다 쓰고 나중에 정리하자”는 전략은 작은 예제에서는 통하지만, 조금만 길어져도 어느 줄이 어느 데이터를 바꾸는지 잃어버립니다. main은 전체 시나리오를 읽는 곳으로 두고, 세부 규칙은 이름이 붙은 메서드로 내리는 습관을 들이면 다음 장의 클래스 설계가 훨씬 쉬워집니다.
반환값이 있는 메서드와 부수 효과가 있는 메서드
메서드는 크게 두 종류로 나누어 생각할 수 있습니다. 하나는 입력을 받아 새 값을 계산해 반환하는 메서드입니다. calculateDamage(12, 3)처럼 같은 입력이면 같은 결과를 주는 함수는 테스트하기 쉽고 재사용하기 좋습니다. 다른 하나는 화면 출력, 파일 저장, 객체 상태 변경처럼 실행 결과가 바깥 세계에 영향을 주는 메서드입니다. printDamage는 값을 반환하지 않지만 콘솔에 문장을 남깁니다.
둘 중 어느 쪽이 항상 더 좋다고 말할 수는 없습니다. 다만 계산과 출력을 한 메서드에 섞으면 재사용성이 크게 떨어집니다. 피해량을 계산하는 규칙은 콘솔 출력뿐 아니라 UI 표시, 로그 저장, 테스트 검증에도 필요할 수 있습니다. 그래서 계산은 반환값으로 분리하고, 출력은 별도 메서드로 두는 편이 안전합니다.
static int calculateDamage(int attack, int defense) {
return Math.max(attack - defense, 0);
}
static String formatDamageMessage(String label, int damage) {
return label + ": " + damage;
}
static void printLine(String message) {
System.out.println(message);
}
위처럼 나누면 formatDamageMessage의 결과를 콘솔이 아니라 파일이나 웹 응답에 사용할 수도 있습니다. 실무에서는 이런 분리가 테스트의 차이를 만듭니다. 콘솔에 찍힌 문장을 눈으로 확인하는 대신 반환 문자열을 비교하면 자동 테스트가 간단해집니다.
매개변수는 필요한 만큼만 받기
메서드를 만들다 보면 편하다는 이유로 많은 값을 한꺼번에 넘기고 싶어집니다. 하지만 매개변수가 많아질수록 호출하는 쪽의 실수 가능성이 커집니다. calculateDamage(12, 3, 20, true, 5) 같은 호출은 각 숫자가 무엇을 뜻하는지 바로 알기 어렵습니다. 이런 상황은 메서드가 너무 많은 규칙을 담당하거나, 아직 객체로 묶어야 할 데이터가 흩어져 있다는 신호입니다.
입문 단계에서는 매개변수 2~3개 안에서 읽히는 메서드를 목표로 삼으세요. 정말 여러 값이 함께 움직인다면 다음 장에서 배우는 클래스로 Player, AttackRule, BattleResult 같은 자료형을 만들 수 있습니다. 아직 클래스를 쓰기 전이라면 변수 이름을 분명히 하고, 같은 타입의 인수가 연속될 때 특히 주의합니다.
int damage = calculateDamage(playerAttack, monsterDefense);
위 호출은 calculateDamage(12, 3)보다 길지만 의도가 잘 보입니다. 숫자 리터럴은 예제에서는 간단하지만, 실제 코드에서는 변수에 의미 있는 이름을 붙여 전달하는 편이 안전합니다.
return은 메서드를 끝내는 신호
return은 값을 돌려주는 역할뿐 아니라 메서드 실행을 끝내는 역할도 합니다. 조건을 만족하지 않으면 더 진행하지 않는 조기 반환에도 자주 사용합니다.
static int calculateReward(int score) {
if (score <= 0) {
return 0;
}
if (score >= 100) {
return 50;
}
return 10;
}
조기 반환은 중첩된 if를 줄여 흐름을 평평하게 만듭니다. 다만 메서드가 길고 return 위치가 너무 많으면 오히려 흐름을 놓칠 수 있습니다. 작은 메서드에서 실패 조건을 먼저 처리하고 핵심 로직을 아래에 두는 방식으로 사용하면 읽기 좋습니다.
void 메서드에서도 return;으로 조기 종료할 수 있습니다.
static void printPositiveDamage(int damage) {
if (damage <= 0) {
return;
}
System.out.println("피해량: " + damage);
}
여기서는 반환값이 없으므로 return damage;처럼 값을 적을 수 없습니다. void와 값 반환 메서드의 차이를 컴파일러가 엄격하게 검사해 주기 때문에 반환형을 먼저 정하고 본문을 작성하는 습관이 좋습니다.
오버로딩을 남용하지 않기
오버로딩은 같은 개념을 여러 입력 방식으로 제공할 때 유용합니다. heal(health, amount)와 heal(health, amount, maxHealth)는 둘 다 회복이라는 같은 행동이므로 자연스럽습니다. 하지만 이름만 같고 실제 의미가 다르면 호출하는 사람이 헷갈립니다.
static int calculate(int attack, int defense) {
return Math.max(attack - defense, 0);
}
static String calculate(String name, int level) {
return name + " Lv." + level;
}
위 예제는 컴파일될 수는 있지만 좋은 설계가 아닙니다. 첫 메서드는 피해량 계산이고 둘째 메서드는 표시 문자열 생성입니다. 이름을 calculateDamage, formatLevelLabel처럼 분리해야 합니다. 오버로딩은 “같은 질문에 입력만 다르게 답한다”는 느낌일 때만 사용하세요.
또 하나의 주의점은 int, long, double처럼 숫자 타입이 섞일 때입니다. Java는 가장 알맞은 메서드를 선택하려고 하지만, 호출이 애매하거나 의도와 다른 오버로드가 선택될 수 있습니다. 초반에는 타입을 단순하게 유지하고, 필요하면 명시적으로 형 변환을 하거나 이름을 나누는 편이 안전합니다.
메서드 테스트를 손으로 설계하기
자동 테스트 도구를 아직 배우지 않았더라도 테스트 사고는 지금부터 연습할 수 있습니다. 계산 메서드를 만들면 정상 입력, 경계 입력, 이상 입력을 최소 하나씩 생각합니다.
calculateDamage라면 다음 경우를 확인합니다.
- 공격력이 방어력보다 크면 차이를 반환합니다.
- 공격력과 방어력이 같으면 0을 반환합니다.
- 방어력이 더 크면 음수가 아니라 0을 반환합니다.
- 공격력이나 방어력이 0일 때도 규칙이 유지됩니다.
이렇게 경우를 적고 main에서 직접 출력해 보면 메서드의 계약이 선명해집니다.
System.out.println(calculateDamage(12, 3)); // 9
System.out.println(calculateDamage(5, 5)); // 0
System.out.println(calculateDamage(3, 10)); // 0
System.out.println(calculateDamage(0, 0)); // 0
주석의 예상값과 실제 출력이 다르면 계산 규칙을 다시 봐야 합니다. 다음 단계에서는 이런 확인을 JUnit 같은 테스트 도구로 자동화하지만, 핵심은 “메서드가 약속한 입력과 출력을 글로 말할 수 있는가”입니다.
실무에서 메서드가 커지는 이유
처음에는 작았던 메서드도 요구사항이 추가되면서 쉽게 커집니다. 예를 들어 피해량 계산에 치명타, 방어 무시, 최소 피해, 최대 피해, 난이도 보정이 차례로 붙으면 한 메서드가 조건문으로 가득 찰 수 있습니다. 이때 무작정 코드를 나누기보다 변화하는 규칙의 이름을 찾아야 합니다. applyCriticalBonus, applyDefense, clampDamage처럼 단계가 보이면 각 단계를 메서드로 분리합니다.
static int calculateDamage(int attack, int defense, boolean critical) {
int baseDamage = Math.max(attack - defense, 0);
int criticalDamage = critical ? baseDamage * 2 : baseDamage;
return Math.min(criticalDamage, 30);
}
위 정도는 한 메서드에 둘 수 있지만, 각 줄에 다시 조건이 늘어난다면 분리할 때입니다. 중요한 것은 줄 수가 아니라 이해 비용입니다. 코드가 10줄이어도 한 번에 설명하기 어렵다면 나누고, 20줄이어도 한 가지 흐름이 분명하면 유지할 수 있습니다.
이번 장의 정리
메서드는 반복을 줄이는 문법을 넘어 프로그램의 생각 단위를 만드는 도구입니다. 매개변수는 입력 계약이고, 반환형은 결과 계약입니다. 계산과 출력, 값 검증과 상태 변경을 구분하면 코드가 읽기 쉬워지고 테스트하기 쉬워집니다. 좋은 메서드는 이름만 보아도 역할이 보이고, 같은 입력에 대해 예측 가능한 결과를 제공합니다. 다음 장에서 클래스를 배우면 이런 메서드들이 객체의 상태와 만나 인스턴스 메서드로 확장됩니다.