6-3. 변수와 메서드

Hyun Jun·2022년 1월 22일
0

자바의 정석

목록 보기
22/52
post-thumbnail
post-custom-banner

변수와 메서드

선언 위치에 따른 분류

종류 선언 위치 생성 시기
클래스 변수 클래스 영역 클래스에 메모리가 올라갈 때
인스턴스 변수 인스턴스가 생성되었을 때
지역 변수 클래스 영역 이외
(메서드, 생성자, 초기화 블럭 내부)
변수 선언문이 수행되었을 때

 

변수는 크게 분류하면 멤버 변수지역 변수로 나눌 수 있음.

멤버 변수 중에 static이 붙은 것은 클래스 변수, 붙지 않은 것은 인스턴스 변수임.

class Variables {
    static int classVar; // 클래스 변수
    int instaceVar; // 인스턴스 변수

    void method() {
        int localVar; // 지역 변수
    }
}

 

1. 클래스 변수

  • 인스턴스 변수 앞에 static을 붙이면 클래스 변수가 됨

    static int classVar;
  • 모든 인스턴스가 공통적인 값을 가져야하는 속성이 있다면 클래스 변수로 선언해야함.

    클래스 변수에 대해서는 모든 인스턴스가 하나의 저장공간을 공유하므로, 한 인스턴스에서 클래스 변수를 변경하면 모든 인스턴스에 반영됨

  • 인스턴스 변수와 달리 인스턴스를 생성하지 않고도 사용 가능.

    Variables.classVar = 7; // 클래스명.변수명
  • 클래스가 메모리에 load 될 때 생성되어 프로그램이 종료될 때까지 유지

    load 시점: 참조변수의 선언, 객체의 생성과 같이 클래스 정보가 필요할 때

    인스턴스가 여러 번 생성되어도 클래스 변수는 처음 한번만 생성되고 반복 생성되지 않음.

  • public을 앞에 붙이면 전역 변수(global variable)가 됨.

    public static int globalVar;

 

2. 인스턴스 변수

  • 인스턴스마다 고유한 상태를 가져야하는 속성이 있다면 인스턴스 변수로 선언해야함.

  • 클래스의 인스턴스를 생성할 때 만들어짐

  • 인스턴스를 생성하지 않고서는 값을 읽어오거나 저장할 수 없음

    Variables v = new Variables(); // 인스턴스를 생성함으로써 인스턴스 변수에 접근 가능해짐
    v.instanceVar = 77; // 생성된 인스턴스 내의 인스턴스 변수에 값을 저장함

 

3. 지역 변수

  • 메서드 안에서 선언되어 메서드의 스코프 안에서만 사용 가능. (메서드가 종료되면 소멸)

  • for문이나 while문 안에서 선언된 지역변수는 선언된 블럭{}안에서만 사용 가능. (벗어나면 소멸)

 

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

가령, 정육각형 모양 쿠키틀로 쿠키를 만든다고 해보자.

쿠키틀(클래스)은 당연하게도 모든 쿠키의 꼭지점을 6개, 각 꼭지점의 각도는 120도로 맞춰줄 것임. 이 쿠키틀로 만들어진 쿠키들은 이러한 공통 속성을 공유함. 이 공통 속성이 바로 static이 붙은 클래스 변수.

이 때 만든 쿠키 중 하나에 스프링클을 뿌려 장식하려고 한다. 장식이 된 쿠키에만 스프링클 속성이 생기는 것. 스프링클처럼 이 쿠키(인스턴스)만이 가진 속성을 다른 말로 인스턴스 변수라고 함.

 

메서드

특정 작업을 수행하는 문장들을 하나로 묶은 것.

 

메서드를 사용하는 이유

  1. 높은 재사용성 (중복된 코드 제거)

    같은 작업을 하는 코드를 여러번 반복해서 작성하는 것보다 메서드로 작성해놓고 필요할 때마다 호출해서 사용하는 것이 효율적.

  2. 프로그램의 구조화

    프로그램의 작업 단위를 나누어 메서드화 해놓으면 main 메서드 안에서 훨씬 직관적인 흐름파악이 가능해지고 구조적인 프로그래밍이 가능해짐. 유지보수 측면에서도 효율적.

 

