변수와 메서드

양성빈·2022년 6월 12일
post-thumbnail

참고
자바의 정석
https://m.blog.naver.com/indigosky94/221710165270

변수와 메서드

선언위치에 따른 변수의 종류

변수는 클래스변수, 인스턴스변수, 지역변수 총 3가지가 존재한다. 변수의 종류를 결정짓는 것은 변수의 선언 위치이다. 멤버변수를 제외하고는 전부 지역변수이고 멤버변수중에 static 키워드가 붙은 것은 클래스 변수, 그 외는 인스턴스 변수이다.

변수의 종류선언 위치생성 시기
클래스 변수클래스 영역클래스가 메모리에 올라갈 때
인스턴스 변수클래스 영역인스턴스가 생성되었을 때
지역 변수클래스 영역 이외의 영역
(메서드, 생성자, 초기화 블럭 내부)
변수 선언문이 수행되었을 때
  1. 인스턴스 변수
    클래스 영역에 선언되며, 클래스의 인스턴스를 생성할 때 만들어진다. 그래서, 인스턴스 변수를 사용하기 위해서는 반드시 인스턴스를 생성해야한다.
    인스턴스는 독립적인 저장공간을 가지기 때문에 서로 다른 값을 가질 수가 있다. 그래서 인스턴스마다 고유 속성을 가지는 경우 인스턴스 변수로 선언하곤 한다.

  2. 클래스 변수
    클래스 변수는 인스턴스 변수 앞에 static 키워드를 붙이기만 하면 된다.
    클래스 변수는 모든 인스턴스가 공통된 저장공간을 공유하게 된다. 한 클래스의 모든 인스턴스가 공통적인 속성이 있는 경우 클래스변수로 선언한다.
    클래스변수는 인스턴스변수와 달리 인스턴스를 생성하지 않고 언제라도 반드시 사용할 수 있다는 특징이 있으며, '클래스이름.클래스변수'로 사용한다.
    클래스가 메모리에 로딩이 될때, 생성되어 프로그램이 종료가 될때까지 유지가 되며, 클래스 변수 앞에 접근제어자 public을 붙이면 어디서든 접근할 수 있는 전역변수의 성격을 지닌다.

Q. 언제 클래스는 메모리에 로딩이 될까?
참조변수의 선언이나 객체의 생성과 같이 클래스의 정보가 필요할 때 클래스는 메모리에 로딩된다.

  1. 지역변수
    메서드 내에 선언되어 메서드 내에서만 사용이 가능하며 메서드가 종료가 되면 소멸된다. 또한, for문이나 while문같은 반복문 내에 선언된 지역변수는 그 블럭내에서만 사용이 가능하며 그 블럭을 벗어나면 소멸되어 사용이 불가능하다.

클래스 변수와 인스턴스 변수

인스턴스 변수는 인스턴스가 생성될 때마다 생성되므로 인스턴스마다 각기 다른 값을 유지할 수 있지만, 클래스 변수는 모든 인스턴스가 하나의 저장공간을 공유하므로, 항상 공통된 값을 갖는다.

보통 클래스변수를 선언할 때 '클래스이름.클래스변수'로 선언할 수 있지만, 참조변수.클래스변수로 값 변경이 가능하다. 하지만 주의할점은 참조변수.클래스변수로 변경할 때 모든 인스턴스들의 값들이 변경되므로 주의하고 왠만하면 앞에서 설명한 '클래스이름.클래스변수'로 선언을 하자.

메서드

메서드는 특정 작업을 수행하는 일련의 문장들을 하나로 묶은 것이다. 일종의 수학에서의 함수와 같다고 생각하면 쉬울 것이다. 어떤 값을 입력하면 작업을 수행하고 결과를 반환한다.

수학의 함수와 다른 점은 수학의 함수는 반드시 입력과 출력 값이 있어야하지만, 메서드는 출력 값이 없을 수도 있으며 심지어 출력값이 없을 수도 있다.

메서드가 작업을 수행할 때, 필요한 값만 넣고 원하는 값만 나오면 될뿐 그 안에 어떻게 구현되어 있는지 어떤 과정을 거치는지 몰라도 된다. 즉, 메서드는 내부가 보이지 않는 블랙박스라고 불린다.

