본문 바로가기
카테고리 없음

클래스3 (메서드)

by 낭만 코딩 2024. 8. 24.

1. 메서드의 구조

2. 접근제한자

3. 리턴타입

4. 매개 변수

5. 리턴 값

6. 메서드 실행(호출)

7. 메서드 실행 순서

8. 메서드 오버로딩

 

 

 

메서드는 클래스 멤버 중 기능에 해당하는 중괄호 블록입니다. 이 블록은 이름을 가지고 있으며, 이 이름을 통해 메서드를 실행하게 되면 중괄호 블록 안에 있는 실행문들이 실행됩니다.

 

 

1. 메서드의 구조

접근제한자 리턴타입 메서드명 (매개변수...) {
    실행문
    ...
    return 값;
}

 

메서드는 선언부와 실행부로 나눠지는데, 선언부에는 접근제한자, 리턴타입, 메서드명, 매개변수가 정의되고, 중괄호로 블록의 영역을 구분합니다. 중괄호 시작부터 끝나는 부분까지가 해당 메서드의 실행부가 됩니다.

 

2. 접근제한자

말 그대로 접근을 제한하는 키워드가 들어오는 자리입니다. 특정 키워드에 따라 이 메서드를 실행할 수 있는 권한을 제한할 수 있습니다. 이 부분은 따로 다루도록 하고 여기서는 메서드 자체에 대해서만 다루도록 하겠습니다.

 

 

3. 리턴타입

메서드의 선언부 앞쪽에 리턴타입이 오는데, 리턴타입이란 리턴되는 값의 타입니다. 리턴은 돌려준다라는 의미로 돌려주는 데이터(값)의 타입이 됩니다. 메서드는 특정 기능을 실행한 후 어떤 값을 실행한 곳으로 돌려줄 수 있는데, 이 돌려주는 값의 데이터 타입(자료형)을 이 곳에 정의합니다. 돌려주는 값을 리턴값이라고 하는데, 메서드는 리턴값이 있을 수도 있고, 없을 수도 있습니다. 만약 리턴값이 없다면 선언부에 void라고 적어줘야 합니다. 우리가 지금까지 만들었던 main() 메서드도 리턴값이 없기 때문에 리턴타입이 void로 정의한 것입니다. 만약 리턴값이 정수라면 int, 문자열이라면 String이라고 적어줘야 하는 것이죠. 반대로 메서드 선언부에 String이라고 적었다면 메서드 중괄호 블록 내에서 반드시 String을 리턴값으로 정의해줘야 합니다. 그렇지 않으면 에러가 발생하게 됩니다.

 

 

4. 매개 변수

매개 변수는 매개역할을 하는 변수입니다. 매개라는 말처럼 한쪽과 한쪽을 연결해주는 역할을 하는데, 메서드 블록 안쪽과 메서드가 실행되는 곳을 변수로 연결해주는 역할을 합니다. 메서드 입장에서 외부로부터 변수를 입력받기 위해 사용되는 것이죠. 예를 들면 두 수의 합계를 리턴해주는 메서드라면 두 개의 수를 매개변수로 입력받아야 합니다.

 

두 수의 합계를 구하는 메서드는 매개변수의 개수가 2개로 이미 정해져 있습니다. 이렇게 이미 매개변수의 개수가 정해져 있는 경우가 대부분이지만, 간혹 매개변수가 몇개가 입력될지 알수 없는 경우가 있습니다. 예를 들어, 두 수가 아니라 여러 수의 합을 구하는 메서드라면 2개 올수도, 10개가 올수도 있겠죠? 이럴 때 첫번째 방법은 매개변수를 배열로 선언하는 방법이고, 두번째는 매개변수를 선언할 때 타입과 변수명 사이에 ... 을 추가하고 선언하는 방법이 있습니다.

 

public class ParamSample {

