본문 바로가기

JAVA 스터디

[JAVA] 객체, 클래스, 인스턴스

객체(Object)

객체 지향 프로그래밍(OOP, Object-Oriented Programming)에서는 모든 데이터를 Object로 취급합니다.
객체의 상태(state)와 행동(behavior)을 구체화하는 형태의 프로그래밍이 바로 객체 지향 프로그래밍입니다.
객체란 우리 실생활에 존재하는 사물에 빗대어 설명할 수 있습니다. 이 글에서는 차를 예시로 설명하겠습니다.

차(Object)를 생성하기 위해서 제작된 설계도를 클래스(class)라고 합니다.

클래스(Class)

자바에서 클래스란 객체를 정의하는 설계도라 할 수 있습니다.

클래스는 객체를 생성하는 데에 사용됩니다.
클래스에는 객체의 상태를 나타내는 필드(field)와 객체의 행동을 수행하는 메소드(method)로 구성됩니다.

인스턴스(Instance)

클래스로부터 객체를 선언하는 과정을 클래스의 인스턴스 화라고 합니다.
이렇게 선언된 해당 클래스 타입의 객체를 인스턴스(instance)라고 합니다.
즉, 인스턴스란 실제로 메모리에 할당된 객체를 의미합니다.

 

메모리 구조

자바에선 하나의 클래스로 여러 개의 인스턴스를 생성할 수 있습니다.
생성된 인스턴스는 힙 영역에 각자 독립된 메모리 공간을 차지하고, 자신만의 필드를 가집니다.
그러나 메소드는 해당 클래스에서 생성된 모든 인스턴스가 공유합니다.
각각의 인스턴스가 자신만의 메소드를 가지게 되면 메모리 낭비가 심해지기 때문에
메소드 영역에 저장된 클래스 정보에서 메소드를 참조하는 것입니다.

 

위 3가지 개념을 간단히 정리하면 아래와 같습니다.

  • 객체는 컴퓨터 상에 구현하고자 하는 대상
  • 클래스는 객체를 구현하기 위해 작성된 설계도
  • 인스턴스는 설계도를 토대로 구현된 실체

클래스의 구성

클래스에는 필드와 메소드와 더불어 생성자(constructor)라는 특별한 메소드를 가집니다.

 

class Car {                     	// 클래스 이름  
  private String modelName;         	// 필드  
  private int modelYear;            	// 필드

  public Car(String modelName, int modelYear) {        // 생성자
      this.modelName = modelName;
      this.modelYear = modelYear;
  }

  public String getModel() {                            // 메소드
      return this.modelYear + "년식 " + this.modelName;
  }
}

 

필드(field)

클래스의 필드란 클래스에 포함된 변수를 의미합니다.
클래스 내에서 필드는 선언된 위치에 따라 다음과 같이 구분됩니다.

  • 클래스 변수(static variable)
  • 인스턴스 변수(instance variable)
  • 지역 변수(local variable)

메소드(method)

메소드란 특정 작업을 수행하기 위한 명령문의 집합니다.
메소드를 사용하면 중복되는 코드를 줄일 수 있고, 가독성이 좋아집니다.
또한, 버그가 발생하거나 기능을 변경이 필요할 때도 쉽게 수정할 수 있습니다.

 

생성자(constructor)

생성자는 클래스를 통해 생성된 인스턴스의 초기화를 수행합니다.

모든 변수는 초기화 작업을 해주지 않으면, 디폴트 값으로 저장되는 데이터가 있습니다.
객체 또한 마찬가지입니다.

따라서 객체의 생성과 동시에 인스턴스 변수를 원하는 값으로 초기화할 수 있는
생성자를 메소드로 제공하는 것입니다.
생성자의 이름은 클래스의 이름과 동일해야 합니다.
위의 코드에서 클래스의 이름이 'Car'이므로 생성자 이름도 'Car'임을 알 수 있습니다.

 


클래스 정의하는 방법

클래스는 추상화한 대상을 직접 구현함으로써 유용하게 쓰이고 있습니다.

