코드가 길어지면 자연스럽게 두 가지 고민이 생긴다. "이 로직을 어떻게 분리하지?"와 "이 데이터를 어떻게 묶지?". 메서드가 전자를, 배열이 후자를 해결한다.

메서드 — 하나의 작업을 수행하는 코드 블록

메서드 는 특정 작업을 수행하는 코드 묶음이에요. Java에서는 모든 코드가 클래스 안에 있어야 하므로, 다른 언어의 "함수"와 같은 역할을 메서드가 담당합니다.

JAVA
static int add(int a, int b) {
    return a + b;
}

public static void main(String[] args) {
    int result = add(3, 5); // 8
}

메서드의 구조

메서드는 접근제어자 반환타입 이름(매개변수) 형태로 선언합니다.

JAVA
public static int multiply(int a, int b) {
//│      │      │    │          └── 매개변수
//│      │      │    └── 메서드 이름
//│      │      └── 반환 타입
//│      └── static (객체 없이 호출 가능)
//└── 접근제어자
    return a * b;
}
  • **반환 타입 **: 메서드가 돌려주는 값의 타입. 반환값이 없으면 void
  • ** 매개변수 **: 메서드에 전달되는 입력값. 0개 이상
  • return: 값을 반환하고 메서드를 종료. void이면 생략 가능

void 메서드는 값을 반환하지 않고 동작만 수행해요.

JAVA
static void greet(String name) {
    System.out.println("안녕하세요, " + name + "님!");
}

매개변수(Parameter) vs 인자(Argument)

이 두 용어는 자주 섞여 쓰이지만, 정확히 구분하면 매개변수는 ** 선언부 의 변수, 인자는 ** 호출 시 넘기는 실제 값입니다.

JAVA
static int add(int a, int b) { // a, b는 매개변수
    return a + b;
}

add(3, 5); // 3, 5는 인자

메서드 오버로딩 (Overloading)

** 같은 이름의 메서드를 매개변수를 다르게 해서 여러 개 만드는 것 **이 오버로딩이에요. 호출 시 컴파일러가 매개변수를 보고 어떤 메서드를 실행할지 결정합니다.

JAVA
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; }

오버로딩의 조건은 ** 매개변수의 타입, 개수, 순서** 중 하나가 달라야 한다는 거예요. 반환 타입만 다른 건 오버로딩이 아니며 컴파일 에러가 발생합니다.

JAVA
// 컴파일 에러 — 매개변수가 같고 반환 타입만 다름
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**입니다. 이 원칙이 기본 타입과 참조 타입에서 어떻게 적용되는지 살펴볼게요.

기본 타입: 값 자체가 복사된다

JAVA
static void changeValue(int x) {
    x = 100;
}

int num = 10;
changeValue(num);
System.out.println(num); // 10 — 변하지 않음

num의 값 10이 x에 복사됩니다. x를 바꿔도 원본 num은 영향 없어요.

참조 타입: 주소 값이 복사된다

JAVA
static void changeElement(int[] arr) {
    arr[0] = 99;
}

int[] numbers = {1, 2, 3};
changeElement(numbers);
System.out.println(numbers[0]); // 99 — 바뀌었다!

"원본이 바뀌니까 Call by Reference 아닌가?" — 아닙니다. 배열의 ** 주소 값 **이 복사된 거예요. 같은 배열을 가리키는 주소가 복사되었기 때문에 배열 내용을 바꿀 수 있는 겁니다.

이걸 증명하는 코드가 있습니다.

JAVA
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는 그대로예요. 주소 값이 복사된 것이지, 변수 자체를 넘긴 게 아니기 때문입니다.

PLAINTEXT
호출 전: numbers → [1, 2, 3]    arr → (같은 배열)
교체 후: numbers → [1, 2, 3]    arr → [99, 88, 77] (새 배열)

배열 — 같은 타입의 데이터를 고정 크기로 묶기

** 배열 **은 같은 타입의 데이터를 고정된 크기로 묶어놓은 자료구조입니다. 한번 생성하면 크기를 바꿀 수 없어요.

