Technology/Java

Java의 클래스와 객체 이해하기

ikjo 2022. 8. 16. 16:59

클래스와 객체란?

우선 클래스란 무엇인가? 클래스란 '객체를 정의해놓은 것' 또는 '객체의 설계도 또는 틀'이라고 할 수 있다. 이러한 클래스는 객체를 생성하는데 사용된다. 그렇다면 객체란 무엇인가? 객체는 '클래스에 정의된 내용대로 메모리에 생성된 것'으로 서로 관련된 속성과 기능으로 구성된다. 이를 통틀어 멤버라고 한다. 이때 클래스로부터 객체를 만드는 과정을 '인스턴스화'라고 하며, 클래스로부터 만들어진 객체를 해당 클래스의 인스턴스(instance)라고 한다.

 

여기서 클래스가 분류의 개념이라면 객체는 해당 분류에 속하는 실체를 의미한다고 볼 수 있다. 예를 들면, 과일이라는 클래스가 있다면 그 안에 바나나, 사과, 딸기 등의 여러 객체들이 존재할 수 있는 것이다. 이처럼 바나나, 사과 등 각각의 유일무이한 객체를 특성(속성과 기능)에 따라 분류하여 객체를 통칭할 수 있는 과일이라는 '집합적인 개념' 즉, 클래스를 설계하는 것을 객체 지향 4대 특성 중 '추상화(모델링)'라고 한다.

 

 

클래스를 정의하는 방법

앞서 클래스는 '객체를 정의해놓은 것'이라고 했다. 이때 객체는 서로 관련된 속성과 기능으로 구성되어 있으므로 클래스를 정의하는 것은 '서로 관련된 속성과 기능'을 정의하는 것과 마찬가지라고 볼 수 있다. (속성과 기능 외에도 생성자와 초기화 블럭이 있다. 생성자는 아래 내용에서 다루고자 한다.) 이때 서로 관련된 속성과 기능이라 함은 특정 관심 영역(Context)에 대한 특성만을 가지고 재조합한 것이라고 볼 수 있다.

 

예를 들어, 로또 프로그램을 구성하는 클래스 중 Lotto 클래스를 살펴보자.

 

import java.util.List;

public class Lotto {

    private final List<Integer> numbers;

    public Lotto(List<Integer> lottoNumberList) {
        numbers = lottoNumberList;
    }

    public int getNumber(int index) {
        return numbers.get(index);
    }

    public boolean containNumber(int number) {
        return numbers.contains(number);
    }
}

 

Lotto 클래스는 속성으로는 로또 번호 리스트를 가지고 있으며, 기능으로는 저장된 로또 번호의 특정 위치에 속하는 번호를 조회하는 메서드특정 번호를 갖고 있는지를 검증하는 메서드를 가지고 있다. 이 속성과 기능들은 서로 연관되어 있으며 모두 로또 번호와 관련되어있다. 즉, 로또 번호라는 관심 영역에 초점을 두어 설계(추상화)된 것이라고 볼 수 있다.

 


 

이제 클래스에 정의된 속성과 기능의 세부 특성을 살펴보자.

 

앞서 살펴봤듯이 속성은 클래스 영역에 선언되는 것으로 '멤버 변수'라고도 한다. 이러한 멤버 변수 중 static이 붙은 것을 클래스 변수, 붙지 않은 것을 인스턴스 변수라고 한다. 클래스 변수는 모든 인스턴스마다 항상 같은 값을 유지하는 속성이며, 인스턴스 변수는 인스턴스마다 고유의 값을 가질 수 있는 속성이다.

 

또한 클래스 변수는 별도 객체 생성 없이 클래스로도 접근할 수 있는 반면, 인스턴스 변수는 반드시 객체를 생성해야 사용할 수 있다는 특성이 있다. 이외에 메서드의 파라미터나 블록 내에 선언된 변수들은 멤버가 아닌 지역 변수라고 하며, 메서드 내에서만 사용 가능하다.

 

기능은 '메서드'를 가리키기도 하는데, '특정 작업'을 수행하는 코드들을 하나로 묶어 놓은 것이다. 메서드 역시 멤버 변수처럼 static이 붙은 클래스 메서드와 붙지 않은 인스턴스 메서드로 나뉜다. 마찬가지로 클래스 메서드는 별도 객체 생성 없이 사용할 수 있으며, 인스턴스 메서드는 반드시 객체 생성이 필요하다는 특성이 있다.

 

