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

Do it! 자바 완전 정복: 7장 클래스 내부 구성 요소

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

클래스 내부에는 필드, 메서드, 생성자, 이너 클래스의 4가지 구성 요소가 포함될 수 있다. 내부 구성 요소 중 이너 클래스는 13장에서 별도로 다루며, 7장에서는 나머지 3개의 내부 구성 요소인 필드, 메서드, 생성자를 학습한다. 이외에 클래스 내부에서 자신의 객체를 가리키는 this 키워드와 자신의 다른 생성자를 호출하는 this() 메서드도 함께 학습한다.

  • 7.1 필드
  • 7.2 메서드
  • 7.3 생성자
  • 7.4 this 키워드와 this() 메서드

7.1 필드

7.1.1 필드와 지역 변수의 구분

필드(Field)는 '클래스에 포함된 변수'로, 필드에 객체의 속성값을 지정할 수 있다. 필드는 지역 변수(Local Variable)와 구분해야 하는데, 지역 변수는 '메서드에 포함된 변수'이다. 따라서 필드와 지역 변수는 어느 중괄호 안에 선언됐는지에 따라 구분할 수 있다. 클래스의 중괄호 안에 선언된 변수를 '필드', 메서드의 중괄호 안에 선언된 변수는 '지역 변수'이다.

 

필드와 지역 변수의 가장 큰 차이점은 생성되는 영역이다. 필드는 힙 영역의 객체 내부에 저장된다. 지역 변수는 스택 영역에 저장된다. 스택에 저장되는 변수는 때가 되면 JVM이 자동으로 삭제하지만, 힙 영역의 객체 안에 저장되는 필드는 객체가 사라지지 않는 한 절대로 삭제되지 않는다.

 

그렇다면 스택 영역의 변수는 언제 자동으로 삭제될까? 선언된 메서드가 더이상 사용되지 않으면, 그 메서드 안의 선언된 모든 지역 변수가 메모리에서 모두 삭제된다. 참고로 1개의 메서드 안에 선언된 모든 지역 변수들의 집합을 프레임(Frame)이라 한다.


7.1.2 필드와 지역 변수의 초기값

필드와 지역 변수의 또 다른 차이점은 초기값이다. 필드는 직접 초기화하지 않아도 강제로 초기화된다. 반면, 지역 변수는 직접 초기화하지 않으면 저장 공간이 빈 공간 그대로 있어 값을 출력할 때 오류가 발생한다. 사실 이는 필드와 지역 변수의 차이가 아니라 위치하는 메모리 영역의 특징 때문이다. 힙 영역은 빈 공간이 저장될 수 없기 때문에 강제로 초기화되고, 스택 영역은 강제로 초기화되지 않기 때문이다.


7.2 메서드

7.2.1 메서드 정의하기

메서드는 클래스의 기능에 해당하는 요소이다. 기본적인 메서드 정의 방법은 다음과 같다.

접근제한자 [static] 리턴(반환)타입 메서드명(입력매개변수1 [, 입력매개변수2, ...]) {
    메서드 내용
}
public static int sum(int a, int b) {
    // 메서드 내용
} 

7.2.2 여러 리턴 타입의 메서드 살펴보기

1. 리턴 타입이 void이고, 입력매개변수가 없는 메서드

void print() {
    System.out.println("안녕");
}

 

2. 리턴타입이 int이고, 입력매개변수가 없는 메서드

int data() {
    return 3;
}

 

3. 리턴 타입이 double이고, 입력매개변수가 2개인 메서드

double sum(int a, double b) {
    return a + b;
}

 

4. 리턴 타입이 void인 메서드 안에서 return 키워드 사용

void printMonth(int m) {
    if (m < 0 || m > 12) {
        System.out.println("잘못된 입력");
        return; // 메서드 종료
    }
    System.out.println(m + "월 입니다.");
}

리턴 타입이 void일 때 return 키워드의 의미는 '메서드를 종료하라.'는 의미를 갖는다.


7.2.3 메서드 호출하기

클래스 외부에서 메서드 호출하기

메서드도 클래스의 멤버이므로 객체 안에 존재할 것이고, 클래스 외부에서 메서드를 사용하려면 먼저 객체를 생성해야 할 것이다. 이어서 객체의 위치를 저장하고 있는 참조 변수를 이용해 메서드를 호출해야 한다.

 

