본문 바로가기
자바/Do it! 자바 완전 정복

Do it! 자바 완전 정복: 9장 자바 제어자 1

by limdae94 2025. 1. 31.
 
Do it! 자바 완전 정복
프로그래밍 초심자들이 어려워하는 프로그램의 동작을 컴퓨터의 두뇌를 사진 찍듯 그림과 함께 설명한 구성이 눈에 띈다. 단기 코딩 학원에서는 다루지 않는 원리와 배경지식까지 배우며 정통 프로그래머로 거듭나는 뿌듯함을 느껴 보자. 여기에 400여 개의 프로그래밍 문제가 들어 있어 시험과 취업 면접도 대비할 수 있다. 컴퓨터공학과 1학년생부터 실무에서 자바를 쓰는 현직 개발자까지, 자바로 코딩하는 사람이라면 반드시 갖춰야 할 기본기를 이 책과 함께 ‘완전 정복’ 해보자
저자
김동형
출판
이지스퍼블리싱
출판일
2021.09.01

9장에서는 대표적인 자바의 제어자 중 접근 제어자와 static을 학습한다. 접근 지정자는 클래스 자체 또는 클래스의 내부 구성 요소 앞에 위치하며, 말 그대로 각 요소의 접근 범위를 지정하는 제어자다. 한편 static 제어자는 객체를 생성하지 않아도 클래스의 내부 구성 요소를 사용할 수 있도록 하는 제어자로, 꼭 메모리와 함께 개념적으로 이해해야 한다.

  • 9.1 접근 지정자
  • 9.2 static 제어자

9.1 접근 지정자

자바 제어자(Modifier)는 클래스, 필드, 메서드, 생성자 등에게 어떠한 특징을 부여하는 문법 요소로 형용사쯤으로 생각하면 된다. 접근 지정자는 자바 제어자의 한 종류로, 클래스, 멤버, 생성자 앞에 위치할 수 있으며, 사용 범위를 정의하는 역할을 한다.

패키지를 공부할 때 다른 패키지에서 클래스를 사용할 수 있도록 하기 위해서는 public 클래스이어야 한다고 했다. 이제 정확한 의미를 알아보자. 접근 지정자는 멤버(필드, 메서드, 이너 클래스)와 생성자, 즉 클래스 내부의 구성 요소 4가지 앞에 붙어 있는 때와 클래스 자체에 붙어 있는 때를 나눠 생각해야 한다. 그럼 먼저 멤버나 생성자 앞에 붙어 사용될 때의 특징을 살펴보자.

9.1.1 멤버 및 생성자의 접근 지정자

멤버 및 생성자에는 public, protected, deafult(또는 package), private라는 4가지 종류의 접근 지정자를 사용할 수 있다. 이때 접근 지정자는 필드, 메서드 또는 생성자 앞에 위치한다. 만약 아무것도 작성하지 않으면 default 접근 지정자가 자동으로 설정된다. 즉, 우리가 지금까지 만들어 왔던 접근 지정자를 지정하지 않은 모든 클래스의 내부 구성 요소에 늘 default 접근 지정자를 사용해 온 것이다.

class Test {
    public int a;
    protected int b;
    int c; // default 접근 지정자 자동 설정
    private int d;
    public void abc() {}
    protected void bcd() {}
    void cde() {} // default 접근 지정자 자동 설정
    private void def() {}
}

4가지 접근 지정자 중에서 public의 사용 범위가 가장 넓으며 private이 가장 좁다. 즉, 접근 범위는 public > protected > default > private 순이다.

9.1.2 클래스의 접근 지정자

클래스에서는 public, default 접근 지정자만 사용할 수 있다. 쉽게 말해 class 키워드 앞에 public이 붙어 있거나(public) 붙어 있지 않거나(default)이다. default 클래스는 같은 패키지 내에서만 사용할 수 있고, public 클래스는 다른 패키지에서도 사용할 수 있다. 따라서 클래스를 default로 정의하면 다른 패키지에서 임포트가 불가능해 사용할 수 없게 된다.

