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

Do it! 자바 완전 정복: 11장 자바 제어자 2

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

11장에서는 상속과 연관된 나머지 자바 제어자인 final과 abstract를 알아본다. 나중에 배울 추상 클래스나 인터페이스를 이해하기 위한 길목에 있는 객체지향 문법 요소이므로 꼭 이해해야 한다.

  • 11.1 final 제어자
  • 11.2 abstract 제어자

11.1 final 제어자

final 제어자는 필드, 지역 변수, 메서드, 클래스 앞에 위치할 수 있으며, 어디에 위치하느냐에 따라 의미가 다르다.
각 순서대로 알아보자.

11.1.1 final 변수

final 제어자는 변수를 선언할 때만 지정할 수 있으며, final 변수는 한 번 대입된 값을 수정할 수 없다.
즉, 한 번 대입된 값이 최종(final)값이 되는 것이다. 다음 예제를 살펴보자.

// final 필드의 예
class A1 { // 선언과 동시에 값을 대입했을 때
    int a = 3;
    final int b = 5;
    A1() {}
}

class A2 { // 선언과 값의 대입을 분리했을 때
    int a;
    final int b;
    A2() {
        a = 3;
        b = 5;
    }
}

class A3 { // final 필드값을 대입한 후에는 추가 값 대입 불가능
    int a = 3;
    final int b = 5;
    A3() {
        a = 7; 
        // b = 9; (불가능)
    }
}

final 제어자를 사용한 변수에 원래 있던 값을 그대로 또 대입하면 어떨까?

final 제어자를 사용한 필드나 지역 변수에 일단 값이 대입되면 절대 변경할 수 없다고했는데, 좀 더 정확히 말하면 일단 값이 대입된 후 값을 입력하는 행위 자체를 할 수 없다. 즉, 다음 예와 같이 이전에 저장된 값과 동일한 값을 대입해도 오류가 발생하므로 유의하기 바란다.

final int a = 3;
// a = 3; (불가능)

final 필드를 포함한 클래스 A의 객체를 생성했을 때 메모리 구조를 살펴보자.

class A {
    int a;
    final int b;
    A() {
        a = 3;
        b = 5;
    }
}

필드는 멤버이므로 final 필드이든, 아니든 객체 속에 포함된다. 하지만 객체가 만들어질 때 final로 선언된 필드값은 상수(final) 영역에 1개가 복사된다. 메모리 구조에서 첫 번째 영역의 이름 중에 왜 상수(final) 영역이 있는지 이제는 이해할 수 있을 것이다. 즉, final로 선언된 모든 필드값이 상수 영역에 복사되므로 첫 번째 메모리 영역을 상수 영역이라고도 부르는 것이다. 지역 변수도 이와 같다. 다음과 같이 클래스 B를 정의하고, B b = new B()와 같이 객체를 생성한 후 b.bcd() 메서드를 호출했을 때의 메모리 구조는 다음과 같다.

class B {
    void bcd() {
        int a = 3;
        final int b = 5;
    }
}

다음은 final 지역 변수를 포함하고 있는 메서드를 호출했을 때의 메모리 구조이다.

메서드의 실행 과정에서 지역 변수들은 스택 영역에 저장되지만, final 지역 변수는 상수 영역에 1개가 복사된다. 값의 복사는 값을 선언한 후 최초로 값이 초기화될 때 딱 한 번 일어난다. 즉, 원본의 복사는 값을 대입할 때 딱 한 번 일어난다는 것이다. 그런데 만일 복사 이후에 원본의 값을 바꿀 수 있다면 어떤 일이 벌어질까? 상수 영역에 복사한 값은 아무런 쓸모가 없게 돼 버린다. 따라서 final로 선언한 필드나 지역 변수는 값을 바꿀 수 없는 것이다.

 

final 변수는 언제 많이 사용할까?

이벤트를 처리할 때 지역 변수를 final로 선언해야 하는 경우가 자주 있다. 스택 영역의 변수값은 자신이 만들어진 메서드가 종료되면 메모리에서 사라진다고 했다. 하지만 이벤트를 처리할 때 메모리에서 사라진 그 변수를 나중에 사용해야 할 때가 있다. 그래서 한 번 생성하면 사라지지 않는 영역인 상수 영역에 복사해 놓는 것이다. 이벤트 처리는 몰라도 '어떤 필요에 따라 복사본을 하나 만들어 놓음으로써 원본이 삭제된 이후에도 그 값을 활용할 수 있도록 하는 것이 final 변수(필드, 지역 변수)의 기능이다.'라고 생각하면 된다.


11.1.2 final 메서드와 final 클래스

상속할 때 부모의 메서드를 오버라이딩하면 자식 클래스에서는 메서드의 기능이 변경된다.
이때 메서드를 final로 정의하면 다음과 같이 자식 클래스에서 해당 메서드를 오버라이딩할 수 없다.

class A {
    void abc() {}
    final void bcd() {}
}