메서드는 객체의 기능을 구현하기 위해 클래스 내부에 구현된 함수라고 한다.
메서드를 구현함으로 객체의 기능을 구현이되며, 메서드의 이름은 사용하는 쪽 (클라이언트 코드)에 맞게 명명하는게 좋다.

메서드를 사용하는 이유

  1. 높은 재사용성
    이미 우리가 경험한 사실이지만, Java API에서 제공하는 메서드들을 사용하면서 몇번이고 호출이 가능하며, 다른 프로그램에서도 사용이 가능하다.

  2. 중복된 코드의 제거
    코딩을 하다보면, 같은 내용의 코드들이 여러 곳에서 반복해서 나타나곤 한다. 이 코드들을 묶어서 하나의 메서드로 작성해 놓으면, 반복되는 코드들 대신에 메서드로 대체가 가능하다. 메서드를 사용하면 코드의 양이 줄어들며 유지보수 차원에서 수정부분이 작아서 오류발생위험이 작아진다.

  3. 프로그램의 구조화
    지금까지 우리는 main 메서드에 코드들을 작성하는 형식으로 코딩을 해왔다. 하지만 이렇게 코딩을 하다보면 작성한 본인도 헷갈릴 때가 많다. 큰 규모의 애플리케이션을 개발할때 여러문장들을 작업단위로 나눠서 여러 개의 메서드로 담아 프로그램을 구조화시키는 것이 좋다. 심지어 메서드의 내용을 작성하지 못하더라도 빈 메서드를 행위 단위로 만들어두고 그 후에 내용을 채워가는게 좋다.

메서드의 선언과 구현

메서드는 크게 선언부와 구현부로 이루어지며, 메서드를 작성하는것은 선언부와 구현부를 작성한다는 의미와 같다.

메서드 선언부

메서드 선언부는 '메서드 이름'과 '매개변수 선언', 그리고 반환타입으로 구성되어 있으며, 메서드 선언부는 작업을 수행하기 위해서 어떤 값들을 필요로 하며, 작업의 결과로 어떤 타입이 반환되는지에 대한 정보를 제공한다. 아래가 메서드를 선언하는 예시 코드이다.

int add (int x, int y) {
	return x + y;
}

메서드 선언부는 후에 되도록 변경되지 않도록 신중히 작성해야한다. 메서드의 선언부를 변경하게 되면, 그 메서드가 호출되는 모든 곳도 같이 변경해야하기 때문이다.

매개변수 선언

매개변수는 메서드가 작업을 수행하는데 필요한 입력 값들을 제공하기 위한 용도이며 매개변수끼리 구분은 ,로 구분한다. 선언은 일반 변수 선언과 같지만 조금 다른 부분은 일반 변수선언할 때는 타입이 같으면 생략이 가능했지만 매개변수는 그렇지 않다는 것이다. 매개변수의 개수는 거의 제한이 없으며 만일 입력해야할 값이 많으면 배열이나 참조변수로 사용하면 된다. 만약, 입력이 전혀 필요없는 경우는 작성하지 않아도 된다.

메서드 이름

메서드 이름은 앞에서 배운 변수의 명명규칙대로 작성을 하면 된다. 또한 누구나 이름만 봐도 알아볼 수 있게 함축적이며 의미있는 이름을 작성하도록 하는 것이 좋다.

반환 타입

메서드의 작업수행 결과인 반환 값 타입을 적는다. 반환 값 타입이 없는 경우 void라는 키워드를 작성한다.

void print() {
	System.out.println("Hello World!");
}

메서드의 구현부

메서드 구현부는 괄호 {} 안에다가 메서드가 호출되었을 때 수행할 코드들을 작성해주면 된다.

return 문

메서드 반환타입이 void가 아닌 경우 구현부 안에 return 문을 반드시 포함시켜야 한다. 이 값은 반환타입과 일치하거나 적어도 자동 형변환이 가능한 것이어야 한다. 여러개의 변수를 선언할 수 있는 매개변수와 달리 return문은 단 하나의 값만 반환할 수 있는데, 메서드로 입력은 여러개일 수 있어도 출력은 최대 하나만 허용하는 것이다.

지역변수

메서드 내에 선언된 변수들을 그 메서드 내에서만 사용할 수 있으므로 서로 다른 메서드라면 같은 이름의 변수를 선언해도 된다.