9.1.3 클래스 접근 지정자와 생성자 접근 지정자의 연관성

클래스 접근 지정자와 생성자 접근 지정자는 매우 밀접한 관련이 있다. 클래스에 생성자가 없을 때 컴파일러는 기본 생성자를 자동으로 추가한다고 했다. 이때 자동으로 추가되는 생성자의 접근 지정자는 클래스의 접근 지정자에 따라 결정된다. 물론 직접 생성자를 정의할 때 얼마든지 클래스와 생성자의 접근 지정자를 다르게 지정할 수 있다. 클래스의 접근 지정자와 생성자의 접근 지정자는 의미가 다르다.

클래스가 public이라는 것은 다른 패키지에서 임포트할 수 있는 것이고, 생성자가 public이라는 것은 생성자를 호출해 객체를 생성할 수 있다는 것이다. 클래스가 default라는 것은 다른 패키지에서 임포트할 수 없으며, 생성자 또한 default이면 호출할 수 없다. 여기에서 주의해야 할 점은 클래스가 임포트 되지 않으면 생성자는 접근 지정자와 상관없이 호출 자체를 할 수 없다는 것이다. 그 이유는 클래스 내부에 생성자가 존재하기 때문이다. 클래스는 public 접근 지정자가 지정돼 있지만, 생성자는 default 접근 지정자로 정의하자. 이때 다른 패키지에서는 클래스를 임포트 할 수 있지만, 생성자를 호출할 수 없으므로 객체 자체를 생성할 수 없다. 클래스는 default 접근 지정자로 지정돼 있고, 생성자는 public으로 선언돼 있다. 이때 앞에 설명한 바와 같이 임포트를 할 수 없으므로 생성자가 public이라도 다른 패키지에서 클래스 자체가 인식되지 않아 객체를 생성할 수 없게 된다.

9.2 static 제어자

static은 클래스의 멤버(필드, 메서드, 이너 클래스)에 사용하는 제어자다. 객체 안에 있을 때 사용할 수 있는 상태가 되는 멤버를 인스턴스 멤버(Instance Member)라고 한다. 쉽게 말해, 인스턴스 멤버는 멤버 앞에 static이 붙어 있지 않은 것을 말한다. 반면 앞에 static이 붙어 있는 멤버를 '정적 멤버(Static Member)'라고 한다. 정적 멤버의 가장 큰 특징은 객체의 생성 없이 '클래스명.멤버명'만으로 바로 사용할 수 있다. 정적 멤버도 인스턴스 멤버처럼 객체를 먼저 생성한 후 '참조변수명.멤버명'과 같이 사용할 수 있지만, 그렇게 사용할 것이라면 애초에 정적 멤버로 만들 필요도 없을 것이다. 또한 호출된 모양만으로 정적 멤버라는 것을 분명히 알 수 있도록 가능한 한 정적 멤버는 '클래스명.멤버명'의 형태로 사용하는 것을 권장한다.

9.2.1 인스턴스 필드와 정적 필드

인스턴스 필드와 정적 필드 1개씩 포함하고 있는 클래스 A를 고려해보자.

인스턴스 필드와 정적 필드는 사용하는 방법이 조금씩 차이가 있다. 그 이유는 각 필드의 저장 위치 때문이다.
메모리에서 인스턴스 필드와 정적 필드의 저장 공간 위치는 다음과 같다.

A a = new A();
System.out.println(a.m); // 3

인스턴스 필드는 객체를 생성한 후에 사용이 가능하다. 또한 저장 공간이 힙 영역에 위치하므로 반드시 해당 저장 공간에 값을 읽거나 쓰기 위해서는 참조변수명을 사용해야 한다.

A a = new A();
System.out.println(a.n); // 5

반면 정적 필드는 '클래스명.정적필드명'처럼 사용한다. 정적 필드인 n은 클래스 내부에 저장 공간을 갖고 있기 때문에 객체 생성 없이 바로 사용할 수 있다.

A a = new A();
System.out.println(a.n); // 5