class B extends A {
    void abc() {}
        // void bcd() {} (불가능)
}

final 클래스는 상속 자체가 아예 불가능하다.

final class A {
    // ...
}
// class B extends A {} (불가능)

참고로 우리가 자주 사용해 왔던 String 클래스도 final 클래스로 정의돼 있으므로 String 클래스를 상속받아 자식 클래스를 생성할 수 없다. 이상의 내용을 정리하면 final 변수는 값을 변경할 수 없고, final 메서드는 오버라이딩을 할 수 없으며, final class는 상속 자체를 할 수 없다.


11.2 abstract 제어자

자바 제어자 중에서 마지막으로 알아볼 것은 abstract 제어자다. abstract의 사전상 의미는 '추상적인'이다. abstract가 붙은 메서드를 '추상 메서드(abstract method)', abstract가 붙은 클래스를 '추상 클래스(abstract class)'라 한다. 먼저 추상 메서드를 살펴보자. 추상 메서드는 중괄호가 없는 메서드로, 다음과 같은 구조를 띤다. 중괄호{}가 없으므로 메서드의 기능 자체가 정의되지 않으며, 세미콜론(;)으로 끝난다.

// 추상 메서드의 구조
abstract 리턴타입 메서드명();

추상 메서드는 12장에서 자세히 다루므로 여기서는 abstract라는 자바 제어자의 특징에 집중하자. 추상 메서드는 아직 무슨 기능을 정의할지 정해지지 않은 미완성 메서드라고 생각하자. 추상 메서드의 쓰임을 알아보기 위해 메서드 오버라이딩에서 다뤘던 예제를 다시 한번 살펴보자.

여기서 Animal 클래스의 cry() 메서드는 내부에서 아무런 기능도 수행하지 않는다. 어차피 자식 클래스에서 cry() 메서드를 오버라이딩해 사용하기 때문이다. 그럼에도 불구하고 아무런 기능이 없는 cry() 메서드를 Animal 클래스에 정의한 이유는 다음 예에서 찾을 수 있다.

Animal animal1 = new Cat();
animal1.cry(); // 야옹
Animal animal2 = new Cat();
animal2.cry(); // 멍멍

즉, Animal animal1 = new Cat()과 같이 다형적 표현을 사용했을 때도 animal1.cry()의 형태로 cry() 메서드를 호출하기 위해서다. 만일 Animal 클래스에 cry() 메서드가 없다면 호출 자체를 할 수 없을 것이다. Animal 클래스 내의 cry() 메서드가 아무런 기능을 수행하지 않는다면, 즉 중괄호 안을 비워둘 것이라면 중괄호 자체가 없는 미완성 메서드인 추상 메서드로 정의하는 것이 효율적이다. 여기서 하나 주의해야 할 점은 추상 메서드를 1개 이상 포함하고 있는 클래스는 반드시 추상 클래스로 정의해야 한다는 것이다. 즉, Animal 클래스의 cry() 메서드를 추상 메서드로 만들면 Animal 클래스는 반드시 추상 클래스여야 한다.

추상 클래스도 클래스이므로 당연히 상속도 할 수 있다. 따라서 자식 클래스들은 Animal 추상 클래스를 상속받아 cry()를 오버라이딩함으로써 앞의 예제와 동일한 작업을 수행할 수 있다.

abstract class Animal { // 추상 클래스
    abstract void cry(); // 추상 메서드
}

class Cat extends Animal {
    void cry() {
        System.out.println("야옹");
    }
}

...

11.2.1 abstract 제어자의 장점

앞의 추상 메서드를 사용하는 이유를 살펴보면 코드가 그렇게 간결해진 것도 아니다. 오히려 중괄호가 없는 새로운 문법을 1개 더 사용해야 하는 번거로움만 추가됐다. 그렇다면 추상 메서드와 추상 클래스를 사용해 얻게 되는 장점은 무엇일까? 다음 예를 살펴보자.

당연히 오타 없이 정확히 메서드를 오버라이딩했다면 내부에는 완성된 메서드 하나만 존재하므로 아무런 문제 없이 동작할 것이다. 겨우 오타를 찾는 정도의 장점이라고 가볍게 볼지는 모르겠지만, 만일 Animal 클래스와 Cat 클래스를 서로 다른 사람 또는 다른 회사가 작성하는 상황이라면 좀 더 필요성을 느낄 수 있을 것이다. 정리하면, 만일 abc()라는 추상 메서드를 포함하고 있는 추상 클래스가 있을 때 '이를 상속한 모든 자식 클래스 내부에는 항상 abc() 메서드가 정의돼 있다.'는 것이 보장되는 것이다. 이는 추상 메서드의 여러 가지 장점 중 하나로, 12장에서 좀 더 자세하게 알아본다.

문법 오류의 발생은 개발자에게 있어 단점이 아니라 실수를 사전에 막아 주는 강력한 장점이다.