메서드 호출

메서드를 정의했어도 호출을 해줘야 메서드 구현부에 작성했던 코드들이 실행이 된다. 단 예외는 main 메서드이다. main 메서드는 프로그램 실행 시, OS에서 알아서 실행시켜 준다.

메서드 이름(1,2, ...);

print();
add(3, 5);

인자와 매개변수

메서드를 호출할 때, 지정해준 값들을 인자라고 부르며, 인자의 개수와 호출된 메서드의 매개변수의 개수와 동일해야한다. 또한 인자의 타입은 매개변수의 타입과 일치하거나 자동형변환이 가능한 것이어야 한다. 이와 같은 조건들을 안 지키면 컴파일 에러가 발생한다. 그리고 메서드는 반환타입이 void가 아닌 경우 메서드가 작업을 수행하고 반환한 값을 대입연산자로 변수에 저장하는 것이 보통이지만 저장하지 않아도 문제는 되지 않는다.

int result = add(3, 5);
add(3,5); // 메서드 add가 반환한 결과를 사용하지 않아도 된다.

하지만 위처럼 변수에 저장하지 않고 호출하는 경우는 경우에 따라 무의미한 코드가 많기 때문에 잘 사용하지는 않는다.

메서드의 실행 흐름

같은 클래스 내에의 메서드끼리는 참조변수를 사용하지 않고도 서로 호출이 가능하지만, static 메서드는 같은 클래스 내의 인스턴스 메서드를 호출할 수 없다.

class MyMath {
    long add(long a, long b) {
        return a + b;
    }

    long subtract(long a, long b) {
        return a - b;
    }

    long multiply(long a, long b) {
        return a * b;
    }

    double divide(double a, double b) {
        return a / b;
    }
}

위의 코드는 사칙연산 메서드들을 정의한 클래스이다. 이 클래스의 add 메서드를 호풀할려면 다음과 같이 하면된다.

MyMath mm = new MyMath();
mm.add(3L, 5L);

위 처럼 호출하면 어떻게 메서드 흐름이 동작을 하는걸까? 간략히 보면 다음과 같다.

  1. main메서드에서 add를 호출한다. 호출시 지정한 3L, 5L이 메서드 add의 매개변수 a, b에 복사된다.
  2. add의 괄호 안에 있는 코드들을 순서데로 수행된다.
  3. 메서드 add의 모든 문장이 실행되거나 return문을 만나면 호출한 메서드로 되돌아와서 이후의 문장들을 실행한다.

메서드가 호출되면 지금까지 실행중이었던 메서드는 실행을 잠시 멈추고 호출된 메서드의 문장들이 실행된다.

return문

return문은 현재 실행중인 메서드를 종료하고 호출한 곳으로 돌아간다. 지금까지 반환값이 있을 때만 return문을 써줬지만 사실은 반환 타입이 void일 때도 써줘야한다. 하지만 생략을 하고 우리는 써왔다. 그 이유는 컴파일 단계에서 컴파일러가 자동으로 추가해주기 때문이다.

반환 값

return문의 반환값으로 주로 변수가 오긴 하지만 꼭 변수만 와야 하는것은 아니다. return문 옆에 수식을 작성하여 수식의 결과를 반환 할 수도 있고 문자열이나 객체, 메서드등 여러가지를 직접 반환할 수도 있다.

String returnString() {
	return "Hello";
}

매개변수의 유효성 검사

메서드의 구현부를 작성할 때 먼저 해야하는 일은 매개변수 값이 적절한 값이 넘겨왔는지 확인을 해야한다. 너무 클라이언트 쪽을 믿으면 안된다. 항상 예외에 대한 상황을 생각해야 한다.

JVM의 메모리 구조