	public static void main(String[] args) {
		
		Param p = new Param();
		p.add(10,5);
		//p.add("10", "5"); //에러
		
		p.add2(10, 5);
		
		p.add3(1,2,3,4,5,6,7,8,9,10);

	}

}

class Param {
	
	void add(int x, int y) {
		int z = x + y;
		System.out.println(z);
	}
	
	void add2(double x, double y) {
		double z = x + y;
		System.out.println(z);
	}
	
	void add3(int ... x) {
		int sum = 0;
		for (int i=0; i<x.length; i++) {
			sum += x[i];
		}
		System.out.println(sum);
	}
}

 

실행 결과

15
15.0

 

Param 클래스에는 add()add2() 메서드가 정의되어 있고, ParamSample 클래스의 main() 메서드에서 Param 클래스의 객체를 생성해서 메서드를 호출하고 있습니다. add(10, 5)  이 코드는 매개변수 값으로 x=10, y=5를 넘겨주면서 실행하고 있는데, Param 클래스의 add 메서드를 살펴볼까요? 이 메서드의 입장에서 생각해보면 두 매개변수를 입력받아 z변수에 두 매개변수의 합을 대입하고 출력하는 기능을 하고 있습니다. 그래서 다시 ParamSample 클래스의 main 메서드에서  실행되는 add(10,5)의 호출결과는 105의 합계 15가 출력되는 실행 결과입니다.

 

다음 그 아래는 정수가 아닌 문자열 “10”“5”를 매개변수로 넘겨주는데, 타입이 맞지 않아 에러가 나는 코드입니다.(위에서는 주석으로 처리해 놓았습니다.)

add()메서드는 정수를 입력받도록 설계했는데, 실제 실행코드에서는 정수가 아니라 문자열을 넘겨주기 때문에 에러가 나는 것입니다. 매개변수의 타입은 정확히 맞춰줘야 에러가 발생하지 않습니다. 그런데, 그 아래 보면 add2() 메서드의 매개변수는 double 자료형을 입력받도록 정의되어 있습니다만 실행되는 코드쪽에서 double이 아닌 int 자료형인 105를 넘겨주는데도, 에러 없이 정상적으로 실행이 됩니다. 타입을 정확히 맞춰 줘야한다고 했는데, 왜 에러가 나지 않는 걸까요?

 

그렇습니다. 정수는 실수 자료형으로 자동 형변환이 가능하기 때문입니다. 범위가 작은 자료형은 큰 자료형으로 자동 형변환이 될 수 있었죠? 반대로 큰 범위의 자료형은 작은 범위의 자료형을 변환하려면 강제로 형변환을 해야만 합니다.

 

public class ParamSample2 {

	public static void main(String[] args) {
		
		Param p = new Param();
		//p.add(10.5,5.5); 에러
		
		p.add((int)10.5, (int)5.5);

	}

}

 

실행 결과

15

 

이 ParamSample2 클래스는 위 ParamSample 클래스와 같은 패키지(폴더)에 존재해야 합니다. 패키지 부분은 다음에 다시 다뤄보기로 하고 여기서는 우선 같은 폴더에 두고 실행해야만 Param 클래스로 객체를 생성할 수 있습니다.

 

주석 처리된 부분은 에러가 나게 됩니다. 앞의 예제에서 add() 메서드의 매개변수는 정수 타입인데 10.55.5는 실수이기 때문입니다. 그래서 그 아래 코드처럼 각 매개변수 앞에 (int)를 추가해서 강제로 형변환을 해준 것이죠. 정수로 형변환이 되면 소수점이 없어지기 때문에 105의 합 15가 출력됩니다.

 

 

5. 리턴 값

메서드를 선언할 때 선언부에 리턴타입을 지정해둔 경우 반드시 메서드 내에서 리턴값을 지정해야 합니다. 반대로 선언부에 리턴타입을 void로 선언한 경우는 리턴값을 지정할 수 없습니다. 따라서 메서드는 리턴값이 있을 수도, 없을 수도 있습니다.

 