이때 특이한 점은 메모리 구조에서도 볼 수 있는 것처럼 객체 내부에 정적 필드인 n도 존재한다는 것이다. 다만 n의 실제 저장 공간은 메서드 영역의 일부인 정적(static) 영역 내부에 있으며, 객체 내부의 n은 실제 정적 필드의 저장 공간의 주소값만을 포함하고 있다. 따라서 인스턴스 필드처럼 참조변수명으로도 사용할 수 있다. 단, 해당 방법은 권장하지 않는 방법으로 정적 필드라는 것을 명시적으로 표기해서 사용하자.

정적 필드의 특징은 바로 '정적 필드는 객체 간 공유 변수의 성질이 있다'는 것이다. 이 말을 이해하기 위해 인스턴스 필드와 정적 필드의 메모리 구조상의 차이점을 정확히 알아야 한다. 다음 예제를 살펴보자.

class A {
    int m = 3; // 인스턴스 필드
    static int n = 5; // 정적 필드
}

A a1 = new A();
A a2 = new A();

a1.m = 5; // a1 객체의 인스턴스 필드 m에 값 5 입력
a2.m = 6; // a2 객체의 인스턴스 필드 m에 값 6 입력
System.out.println(a1.m); // 5
System.out.println(a2.m); // 6

a1.n = 7; // a1 객체의 정적 필드 n에 값 5 입력
a2.n = 8; // a2 객체의 정적 필드 n에 값 6 입력
System.out.println(a1.m); // 8
System.out.println(a2.m); // 8

A.n = 9; // 클래스 A의 정적 필드 n에 값 9 입력
System.out.println(a1.m); // 9
System.out.println(a2.m); // 9

객체 a1, a2를 생성하면 힙 영역에 2개의 객체 생성된다. 이때 메모리 구조는 다음과 같다.

각 객체 안에는 멤버가 2개씩 있다. 인스턴스 필드 m은 객체 안에 실제 데이터 값을 저장한다. 정적 필드 n은 정적(클래스) 영역의 클래스 A 안에 실제 데이터값이 있고, 객체 안에는 실제 데이터값의 주소값을 저장한다. 즉, a1 객체의 정적 필드 n과 a2 객체의 정적 필드 n은 모두 클래스 A에 저장된 값을 똑같이 가리키고 있는 것이다. 이와 같은 이유로 정적 필드가 객체 사이에 공유 변수의 역할을 한다고 말하는 것이다.

 

정적 필드가 사용되는 예를 들면 은행통장 클래스로 10 사람의 은행통장 객체를 생성하는 경우가 있을 수 있다. 이때 이자 필드를 정적 필드로 구성하는 것이 적절하다. 금리가 바뀌어 '은행통장클래스.이자'를 변경하면 모든 은행통장 객체들이 모두 이 값을 적용받게 된다. 만일 이자가 인스턴스 필드이면 10개 객체 모두 이자 필드값을 수정해야 할 것이다.

9.2.2 인스턴스 메서드와 정적 메서드

인스턴스 메서드와 정적 메서드를 비교해 보자. 앞에서 살펴본 필드와 마찬가지로 인스턴스 메서드는 반드시 객체를 생성한 후에 사용할 수 있지만, 정적 메서드는 클래스명으로도 바로 접근할 수 있고, 인스턴스 메서드처럼 객체로 호출할 수 있다. 다음 클래스 A는 인스턴스 메서드와 정적 메서드를 1개씩 포함하고 있는 클래스이다.

// 인스턴스 메서드와 정적 메서드를 1개씩 포함하고 있는 클래스의 예
class A {
    void abc() {
        System.out.println("instance 메서드");
    }
    static void bcd() {
        System.out.println("static 메서드");
    }
}

인스턴스 메서드와 정적 메서드의 활용 방법은 인스턴스 필드와 정적 필드와 같다.

// 인스턴스 메서드의 활용 방법
A a = new A();
a.abc(); // 인스턴스 메서드
// 정적 메서드의 활용 방법 1
A.bcd(); // 정적 메서드
// 정적 메서드의 활용 방법 2 (권장하지 않음)
A a = new A();
a.abc(); // 정적 메서드