클래스 내부에서 메서드 호출하기

클래스 내부에 있는 메서드끼리는 객체를 생성하지 않고 서로를 호출할 수 있다. 말 그대로 같은 멤버이기 때문이다. 필드 또한 멤버이므로 클래스 내부의 모든 메서드 안에서 객체를 생성하지 않고 자신이 속한 클래스의 필드를 사용할 수 있다. 단, 메서드 앞에 static이 붙어 있을 때는 static이 붙은 필드 또는 메서드만 호출할 수 있다. 여기에서는 '같은 멤버끼리는 클래스 내부에서 얼마든지 객체를 생성하지 않고 서로를 호출할 수 있다.'는 사실만 기억하자. static은 클래스를 생성하지 않고 이름만으로 바로 사용할 수 있는 제어자로, 9장에서 자세히 학습한다.

 

기본 자료형 입력매개변수와 참조 자료형 입력매개변수의 차이

배열과 같은 참조 자료형이 입력매개변수로 넘겨질 때 실제 객체가 전달되는 것이 아니라 객체의 주소값이 전달된다. 그 결과, 기본 자료형이 입력매개변수로 넘겨질 때와 다른 동작을 수행한다. 먼저 기본 자료형이 입력매개변수로 전달될 때를 살펴보자.

기본 자료형일 때는 실제값이 전달된다. 좀 더 정확하게 말하면, 기본 자료형의 값이 메서드의 지역 변수에 복사되는 것이다.

twice() 메서드의 실행을 완료하고 main() 함수로 돌아온 시점에는 스택 영역의 twice() 메서드는 수행을 모두 마친 후 제거된 이후이므로 twice() 메서드의 지역 변수 a는 메모리상에 존재하지도 않는다. 이렇게 기본 자료형을 입력매개변수로 전달받은 메서드는 값을 복사해 사용한다.

 

이제 다음으로 참조 자료형을 입력매개변수로 넘겼을 때를 학습한다. 이때도 입력매개변수로 넘겨진 변수에는 주소값이 복사돼 사용되는 것은 동일하지만, 참조 자료형은 스택 영역에 객체의 주소값을 저장하고 있으므로 실제 객체가 아니라 객체의 주소값이 전달돼 복사된다. 그래서 호출한 메서드와 호출된 메서드에서 모두 동일한 객체를 바라보게 된다. 따라서 호출된 메서드에서 객체의 값을 변견한 후 호출한 메서드로 돌아오면 값이 변경되어 있다.

위 예제를 실행하는 과정에서 메모리에 생성되는 데이터의 모양은 다음과 같다.

 

 

7.2.4 오버로딩된 메서드

메서드 오버로딩을 이해하기 위해서는 먼저 메서드 시그니처(Method Signature)의 의미를 알고 있어야 한다. 메서드 시그니처는 메서드명과 입력매개변수의 자료형을 의미한다. JVM은 메서드 시그니처가 다르면 메서드명이 동일하더라도 다른 메서드로 인식한다. 메서드 오버로딩(Method Overloading)은 이러한 특징을 이용한 것으로, 입력매개변수의 개수나 자료형이 다른 여러 개의 동일한 이름을 지닌 메서드를 같은 공간에 정의하는 것을 의미한다.

참고로 메서드 호출 과정에서 리턴 타입을 사용하지 않으므로 리턴 타입으로는 메서드를 구분할 수 없다. 이것이 바로 리턴 타입이 시그니처에 포함되지 않은 이유이다.

오버로딩된 메서드의 호출은 일반 메서드 호출과 전혀 다를 게 없다. 다만 동일한 이름의 메서드가 많기 때문에 입력매개변수에 따라 실제 어떤 메서드가 호출된 것인지만 구분해 주면 된다.

화면 출력을 위한 System.out.println() 메서드는 자바 API 문서에서 print() 메서드를 살펴보면 다음과 같이 10개의 메서드가 오버로딩된 것을 알 수 있다. 그래서 출력할 때 정수와 실수 그리고 문자열도 잘 출력됐던 것이다.

7.2.5 가변 길이 배열 입력매개변수 메서드

