1. 접근 제어자
자바는 public이나 private 같은 접근 제어자(access modifier)를 제공한다. 접근 제어자를 사용하면 해당 클래스 외부에서 특정 필드나 메서드에 접근하는 것을 허용하거나 제한할 수 있다.
접근 제어자 종류
자바는 4가지 종류의 접근 제어자를 제공한다. 접근 제어자를 명시하지 않으면, 같은 패키지 안에서의 호출을 허용하는 default 접근 제어자가 적용된다. default라는 용어는 해당 접근 제어자가 기본값으로 사용되기 때문에 붙여진 이름이지만, 실제로는 package-private가 더 정확한 표현이다.
- private
- 모든 외부 호출을 막는다.
- 나의 클래스 안으로 속성과 기능을 숨길 때 사용한다.
- default(package-private)
- 같은 패키지 안에서의 호출은 허용한다.
- 나의 패키지 안으로 속성과 기능을 숨길 때 사용한다.
- protected
- 같은 패키지 안에서의 호출은 허용한다.
- 패키지가 달라도 상속 관계라면 호출을 허용한다.
- 상속 관계로 속성과 기능을 숨길 때 사용한다.
- public
- 모든 외부 호출을 허용한다.
- 속성과 기능을 숨기지 않고 어디서든 호출할 수 있게 공개한다.
접근 제어자는 필드와 메서드, 생성자에 사용되며 추가로 클래스 레벨에서도 일부 접근 제어자를 사용할 수 있다.
public class Speaker { // 클래스 레벨
private int volumne; // 필드
public Speaker(int volume) {} // 생성자
public void volumeUp() {} // 메서드
...
}
참고
생성자도 접근 제어자 관점에서 메서드와 같다.
클래스 레벨의 접근 제어자
클래스 레벨의 접근 제어자는 public과 default만 사용할 수 있다. 이때, public 클래스는 반드시 파일명과 이름이 같아야 한다.
- 하나의 자바 파일에 public 클래스는 하나만 등장할 수 있다.
- 하나의 자바 파일에 default 접근 제어자를 사용하는 클래스는 무한정 만들 수 있다.
public class PublicClass {
public static void main(String[] args) {
PublicClass publicClass = new PublicClass();
DefaultClass1 c1 = new DefaultClass1();
DefaultClass2 c2 = new DefaultClass2();
}
}
class DefaultClass1 { // default
}
class DefaultClass2 { // default
}
캡슐화
캡슐화(Encapsulation)는 객체 지향 프로그래밍의 중요한 개념 중 하나로, 데이터와 해당 데이터를 처리하는 메서드를 하나로 묶어서 외부에서의 접근을 제한하는 것을 말한다. 데이터의 직접적인 변경을 방지하거나 제한하고, 외부에 꼭 필요한 기능만 노출하고 나머지는 숨길 수 있다는 특징이 있다.
- 캡슐화를 안전하게 완성하도록 해주는 장치가 바로 접근 제어자다.
어떤 것을 노출하고 어떤 것을 숨겨야 할까?
- 데이터를 숨겨라
- 캡슐화에서 가장 필수로 숨겨야 하는 것은 속성(데이터)이다. 객체 내부의 데이터를 외부에서 함부로 접근하게 두면, 클래스 안에서 데이터를 다루는 모든 로직을 무시하고 데이터를 변경할 수 있다. 따라서 캡슐화가 깨지게 된다.
- 객체의 데이터는 객체가 제공하는 기능인 메서드를 통해 접근해야 한다.
- 기능을 숨겨라
- 객체의 기능 중 외부에서 사용하지 않고 내부에서만 사용하는 기능들이 있다. 이런 기능도 모두 감추는 것이 좋다.
- 사용자 입장에서 꼭 필요한 기능만 외부에 노출하고 나머지 기능은 모두 내부로 숨기자.
접근 제어자와 캡슐화를 통해 데이터를 안전하게 보호하는 것은 물론이고, 객체를 사용하는 개발자 입장에서 해당 기능을 사용하는 복잡도도 낮출 수 있다. 컴파일 오류를 통해 잘못된 접근을 빠르게 확인할 수 있기도 하다.
2. 자바 메모리 구조와 static
자바 메모리 구조
자바 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역으로 나눌 수 있다.
- 메서드 영역(Method Area)
- 프로그램을 실행하는 데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.
- 클래스 정보 = 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재한다.
- static 영역 = static 변수들을 보관한다.
- 런타임 상수 풀 = 프로그램을 실행하는 데 필요한 공통 리터럴 상수를 보관한다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다. (참고로 문자열을 다루는 문자열 풀은 자바 7부터 힙 영역으로 ㅣ이동했다.)
- 프로그램을 실행하는 데 필요한 공통 데이터를 관리한다. 이 영역은 프로그램의 모든 영역에서 공유한다.
- 스택 영역(Stack Area)
- 자바 실행 시 각 스레드별로 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
- 스택 프레임 = 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
- 자바 실행 시 각 스레드별로 하나의 실행 스택이 생성된다. 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
- 힙 영역(Heap Area)
- 객체(인스턴스)와 배열이 생성되는 영역이다. GC(Garbage Collection)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.
아래 예시를 보면서 간단하게 살펴보자.
- 자바에서 특정 클래스로 100개의 인스턴스를 생성하면, 힙 메모리에 100개의 인스턴스가 생긴다.
- 이때 각각의 인스턴스는 내부에 변수와 메서드를 가진다. 같은 클래스로부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만 메서드는 공통된 코드를 공유한다.
- 따라서 객체가 생성될 때 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다.
- 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.
참고
힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 된다.
자바는 스택 영역을 사용해서 메서드 호출과 지역 변수(매개변수 포함)를 관리한다.
- 메서드를 계속 호출하면 스택 프레임이 계속 쌓인다.
- 스택 프레임이 종료되면 지역 변수도 함께 제거된다.
- 스택 프레임이 모두 제거되면 프로그램도 종료된다.
- 처음 자바 프로그램을 실행하면 main()을 실행하고, main()을 위한 스택 프레임이 하나 생성된다. 이후 main() 스택 프레임이 제거되면 더 이상 호출할 메서드가 없고, 스택 프레임도 완전히 비워진 상태기 때문에 프로그램도 종료된다.
static 변수
지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리된다. 메서드 영역이 관리하는 변수도 존재한다. 이것을 이해하기 위해 먼저 static 키워드를 알아야 한다.
static 키워드를 사용하면 특정 클래스에서 공용으로 함께 사용하는 변수를 만들 수 있다. 주로 멤버 변수(필드)와 메서드에 사용된다.
- 멤버 변수(필드) 앞에 static을 붙이면 static 변수, 정적 변수 또는 클래스 변수라고 부른다.
- 정적 변수는 스택 영역이 아닌, 메서드 영역에서 관리하며, 자바 프로그램을 시작할 때 딱 1개가 만들어진다.
- static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에서 바로 접근해서 사용할 수 있고, 클래스 자체에 소속돼 있다.
public class Data {
public String name;
public static int count; // static 변수
public Data(String name) {
this.name = name;
count++;
}
}
///
public class DataCountMain {
public static void main(String[] args) {
Data d1 = new Data("A");
System.out.println("A count = " + Data.count); // A count = 1
Data d2 = new Data("B");
System.out.println("B count = " + d1.count); // B count = 2
Data d3 = new Data("C");
System.out.println("C count = " + Data.count); // C count = 3
}
}
참고
static 변수(정적 변수, 클래스 변수)는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있다. 클래스를 통해 접근하는 방식을 사용하도록 하자.
변수와 생명주기
지역 변수(매개변수 포함)는 스택 영역에 있는 스택 프레임 안에 보관된다.
- 메서드가 종료되면 스택 프레임도 제거되는데, 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거된다. 따라서 생존 주기가 짧다.
인스턴스에 있는 멤버 변수를 인스턴스 변수라고 하며, 힙 영역을 사용한다.
- 힙 영역은 GC가 발생하기 전까지는 생존하기 때문에 보통 지역 변수보다 생존 주기가 길다.
클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이다.
- 메서드 영역은 프로그램 전체에서 사용되는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩되는 순간 생성된다. 그리고 JVM이 종료될 때까지 생명주기가 이어진다. 따라서 가장 긴 생명주기를 가진다.
static이 정적이라는 이유는 바로 여기에 있다.
힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고 제거된다. 반면에 static이 붙은 정적 변수는 거의 프로그램 실행 시점에 만들어지고, 프로그램 종료 시점에 제거된다.
static 메서드
메서드 앞에도 static을 붙일 수 있다. 이것을 정적 메서드 또는 클래스 메서드라고 부른다.
- static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + .(점, dot) + 메서드명으로 바로 호출할 수 있다. 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용한다.
- 정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용한다.
- ex. 간단한 메서드 하나로 끝나는 유틸리티성 메서드
정적 메서드는 언제나 사용할 수 있는 것이 아니다.
- static 메서드는 static만 사용할 수 있다.
- 클래스 내부 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.
- 즉, 정적 메서드는 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
- 반대로 모든 곳에서 static을 호출할 수 있다.
- 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면, 클래스를 통해 모든 곳에서 static을 호출할 수 있다.
public class DecoData {
private int instanceValue;
private static int staticValue;
public static void staticCall() {
// instanceValue++; // 인스턴스 변수 접근 불가 -> 컴파일 에러
// instanceMethod(); // 인스턴스 메서드 접근 불가 -> 컴파일 에러
staticValue++;
staticMethod();
}
public void instanceCall() {
instanceValue++;
instanceMethod();
staticValue++;
staticMethod();
}
public void instanceMethod() {
System.out.println("instanceValue");
}
public static void staticMethod() {
System.out.println("staticValue");
}
}
정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스처럼 참조값 개념이 없다.
- 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
- 아래와 같이 객체의 참조값을 직접 매개변수로 전달하면, 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있다.
public static void staticCall(DecoData data) {
data.instanceValue++;
data.instanceMethod();
}
참고
static 메서드는 static 변수와 마찬가지로 클래스를 통해 바로 접근할 수 있고, 인스턴스를 통해서도 접근할 수 있다. 동일하게 클래스를 통해 접근하는 방식을 사용하도록 하자.
static import
정적 메서드를 사용할 때, 해당 메서드를 자주 호출한다면 static import 기능을 사용하자. 이 기능을 사용하면 다음과 같이 클래스 명을 생략하고 메서드를 호출할 수 있다.
- static import는 정적 메서드뿐만 아니라 정적 변수에도 사용할 수 있다.
import static static2.DecoData.*;
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("정적 호출");
staticCall(); // static import -> 클래스 명 생략
}
}
인스턴스 생성 없이 실행하는 가장 대표적인 정적 메서드가 바로 main() 메서드다.
- 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있다. 따라서 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용한다.
public class ValueDataMain {
public static void main(String[] args) {
ValueData valueData = new ValueData();
add(valueData);
}
static void add(ValueData valueData) {
valueData.value++;
System.out.println("value = " + valueData.value);
}
}
3. final
final 변수
변수에 final 키워드가 붙으면 더 이상 값을 변경할 수 없으며, final 키워드는 클래스나 메서드를 포함한 여러 곳에 붙을 수 있다.
- 특정 변수에 값을 할당한 후 값을 변경하지 않아야 한다면 final 키워드를 사용하자. 값을 변경하려고 하면 컴파일 오류가 발생한다.
- final 키워드를 지역 변수에 설정할 경우, 최초 한 번만 할당할 수 있다.
- 지역 변수 선언 시 바로 초기화한 경우엔 이미 값이 할당돼 있기 때문에 값을 할당할 수 없다.
- 매개변수에 final이 붙으면 메서드 내부에서 매개변수의 값을 변경할 수 없다. 따라서 메서드 호출 시점에 사용된 값이 끝까지 사용된다.
- final 키워드를 멤버 변수(필드)에 사용할 경우, 해당 필드는 생성자를 통해 한 번만 초기화할 수 있다.
- 멤버 변수(필드)에서 초기화하면 이미 값이 할당돼 있기 때문에 생성자를 통해서도 초기화할 수 없다.
- static 변수에도 final 키워드를 사용할 수 있다.
public class FinalMain {
final int value;
static final int CONST_VALUE = 10; // static + final
public static void main(String[] args) {
final int d1;
d1 = 10; // 최초 한 번만 할당 가능
// d1 = 20; // 컴파일 오류
finalMethod(10);
}
static void method(final int param) {
// param = 20; // 컴파일 오류
}
public FinalMain(int value) {
this.value = value; // 생성자 초기화
}
}
static + final
final 멤버 변수(필드)를 멤버 변수에서 초기화하는 경우, 필드의 코드에 초기화 값이 미리 정해져 있기 때문에 인스턴스의 모든 value 값이 10으로 같아진다.
- 모든 인스턴스가 같은 값을 사용하기 때문에 결과적으론 메모리를 낭비하게 된다.
- 또, 같은 값이 계속 생성되는 것은 명확한 중복으로 볼 수 있다.
- 이럴 때 사용하면 좋은 것이 바로 static 영역이다.
public class FinalMain {
final int value;
static final int CONST_VALUE = 10; // static + final
public FinalMain(int value) {
this.value = value; // 생성자 초기화
}
}
위의 코드에서 FinalMain.CONST_VALUE는 static 영역에 존재한다. 그리고 final 키워드를 사용했기 때문에 초기화 값이 변하지 않는다.
- static 영역은 단 하나만 존재하는 영역이다. CONST_VALUE 변수는 JVM 상에서 하나만 존재하므로 위에서 설명한 중복과 메모리 비효율 문제를 모두 해결할 수 있다.
- 따라서 멤버 변수에 final + 필드 초기화를 사용하는 경우, static을 붙여서 사용하는 것이 효과적이다.
상수
자바에서 상수(Constant)는 보통 단 하나만 존재하는, 변하지 않는 고정된 값을 말한다. 따라서 상수는 static final 키워드를 사용한다.
- 일반적인 변수와 상수를 구분하기 위해 관례상 대문자를 사용하고, 구분은 _(언더스코어, underscore)로 한다.
- 필드를 직접 접근해서 사용한다.
- 상수는 기능이 아니라 고정된 값 자체를 사용하는 것이 목적이다.
- 상수는 값을 변경할 수 없기 때문에 필드에 직접 접근해도 데이터가 변하는 문제가 발생하지 않는다.
- 아래 예시 같은 상수들은 애플리케이션 전반에서 사용되기 때문에 public을 자주 사용한다. 물론 특정 위치에서만 사용된다면 다른 접근 제어자를 사용하면 된다.
- 상수를 사용하면 중앙에서 값을 하나로 관리할 수 있다.
- 상수는 런타임에 변경할 수 없다. 따라서 상수를 변경하려면 프로그램을 종료하고 코드를 변경한 뒤 다시 실행해야 한다.
public class Constant {
public static final double PI = 3.14;
public static final int SECONDS_IN_MINUTE = 60;
public static final int MAX_USERS = 1000;
...
}
참고
final 키워드를 기본형/참조형 변수에 사용하면 각각 값/참조값을 변경할 수 없다. 여기서 참조형 변수에 final이 붙으면 참조 대상 자체를 다른 대상으로 변경하지 못할 뿐이고, 참조하는 대상의 값에 final이 없다면 해당 값은 변경할 수 있다.