Method란?
클래스(Class) 내의 함수(function)를 메서드(method)라고 부른다. method의 사전적의미는 어떤 결과물을 내기 위한, systematic procedure(절차)이다.
예를 들면, 카페의 알바생이 블루베리 스무디를 만들려고 한다. 카페에서 제공하는 레시피를 보고서, 필요한 재료를 넣고 일련의 과정대로 수행을 하면 블루베리 스무디가 만들어진다. 그 레시피가 메서드이다.
이처럼, 알바생은 어떻게 블루베리 스무디를 만들어지는 원리를 모르는 상태에서도 방법만 알면 쉽게 만들수 있다. 알바생은 그저 재료(input)을 넣고 일련의 과정을 수행하면 원하는 블루베리 스무디(output)값을 설계한 것이 메서드이다.
이러한 블루베리 스무디 레시피를 만들어 놓으면 어떤 알바생이건 몇 번이든 쓸 수 있다. 레시피 자체가 시스템인 것이다. 이로써 코드의 중복을 제거하고 재사용성이 매우 올라간다.
void showBlueberrySmoothieRecipe() {
BlueberrySmoothie blueberrySmoothie;
// 제조 레시피
}
public static void main(String[] args) {
showBlueberrySmoothieRecipe();
showBlueberrySmoothieRecipe();
showBlueberrySmoothieRecipe();
}
그리고 카페에서의 모든 작업을 체계적으로 수행하기 위하여, 주문 받는 절차, 포장하는 절차, 결제하는 절차, 메뉴별 레시피 절차 등 작업 종류별로 메서드를 만든다. 이렇게 여러개의 메서드(작업)들을 묶어놓으면, 카페에서의 손님 방문에 따른 구조화된 작업 처리가 가능하다. 이로써 프로그램을 구조화 할수 있다.
void showRecipe();
void takeOrder();
void pack();
void pay();
switch(procedure) {
case 1: showRecipe();
case 2: takeOrder();
case 3: pack();
case 4: pay();
default: break;
}
메서드 선언과 구현
메서드의 구조
메서드는 선언부(header)와 구현부(body)로 구성되어 있다. 메서드는 기능을 수행하는 동작(작업)의 단위이라서 동사로 네이밍하는 것이 일반적이다. 또한 메서드 이름을 보자마자, 어떤 동작을 수행할지 예측이 가야, 다른 개발자들이 이해하기도 쉽다. 그러므로 메서드 이름 또한 신경써서 작성해야한다.
메서드 반환 타입은 메서드 작업 수행 결과값의 타입으로 자동 타입 캐스팅(Implicit Type Casting)이 되거나 동일한 타입이어야한다. 또한 인자(argument) 타입이 매개변수(parameter) 타입으로 자동 타입캐스팅 되거나 동일한 타입이어야한다.
매개변수가 많을 경우, 참조 변수를 사용하거나 배열을 사용하여 값을 받는 것이 가독성에 좋다. 허나 반환값은 하나의 값만 반환 할 수 있다. 반환값이 없는 경우에도, 반드시 'return' 문으로 호출한 메서드로 되돌아가야한다. 반환타입이 void인 경우에는 'return'문을 쓰지 않았지만, 컴파일러가 자동적으로 메서드의 마자막에 'return;'을 자동적으로 추가해준것이다.
[Return Type] [Method Name]([Parameter Type] [Parameter Name], [Parameter Type] [Parameter Name]) { // header
// body
Return Type x;
// ...
return x;
}
// method call
Return Type = [Instance Name].[Method Name]([Argument Type] [Argument Name], [Argument Type] [Argument Name]);
매개 변수의 전달
메서드 호출시에, 인자의 값이 지정된 매개변수로 복사(대입) 된다. 이렇게 원본의 값이 아닌 복사에 의한 호출을 '값에 의한 호출(Call By Value)' 이라고 부른다. 그러므로 인자가 기본형(primitive type) 이면 값이 복사되어서 매개변수가 변경되어도, 원본 데이터에는 변화가 없다. 헌데, 인자가 참조 타입(reference type) 이면 주소 값을 전달 받아서, 주소를 통해 원본 데이터에 접근 할 수 있다. 그리하여 참조 타입의 변수를 매개변수로 넘겼을 경우에는 원본값의 변경이 발생 할 수 있다.
다음 코드에서는 기본형(primitive type)의 num을 매개변수로 넘겼을때 값만 복사되어, 매개변수의 값을 수정해도 원본값에는 변화가 없다. 또한 참조 변수인, arr을 매개 변수로 넘겨 매개변수 값을 변경하였다. arr에는 배열의 주소 값이 저장된 변수이므로 주소값이 복사된다. 따라서 넘겨받은 주소를 통해 배열에 접근하여 원본값이 변경되었다. 그러나 String 타입의 객체를 가리키는 참조 변수 str를 매개 변수로 값을 변경을 하였지만, 원본값에는 변화가 없었다. String 타입의 객체는 문자열 변경이 불가하므로, 새로운 문자열을 참조할때 새로운 객체를 할당하기 때문이다.
public class MethodCallByValueTest {
void changeReferenceTypeStr(String str) {
// str은 원본 주소를 참조
// "Halo"을 가진 새로운 객체를 생성하고 str 매개변수가 해당 객체를 참조하도록 변경된다. (String 타입 문자열 변경 불가)
// 그리하여 원본값 변경 불가능
str = "Halo";
}
void changeReferenceTypeChArr(char[] arr) {
arr[0] = 'H'; // arr은 원본 주소를 참조하므로 원본 값 변경 가능
}
void changeValueTypeInt(int num) {
num = 10; // num은 값만 전달받아 원본 값 변경 불가능
}
public static void main(String[] args) {
MethodCallByValueTest m = new MethodCallByValueTest();
String str = new String("Hello World");
System.out.println(str);
m.changeReferenceTypeStr(str);
System.out.println(str);
char[] arr = {'h', 'e', 'l', 'l', 'o'};
System.out.println(arr);
m.changeReferenceTypeChArr(arr);
System.out.println(arr);
int num = 1;
System.out.println(num);
m.changeValueTypeInt(num);
System.out.println(num);
}
}
참고로 반환값이 있는 메서드를 반환값이 없는 메서드로 바꾸는 방법도 있다. 배열은 참조형 변수이기 때문에, 여러개 값을 받고자 할때에도 객체를 쓰지않고 유용하게 쓸수도 있다.
int add(int a, int b) {
return a + b;
}
void add(int a, int b, int[] sum) {
sum[0] = a + b; // 원본 값 변경
}
물론, 객체의 주소를 반환하는 참조형 반환 타입도 가능하다.
char[] hello() {
char[] arr = {'h', 'e', 'l', 'l', 'o'};
return arr; // 생성한 객체 주소 반환
}
char[] arr = example.hello();
System.out.println(arr); // hello
매개 변수 유효성 검사
메서드를 구현할때 가장 중요한 것이 있다. 적절한 데이터가 메서드로 넘어왔는지 확인이 필요하다. 호출하는 쪽에서 적절한 값을 넘겨줄것이라고 생각하면 안된다. 타입만 맞으면 어떤한 값도 매개변수로 넘어올 수 있다. 따라서 모든 경우의 수를 고려하여, 매개변수가 올바른 값이 넘어왔는지 테스트가 필요하다.
마찬가지로, 클라이언트와 서버와 통신할때도 메서드의 매개변수와 같이 적절하지 못한 데이터값을 통신하는 상황이 발생한다. 그러므로 클라이언트로부터 서버로 전달된 값이 올바른 값인지 테스트를 하는 validation 작업이 필요하다.
따라서 잘못된 값이 매개변수로 넘어올 경우에 매개변수의 값을 보정하거나, 보정하는 것이 불가능하면 return 문을 사용하여 작업을 중단하고 호출한 메서드로 되돌아가야한다.
아래의 코드에서 나누는 값이 0이 전달될 수 있다. 숫자를 0으로 나누면 나눗셈 연산 수행이 불가능하므로, 아래처럼 예외처리를 하거나 return 문으로 메서드를 빠져나와서 호출한 메서드로 되돌아가던지 해야한다.
float divide(int x, int y) {
if (y == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return x / y;
}
mv.divide(10, 0);
메서드의 종류
메서드의 종류에는 클래스 메서드(Class Method)와 인스턴스 메서드(Instance Method)있다. static을 붙이면 클래스 메서드이고, 붙이지 않으면 인스턴스 메서드이다.
메서드는 데이터에 대한 동작을 수행하는 기능이다. 인스턴스 메서드는 인스턴스 변수를 사용하여 작업을 수행하기 위한 것이고 클래스 메서드(static method)는 클래스 변수(static variable)를 사용한 작업을 하기 위해서 있다. 그리하여 인스턴스 변수를 사용하지 않으면, 일반적으로 클래스 메서드로 선언한다. 선언에 따른 변수의 종류
클래스 파일이 메모리에 로드되는 시점에 클래스 메서드와 인스턴스 메서드의 코드는 모두 동시에 로드되지만, 메서드에 참조 가능한 시점이 다르다. 클래스 메서드는 클래스 파일이 로드될 때 메서드의 참조를 가지게 되고, 인스턴스 메서드는 각 인스턴스가 생성될 때 해당 메서드의 참조를 가지게 된다. 따라서 인스턴스 메서드는 클래스 메서드를 호출할 수 있지만, 클래스 메서드는 인스턴스 메서드를 호출 할 수 없다.
아래는 코드에는 인스턴스 메서드(Instance Method)와 클래스 메서드(Class Method), 인스턴스 변수(Instance Variable)와 클래스 변수(Class Variable)가 있다. 메서드 종류와 메서드명, 변수의 종류와 변수명을 일치하여 선언하였다.
public class VariablesMethods {
static int classVariable = 10;
int instanceVariable = 20;
void instanceMethod() {
int localVariable = 100;
if (true) {
// java: variable localVariable is already defined in method instanceMethod()
// Scope error -> 중괄호 있더라도 같은 스택 프레임 내의 동일 변수명 구별 불가
int localVariable = 0;
int localVariableInBlock = 0;
localVariableInBlock += localVariable;
}
}
void otherInstanceMethod() {
instanceMethod(); // 인스턴스 메서드끼리는 서로 호출 가능
}
static void classMethod() {
classVariable = 100; // static variable 값 변경
// java: non-static variable instanceVariable cannot be referenced from a static context
// instance variable 접근 불가
instanceVariable = 30;
// java: non-static method method() cannot be referenced from a static context
// instanceMethod 호출 불가
instanceMethod();
}
static int otherClassMethod() {
classMethod(); // static mathod 끼리 서로 호출 가능
int localVariable = 300;
return localVariable;
}
public static void main(String[] args) {
VariablesMethods variablesMethods = new VariablesMethods(); // 객체 생성
variablesMethods.instanceMethod(); // 인스턴스 생성해야 호출 가능
System.out.println(variablesMethods.classVariable); // 인스턴스 생성해야 접근 가능
VariablesMethods.classMethod(); // 인스턴스 생성 없이 실행 가능
System.out.println(VariablesMethods.classVariable); // 인스턴스 생성 접근 가능
int num = VariablesMethods.otherClassMethod();
System.out.println(num); // 300
}
}
인스턴스 메서드(Instance Method)
인스턴스 메서드는 인스턴스를 생성하고 인스턴스를 참조하여 호출이 가능하다.
메서드 내에서 인스턴스 변수를 사용하지 않다면 static을 붙이는 것을 고려하는 것이 좋다. 인스턴스 메서드는 실행시, 호출되어야 할 메서드를 찾는 과정이 추가적으로 필요하다. 해당 인스턴스가 어떤 클래스에 속하는지를 확인하고, 그 클래스의 메서드를 검색해야한다. 그러나 클래스 메서드는 클래스 레벨의 메서드로 검색없이 바로 실행될 수 있다. 이렇게 호출시, 클래스 메서드로 실행 속도면에서 이점을 얻을 수 있다. 자세한 내용은 뒤의 글들에서 찾아볼수 있다. 메서드의 종류와 호출 과정 및 디스패치, Bytecode 실행 과정에 따른 Runtime Data Area의 변화
인스턴스 메서드를 검색 과정은 다음과 같으며, 클래스 상속과 관련하여 추가적인 검색 작업으로 인한 오버헤드가 발생할 수 있음을 알 수 있다.
- 메서드 영역(Method Area)에서 인스턴스가 속한 클래스에서 메서드를 찾는다. 해당 클래스에 메서드가 정의되어 있으면 그 메서드를 호출한다.
- 만약 인스턴스가 속한 클래스에 해당 메서드가 없으면, 상위 클래스로 올라가면서 메서드를 찾는다.
- 메서드가 발견되면 해당 메서드를 호출한다.
하지만, 초기 호출 이후에는 vtable에 해당 상위 클래스의 메서드 위치를 저장하여서 추가적인 검색없이 호출이 가능하다.
클래스 메서드(Class Method)
클래스 메서드(static method)는 static vairable과 같이, 클래스 코드를 읽을때 메서드 영역(Method Area)에 할당되며, 클래스의 라이프 타임 동안 메모리에 유지된다. 따라서 인스턴스화 필요없이, 바로 클래스 메서드를 호출이 가능하다.
클래스 메서드는 클래스 변수를 변경할 수 있으므로, 클래스 메서드의 동작은 모든 인스턴스에 영향을 끼친다. 클래스를 설계할때, 멤버(변수, 메서드)중에 모든 인스턴스가 동일한 값을 사용하는 변수나 메서드에 static을 붙임면 된다.
클래스 메서드(classMethod) 내에서 인스턴스 변수(instanceVariable)에 접근하지 못한다. classMethod는 클래스 레벨에 속하고 특정 인스턴스와 관련이 없기 때문에 금지를 해놓았다. 또한 클래스 메서드를 호출할 시점에는 인스턴스가 생성이 안되어있을 수도 있기 때문이다.
하지만 반대로 인스턴스 메서드는 클래스 변수(static)이 붙은 변수나 메서드인, 멤버(member)에 접근이 가능하다. 인스턴스 메서드를 호출이 가능한 시점에는, 클래스 변수와 클래스 메서드는 메모리에 접근 가능하기 때문이다. 또한 같은 이유로 클래스 메서드에서 인스턴스 메서드를 호출이 불가능하고, 클래스 메서드 끼리의 호출은 가능하다.
그러나 인스턴스 메서드에서는 static 메서드에 보통 접근하지는 않는다. 인스턴스 메서드는 인스턴스 변수에 접근하여 작업을 수행하는 역할의 메서드이다. 클래스 변수의 접근을 할려면, 클래스 메서드를 호출하여 작업을 수행하는 것이 명확하다.
메서드 호출 및 실행 과정
인스턴스 메서드와 클래스 메서드는 호출시, 메서드 실행에 필요한 매개변수, 지역변수, 반환 주소 등의 저장 공간이 필요한다. 그러므로 메서드 호출시, 호출 스택(Call Stack) 영역에 메모리 할당을 위한 스택 프레임(Stack Frame)을 생성을 하여 쌓이게된다. 그리고 스택 프레임에 메서드 실행에 필요한 데이터를 저장한다.
메서드 호출이란 것은 특정 위치에 저장되어 있는 명령어들을 찾는 행위 이며, 메서드 실행은 해당 명령어들을 수행하는 것이다. 그리고 작업 수행 과정에서 데이터를 저장할 변수들이 필요하다.
메서드를 호출 및 실행에 따른 자세한 과정은 다음과 같다.
- 메서드 호출: 프로그램이 실행 중에 메서드를 호출하면, 메서드 이름과 필요한 매개변수를 전달하여 수행된다. 인자 값은 지정된 매개변수로 값이 복사된다. 그리고 기존의 실행중이던 스택 프레임은 대기상태(스레드의 대기 상태)로 남아있게 된다.
- 메서드 검색: JVM은 해당 메서드를 찾기 위해 메서드 영역(Method Area)의 클래스 파일(.class)에서 메서드의 정의를 찾는다. 메서드는 클래스 파일(.class) 안에 바이트 코드로 저장되어 있으며, 호출된 클래스의 메서드 테이블에서 검색된다.
- 메서드 호출 스택에 프레임 push: 메서드 호출은 콜 스택에 새로운 프레임을 push한다. 이 프레임은 호출된 메서드의 지역변수, 매개변수, 반환 주소(호출된 메서드에서 다음으로 실행할 명령어의 주소) 등을 저장하는 데 사용된다. 메서드 수행에 필요한 만큼의 메모리를 스택에 할당받는 것이다.
- 메서드 실행: 호출된 메서드의 바이트 코드가 실행된다. JVM은 바이트 코드를 순차적으로 실행하며, 메서드 내부의 모든 명령문을 수행한다.
- 호출한 메서드의 스택 프레임으로 복귀: 호출된 메서드의 실행이 완료되면, 반환 주소를 사용하여 호출한 메서드의 다음 지점으로 복귀한다. 이때 반환 값이 있으면 이 값도 함께 반환된다.
- 호출된 메서드의 스택 프레임 제거: 호출한 메서드로 복귀하였다면, 호출된 메서드의 스택 프레임(TOP)을 제거한다.
메서드는 인자(argument)의 주소가 아닌 인수의 값을 복사에 의한 호출을 로 인하여, 호출한 메서드와 관계없이 독립적인 작업(Task) 수행이 가능하다.
스택영역에는 여러 스택 프레임들이 있는데, 스택 프레임간의 매개변수로 전달된 값의 동기화를 신경쓰지 않고 작업(Task)를 수행할 수 있다는 것이다. 그리하여 보다 에측 가능하고 안전하게 프로그램이 실행될 수 있다.
그리고 스택의 맨 위(Top)에 있는 프레임이 현재 실행 중이 메서드의 프레임이고, 아래있는 프레임이 현재 실행중인 메서드를 호출한 메서드의 프레임이다. 따라서 호출 스택을 조사해 보면, 메서드 간의 호출 관계와 현재 수행중인 메서드가 어느 것인지 알수 있다. 이것을 스택 트레이스(Stack Trace)라고 하며, 프로그램의 실행 흐름을 추적하고 문제를 파악할때 좋다.
재귀 호출 (Recursive Call)
메서드의 내부에서 메서드 자신을 호출하는 것을 재귀 호출(Recursive Call)이라한다. 또한 재귀 호출을 하는 메서드를 재귀 메서드(Recursive Method)라 한다.
void recursiveMethod() {
recursiveMethod();
}
recursiveMethod();
재귀 메서드는 어떠한 조건으로 빠져나가지 않으면, 무한히 자기 자신을 호출하게 된다. 따라서 재귀 메서드에는 반드시 어떤 값으로 수렴하는 패턴이 있어야하고 특정 값을 만족했을 경우에는 반환을 해야한다.
void recursiveMethod(n) {
if (n <= 0) {
return;
}
recursiveMethod(--n);
}
recursiveMethod(10);
또한 재귀 메서드를 사용할때 유의할 점이 있다. 재귀 메서드가 호출될때 마다, 매개변수 복사와 호출한 메서드의 반환 주소 저장 등 메모리 할당 작업이 일어난다.
이것이 반복적으로 일어나면, 반복문 보다 재귀 호출의 수행 시간이 더 오래걸린다. 또한 스택 오버 플로우(Stack Overflow) 문제도 발생할 수 있다. 그리하여 보통 실무에서는 쓰진 않는다.
하지만 재귀 함수를 쓰면, 간결하게 처리할수 있는 문제도 있다. 간결하다는 것은 이해하기 좋은 코드란 것이며, 다른 사람이 봐도 파악하기 쉽다. 물론, 간결함이 성능보다 더 이득인 경우에 좋은 것이다.
그리하여 어떤 작업을 반복적으로 처리해야할때, 먼저 반복문으로 작성한다. 그리고 너무 복잡하면 재귀 함수로 구현을 시도해보는 것이 좋은 방법일 수 있다.
팩토리얼(factorial)을 재귀함수로 구하는 것이 대표적인 예이다. 아래는 반복문과 재귀 메서드로 팩토리얼을 구하는 코드이다.
public class RecursiveMethod {
// 반복문
public static int factorialIterative(int n) {
if (n <= 0 || n > 12) { // 13!는 int 타입의 최대값인 20억 보다 크다.
return -1;
}
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}
return result;
}
// 재귀함수
public static int factorialRecursive(int n) {
if (n <= 0 || n > 12) {
return -1;
}
if (n <= 1) {
return 1;
}
return n * factorialRecursive(n - 1);
}
public static void main(String[] args) {
int factorial;
int n = 5;
factorial = factorialIterative(n);
System.out.println("반복문을 사용한 " + n + "의 팩토리얼: " + factorial);
factorial = factorialRecursive(n);
System.out.println("재귀 함수를 사용한 " + n + "의 팩토리얼: " + factorial);
}
}
챰고 자료
- 자바의 정석 (남궁성 지음)
'Java > Java Language' 카테고리의 다른 글
[Java] 생성자 (Constructor) (0) | 2024.06.13 |
---|---|
[Java] 오버로딩 (Overloading) (0) | 2024.06.13 |
[Java] 메서드의 종류와 호출 과정 및 디스패치 (0) | 2024.06.12 |
[Java] 선언에 따른 변수의 종류 (0) | 2024.06.03 |
[Java] 객체 지향 개념과 객체화 (절차 지향과의 비교) (0) | 2024.06.01 |