우리가 보통 자바의 클래스에 대해 배울 때 클래스는 객체의 설계도라고 배웠다. 그리고 클래스 멤버(필드, 메서드)는 당연히 객체에 포함되어 있어야 한다. 하지만 생각해 보면 같은 클래스로부터 만들어진 객체에도 객체들끼리의 공통 클래스 멤버가 존재할 수 있는데 이 멤버들을 객체를 생성할 때마다 공통 멤버도 같이 생성하는 게 과연 효율적인가에 대해 질문해 볼 수 있다.
예를 들어보자면, 객체마다 필드 값이 달라야 한다면 해당 필드는 객체마다 따로 가지고 있는 게 맞다, 하지만 객체마다 필드의 값이 모두 같아야 한다면? 예를 들어, 원주율 같은 필드가 있다고 했을 때 원주율이 객체마다 다를 필요 없이 모두 같아야 한다. 만약 객체마다 원주율 필드를 따로 가지고 있다면 메모리 낭비가 될 수 있으며, 모든 객체의 필드값을 같게 맞추는 추가적인 작업이 필요할 수도 있다. 오히려 이런 원주율 같은 필드는 한 곳에 위치시키고 객체들이 공유하는 것이 더 효율적일 수 있다.
자바는 이런 경우를 위해 클래스 멤버를 인스턴스 멤버와정적 멤버로 구분해서 선언할 수 있도록 지원하고 있다.
인스턴스 멤버 = 객체마다 가지고 있는 멤버
정적 멤버 = 클래스에 위치시키고 객체들이 공유하는 멤버
인스턴스 멤버와 this
인스턴스 멤버란 객체(인스턴스)를 생성한 후 사용할 수 있는 메서드와 필드를 의미한다. 이들을 각각 인스턴스 메서드, 인스턴스 필드라고 부른다. 또한, 이들은 객체에 속한 멤버들이기 때문에 객체 없이 사용할 수 없다.
객체.필드;
객체.메서드();
인스턴스 멤버 선언
public class Person{
// 필드
String name;
// 메서드
void coding(String language){
...
}
}
인스턴스 필드와 메서드를 선언하는 방법은 위와 같이 평소 클래스를 정의하는 대로 해주면 된다. 그러면 해당 멤버들은 객체에 속한 멤버들로 정의된다.
Person person1 = new Person();
person.name = "Jack";
person.coidng("Java");
Person person2 = new Person();
person.name = "Snider";
person.coidng("Python");
위처럼 인스턴스 멤버들은 객체에 속한 멤버들이기 때문에 person1의 멤버와 person2의 멤버들은 각각 독립적인 존재들이다.
방금 살펴보았던 예제 코드를 실행한 후 메모리 상태를 살펴보면 위의 그림과 같다고 할 수 있다. 그림에서 살펴보면 인스턴스 필드인 String name은 각 객체에서 따로 관리하고 메서드인 coding(String language)는 메서드 영역에 저장되어 공유하고 있다.
📢 인스턴스 메서드는 객체에 소속된다고 했는데, 왜 인스턴스 필드처럼 객체마다 따로 관리하지 않고 메서드 영역에 저장돼서 공유를 하고 있을까?
메서드는 코드 블록이므로 객체마다 동일한 코드 블록을 가지고 있을 필요가 없기 때문이다. 하지만 메서드 블록 내에서도 인스턴스 필드가 사용되는 경우가 있기 때문에 인스턴스 메서드라고 불리는 것이다.
this
객체 외부에서 인스턴스 멤버에 접근하기 위해는 참조 변수를 사용한다. 그리고 객체 내부에서 인스턴스 멤버끼리 서로 집근하기 위해서 사용하는 것이 this 키워드이다. 객체 자신을 this라고 가리키는 것이라고 생각하면 된다.
public class Person{
// 필드
String name;
Person(String name){
this.name = name;
}
}
위의 생성자에서 this.name은 현재 클래스 내에서 정의된 name 필드를 가리키는 것이고 = 뒤에 오는 name은 생성자의 파라미터로 들어오는 name을 가리키는 것이다. 즉, 생성자의 파라미티로 들어온 name의 값을 클래스 내부에 정의되어 있는 name 필드에 초기화하는 코드이다.
정적 멤버와 static
정적 멤버는 처음 언급했다시피 값이 고정되어 있는 멤버이다. 정적 멤버는 객체를 생성하지 않고 사용할 수 있는 필드와 메서드를 의미하기도 한다.
정적 멤버 선언
정적 멤버는 인스턴스 멤버를 선언할 때와 크게 다를 건 없지만 앞에 static 키워드만 추가해 주면 된다.
public 클래스{
// 정적 필드
static 타입 필드이름;
// 정적 메서드
static 타입 메서드이름(파라미터, ...) { ... }
}
정적 멤버는 클래스에 고정된 멤버이므로 클래스 로더가 클래스(바이트 코드)를 로딩해서 메서드 메모리 영역에 적재할 때 클래스별로 관리된다. 따라서 클래스의 로딩이 끝나면 바로 사용할 수 있다.
클래스를 정의할 때 정적 필드로 할 것인가, 인스턴스 필드로 할 것인가의 판단 기준이 필요하다. 개발자가 생각하기에 객체마다 가지고 있어야 할 데이터라면 인스턴스 필드로 선언해야 하고, 객체마다 가지고 있을 필요 없이 공용 데이터로 사용해야 한다면 정적 필드로 선언하는 것이 맞다.
public class Calculator{
String color; // 인스턴스 필드
static double pi = 3.14159; // 정적 필드
}
위와 같이 Calculator라는 클래스가 있을 때, 계산기의 색은 각각 다를 수 있겠지만 pi라는 고유의 값을 어떤 계산기에서도 전부 같을 것이므로 객체들이 해당 데이터는 공유하는 것이 더 효율적이다.
메서드 역시 필드와 마찬가지로 개발자가 판단하기에 정적으로 할 것인가 인스턴스로 할 것인가에 대한 판단 기준을 정해야 한다.
public class Calculator{
String color; // 인스턴스 필드
static double pi = 3.1459; // 정적 필드
void setColor(String color){ // 인스턴스 메서드
this.color = color;
}
static int plus(int x, int y){ // 정적 메서드
return x + y;
}
static int minus(int x, int y){ // 정적 메서드
return x - y;
}
}
마찬가지로 color 필드와 그 color을 세팅해 주는 setColor 메서드는 계산기마다 색이 다를 수 있으므로 인스턴스 필드와 메서드로 정의하는 게 효율적이고, plus나 minus와 같은 메서드들은 모든 계산기에 공통으로 있는 기능이므로 객체들이 공유하는 게 더 효율적일 수 있다.
double pi = Calculator.pi;
int n = Calculator.plus(1,2);
int m = Calculator.minus(2,1);
클래스가 메모리로 로딩되면 정적 멤버를 바로 사용할 수 있는데, 위와 같이 도트(.) 연산자로 접근할 수 있다.
정적 메서드 선언 시 주의할 점
앞서 정적 멤버는 객체가 없어도 접근하여 실행할 수 있다고 하였다, 이러한 특징 때문에 정적 메서드를 선언할 때는 메서드 내부에 인스턴스 필드나 인스턴스 메서드를 사용할 수 없다. 또한 객체 자신의 참조인 this 키워드도 사용이 불가능하다.