응용프로그램이 실행되면 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

  1. 메서드 영역
    프로그램 실행 중 어떤 클래스가 사용되면 JVM은 해당 클래스의 .class파일을 읽어서 분석하여 클래스에 대한 정보를 이곳에 저장한다. 이 때 클래스 변수도 이 영역에 할당된다.


  2. 인스턴스가 생성되는 공간이다. 프로그램 실행중에 생성된 인스턴스는 이 영역에 할당되며 인스턴스 변수도 이 공간에 할당된다.

  3. 콜 스택
    콜 스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면 호출된 메서드에 대한 메모리가 할당되고 메서드가 실행되는 동안 지역변수, 연산 중간결과등 이런 것들을 저장하는데 사용된다. 메서드가 종료되면 이 공간에 할당된 메모리는 비워진다. 각 메서드를 위한 메모리상의 작업공간은 서로 구별되며, 첫번째 메서드가 호출되면 콜 스택 맨 처음으로 블록이 쌓이고 첫번째 메서드 수행중에 다른 메서드가 호출되면 순차적으로 메서드 블럭이 쌓인다. 메서드 수행이 마치면 할당하고 있던 메모리를 반환한다. 그러면 콜 스택에 대해 정리하자면 아래와 같다.

정리

  • 메서드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당 받는다.
  • 메서드가 수행을 마치고나면 사용했던 메모리를 반환하고 스택에서 제거된다.
  • 콜 스택의 제일 위에 있는 메서드가 현재 실행중인 메서드이다.
  • 아래에 있는 메서드가 바로 위의 메서드를 호출한 메서드이다.

기본형 매개변수와 참조형 매개변수

자바에서 메서드를 호출할 때 매개변수로 지정한 값을 메서드의 매개변수에 복사하여 전달해준다. 매개변수의 타입이 기본형이면 기본형의 값이 복사되고, 참조형이면 인스턴스의 주소가 복사되어 전달된다. 또한 기본형 매개변수는 단순히 저장된 값만 얻지만 참조형으로 선언하면 값이 저장된 주소를 알수 있기 때문에 값을 읽어 오는것은 물론 변경도 가능하다.

기본형 매개변수: 변수의 값을 읽기만 할 수 있다. 일종의 ROM의 역할이다.
참조형 매개변수: 변수의 값을 읽고 변경할 수 있다. 일종의 RAM의 역할이다.

좀 더 자세히 보면 기본형 매개변수는 매개변수를 전달하는 것이 아닌 복사본을 전달하기때문에 값을 변경해도 복사본이 변경될 뿐 원본은 변경되지 않고. 참조형 매개변수를 전달하면 주소값이 전달하기 때문에 변경이 가능한것이다.

참조형 반환타입

매개변수뿐만 아니라 반환타입도 참조형이 될 수 있다.

반환타입이 참조형이라는 것은 메서드가 객체의 주소를 반환하는 것을 의미한다.

재귀호출

메서드 내부에서 자기 자신을 호출하는 것을 재귀호출이라고 하며 호출하는 메서드를 재귀 메서드라고 한다.

void method() {
	method(); // 재귀호출
}

호출된 메서드는 값에 의한 호출 (call by value)를 통해 원래의 값이 아닌 복사된 값으로 작업하기 때문에 호출한 메서드와 관계없이 독립적인 작업수행이 가능함으로 자기자신을 호출할 수 있는것이다.

재귀호출을 할때 주의할점이 있다. 위의 코드처럼 재귀호출을 할려면 무한루프에 빠져버리므로 재귀호출을 할려면 아래처럼 조건문이 필수적으로 따라다녀야 한다.

void method(int n) {
	if (n == 0) return;
    
    method(--n);
}

재귀호출 자체를 살펴보면 뭔가 반복문과 비슷하다는 점을 느낄 것이다. 조건문을 통하여 위의 코드처럼 작성하여도 상관은 없지만 반복문을 통해 많이 사용하곤 한다.

void method(int n) {
	while(n != 0) {
    	method(--n);
    }
}

재귀호출과 반복문이 유사하다고 해서 둘이 완전히 같은 것은 아니다. 수행시간과 성능을 비교했을 때 재귀호출은 어쨌든 메서드를 호출하는 것으로 매개변수 복사와 종료 후 복귀할 주소저장 등 추가적으로 필요한 작업이 있기 때문에 반복문보다 재귀호출의 수행시간이 더 오래 걸린다.

그러면 성능도 안 좋은데 우리는 재귀호출을 왜 배워야 하고 재귀호출을 많은 개발자분들이 사용하시는걸까?
바로 재귀호출이 주는 논리적 간결함 때문이다. 반복문과 조건문으로 복잡한 코드들이 재귀호출로 간결하게 표현이 가능하다. 아무리 효율적인 코드라도 가독성이 없는 코드를 작성하는것 보단, 성능이 그렇게 차이가 나지 않으면 다소 효율이 떨어지더라도 가독성이 좋은 코드가 나중에 유지보수적 측면을 생각을 했을때 장점이 띈다.

