메서드와 배열 — 코드를 나누고 데이터를 묶기
코드가 길어지면 자연스럽게 두 가지 고민이 생긴다. "이 로직을 어떻게 분리하지?"와 "이 데이터를 어떻게 묶지?". 메서드가 전자를, 배열이 후자를 해결한다.
메서드 — 하나의 작업을 수행하는 코드 블록
메서드 는 특정 작업을 수행하는 코드 묶음이에요. Java에서는 모든 코드가 클래스 안에 있어야 하므로, 다른 언어의 "함수"와 같은 역할을 메서드가 담당합니다.
static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
int result = add(3, 5); // 8
}
메서드의 구조
메서드는 접근제어자 반환타입 이름(매개변수) 형태로 선언합니다.
public static int multiply(int a, int b) {
//│ │ │ │ └── 매개변수
//│ │ │ └── 메서드 이름
//│ │ └── 반환 타입
//│ └── static (객체 없이 호출 가능)
//└── 접근제어자
return a * b;
}
- **반환 타입 **: 메서드가 돌려주는 값의 타입. 반환값이 없으면
void - ** 매개변수 **: 메서드에 전달되는 입력값. 0개 이상
- return: 값을 반환하고 메서드를 종료.
void이면 생략 가능
void 메서드는 값을 반환하지 않고 동작만 수행해요.
static void greet(String name) {
System.out.println("안녕하세요, " + name + "님!");
}
매개변수(Parameter) vs 인자(Argument)
이 두 용어는 자주 섞여 쓰이지만, 정확히 구분하면 매개변수는 ** 선언부 의 변수, 인자는 ** 호출 시 넘기는 실제 값입니다.
static int add(int a, int b) { // a, b는 매개변수
return a + b;
}
add(3, 5); // 3, 5는 인자
메서드 오버로딩 (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; }
오버로딩의 조건은 ** 매개변수의 타입, 개수, 순서** 중 하나가 달라야 한다는 거예요. 반환 타입만 다른 건 오버로딩이 아니며 컴파일 에러가 발생합니다.
// 컴파일 에러 — 매개변수가 같고 반환 타입만 다름
static int calculate(int a) { return a; }
static double calculate(int a) { return a; } // 불가!
System.out.println()이 int, double, String 등 다양한 타입을 받을 수 있는 것도 오버로딩 덕분이에요.
Call by Value — Java는 항상 값을 복사한다
Java의 메서드 호출은 ** 항상 Call by Value**입니다. 이 원칙이 기본 타입과 참조 타입에서 어떻게 적용되는지 살펴볼게요.
기본 타입: 값 자체가 복사된다
static void changeValue(int x) {
x = 100;
}
int num = 10;
changeValue(num);
System.out.println(num); // 10 — 변하지 않음
num의 값 10이 x에 복사됩니다. x를 바꿔도 원본 num은 영향 없어요.
참조 타입: 주소 값이 복사된다
static void changeElement(int[] arr) {
arr[0] = 99;
}
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}; // 새 배열로 교체
}
int[] numbers = {1, 2, 3};
replaceArray(numbers);
System.out.println(numbers[0]); // 1 — 변하지 않음!
arr이 새 배열을 가리키도록 바꿔도 원본 numbers는 그대로예요. 주소 값이 복사된 것이지, 변수 자체를 넘긴 게 아니기 때문입니다.
호출 전: numbers → [1, 2, 3] arr → (같은 배열)
교체 후: numbers → [1, 2, 3] arr → [99, 88, 77] (새 배열)
배열 — 같은 타입의 데이터를 고정 크기로 묶기
** 배열 **은 같은 타입의 데이터를 고정된 크기로 묶어놓은 자료구조입니다. 한번 생성하면 크기를 바꿀 수 없어요.
선언과 초기화
int[] numbers = {1, 2, 3, 4, 5}; // 값과 함께 초기화
int[] scores = new int[5]; // 크기만 지정 (0으로 초기화)
String[] names = new String[3]; // null로 초기화
기본 타입 배열은 0(정수), 0.0(실수), false(boolean)으로 초기화되고, 참조 타입 배열은 null로 초기화됩니다.
배열 접근과 순회
인덱스는 0부터 시작합니다. length는 배열 크기(개수)이므로 마지막 인덱스는 length - 1이에요.
int[] numbers = {10, 20, 30, 40, 50};
System.out.println(numbers[0]); // 10
System.out.println(numbers.length); // 5
범위를 벗어나면 ArrayIndexOutOfBoundsException이 발생합니다.
순회는 기본 for문과 for-each 두 가지 방식이 있어요.
// 인덱스 필요할 때
for (int i = 0; i < numbers.length; i++) {
System.out.println(i + ": " + numbers[i]);
}
// 인덱스 필요 없을 때
for (int num : numbers) {
System.out.println(num);
}
Arrays 유틸리티
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]
Arrays.toString()은 디버깅에 필수예요. 배열을 println()에 직접 넘기면 [I@6d06d69c 같은 해시코드가 출력되기 때문입니다.
System.out.println(numbers); // [I@6d06d69c (쓸모없음)
System.out.println(Arrays.toString(numbers)); // [1, 2, 5, 8, 9] (유용)
기타 유용한 메서드: Arrays.copyOf(), Arrays.fill(), Arrays.equals(), Arrays.binarySearch() (정렬된 배열에서만 동작).
다차원 배열
Java의 2차원 배열은 "배열의 배열"입니다.
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문을 씁니다.
for (int[] row : matrix) {
for (int value : row) {
System.out.printf("%3d", value);
}
System.out.println();
}
"배열의 배열"이기 때문에 각 행의 길이가 달라도 됩니다(가변 길이 배열). 실무에서 자주 쓰이진 않지만, 배열의 내부 구조를 이해하는 데 도움이 돼요.
가변인자 (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 (빈 배열)
가변인자는 ** 매개변수 목록의 마지막 에만 올 수 있고, 하나의 메서드에 ** 하나만 쓸 수 있습니다.
static void print(String prefix, int... numbers) { // OK
for (int num : numbers) {
System.out.println(prefix + num);
}
}
System.out.printf()와 String.format()이 가변인자를 활용한 대표적인 메서드입니다.
주의할 점
1. 배열 크기를 바꿀 수 없다
배열은 생성 시 크기가 고정됩니다. 나중에 데이터를 더 넣어야 하면 새 배열을 만들고 복사해야 해요. 크기가 유동적이어야 하는 상황이라면 ArrayList를 쓰는 게 맞습니다.
int[] arr = new int[3];
// arr.length = 5; // 불가능! length는 읽기 전용
2. Call by Value 오해로 인한 버그
참조 타입을 메서드에 넘기면 "원본이 바뀌는" 것처럼 보이지만, 실제로는 주소 값이 복사된 거예요. 메서드 안에서 객체를 ** 새로 할당 **하면 원본에 영향이 없습니다. 이 차이를 모르면 "값이 바뀌어야 하는데 안 바뀐다"는 버그에 빠지게 돼요.
3. 가변인자와 오버로딩 충돌
가변인자 메서드와 매개변수가 명시된 메서드가 동시에 있으면 호출이 모호해질 수 있어요.
static void print(int a, int b) { /* ... */ }
static void print(int... nums) { /* ... */ }
print(1, 2); // 어떤 메서드가 호출될까? → 명시된 쪽이 우선
컴파일은 되지만 혼란을 줄 수 있으므로, 가변인자와 고정 매개변수 메서드를 같은 이름으로 만드는 건 피하는 게 좋습니다.
▸ TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.
정리
| 항목 | 설명 |
|---|---|
| 메서드 | 하나의 작업을 수행하는 코드 블록. 반환타입 이름(매개변수) |
| 오버로딩 | 같은 이름, 다른 매개변수. 반환 타입만 다른 건 불가 |
| Call by Value | Java는 항상 값을 복사. 참조 타입은 주소 값이 복사됨 |
| 배열 | 고정 크기, 같은 타입. 인덱스는 0부터 시작 |
| Arrays 유틸 | sort(), toString(), copyOf(), equals() 등 |
| 가변인자 | 타입... 이름. 내부적으로 배열, 매개변수 마지막에만 가능 |
다음 글에서는 클래스와 객체를 다룹니다. Java가 "객체지향"이라고 불리는 이유와, 클래스를 어떻게 설계하는지 살펴볼 예정이에요.