아울러, 클래스 메서드는 인스턴스 변수에는 접근할 수 없는 특성이 있다. 왜냐하면 클래스 메서드가 존재하는 시점에서는 인스턴스 변수가 존재하지 않을 수 있기 때문이다. 반면 인스턴스 메서드는 클래스 변수나 인스턴스 변수 모두 접근이 가능하다. 

 

 

객체를 생성하는 방법

앞서 객체는 클래스에 정의된 내용(속성과 기능)대로 메모리에 생성되는 것이라고 했다. 그렇다면 객체는 어떻게 생성하는 것일까? 일반적으로는 new 연산자와 생성자를 이용한다. 예를 들어, 앞서 언급했던 Lotto 클래스로부터 다음과 같이 객체를 생성할 수 있다.

 

    public Lotto getLotto() {
        Lotto lotto = new Lotto(makeLottoNumber()); // 로또 객체 생성

        return lotto;
    }

    private List<Integer> makeLottoNumber() {
        List<Integer> numberList = new ArrayList<>();

        String[] numbers = manualLottoList.get(manualLottoIndex).split(SEPARATOR);
        for (int i = 0; i < 6; i++) {
            numberList.add(Integer.parseInt(numbers[i]));
        }

        return numberList;
    }

 

Lotto 객체의 경우 속성(멤버 변수)인 리스트를 생성자를 통해 초기화 해주는 방식을 취하고 있다. 그리하여 생성자를 호출하기 앞서 임의의 로또 번호들을 저장하는 리스트를 반환해주는 별도의 메서드(makeLottoNumber)를 호출해줌으로써 반환된 데이터를 Lotto 생성자의 인자로 두고 new 연산자를 통해 객체를 생성해주고 있다. 참고로 객체는 실질적으로 new 연산자를 통해 생성(JVM 힙 메모리 상에 저장)되는 것이다.

 

이렇게 대입 연산자(=)에 의해 해당 객체의 주소값(포인터)가 객체 참조 변수 lotto에 할당된다. (모든 메모리는 각자의 주소를 가진다.) 이제 이 lotto 참조 변수와 참조 연산자(.)를 통해 해당 Lotto 객체에 접근할 수 있게 된다. 참고로 현재는 생성자를 통해 속성을 초기화 해주고 있지만, 별도로 초기화 처리를 하지 않을 경우 해당 자료형에 맞는 기본값으로 초기화된다.

 

 

클래스 내에 메서드 정의하기

메서드란?

앞서 언급했듯이 메서드는 클래스에 선언되며, 특정 작업을 수행하는 코드들을 하나로 묶은 것이라고 할 수 있다. 이때 메서드의 사용자는 해당 메서드가 내부적으로 어떻게 결과를 만들어 내는지 관심을 갖지 않아도 된다. 이러한 특성으로 메서드를 활용하면 높은 재사용성중복된 코드의 제거 효과를 얻을 수 있게 된다. 아울러 프로그램을 구조화시킴으로써 유지보수에도 용이하다.

 

메서드의 구성요소

메서드는 크게 '선언부''구현부'로 구성된다. 이때 선언부는 다시 '메서드의 이름''매개변수' 그리고 '반환 타입'으로 구성되며, 구현부는 '블록 { }'으로 구성된다.

 

메서드 정의 예시

다음은 사용자가 입력한 값이 올바른 형식으로 입력되었는지를 검증해주는 메서드에 대한 예시이다.

 

    public boolean validateBonusNumber(String bonusNumberOfUserInput) {
        int bonusNumber;
        try {
            bonusNumber = Integer.parseInt(bonusNumberOfUserInput);
            if (bonusNumber < 1 || bonusNumber > 45) {
                throw new Exception();
            }
        } catch (Exception e) {
            InputView.informError();
            return false;
        }

        return true;
    }

 

우선 선언부를 보면 메서드의 이름은 validateBounusNumber로서 보너스 번호를 검증해주는 메서드임을 유추해볼 수 있다. 매개변수로는 사용자가 입력한 보너스 번호를 받고 있는데, 해당 매개변수의 값이 프로그램의 규칙 상 올바른 형식의 값인지를 검증하여 올바를 경우 true를, 올바르지 않을 경우 false를 반환해주고 있다.

 

구현부는 블록 { } 내에 지역 변수와 return문 등으로 구성되어있다. 지역 변수는 메서드 내에서만 사용되는 변수로 메서드 종료 시 소멸된다. 내부적으로 처리가 끝나면 반환 결과를 return문에 할당하여 해당 메서드를 종료시킨다. 이외에도 메서드 내에서 또 다른 메서드를 호출하는 것이 가능하다.

 

 

생성자를 정의하는 방법

생성자란?