키워드는 return이라는 명령어를 사용하는데, return 문의 용도는 두 가지로 기억하시면 됩니다. 하나는 값을 가지고 돌아가거나, 다른 하나는 그냥 돌아가는 경우입니다. return은 돌아간다는 의미로, 해당 메서드를 실행한 곳으로 돌아가는데, return 뒤에 값이 있으면 값을 가지고 돌아가고, 없으면 그냥 돌아가게 됩니다.

그래서 return문을 메서드의 실행을 중지하는 용도로도 사용하는 경우도 있습니다.

 

public class ReturnSample {
	
	public static void main(String[] args) {
		
		Return obj = new Return();
		
		String name = obj.getName();
		int age = obj.getAge();
		
		System.out.println(name);
		System.out.println(age);
		System.out.println(obj.getName());
		System.out.println(obj.getAge());
		
	}

}


class Return {
	
	String getName() {
		return "홍길동";
	}
	
	int getAge() {
		return 30;
	}
}

 

실행 결과

홍길동
30
홍길동
30

 

Return 클래스에 메서드가 두개 있는데, getName() 메서드는 리턴타입이 String 이라고 정의했기 때문에 반드시 문자열을 리턴 해줘야 합니다. 만약 리턴하지 않거나, 문자열이 아닌 다른 문자열을 리턴하면 에러가 나게 됩니다.

getAge() 메서드도 마찬가지 입니다. 선언부에 int라고 정의했기 때문에 반드시 int 타입을 리턴해야 합니다. ReturnSample 클래스를 보면 Return 클래스 타입의 obj라는 객체를 생성한 후 String 변수 nameobj.getName() 메서드의 리턴값 홍길동이 대입되고, int 변수 age에는 obj.getAge() 메서드의 리턴값 30이 대입됩니다.

 

출력문 System.out.println() 메서드 안에 obj.Name(), obj.getAge() 메서드가 들어가 있는데, 이 코드는 메서드가 출력되는 것이 아니라 메서드의 리턴값 홍길동30이 출력되는 코드입니다.

 

이번엔 return문을 이용해 메서드를 중지시키는 경우를 살펴보겠습니다.

 

public class ReturnSample2 {
	
	public static void main(String[] args) {
		
		Return2 obj = new Return2();
		
		obj.getTest(0);
		obj.getTest(1);
		
		System.out.println(obj.getName(0));
		System.out.println(obj.getName(1));
		
	}

}


class Return2 {
	
	void getTest(int type) {
		System.out.println("getTest() 메서드 시작");
		
		if (type == 1) {
			return;
		}
		
		System.out.println("getTest() 메서드 끝");
	}
	
	String getName(int type) {
		
		if (type == 1) {
			return "";
		}
		
		return "홍길동";
	}
	
	String getAge(int type) {
		
		return "";
		
		//return "홍길동"; // 에러발생
	}
}

 

실행 결과

getTest() 메서드 시작
getTest() 메서드 끝
getTest() 메서드 시작
홍길동

 

ReturnSample2 클래스에서 Return2 객체를 생성해서 메서드를 호출하는 예제입니다.

getTest() 메서드는 리턴타입이 void이고, 매개변수 type을 입력받아 type1이면 그냥 return문만 있습니다. 이 경우 “getTest() 메서드 끝을 출력하는 실행문이 실행되지 않습니다. 리턴값이 없어서 return문 뒤에 아무 값도 없지만, 메서드가 종료되는 것입니다.

아래 getName() 메서드도 마찬가지입니다. type1이면, “” 빈 문자열을 리턴하고 메서드는 종료됩니다. type1이 아니라면 홍길동이 리턴 됩니다.

그럼 if문 없이 return문을 여러 개 쓰면서 중단 시킬 수 있을까요?

 

String getAge (int type) {
    return "";
    return "홍길동"; // 에러발생
}

 