많은 개발자들의 편의를 위해서 활용도가 높은 클래스는 미리 정의해서 자바가 제공하기도 합니다.

그리고 개발자가 원하는 형태로 직접 구현하는 것도 가능합니다.

 

클래스를 정의하는 방법은 아래와 같이 작성하면 됩니다.

 

클래스 정의 방법

 

접근 제어자는 객체 지향 언어의 특징인 은닉성을 보장하기 위한 키워드입니다.

public 접근 제어자는 모든 클래스에서 접근 가능하도록 해주고,

private 접근 제어자는 해당 클래스 내의 멤버 변수 혹은 메소드만 접근할 수 있습니다.

 


인스턴스 만드는 방법(new 키워드 이해하기)

Car myCar;				// 선언
myCar = new Car();			// 초기화

위에서 선언한 Car 클래스를 통해서 Car 객체의 인스턴스를 생성했습니다.

이후에 new 연산자를 통해서 Car의 생성자를 호출해서 인스턴스를 생성하고,

미리 선언한 myCar에 저장했습니다.

 

아래의 코드는 위의 과정을 한 번에 수행한 것입니다.

Car myCar = new Car();			// 선언 및 초기화

new는 클래스 타입의 인스턴스를 생성해주는 역할을 수행합니다.

new 연산자를 통해서 힙 메모리 영역에 데이터를 저장할 공간을 할당받고

그 공간의 참조값(reference value)를 객체에게 반환합니다.

이어서 클래스에 정의된 생성자를 호출해서 인스턴스 생성을 완료하는 것입니다.

 

클래스 객체변수 new 클래스();
자료형 참조값 저장
(인스턴스 핸들)
메모리(Heap) 할당
인스턴스 생성,
참조값 리턴
생성자 호출

 


메소드 정의 및 호출하는 방법

앞서 메소드는 특정 작업을 수행하기 위해서 작성된 명령문의 집합이라고 했습니다.

 

클래스 내부에 메소드를 작성함으로써 코드가 중복되는 것을 막을 수 있습니다.

또한, 모듈화로 인해 가독성도 좋아집니다.

그리고 버그가 발생하거나 기능적인 부분을 수정해야 한다면, 해당 부분만 수정하면 됩니다.

덕분에 유지보수도 수월해집니다.

 

주의할 점은 하나의 메소드가 하나의 기능만 수행하도록 작성하는 것이 좋습니다.

 

메소드를 정의하는 방법은 아래와 같습니다.

접근제어자 반환타입 메소드이름(매개변수) {	// 선언부
	// 코드
}
  • 접근제어자 : 해당 메소드에 접근할 수 있는 범위
  • 반환 타입 : 메소드가 반환하는 결과값의 데이터 타입
  • 메소드 이름 : 메소드를 호출하기 위한 이름
  • 매개변수 : 메소드로 전달되는 인수 값을 저장할 변수
  • 코드 : 메소드의 기능 구현

아래 코드는 자동차의 남은 기름을 채워 넣는 메소드를 구현한 것입니다.

class Car {
    private int fuel;
    ...
    public void addFuel(int add) {	// 선언
        // 구현
        fuel = fuel + add;
        System.out.println(add + "만큼 기름을 넣었습니다.");
    }
    ...
}

위 예제에서 'addFuel'라는 이름으로 메소드를 정의했습니다.

접근제어자가 public이기 때문에 해당 객체를 사용하는 프로그램

어디서든 addFuel 메소드를사용할 수 있습니다.

반환 타입은 void로 아무런 값도 반환하지 않음을 알 수 있습니다.

매개변수는 int형 변수로 add가 선언되어 있습니다. 필요에 따라 더 선언할 수 있습니다.

해당 메소드가 호출되면 fuel에 add만큼 더하고, 얼마만큼 넣었는지 출력해줍니다.

 

아래 예제는 메소드 정의와 호출을 함께 보여주는 예제입니다.

class Car {
    private int fuel;
    
    public void addFuel(int add) {	// 선언
        // 구현
        fuel = fuel + add;
        System.out.println(add + "만큼 기름을 넣었습니다.");
    }
}