선언과 초기화

JAVA
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이에요.

JAVA
int[] numbers = {10, 20, 30, 40, 50};
System.out.println(numbers[0]);     // 10
System.out.println(numbers.length); // 5

범위를 벗어나면 ArrayIndexOutOfBoundsException이 발생합니다.

순회는 기본 for문과 for-each 두 가지 방식이 있어요.

JAVA
// 인덱스 필요할 때
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 클래스는 배열 작업을 편하게 해주는 정적 메서드를 제공합니다.

JAVA
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 같은 해시코드가 출력되기 때문입니다.

JAVA
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차원 배열은 "배열의 배열"입니다.

JAVA
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문을 씁니다.

JAVA
for (int[] row : matrix) {
    for (int value : row) {
        System.out.printf("%3d", value);
    }
    System.out.println();
}

"배열의 배열"이기 때문에 각 행의 길이가 달라도 됩니다(가변 길이 배열). 실무에서 자주 쓰이진 않지만, 배열의 내부 구조를 이해하는 데 도움이 돼요.

가변인자 (Varargs)

메서드에 인자 개수를 유동적으로 넘기고 싶을 때 가변인자를 씁니다. 내부적으로는 배열로 처리돼요.

JAVA
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 (빈 배열)

가변인자는 ** 매개변수 목록의 마지막 에만 올 수 있고, 하나의 메서드에 ** 하나만 쓸 수 있습니다.

JAVA
static void print(String prefix, int... numbers) { // OK
    for (int num : numbers) {
        System.out.println(prefix + num);
    }
}

System.out.printf()String.format()이 가변인자를 활용한 대표적인 메서드입니다.

주의할 점

1. 배열 크기를 바꿀 수 없다

배열은 생성 시 크기가 고정됩니다. 나중에 데이터를 더 넣어야 하면 새 배열을 만들고 복사해야 해요. 크기가 유동적이어야 하는 상황이라면 ArrayList를 쓰는 게 맞습니다.

JAVA
int[] arr = new int[3];
// arr.length = 5; // 불가능! length는 읽기 전용

2. Call by Value 오해로 인한 버그

참조 타입을 메서드에 넘기면 "원본이 바뀌는" 것처럼 보이지만, 실제로는 주소 값이 복사된 거예요. 메서드 안에서 객체를 ** 새로 할당 **하면 원본에 영향이 없습니다. 이 차이를 모르면 "값이 바뀌어야 하는데 안 바뀐다"는 버그에 빠지게 돼요.

3. 가변인자와 오버로딩 충돌

가변인자 메서드와 매개변수가 명시된 메서드가 동시에 있으면 호출이 모호해질 수 있어요.

JAVA
static void print(int a, int b) { /* ... */ }
static void print(int... nums)  { /* ... */ }

print(1, 2); // 어떤 메서드가 호출될까? → 명시된 쪽이 우선

컴파일은 되지만 혼란을 줄 수 있으므로, 가변인자와 고정 매개변수 메서드를 같은 이름으로 만드는 건 피하는 게 좋습니다.

TIP 이 글의 코드 예제를 직접 실행해보고 싶다면 Java 기본기 핸드북을 확인해보세요.

정리

항목설명
메서드하나의 작업을 수행하는 코드 블록. 반환타입 이름(매개변수)
오버로딩같은 이름, 다른 매개변수. 반환 타입만 다른 건 불가
Call by ValueJava는 항상 값을 복사. 참조 타입은 주소 값이 복사됨
배열고정 크기, 같은 타입. 인덱스는 0부터 시작
Arrays 유틸sort(), toString(), copyOf(), equals()
가변인자타입... 이름. 내부적으로 배열, 매개변수 마지막에만 가능

다음 글에서는 클래스와 객체를 다룹니다. Java가 "객체지향"이라고 불리는 이유와, 클래스를 어떻게 설계하는지 살펴볼 예정이에요.

댓글 로딩 중...