return문은 두개 이상 존재할 수 없습니다. 이 전 예제는 if문을 통해 특정 조건에만 return문을 만나게 되므로, 무조건 하나의 return문만 실행되었던 것입니다. 이런 부분은 이클립스와 같은 IDE에서 코딩할 때 친절히 알려주므로 외울 필요는 없고, ‘이런게 있구나하고만 넘어가시면 됩니다.

 

6. 메서드 실행(호출)

메서드는 보통 하나의 기능 단위로 선언하고, 다른 곳에서 그 기능이 필요할 때 실행하게 됩니다. 문서나 대화 중 메서드를 실행 또는 호출한다는 표현을 혼용해서 사용하는데, 영어 원문 용어가 call 이라는 단어라서 그렇습니다. call을 번역하는 과정에서 호출이라는 표현도 같이 쓰므로, 실행과 호출은 같은 의미로 받아들이면 됩니다.

 

 

메서드를 실행하는 곳을 크게 같은 클래스 내부에서 호출하는 경우와 다른 클래스 외부에서 호출하는 경우로 나눌 수 있는데, 메서드 선언부의 접근 제한자 부분은 뒤에 좀 더 자세히 다룰테니 여기서는 메서드를 실행하는 방법만 살펴 보겠습니다.

 

클래스 내부에서 즉, 같은 클래스에서 실행하는 경우는 그냥 메서드 이름만 적어주면 실행이 가능합니다. 하지만 클래스 외부, 다른 클래스에 있는 메서드를 실행하는 경우는 먼저 해당 클래스를 객체로 생성한 후 객체를 통해서 메서드를 실행해야 합니다. 메서드를 실행할 때 주의할 점은 매개변수의 타입과 개수에 맞게 값을 넘겨줘야 하고, 리턴값 역시 값을 돌려 받을 때 리턴타입에 맞춰서 받아야 합니다.

여기서 매개변수의 타입과 리턴값의 타입이 앞에서 배웠던 자료형에서 형변환이 가능한 경우에만 실행이 가능합니다.

 

메서드도 변수와 마찬가지로 클래스 메서드와 인스턴스 메서드가 있습니다. 클래스 변수는 클래스명으로 직접 실행이 가능하고, 인스턴스 메서드는 객체를 통해 실행할 수 있습니다. 객체 생성 없이 바로 실행 할 수 있는 메서드는 선언부에 static을 붙여 주면 됩니다.

 

public class MethodCall {

	public static void main(String[] args) {
		
		// 직접 실행
		Method.printName();
		
		// 객체를 생성해서 실행
		Method m = new Method();
		m.printEmail();

	}

}

class Method {
	
	static void printName() {
		System.out.println("printName() 실행");
	}
	
	void printEmail() {
		System.out.println("printEmail() 실행");
		
		printId(); // 다른 메서드 실행
	}
	
	void printId() {
		System.out.println("printId() 실행");
	}
}

 

실행 결과

printName() 실행
printEmail() 실행
printId() 실행

 

Method 클래스를 보면 printName() 메서드의 선언부 앞쪽에 static이라는 키워드가 붙어있습니다. 이 메서드는 객체 생성없이 클래스명으로 직접 실행이 가능합니다. 그래서 Method.printName() 라는 코드로 실행하고 있습니다.

printEmail() 메서드와 printId() 메서드는 static 키워드가 없으므로 객체 생성 후 실행해야 하며, printEmail() 메서드 안에서 printId() 메서드를 실행하고 있습니다. printEmail() 메서드를 실행하면 printId() 메서드도 같이 실행되었습니다.

 

printEmail()와 printId() 메서드는 같은 클래스 안에 존재하므로 다른 메서드를 실행할 때는 객체 생성 없이 실행이 가능합니다.

 

 

7. 메서드 실행 순서

자료구조 중에 스택(stack)이라는 자료구조가 있습니다. 이 스택은 먼저 들어온 값이 가장 나중에 꺼낼 수 있는 자료구조입니다.