public Example {
    public static void main(String[] args) {
    Car myCar = new Car();
        myCar.addFuel(50);	// myCar 인스턴스의 addFule() 메소드 호출
    }
}
실행 결과 : "50만큼 기름을 넣었습니다."

addFuel() 메소드를 호출하기 위해서 도트 연산자(.)를 사용하고 있음을 볼 수 있습니다.

 


생성자(constructor) 정의하는 방법

클래스를 통해 객체를 생성하면, 클래스 필드들은 변수의 타입에 따라서 초기값으로 생성됩니다.

사용자가 원하는 값으로 초기화를 진행하고 싶으면 일반적인 방법으론 불가능합니다.

앞서 설명한 접근제어자 'private' 때문입니다.

접근 제어자 private을 사용하는 이유는 정보 은닉을 보장해야 하기 때문입니다.

 

객체 지향 언어에서는 객체에 대한 구체적인 정보를 노출시키지 않도록 주의해야 합니다.

예를 들어, 집에 가족 이외의 사람은 들어오지 못하게 도어락을 설치했습니다.

그런데 도어락의 비밀번호가 모종의 이유로 유출되어서 많은 사람들에게 공유되고 있었습니다.

집을 비운 사이 도둑과 강도가 들어와서 재산을 털어가는 사건이 발생하게 된 것입니다.

 

이러한 일은 객체에서도 발생할 수 있습니다.

허가되지 않은 사용자에 의해서 정보가 변경되는 것을 막기 위해서 private 접근 제어자를 사용하는 것 입니다.

따라서 객체를 초기화하기 위해선 생성자를 통해서만 이루어지도록 설계해야 합니다. 

 

객체의 생성과 동시에 인스턴스 변수를 원하는 값으로

초기화할 수 있는 생성자(constructor)를 메소드로 제공합니다.

생성자의 이름은 해당 클래스의 이름과 동일해야 합니다.

생성자는 아래와 같은 특징을 가지고 있습니다.

 

  1. 반환값이 없지만, 그렇다고 반환 타입을 작성하지도 않습니다.
  2. 생성자는 초기화를 위한 데이터를 매개변수로 전달받을 수 있습니다.
  3. 객체를 초기화하는 방법이 여러가지일 경우, 하나의 클래스가 여러 개의 생성자를 지닐 수 있습니다.

아래 예제는 Car 클래스의 생성자를 선언하는 것입니다.

class Car {
    private int fuel;
    private int year;
    private String model;
    
    public Car(int f, int y, String m) {	// 매개변수가 있는 생성자
    	fuel = f;
        year = y;
        model = m;
    }
}

 

생성자에 매개변수가 선언되어 있습니다.

매개변수들로 데이터를 받아와서 초기화해주는 모습을 볼 수 있습니다.

이렇게 생성된 생성자는 new 키워드를 사용하여 객체를 생성할 때 자동으로 호출됩니다.

 

특별한 생성자를 정의하지 않더라도 인스턴스를 초기화할 수 있습니다.

이는 자바에서 제공하는 '기본 생성자(constructor)'가 있기 때문입니다.

기본 생성자는 매개변수를 하나도 가지지 않으며, 아무런 명령어도 포함하고 있지 않습니다.

 

아래 예제가 기본 생성자의 예제입니다.

Car() {}

기본 생성자는 어떤 매개변수도 전달받지 않고, 기본적으로 아무런 동작도 하지 않습니다.

 

아래 예제는 생성자를 정의하지 않고 기본 생성자를 호출한 예제입니다.

class Car {
    private int fuel = 50;
    priavte int year = 2021;
    priavte String model = "G70";
    
    public void getInfo() {
        System.out.println(year + "년식" + model + ", 잔여 연료 " + fuel);
    ]
]

public class Example {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.getInfo();		// 2021년식 G70, 잔여 연료 50
    }
}

이처럼 인스턴스 변수의 초기화를 클래스 필드에서 바로 수행할 수도 있습니다.

 

여기서 주의해야 할 사항이 있습니다.