생성자는 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메서드'로서 주로 인스턴스 변수의 초기화 작업에 사용된다. 앞서 로또 객체를 생성할 때 new 연산자 키워드와 함께 생성자를 호출해주었는데, 이를 통해 해당 객체의 인스턴스 변수(로또 번호 리스트)의 초기화 작업을 해주었던 것이다. 또한 일종의 메서드와 같아 클래스에 선언된다.

 

public class Lotto {

    private final List<Integer> numbers;

    public Lotto(List<Integer> lottoNumberList) {
        numbers = lottoNumberList;
    }
    
    // ...

 

이때 생성자 역시 메서드로서 클래스 영역에 선언되며, 모든 생성자는 반환값이 없으므로 void를 생략해서 사용한다. 또한 생성자의 이름은 클래스의 이름과 동일해야하고 매개변수를 달리하여 여러개의 생성자를 선언하는 것(오버로딩)이 가능하다.

 

주의할 점으로는 생성자가 인스턴스를 생성하는 것이 아니라는 점이다. 생성자는 인스턴스 변수들을 초기화해주는 역할이지, 실제로 인스턴스를 생성해는 것은 new 연산자에 의한 것이다.

 

기본 생성자

모든 클래스에는 반드시 하나 이상의 생성자가 정의되어 있어야 하는데, 별다른 생성자를 정의하지 않고도 객체를 생성하는데 아무런 지장이 없다. 그 이유는 클래스에 생성자가 하나도 정의되어있지 않은 경우 컴파일러가 기본 생성자를 추가해주기 때문이다. 이때 기본 생성자란 매개변수도 없고 아무런 내용도 없는 생성자를 의미한다.

 

public class Lotto {

    private List<Integer> numbers;

    public Lotto() { // 기본 생성자
    
    }

    public Lotto(List<Integer> lottoNumberList) {
        numbers = lottoNumberList;
    }
    
    // ...

 

this()와 this

같은 클래스 내에서 인스턴스 메서드들 간의 호출이 자유로운 것처럼 생성자가 여러 개 선언된 경우 생성자들 간의 호출 역시 자유롭다. 이때 어떤 생성자에서 다른 생성자를 호출 시에는 생성자 이름(클래스 이름)이 아닌 this를 사용해야하며, 반드시 첫 번째 줄에서 호출해야한다. 예를 들면 다음과 같이 기본 생성자를 호출 시 다른 생성자를 호출하도록 하여 인스턴스 변수를 프로그램 규칙상의 기본값으로 초기화해줄 수 있다.

 

public class Lotto {

    private List<Integer> numbers;

    public Lotto() {
        this(List.of(1,2,3,4,5,6)); // this를 통해 다른 생성자 호출
    }

    public Lotto(List<Integer> lottoNumberList) {
        numbers = lottoNumberList;
    }
    
    // ...

 

 

또한 현재 Lotto 클래스의 인스턴스 멤버 변수명은 numbers로 되있는 반면, 매개변수가 있는 생성자에는 매개변수명이 lottoNumberList로 되어있다. 만일 이들의 이름을 같게 한다면 지역 변수인 매개변수가 우선이 되어 인스턴스 변수를 초기화할 수 없게 된다. 이때 인스턴스 멤버 변수와 매개변수가 이름이 동일할 경우 this를 통해 구별할 수 있다.

 

public class Lotto {

    private final List<Integer> numbers;

    public Lotto() {
        this(List.of(1,2,3,4,5,6));
    }

    public Lotto(List<Integer> numbers) {
        this.numbers = numbers;
    }
    
    // ...

 

 

생성자의 매개변수로 인수턴스 변수를 초기화하는 경우가 많기 때문에 이처럼 이름을 동일시 하되 this를 이용해 인스턴스 변수와 매개변수를 구분하는 것이 가독성이 좋다.

 

여기서 this는 일종의 참조변수로서 인스턴스 자기 자신을 가리킨다. 따라서 this를 사용할 수 있는 것은 생성자를 포함한 인스턴스 메서드뿐이며, (클래스 메서드는 인스턴스 멤버에 접근할 수 없다.) this를 사용하지 않더라도 자기 자신의 인스턴스 변수를 참조할 때는 앞에 this가 지역 변수로서 숨겨진 채로 존재하게 된다. 물론, this를 사용하지 않으면 인스턴스 변수와 지역 변수와 이름이 동일할 경우 지역 변수가 우선시 된다.

 

 

 

참고자료

  • 도우출판 "자바의 정석"
  • 위키북스 "스프링 입문을 위한 자바 객체 지향의 원리와 이해"