스택(stack) 자료구조

위 그림처럼 input에서 1이 먼저 들어가고, 2,3 순으로 저장이 되는데, 스택이라는 자료구조는 1을 먼저 꺼낼 수가 없어 3을 먼저 꺼내게 됩니다. 따라서 output 순서는 3, 2, 1 순이 됩니다. 이것을 스택 자료구조라고 하고, 메서드의 실행 순서도 스택처럼 먼저 실행된 메서드가 나중에 종료 되게 됩니다.

public class MethodOrder {

	public static void main(String[] args) {
		
		MethodEx me = new MethodEx();
		
		me.one(); // 메서드 실행

	}

}

class MethodEx {
	
	void one() { // 1.
		two();
		System.out.println("one");
	}
	
	void two() { // 2.
		three();
		System.out.println("two");
	}
	
	void three() { // 3.
		System.out.println("three");
	}
}

 

실행 결과

three
two
one

 

이 예제는 main() 메서드에서 one() 메서드를 실행하고, one() 메서드는 two() 메서드를, two() 메서드는 three() 메서드를 실행합니다. 실행 순서는 one() -> two() -> three() 인데, 출력결과를 보면 three -> two -> one 순으로 출력이 되었습니다.

 

위의 스택 자료구조 이미지와 다시 비교해 보세요. 메서드도 스택처럼 나중에 실행된 메서드가 먼저 끝나게 됩니다.

이 실행 순서는 나중에 메서드가 여러 개 중첩된 소스를 보고 해석할 때 도움이 될 것입니다. 다음 예제를 보고 실행순서를 그려보며 결과를 예상해보세요.

 

public class MethodSample2 {

	public static void main(String[] args) {
		
		System.out.println(divide(pow(add(3,3))));

	}
	
	static int add(int x, int y) {
		return x + y;
	}
	
	static int pow(int x) {
		return x * x;
	}
	
	static int divide(int x) {
		return x / 2;
	}
	

}

 

실행 결과

18

 

main() 메서드에서 실행문은 한줄 밖에 없습니다. println() 메서드 안에 divide() 메서드 안에 pow() 메서드, 또 그 안에 add() 메서드가 있습니다. 총 4개의 메서드가 중첩되어 있지만 밖에서부터 해석하는 것이 아니라 가장 안쪽부터 해석해야 합니다.

 

메서드는 가장 나중에 실행된 것이 가장 먼저 실행이 끝나게 됩니다. 실행이 끝나는 시점은 가장 안쪽 부터 add() -> pow() -> divide() -> println() 순입니다. add(3,3)은 두 매개변수를 더해서 리턴 하므로 pow(6) 이 되고, 이 메서드의 리턴값은 6*6 = 36이 되므로 divide(36) 이 되어 36 / 2를 리턴하므로, 최종적으로 println(18)이 됩니다.

 

프로그램 코드를 해석할 땐 중첩된 코드가 많아 복잡해 보이지만, 천천히 하나씩 따로 떼어 해석하는 연습을 하면, 금방 실력이 좋아지는 것을 느끼게 될 것입니다.

 

 

8. 메서드 오버로딩

클래스 내에서 이름이 같은 메서드가 여러개 있을 수 있는데 이것을 오버로딩(overloading)이라고 합니다. 단어의 의미만 보면 over 과하게, load 적재하다. 사전적으로는 과하게 많이 적재한다는 의미로 직역할수 있겠네요. 조금 더 확장해서 생각해보면 같은 이름을 가진 메서드이면서 매개변수의 자료형, 매개변수의 개수, 순서 중에 하나 이상이 달라야 합니다.

 

메서드 오버로딩이 필요한 이유는 매개변수를 다양하게 입력받게 하기 위함입니다. 두 수의 곱을 구해주는 메서드를 만들어 보겠습니다.

public class Overloading {