그러면 지금 학습자들은 조금 헷갈릴 것이다. 과연 반복문을 쓰는게 좋은지? 재귀호출을 쓰는게 좋은지?
일종의 가이드를 주자면 먼저 반복문을 작성해보고 코드가 복잡해지면 재귀호출 방식으로 바꿔보자!

결론적으로 재귀호출은 비효율적이므로 재귀호출에 드는 비용보다 재귀호출의 간결함이 주는 이득이 큰 경우에만 사용해야 한다는것을 잊지 말자.

⚠️ 주의
재귀호출을 할 때 주의해야 할 점은 무한루프를 삐져나갈 수 있는 조건을 잘 걸어야 한다는 것이다.
그렇지 못하면 어느 시점에 이르러서는 결국 스택의 저장한계를 넘게 되고 스택오버플로우 에러가 발생한다.

클래스 메서드와 인스턴스 메서드

변수에서 그랬듯이 메서드 앞에 static이 붙어 있으면 클래스 메서드이고 그 외에는 인스턴스 메서드이다.
클래스 메서드도 클래스 변수처럼 객체를 생성하지 않고 '클래스이름.메서드이름'형식으로 호출이 가능하다. 반면에 인스턴스 메서드는 반드시 객체를 생성해야 한다. 그러면 클래스 메서드와 인스턴스 메서드는 어느 경우에 서로 정의를 해야할까? 서로의 정의를 살펴보면 감이 올 것이다.

인스턴스 메서드: 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다.
클래스 메서드: 인스턴스와 관계없는 (인스턴스 변수나 인스턴스 메서드를 사용하지 않는) 메서드라고 한다.

그리고 클래스 메서드와 인스턴스 메서드를 정의하고 하면서 주의해야할 사항이 있는데 하나하나 살펴보자.

  1. 클래스를 설계할 때, 멤버변수 중 모든 인스턴스에 공통으로 사용하는 것에 static을 붙인다.
    • 생성된 각 인스턴스는 서로 독립적이기 때문에 각 인스턴스 변수는 서로 다른 값을 유지한다. 그러나 모든 인스턴스에서 같은 값이 유지되어야 하는 것은 static 키워드를 붙여서 클래스 변수로 정의해야 한다.
  2. 클래스 변수는 인스턴스를 생성하지 않아도 사용할 수 있다.
    • static이 붙은 변수는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성된다.
  3. 클래스 메서드는 인스턴스 변수를 사용할 수 없다.
    • 인스턴스 변수는 인스턴스가 반드시 존재해야만 한다. 하지만, 클래스 메서드는 인스턴스 생성없이 호출이 가능하므로 클래스 메서드가 호출되었을 때, 인스턴스 변수가 존재하지 않을 수 있기 때문이다. 하지만, 그 반대는 가능하다. 인스턴스 메서드를 호출할 때 클래스 변수는 반드시 존재하기 때문이다.
  4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다.
    • 메서드 작업내용 중에서 인스턴스 변수를 필요로 한다면, static을 붙일 수 없다. 반대로 인스턴스 변수를 필요로 하지 않는다면 static을 붙이자. 메서드 호출시간이 짧아지므로 성능이 향상된다.
  • 클래스의 멤버변수 중 모든 인스턴스에 공통된 값을 유지해야하는 것이 있는지 살펴보고 있으면, static을 붙여준다.
  • 작성한 메서드 중에서 인스턴스 변수나 인스턴스 메서드를 사용하지 않는 메서드에 static을 붙일 것을 고려한다.

클래스 멤버와 인스턴스 멤버간의 참조 호출

같은 클래스에 속한 멤버들 간에 별도의 인스턴스를 생성하지 않고 서로 참조 또는 호출이 가능하다. 단, 클래스 멤버가 인스턴스 멤버를 참조 또는 호출할 때는 객체를 생성해야한다. 왜냐하면, 인스턴스 멤버가 존재할 때 클래스 멤버는 항상 존재하지만. 그 역은 거짓이 될 수 있기 때문이다.

profile
모든 것을 즐길줄 아는 개발자입니다!

0개의 댓글