리턴타입 메서드명(자료형... 참조변수명) {
    ...
}

 

자료형 다음에 말줄임표(...)를 작성하면 가변 길이 입력매개변수가 된다.

method1(int... values) {}
method2(String... values) {}

method1(int... values)는 개수와 상관없이 정수를 입력으로 받을 수 있고, method2(String... values)는 개수와 상관없이 문자열을 입력으로 받을 수 있다. 두 메서드를 호출할 때 입력매개변수로 각각 2개, 3개, 0개를 넘기는 게 가능하다. 오버로딩은 적어도 3개의 메서드 오버로딩을 수행해야 하고, 입력매개변수의 개수가 좀 더 다양하면 그만큼 오버로딩을 더 많이 수행해야 한다. 하지만 가변 길이 배열 입력매개변수를 사용하면 단 1개의 메서드만 정의해 위의 모든 메서드 호출에 대응할 수 있게 된다. 대부분의 문법이 그렇듯이 메서드 오버로딩의 불편함을 덜고자 만들어진 문법이 가변 길이 배열 입력매개변수이다.


7.2 생성자

생성자(Constructor)는 객체를 생성하는 역할을 지닌 클래스의 내부 구성 요소이다.
또한 객체 내에 포함되는 필드의 초기화 또한 주로 생성자 내에서 수행한다.

7.3.1 생성자의 특징

생성자를 작성할 때 꼭 지켜야 하는 문법적 규칙은 2가지이다. 첫 번째는 반드시 클래스명과 동일한 이름으로 지어야 한다. 클래스명과 다르면 더 이상 생성자가 아니다. 두 번째는 메서드와 비슷한 구조를 지니고 있지만, 리턴 타입이 없다. 주의해야 할 점은 '리턴 타입이 없다'와 리턴하지 않는다(void)'는 전혀 다르므로, 생성자는 리턴 타입 자체가 없다.

클래스명(입력매개변수) { ... }

7.3.2 기본 생성자의 자동 추가

생성자를 만들지 않으면 컴파일러가 컴파일 시점에 기본 생성자(Default Constructor)를 자동으로 추가한다.
여기에서 기본 생성자는 입력매개변수가 없는 생성자를 말한다.

class A {
    A() { } // 기본 생성자
}

만약 입력매개변수가 있는 생성자가 존재한다면 기본 생성자는 자동으로 추가되지 않는다. 개발자가 작성한 코드의 생성자로만 해당 클래스의 객체를 생성할 수 있다. 만약 기본 생성자도 추가하려면 직접 기본 생성자를 작성해야 한다.

class A {
    A() { } // 기본 생성자
    A(int n) { } // 생성자
}

7.3.3 생성자와 객체의 생성 방법

생성자의 모양에 따라 객체를 생성하는 방법은 결정된다. 메서드처럼 생성자의 입력매개변수 자료형이나 개수에 따라 여러 개의 생성자를 정의할 수 있다.

class A {
    A() { } // 기본 생성자
    A(int n1) { } // 생성자
    A(int n1, String s) { } // 생성자
    A(int[] arr, String s) { } // 생성자
}

7.4 this 키워드와 this() 메서드

클래스의 외부에서 멤버(필드, 메서드, 이너 클래스)를 호출하기 위해서는 객체를 먼저 생성한 후 '참조변수명.멤버명'의 형태로 호출하지만, 클래스 내부에서는 객체의 생성 없이 필드와 메서드를 바로 사용할 수 있다고 했다. 하지만 모든 사용할 수 있는 상태의 멤버는 항상 객체 속에만 존재한다. 그렇다면 어떻게 클래스 내부에서는 객체를 생성하지 않고 필드와 메서드를 사용할 수 있을까?

7.4.1 내부 객체 참조 변수명인 this 키워드

모든 메서드에는 자신이 포함된 클래스의 객체를 가리키는 this라는 참조 변수가 있다. 다시 한번 말하지만, 모든 멤버는 객체 속에 존재하는 것이므로 우리가 int m = 3이라는 필드를 클래스 내부에서 출력하고자 할 때도 System.out.println(this.m)으로 작성해야 한다. 다만 this.를 생략하면 컴파일러가 자동으로 this.를 추가해 주기 때문에 지금까지 클래스 내부에서 필드와 메서드를 그대로 사용할 수 있었던 것이다. 그리고 지역 변수는 멤버가 아니므로 this.가 자동으로 붙지 않는다. 정확히 말하면, 인스턴스 메서드 내부에서 this를 사용할 수 있지만, static 메서드 내부에서는 사용할 수 없다. 9장에서 자세히 살펴보자.