	public static void main(String[] args) {
		
		Operator op = new Operator();
		
		System.out.println(op.multiply(4, 3));
	}

}

class Operator {
	
	int multiply(int x, int y) {
		System.out.println("(int, int)");
		return x * y;
	}
}

 

실행 결과

12

 

multiply() 메서드는 정수 타입의 매개변수 2개를 입력받아 두 변수의 곱을 리턴하는 메서드입니. 실행 결과 역시 정상적으로 12가 출력되었습니다. 여기까지는 아무런 문제가 없지만, 정수 타입이 아니라, 실수 타입의 곱을 구해야 한다면 이 메서드를 사용할 수 없습니다. 다른 메서드 이름으로 만들어도 상관없지만, 만약 2개의 매개변수가 둘다 정수이거나, 실수가 아니라, 정수하나, 실수하나라면 또 다른 이름의 메서드를 하나 더 만들어야 합니다. 이렇게 되면 multiply라는 이름만으로 메서드를 관리하기가 힘들어지겠죠? 그래서 같은 이름의 매개변수의 타입이나 개수, 순서만 다르게 처리할 수 있습니다. 이것을 바로 오버로딩(overloading)이라고 부릅니다.

 

위 예제를 수정해보겠습니다.

public class Overloading {

	public static void main(String[] args) {
		
		Operator op = new Operator();
		
		System.out.println(op.multiply(4, 3));
		System.out.println(op.multiply(4.5, 3.5));
		System.out.println(op.multiply(4, 3.5));
		System.out.println(op.multiply(4.5, 3));

	}

}

class Operator {
	
	int multiply(int x, int y) {
		System.out.println("(int, int)");
		return x * y;
	}
	
	double multiply(double x, double y) {
		System.out.println("(double, double)");
		return x * y;
	}
	
	double multiply(int x, double y) {
		System.out.println("(int, double)");
		return x * y;
	}
	
	double multiply(double x, int y) {
		System.out.println("(double, int)");
		return x * y;
	}
}

 

실행 결과

(int, int)
12
(double, double)
15.75
(int, double)
14.0
(double, int)
13.5

 

Operator 클래스에는 multiply 메서드가 총 4개가 있습니다. 모두 이름은 같지만 매개변수 타입이 다른 오버로딩된 메서드들이죠. main 메서드에서 각 메서드를 실행해서 결과를 출력하고 있는데, 실행 결과를 보면 모두 다른 메서드가 실행된 것을 알 수 있습니다. 대표적인 오버로딩 메서드는 여러분들이 지금까지 매 예제 마다 사용해 왔던 println() 메서드 입니다. 이 메서드의 매개변수로 어떤 값들을 넣어도 전부 에러없이 출력이 잘 되었었죠? 아래 코드 처럼요.

 

public class Overloading2 {

	public static void main(String[] args) {
		
		System.out.println(1);
		System.out.println(5.5);
		System.out.println((long)100);
		System.out.println("홍길동");
		System.out.println('a');
		System.out.println(true);
		System.out.println(new Overloading2());
		System.out.println(new int[5]);
		
	}

}

 

실행 결과

1
5 .5
100
홍길동
a
true
Overloading2@28a418fc
[I@5305068a

 

어떤 값을 넣어도 출력이 잘 되는 것을 알 수 있습니다. 객체는 알아볼 수 없는 형태로 출력되긴 했지만, 에러없이 출력이 됩니다. 그럼 이클립스에서 컨트롤키를 누른 상태에서 println() 메서드를 클릭해보세요.

 

새로운 탭으로 PrintStream class 클래스의 해당 println() 메서드로 이동될 것입니다. 소스를 살펴보면 예상대로 오버로딩된 메서드들이 있는 것을 알 수 있는데요, 이클립스 우측에 Outline을 보면 이 클래스의 전체 구조가 한눈에 보입니다.

 

println() 메서드만 10개가 존재합니다. 이제 왜 오버로딩이 필요한지 감이 오시겠죠?