매개변수를 지니는 생성자를 클래스 내에 단 하나라도 정의했다면,

기본 생성자는 자동으로 추가되지 않습니다.

 

따라서 매개변수를 지니는 생성자를 하나 정의한 다음 기본 생성자를 호출하면

오류가 발생하게 됩니다.

 

이러한 문제는 '메소드 오버로딩(method overloding)'을 이용해서

기본 생성자와, 매개변수를 지니는 생성자 2개다 정의해주면 됩니다.

메소드 오버로딩은 다음에 다루기로 하겠습니다.


this 키워드

this 키워드는 클래스가 인스턴스화 되었을 때 자기자신의 메모리 주소를 가리키는 키워드입니다.

 

객체 스스로의 메모리 주소를 가지고 있기 때문에 멤버 변수와 메소드에 접근할 수도 있습니다.

멤버 변수와 메소드에 접근할 때에는 도트 연산자(.)를 사용합니다.

class Car {
    private int fuel;
    
//  public void addFuel(int add)
    public void addFuel(int fuel) {	// 선언
        // 구현
        //fuel = fuel + add;
        this.fuel = this.fuel + fuel;
        System.out.println(add + "만큼 기름을 넣었습니다.");
    }
}

public Example {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.addFuel(50);	// myCar 인스턴스의 addFule() 메소드 호출
    }
}

 

위의 예제는 메소드를 설명하는 란에서 사용한 예제입니다.

addFuel() 메소드의 구현 부분에서 아래의 코드를

fuel = fuel + add

 

다음과 같이 변경했습니다.

this.fuel = this.fuel + fuel;

 

메소드도 도트 연산자를 통해서 호출하고 있습니다.

myCar.addFuel(50);

 

클래스 내부에서 this()를 호출하면 해당 클래스의 생성자가 호출됩니다.

class Car {
    private int fuel;
    private int year;
    private String model;
    
    public Car(int fuel, int year, String model) {	// 생성자
    	this.fuel = fuel;
        this.year = year;
        this.model = model;
    }
    
    public Car(int fuel) {				// 생성자
    	this(fuel, "2021", "Porsche911");		// this()로 생성자 호출
    }
    
    public void getInfo() {
        System.out.println(this.year + "년 " + this.model + "모델 남은 연료 : " + this.fuel);
}

public Example {
    public static void main(String[] args) {
    	Car myCar = new Car(50);
        myCar.getInfo();
    }
}
실행 결과 : "2021년 Porsche911모델 남은 연료 : 50"

위 코드에서 매개변수가 하나인 생성자에서 매개변수가 3개인 생성자를 호출하고 있습니다.

 

this()를 통해 생성자를 호출할 때는 다음의 주의사항이 있습니다.

  1. 생성자에서만 호출 가능
  2. 제일 첫 문장에서 호출
  3. 생성자 자신을 호출할 수 없음(재귀호출 불가)

추가

1.

this()를 통해서 생성자 내부에서 다른 생성자를 호출할 때, 제일 첫 문장에서 호출해야 한다고 말했습니다.

오라클(oracle) java tutorials에서도 해당 내용에 대해 '왜 이런지'에 대한 설명은 확인하지 못 했습니다.

단순히 정의된 방식이기 때문에 주의사항대로 사용하면 되겠습니다.

관련 내용 링크(docs.oracle.com/javase/tutorial/java/javaOO/thiskey.html)

 

2.

this 키워드는 인스턴스에 대한 참조로 사용된다고 합니다.

따라서 static method 내부에선 this 키워드를 사용할 수 없다고 합니다.

static method 즉, 정적 메소드는 인스턴스에 속하지 않으므로, 메소드 내에서 this를 사용할 수 없기 때문입니다.

static 키워드를 사용하면 정적(static) 메모리 영역에 속하고, 그렇지 않으면 동적(heap) 메모리 영역에 속합니다.

서로 다른 영역에 존재하기 때문에 static method 내부에서 인스턴스에 대한 참조로 사용하는 this를 

사용하지 못하는 것은 당연하다 생각이 듭니다.