this.를 생략해도 항상 컴파일러가 추가해 주므로 굳이 신경쓸 필요가 없어 보이지만, 그렇지 않다. this.를 명시적으로 붙여 줘야 할 때가 있다. 다음 아래의 예제를 살펴보자.

class A {
    int m;
    int n;
    void init(int m, int n) {
        m = m;
        n = n;
    }
}

위의 예제 코드에서 필드와 지역 변수는 모두 m, n으로 선언되어 사용하고 있다. 이처럼 지역 변수와 필드를 모두 사용할 수 있는 영역에서는 사용 범위가 좁은 변수, 즉 지역 변수로 인식한다. 따라서 init() 메서드 안에서 m = m, n = n을 컴파일러는 이들 모두 지역 변수로 인식하므로 this.는 당연히 추가되지 않을 것이다. 지역 변수에 지역 변수값을 다시 대입하는 형태이므로 필드값은 전혀 변경되지 않는다. 따라서 다음과 같이 객체를 생성한 후 메서드를 호출하고 필드값을 확인하면 모두 값이 0으로 출력된다.

필드는 값을 초기화하지 않으면 JVM이 강제로 값을 초기화한다. Boolean은 false, 정수(byte, short, int, long)는 0, 실수(float, double)는 0.0으로 초기화되며, 나머지 모든 참조형 필드는 null 값으로 강제 초기화된다.

A a = new A();
a.init(3, 4);
System.out.println(a.m); // 0
System.out.println(a.n); // 0

따라서 의도한 바와 같이 넘겨받은 지역 변수 m, n의 값을 필드 m, n에 대입하기 위해서는 다음과 같이 this.m = m, this.n = n과 같이 필드에 this.를 붙여야만 한다.

class B {
    int m;
    int n;
    void init(int m, int n) {
        this.m = m; // 필드에 this. 추가
        this.n = n; // 필드에 this. 추가
    }
}
A a = new A();
a.init(3, 4);
System.out.println(a.m); // 3
System.out.println(a.n); // 4

정리하자면, 이러한 문제점은 지역 변수와 필드명이 동일하기 때문에 발생한다. 애초에 이름이 서로 달랐다면 this.m = m처럼 필드와 징겨 변수를 명시적으로 구분할 필요가 없겠지만, 자바에서 제공하는 대부분의 API에는 메서드의 지역 변수명이 필드명과 동일하게 구성돼 있다. 따라서 반드시 this.m = m과 같은 표현은 꼭 이해하고 숙지해야 한다.

7.4.2 클래스 내 다른 생성자를 호출하는 this() 메서드

this() 메서드는 this 키워드와 매우 비슷하게 생겼지만, 의미는 전혀 다르다. this() 메서드는 자신이 속한 클래스 내부의 다른 생성자를 호출하는 명령이다. this() 메서드를 구성할 때는 반드시 2가지 문법적 규칙을 지켜야 한다. 첫 번째는 생성자의 내부에서만 사용할 수 있다. 즉, 생성자의 내부에서만 또 다른 생성자를 호출할 수 있다는 의미이다. 두 번째는 생성자의 첫 줄에 위치해야 한다. 이 둘 중 어느 하나라도 지켜지지 않으면 바로 오류가 발생한다.

class A {
    A() {
        System.out.println("첫 번째 생성자");
    }
    A(int a) {
        this(); // 반드시 생성자의 첫 번째 줄에 위치해야 한다.
        System.out.println("두 번째 생성자");
    }
    /*
    void abc() {
        this(); // 메서드에서는 this() 사용 불가능
    }
    */
}

public class ThisMethod_1 {
    public static void main(String[] args) {
        A a1 = new A(); // 기본 생성자 호출
        System.out.println();
        A a2 = new A(3); // 생성자 호출
    }
}
// 실행 결과
첫 번째 생성자

첫 번째 생성자
두 번째 생성자