메서드의 선언과 구현

메서드는 선언부와 구현부 2개의 파트로 나뉘어있음.

메서드 구현 = 선언부와 구현부를 작성하는 것

 

반환타입 메서드명 (타입 매개변수명1, 타입 매개변수명2, ...) {
  // 메서드 호출 시 수행되는 코드
}
int getSquareSize(int width, int height) // 여기까지가 선언부
{
  int result = width * height;
  return result;
} // 블럭{} 안쪽이 구현부

✏️ 선언부

1. 반환 타입

메서드가 return할 값의 타입 명시. return값이 없는 메서드라면 void.

 

2. 메서드 이름

변수의 네이밍 컨벤션과 동일한 규칙을 따름. 특정 작업을 대표하는 이름이므로 함축적이면서도 기능을 바로 파악할 수 있게끔 짓는 네이밍 센스가 필요함.

 

3. 매개 변수 선언

괄호 안에 필요한 파라미터의 타입과 이름을 각각 선언. 쉼표로 구분

만약 파라미터가 없어도 되면 빈 괄호로 둠

 

✏️ 구현부

1. 지역 변수

메서드 내에 선언된 변수로서, 메서드 스코프 안에서만 사용 가능함.

파라미터도 메서드 내에 선언된 것으로 간주하므로 지역 변수임.

 

2. return문

반환 타입이 void인 경우를 제외하고는 구현부 블럭{}안에 반드시 return문이 포함되어야 하며, 없을 시 컴파일 에러가 발생함. 반환값은 딱 한개만 허용함.

엄밀히는 반환값이 void인 경우, 컴파일러가 자동적으로 메서드 마지막에 return;을 추가해주기 때문에 따로 작성할 필요가 없는 것.

 

❗️ [유의] 매개변수의 유효성 검사

호출하는 쪽에서 알아서 적절한 값을 넘길 것이라는 안일함은 금물.

인자의 타입이 매개변수의 타입과 맞는다고 해서 항상 유효한 것은 아니기 때문.

따라서 구현부에서는 매개변수의 값이 적절한지 확인 절차를 넣어줄 필요가 있음.

float divide(int x, int y) {
  if (y == 0) {
    System.out.println("0으로는 나눌 수 없습니다.");
    return 0;
  }
  return x / (float)y; // 정수끼리 나누어도 부동소수값이 나올 수 있기 때문에 float으로 형변환시켰음
}

위 예시 메서드는 매개변수 x를 y로 나눈 값을 float형으로 반환하는데, 이 때 y가 0일 경우에는 나눗셈이 불가능하므로 return문으로 작업을 중단시킴

 

메서드의 호출

메서드를 정의한 후에 그 메서드를 호출해야만 구현부의 코드가 실행됨.

메서드명(인자1, 인자2, ...);
getSquareSize(4, 6);

흔히 인자(argument)와 매개변수(parameter)라는 표현을 혼용하곤 하는데, 엄밀히는 다음과 같은 의미 차이가 있음.

  • 인자: 매개변수에 대입될 실제 값

  • 매개변수: 메서드를 정의할 때 선언부에 선언된 괄호 안의 변수

 

