메서드와 배열 — 코드를 나누고 데이터를 묶기
코드가 길어지면 자연스럽게 두 가지 고민이 생긴다. "이 로직을 어떻게 분리하지?"와 "이 데이터를 어떻게 묶지?". 메서드가 전자를, 배열이 후자를 해결해준다. 이번 글에서는 이 두 가지를 한 번에 정리한다.
메서드란?
메서드는 하나의 작업을 수행하는 코드 블록이다. 다른 언어에서는 함수(function)라고도 한다. Java에서는 모든 코드가 클래스 안에 있어야 하므로, 함수 대신 메서드라는 이름을 쓴다.
public class Calculator {
// 메서드 선언
static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
int result = add(3, 5); // 메서드 호출
System.out.println(result); // 8
}
}
메서드의 구조
접근제어자 반환타입 메서드이름(매개변수) {
// 본문
return 결과;
}
public static int multiply(int a, int b) {
//│ │ │ │ └── 매개변수(parameter)
//│ │ │ └── 메서드 이름
//│ │ └── 반환 타입
//│ └── static (객체 없이 호출 가능)
//└── 접근제어자
return a * b;
}
- 반환 타입: 메서드가 돌려주는 값의 타입. 반환값이 없으면
void - 매개변수: 메서드에 전달되는 입력값. 0개 이상
- return: 값을 반환하고 메서드 종료.
void이면 생략 가능
void 메서드
반환값이 없는 메서드는 void로 선언한다.
static void greet(String name) {
System.out.println("안녕하세요, " + name + "님!");
// return 생략 가능
}
public static void main(String[] args) {
greet("Alice"); // 안녕하세요, Alice님!
}
매개변수(Parameter) vs 인자(Argument)
이 두 용어는 자주 섞여 쓰이지만 정확히는 다르다.
static int add(int a, int b) { // a, b는 매개변수(parameter)
return a + b;
}
add(3, 5); // 3, 5는 인자(argument)
- 매개변수: 메서드 선언부에 있는 변수
- 인자: 메서드 호출 시 실제로 넘기는 값
메서드 오버로딩 (Overloading)
같은 이름의 메서드를 매개변수를 다르게 해서 여러 개 만들 수 있다.
static int add(int a, int b) {
return a + b;
}
static double add(double a, double b) {
return a + b;
}
static int add(int a, int b, int c) {
return a + b + c;
}
add(3, 5); // int add(int, int) 호출
add(3.0, 5.0); // double add(double, double) 호출
add(1, 2, 3); // int add(int, int, int) 호출
오버로딩의 조건
- 매개변수의 타입, 개수, 순서 중 하나가 달라야 한다
- 반환 타입만 다른 건 오버로딩이 아니다 (컴파일 에러)
// 컴파일 에러 — 매개변수가 같고 반환 타입만 다름
static int calculate(int a) { return a; }
static double calculate(int a) { return a; } // 불가!
System.out.println()이 int, double, String, boolean 등 다양한 타입을 받을 수 있는 것도 오버로딩 덕분이다.
Call by Value — Java는 항상 값을 복사한다
Java에서 메서드에 인자를 넘기면 항상 값이 복사된다. 이걸 Call by Value라고 한다.
기본 타입의 경우
static void changeValue(int x) {
x = 100;
}
public static void main(String[] args) {
int num = 10;
changeValue(num);
System.out.println(num); // 10 — 변하지 않음!
}
num의 값이 복사되어 x에 전달된다. x를 바꿔도 원본 num은 영향 없다.
참조 타입의 경우
static void changeElement(int[] arr) {
arr[0] = 99; // 배열 내용을 변경
}
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
changeElement(numbers);
System.out.println(numbers[0]); // 99 — 변했다!
}
"참조 타입이면 원본이 바뀌잖아요? 그러면 Call by Reference 아닌가요?"
아니다. 참조(주소) 값이 복사되는 거다. 배열의 주소가 복사되어 같은 배열을 가리키게 된 것이지, 원본 변수 자체가 넘어간 건 아니다.
static void replaceArray(int[] arr) {
arr = new int[]{99, 88, 77}; // 새 배열로 교체
}
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
replaceArray(numbers);
System.out.println(numbers[0]); // 1 — 변하지 않음!
}
arr이 새 배열을 가리키도록 바꿔도 원본 numbers는 그대로다. 주소 값이 복사된 것이지, 변수 자체를 넘긴 게 아니기 때문이다.
main: numbers → [1, 2, 3] numbers → [1, 2, 3] (원본 유지)
↑
method: arr ────────┘ arr → [99, 88, 77] (새 배열 가리킴)
(복사된 주소)
이 차이를 이해하면 Java의 Call by Value가 명확해진다.
배열
배열 선언과 초기화
배열은 같은 타입의 데이터를 고정 크기로 모아놓은 것이다.
// 선언 + 초기화
int[] numbers = {1, 2, 3, 4, 5};
// 크기만 지정 (기본값으로 초기화)
int[] scores = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]
boolean[] flags = new boolean[3]; // [false, false, false]
// 선언 후 나중에 초기화
int[] data;
data = new int[]{10, 20, 30};
기본 타입 배열의 기본값:
| 타입 | 기본값 |
|---|---|
int, long | 0 |
double, float | 0.0 |
boolean | false |
char | '\u0000' |
| 참조 타입 | null |
배열 접근
int[] numbers = {10, 20, 30, 40, 50};
System.out.println(numbers[0]); // 10 (첫 번째)
System.out.println(numbers[4]); // 50 (마지막)
System.out.println(numbers.length); // 5 (배열 크기)
numbers[2] = 99; // 값 변경
System.out.println(numbers[2]); // 99
인덱스는 0부터 시작한다. length는 5인데 마지막 인덱스는 4라는 점을 주의.
// ArrayIndexOutOfBoundsException!
System.out.println(numbers[5]); // 인덱스 범위 초과
배열 순회
int[] numbers = {10, 20, 30, 40, 50};
// 기본 for문 — 인덱스가 필요할 때
for (int i = 0; i < numbers.length; i++) {
System.out.println(i + ": " + numbers[i]);
}
// for-each — 인덱스가 필요 없을 때
for (int num : numbers) {
System.out.println(num);
}
배열 유틸리티 (java.util.Arrays)
import java.util.Arrays;
int[] numbers = {5, 2, 8, 1, 9};
// 정렬
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 5, 8, 9]
// 검색 (정렬된 배열에서)
int index = Arrays.binarySearch(numbers, 5);
System.out.println(index); // 2
// 복사
int[] copy = Arrays.copyOf(numbers, numbers.length);
int[] partial = Arrays.copyOfRange(numbers, 1, 4); // [2, 5, 8]
// 채우기
int[] filled = new int[5];
Arrays.fill(filled, 7);
System.out.println(Arrays.toString(filled)); // [7, 7, 7, 7, 7]
// 비교
System.out.println(Arrays.equals(numbers, copy)); // true
Arrays.toString()은 디버깅할 때 매우 유용하다. 배열을 그냥 println()하면 [I@hashcode 같은 쓸모없는 출력이 나온다.
int[] arr = {1, 2, 3};
System.out.println(arr); // [I@6d06d69c (쓸모없음)
System.out.println(Arrays.toString(arr)); // [1, 2, 3] (유용!)
다차원 배열
2차원 배열
// 3행 4열 배열
int[][] matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
System.out.println(matrix[1][2]); // 7 (2행 3열)
System.out.println(matrix.length); // 3 (행 수)
System.out.println(matrix[0].length); // 4 (열 수)
2차원 배열 순회
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.printf("%3d", matrix[i][j]);
}
System.out.println();
}
또는 for-each로:
for (int[] row : matrix) {
for (int value : row) {
System.out.printf("%3d", value);
}
System.out.println();
}
가변 길이 배열 (Ragged Array)
Java의 2차원 배열은 사실 "배열의 배열"이다. 각 행의 길이가 달라도 된다.
int[][] ragged = new int[3][];
ragged[0] = new int[]{1, 2};
ragged[1] = new int[]{3, 4, 5};
ragged[2] = new int[]{6};
실무에서 자주 쓰이진 않지만, 자바 배열의 내부 구조를 이해하는 데 도움이 된다.
가변인자 (Varargs)
메서드에 인자 개수를 유동적으로 넘기고 싶을 때 쓴다.
static int sum(int... numbers) {
int total = 0;
for (int num : numbers) {
total += num;
}
return total;
}
// 호출
sum(1, 2); // 3
sum(1, 2, 3, 4, 5); // 15
sum(); // 0
int... numbers는 내부적으로 int[] 배열이 된다. 그래서 for-each로 순회할 수 있다.
가변인자 규칙
// 가변인자는 매개변수 목록의 마지막에 와야 한다
static void print(String prefix, int... numbers) { // OK
for (int num : numbers) {
System.out.println(prefix + num);
}
}
// static void print(int... a, int... b) {} // 컴파일 에러!
// 가변인자는 하나만 쓸 수 있다
printf, String.format 같은 메서드가 가변인자를 활용한 대표적인 예다.
System.out.printf("이름: %s, 나이: %d%n", "Alice", 25);
메서드와 배열을 함께 쓰기
배열을 매개변수로 받기
static double average(int[] scores) {
int sum = 0;
for (int score : scores) {
sum += score;
}
return (double) sum / scores.length;
}
int[] scores = {85, 92, 78, 96, 88};
System.out.println(average(scores)); // 87.8
배열을 반환하기
static int[] createRange(int start, int end) {
int[] result = new int[end - start];
for (int i = 0; i < result.length; i++) {
result[i] = start + i;
}
return result;
}
int[] range = createRange(1, 6);
System.out.println(Arrays.toString(range)); // [1, 2, 3, 4, 5]
실전 예제: 간단한 성적 관리
import java.util.Arrays;
public class GradeManager {
// 평균 계산
static double getAverage(int[] scores) {
int sum = 0;
for (int score : scores) {
sum += score;
}
return (double) sum / scores.length;
}
// 최고점
static int getMax(int[] scores) {
int max = scores[0];
for (int i = 1; i < scores.length; i++) {
if (scores[i] > max) {
max = scores[i];
}
}
return max;
}
// 등급 판정
static String getGrade(double average) {
if (average >= 90) return "A";
if (average >= 80) return "B";
if (average >= 70) return "C";
return "F";
}
public static void main(String[] args) {
int[] scores = {85, 92, 78, 96, 88};
System.out.println("점수: " + Arrays.toString(scores));
System.out.println("평균: " + getAverage(scores));
System.out.println("최고점: " + getMax(scores));
System.out.println("등급: " + getGrade(getAverage(scores)));
}
}
출력:
점수: [85, 92, 78, 96, 88]
평균: 87.8
최고점: 96
등급: B
이 정도 규모에서도 메서드로 분리해두면 각 기능을 독립적으로 테스트하고 수정할 수 있다. 코드가 길어질수록 이 분리의 가치가 커진다.
▸ TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.
정리
- 메서드: 하나의 작업을 수행하는 코드 블록.
반환타입 이름(매개변수)형태 - 오버로딩: 같은 이름, 다른 매개변수. 반환 타입만 다른 건 불가
- Call by Value: Java는 항상 값을 복사. 참조 타입은 주소 값이 복사되는 것
- 배열: 고정 크기, 같은 타입. 인덱스는 0부터 시작
- Arrays 유틸:
sort(),toString(),copyOf(),equals()등 - 가변인자:
타입... 이름— 내부적으로 배열, 매개변수 마지막에만 가능
다음 글에서는 클래스와 객체를 다룬다. Java가 "객체지향"이라고 불리는 이유와, 클래스를 어떻게 설계하는지 살펴볼 예정이다.