인스턴스 필드와 정적 필드의 다른 점은 인스턴스 메서드와 정적 메서드는 모두 메모리의 첫 번째 영역에 위치한다는 것이다. 다만 인스턴스 메서드는 인스턴스 메서드 영역, 정적 메서드는 클래스 내부에 존재하는 것만 차이가 난다. 메모리의 첫 번째 영역에는 클래스, 메서드, 정적 필드와 정적 메서드, 상수값이 저장된다. 이렇게 여러 가지 데이터가 저장되므로 클래스 영역, 메서드 영역, 상수 영역이라는 여러 가지 이름으로 불린다.


9.2.3 정적 메서드 안에서 사용할 수 있는 필드와 메서드

정적 메서드내에서는 정적 필드 또는 정적 메서드만 사용할 수 있다. 즉, 인스턴스 필드나 인스턴스 메서드는 사용할 수 없다는 말이다. 왜 그럴까? 개념만 잘 이해하면 너무 당연한 일이다. 정적 멤버(정적 필드, 정적 메서드)는 객체의 생성 없이 실행될 수 있어야 한다. 하지만 인스턴스 멤버(인스턴스 필드, 인스턴스 메서드)는 반드시 객체를 생성한 후에 사용할 수 있다. 만일 정적 메서드 내에서 인스턴스 멤버를 사용한다면 결국 정적 메서드도 객체를 생성한 후에 동작할 수 있을 것이다. 따라서 객체 생성 이전에 실행하려면 내부에는 객체 생성 이전에 사용할 수 있는 요소들로만 구성돼 있어야 한다. 이것이 바로 정적 메서드 내부에 정적 메서드만 올 수 있는 이유이다.

이를 프로그래밍 문법의 관점에서 설명하면, 정적 메서드 내부에서는 클래스 내부에서 자신의 객체를 가리키는 this 키워드를 사용할 수 없다는 것을 의미한다. 따라서 this.가 자동으로 붙어야 하는 인스턴스 멤버는 올 수 없는 것이다. 어떤 방버븡로 이해하든 '정적 메서드 내부에서는 정적 멤버만 사용할 수 있다'는 것만 꼭 기억하자. 참고로 인스턴스 메서드 내에서는 인스턴스 멤버와 정적 멤버 모두 사용할 수 있다.


9.2.4 정적 초기화 블록

정적 필드는 객체 생성 이전에도 사용할 수 있도록 정적 필드 초기화를 위한 문법을 별도로 제공하는데, 이것이 정적 초기화 블록(static {})이다.

static {
    // 클래스가 메모리에 로딩될 때 실행되는 내용
}

정적 초기화 블록은 클래스가 메모리에 로딩될 때 가장 먼저 실행되므로 여기에 정적 필드의 초기화 코드를 넣어 두면 클래스가 로딩되는 시점에 바로 초기화할 수 있다.


9.2.5 static main() 메서드

지금까지 작성했던 public void main(String[] args)도 정적 메서드다. 프로그램을 시작하면 가장 먼저 실행되는 main() 메서드는 왜 정적 메서드로 구성됐을까? 그 이유는 JVM이 main() 메서드를 실행시켜 주기 때문이다. 그렇다면 JVM은 어떤 방식으로 main() 메서드를 실행할까? main() 메서드가 정적 메서드이므로 JVM은 '실행클래스명.main()'을 호출하는 것만으로도 이 메서드를 실행하는 것이다. 그래서 우리는 main() 메서드명도 바꿀 수 없다. main() 메서드가 인스턴스 메서드라면 아무리 JVM이라 하더라도 객체를 먼저 생성하고, 객체의 참조 변수를 이용해 main() 메서드를 호출해야만 할 것이다. 정리하면 JVM은 프로그램을 실행할 때 '실행클래스명.main()'을 실행하므로 가장 먼저 실행되는 메서드인 main() 메서드를 정적 메서드로 구성한 것이다.