메서드 호출 시 주의사항

  • 인자의 타입은 매개변수의 타입과 일치하거나 적어도 자동 형변환이 가능한 것이어야 함.

    이 때, 인자의 자동 형변환은 작은 범위의 타입에서 큰 범위의 타입으로만 가능함.

    ex) long → double

  • 인자의 개수를 매개변수의 개수보다 적거나 많게 넣으면 컴파일 에러 발생

  • 같은 클래스 내의 메서드끼리는 참조변수를 사용하지 않고도 서로 호출 가능.

  • static 메서드는 (인스턴스 생성 없이는) 같은 클래스 내의 인스턴스 메서드 호출 불가능.

    인스턴스 메서드는 인스턴스 변수를 활용하는 메서드임. 그런데 static 메서드는 직접적으로 인스턴스 변수를 참조할 수 없으므로 인스턴스 메서드를 호출할 수 없는 것.

    정리하자면,

    👉 static 메서드: 다른 static 메서드, (인스턴스 생성 후 참조변수를 통해서만) 인스턴스 메서드 호출 가능

    👉 인스턴스 메서드: static 메서드, 다른 인스턴스 메서드 모두 호출 가능

    class Rectangle {
      int width;
      int height;
    
      int getSize() {
        return width * height;
      } // 인스턴스 메서드 (인스턴스 변수 width와 height 사용)
    
      static boolean isSquare(int w, int h) {
        return w == h ? true : false;
      } // static 메서드 (= 클래스 메서드)
    }
    
    class RectangleMethodTest {
      public static void main(String[] args) {
        getSize(); // 인스턴스 메서드이므로 이렇게 인스턴스 생성 없이 호출하면 안됨
    
        Rectangle r = new Rectangle(); // 참조 변수 r에다 생성한 인스턴스 주소 할당
    
        r.width = 4;
        r.height = 5;
    
        int s = r.getSize(); // 참조 변수 r을 통해 호출했으므로 OK
    
        boolean b = Rectangle.isSquare(2, 3); // static 메서드는 인스턴스 생성 없이도 호출 가능하므로 OK
      }
    }

 

메서드의 실행 흐름

  1. 매개 변수 안에 지정한 인자의 값이 모두 대입됨

  2. 메서드의 블럭{}안에 있는 문장들이 순서대로 수행됨

  3. 메서드의 모든 문장이 실행되었거나, return문을 만나면 자신을 호출했던 메서드(보통은 main 메서드)로 돌아와 이후의 문장들이 실행됨.

 

JVM의 메모리 구조

JVM은 애플리케이션 실행 시, 시스템으로부터 메모리를 할당 받고 이를 용도에 맞게 나누어 관리함. (아래는 주요 3가지 영역)

Method Area 클래스 데이터 (+ 클래스 변수)
Call Stack 메서드 (+ 지역 변수)
Heap 인스턴스 (+ 인스턴스 변수)

 

1. Method Area

프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스 파일(.class)을 읽어 클래스의 정보를 이곳에 저장.

 

2. Call Stack

메서드의 작업에 필요한 메모리 공간 제공. 호출된 메서드를 위해 메모리를 할당해주고, 이 메모리는 지역변수와 연산 과정 중의 데이터를 저장하는데에 사용.

  • 스택이므로 Last-In-First-Out

  • 스택 최상단에 있는 메서드가 현재 실행중인 메서드 (나머지는 대기)

  • 실행 중인 메서드가 작업을 마치면 메모리 공간을 반환하고 스택에서 제거됨.

 

3. Heap

인스턴스가 생성되는 공간. 인스턴스와 함께 움직이는 인스턴스 변수들도 이곳에 생성됨.

 

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

메서드를 호출할 때, 각 인자를 각 매개변수에 복사해서 넘겨줌. 이 때 매개변수가 기본형인지 참조형인지에 따라 값에 대한 권한이 다름.

  • 기본형 매개변수: 저장된 값 복사. 변수의 값을 읽는 것만 가능 (read-only)

  • 참조형 매개변수: 인스턴스 주소 복사. 변수의 값 읽기, 변경하기 가능 (read & write)

 

class PrimitiveData {
  int x;
}

class PrimitiveParamEx {
  public static void main(String[] args) {
    PrimitiveData p = new PrimitiveData();
    p.x = 7;
    System.out.printf("x의 값: %d%n", p.x);

    changeX(p.x);
    System.out.printf("changeX 메서드 실행 종료 후 x의 값: %d%n", p.x);
  }

  static void changeX(int x) {
    x = 7777;
    System.out.printf("changeX 메서드를 통해 수정된 x의 값: %d", x);
  }
}

실행 결과:

x의 값: 7
changeX 메서드를 통해 수정된 x의 값: 7777
changeX 메서드 실행 종료 후 x의 값: 7

changeX 메서드에서 변경한 값은 기본형 매개변수에 복사된 값이므로 원본에 영향을 주지 않음. 또한, 메서드가 종료된 후에는 메서드와 매개변수 데이터가 Call Stack에서 제거되면서 변경한 값도 사라짐.

 

class ReferenceData {
  int x;
}

class ReferenceParamEx {
  public static void main(String[] args) {
    ReferenceData r = new ReferenceData();
    r.x = 7;
    System.out.printf("x의 값: %d%n", r.x);

    changeX(r);
    System.out.printf("changeX 메서드 실행 종료 후 x의 값: %d%n", r.x);
  }

  static void changeX(ReferenceData d) {
    d.x = 7777;
    System.out.printf("changeX 메서드를 통해 수정된 x의 값: %d", d.x);
  }
}

실행 결과:

x의 값: 7
changeX 메서드를 통해 수정된 x의 값: 7777
changeX 메서드 실행 종료 후 x의 값: 7777

changeX 메서드에서 변경한 값은 참조형 매개변수에 복사된 객체 주소를 통해 접근한 원본 값이므로, 메서드가 종료된 후에도 변경한 값이 유지됨.

 

참조형 반환타입

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

 

재귀 호출 (recursive call)

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

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

다만 위 예시처럼 작성하게 되면 무한 반복에 빠지게 되므로, 조건문을 붙여 그런 현상을 방지해주어야 함

void recursiveMethod(int n) {
  if (n == 0) return; // 재귀 호출이 반복되다가 인자인 n이 0이 될 때 멈춤
  System.out.println(n);

  method(--n); // n의 값을 1 감소시킨 후, 다시 재귀 메서드에 대입
}

이렇게 조건문으로 재귀 호출을 다듬고 나면 반복문과 유사하다는 느낌을 받게 되는데,

void method(int n) {
  while (n != 0) {
    System.out.println(n);
  }
}

이렇게 웬만한 재귀 호출은 반복문으로도 구현 가능함

재귀 호출은 사실 반복문보다 효율이 떨어지지만, 그럼에도 특정 케이스에서 반복문보다 선호되는 이유가 논리적 간결함 때문임.

반복문이 지나치게 복잡해진다 싶을 때에 고려해볼만한 사항.

 

int factorial(int n) {
  if (n < 0 || n > 12) return 0;
  if (n == 1 || n == 0) return 1;
  return n * factorial(n-1);
}

팩토리얼을 구하는 과정을 재귀 호출로 구현한 코드임. 만약 조건문으로 적절하게 예외 처리를 해주지 않았다면,

  • 호출이 무한히 반복되면서 Call Stack이 감당할 수 있는 수준을 넘어서면 StackOverflowError 발생

  • (12보다 큰 정수가 인자로 들어오는 경우) 반환값 타입인 int의 최대값 20억을 넘으면서 비정상적인 값 출력

매개변수 유효성 검사의 중요성을 다시 한번 강조!

 

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

static 수식어가 있으면 클래스 메서드, 없으면 인스턴스 메서드라고 부름

작성하고자 하는 메서드가 인스턴스 변수와 무관하다면 static을 붙여 클래스 메서드로 정의하면 됨.

인스턴스 메서드는 실행 시 호출될 메서드를 찾는 과정이 들기 때문에, static을 붙여 클래스 메서드로 정의하면 호출 시간이 짧아지는 이점이 있음

 

위에 언급되었던대로, 클래스 메서드는 인스턴스 생성 없이는 인스턴스 멤버(변수, 메서드)에 접근할 수 없음.

반면, 인스턴스 멤버가 존재한다는 것은 클래스 멤버가 이미 존재한다는 것을 의미하므로, 인스턴스 메서드는 클래스 멤버를 제약 없이 호출할 수 있음.

profile
Back-end Engineer 👨‍💻
post-custom-banner

0개의 댓글