학습 자료

클래스와 객체 — 설계도와 실물


Article

책 세 권의 제목과 가격을 다뤄야 한다. 배열 두 개면 될 것 같다.

String[] titles = { "자바", "스프링", "코틀린" };
int[]    prices = { 20000, 30000, 25000 };

System.out.println(titles[1] + " / " + prices[1]);   // 스프링 / 30000

돌아간다. 그런데 가운데 책을 빼야 한다면?

titles 에서 하나 지우고, prices 에서도 같은 번호를 지워야 한다. 하나만 지우면 이때부터 제목과 가격이 어긋난다. 아무 에러도 없이, 조용히.

책 한 권이 두 배열에 흩어져 있는 게 문제다. 제목과 가격은 원래 한 덩어리였다.

클래스는 설계도다

한 덩어리로 묶는다. 그 덩어리의 모양을 적어둔 것이 클래스다.

class Book {
    String title;
    int price;
}

클래스 안에 선언한 변수를 필드라고 부른다. Book 이라는 것에는 제목과 가격이 있다는 선언이다.

이건 설계도일 뿐이다. 아직 책 한 권도 없다. 실물을 만들려면 new 를 쓴다.

Book b = new Book();
b.title = "자바";
b.price = 20000;

System.out.println(b.title + " / " + b.price);
콘솔
  • 자바 / 20000

new Book() 으로 만들어진 실물을 객체라고 한다. 설계도 하나로 객체는 얼마든지 찍어낼 수 있다.

Book b = new Book();
  1. Bookb 상자에 담을 타입
  2. new Book()실물을 만든다. 저 멀리 자리를 잡는다
  3. =만들어진 곳의 주소를 b 에 넣는다

Book 은 대문자로 시작한다. 4강에서 대문자로 시작하면 참조형이라고 했다. 우리가 만든 클래스도 예외가 아니다.

객체를 출력하면 주소가 나온다

9강에서 배열을 출력했더니 이상한 글자가 나왔다. 객체도 마찬가지다.

Book b = new Book();
System.out.println(b);
콘솔
  • Book@681a9515

Book@681a9515. 9강의 [I@3af49f1c 와 같은 생김새다. b 라는 상자에는 주소가 들어 있다.

Book b = new Book();

상자에는 주소가, 실물은 저쪽에

b
0x681a9515
→ Book { title: null, price: 0 }
b.title 은 "주소를 따라가서 title 칸을 본다" 는 뜻이다.

titlenull 이고 price0 인 것도 눈여겨보자. 값을 넣은 적이 없는데 들어 있다.

필드는 배열의 칸처럼 기본값으로 채워진다. 참조형은 null, 기본형은 0. 3강의 지역 변수와 다르다. 지역 변수는 값을 안 넣으면 컴파일러가 막았다.

생성자 — 만들 때 값을 넣는다

new Book() 한 뒤에 한 줄씩 채우는 건 번거롭다. 빠뜨리기도 쉽다.

만드는 순간에 값을 받자. 그 일을 하는 것이 생성자다.

class Book {
    String title;
    int price;

    Book(String title, int price) {
        this.title = title;
        this.price = price;
    }
}
Book b = new Book("자바", 20000);

생성자는 메서드처럼 생겼지만 두 가지가 다르다.

메서드생성자
이름마음대로클래스 이름과 똑같이
반환 타입int, void 등 반드시 적는다아예 적지 않는다
부르는 법b.print()new Book(...)
언제 도나부를 때마다객체를 만들 때 한 번

void 도 적지 않는다. 적으면 생성자가 아니라 그냥 메서드가 되어버린다. 자바 입문자가 한 번씩 겪는 함정이다.

1강의 그 수상한 줄

1강에서 javap -c 로 바이트코드를 들여다봤을 때 이런 게 있었다.

터미널
  • public class Hello {
  • public Hello();
  • Code:
  • 0: aload_0
  • 1: invokespecial #1
  • 4: return
  • ...

우리는 Hello() 라는 걸 쓴 적이 없었다. 그때 "javac 가 몰래 하나 만들어 넣었다"고만 하고 넘어갔다.

그게 생성자다. 클래스에 생성자를 하나도 안 적으면, 컴파일러가 매개변수 없는 빈 생성자를 대신 넣어준다. 이걸 기본 생성자라고 한다.

new Book() 이 그냥 됐던 이유가 이것이다. 우리가 안 만들었는데 있었다.

그런데 생성자를 하나라도 직접 만들면 기본 생성자는 사라진다.

class Book {
    String title;
    int price;

    Book(String title, int price) { ... }
}

Book b = new Book();     // 이제 안 된다
터미널
  • error: constructor Book in class Book cannot be applied to given types;
  • required: String,int
  • found: no arguments

"String, int 가 필요한데 아무것도 안 줬다"고 한다.

컴파일러는 우리가 아무 생각이 없을 때만 도와준다. 직접 만들기 시작하면 손을 뗀다. 둘 다 쓰고 싶으면 둘 다 적으면 된다. 10강의 오버로딩이다.

Book() { }
Book(String title, int price) { ... }

this — 지금 이 객체

생성자 안의 this.title = title; 이 이상해 보인다. 왜 this 를 붙였을까.

빼보면 안다.

Book(String title, int price) {
    title = title;
    price = price;
}
Book b = new Book("자바", 20000);
System.out.println(b.title);
콘솔
  • null

컴파일도 되고 실행도 되는데 값이 안 들어갔다.

매개변수 이름과 필드 이름이 똑같다. 이럴 때 자바는 가까운 쪽을 고른다. 매개변수다. 그러니 title = title 은 매개변수를 자기 자신에게 넣는, 아무 의미 없는 문장이었다.

this지금 만들어지고 있는 이 객체를 가리킨다. this.title 이라고 적으면 "매개변수 말고 필드"라는 뜻이 된다.

메서드가 필드를 쓴다

이제 클래스 안에 메서드를 넣어보자. 10강과 달리 static 이 없다.

class Book {
    String title;
    int price;

    Book(String title, int price) {
        this.title = title;
        this.price = price;
    }

    void print() {
        System.out.println(title + " / " + price);
    }

    int discounted(int percent) {
        return price * (100 - percent) / 100;
    }
}
Book b = new Book("자바", 20000);
b.print();
System.out.println(b.discounted(10));
콘솔
  • 자바 / 20000
  • 18000

print() 안에서 title 을 그냥 썼다. this.title 을 줄여 쓴 것이다. 겹치는 이름이 없으면 this 를 생략해도 된다.

이게 객체지향의 핵심이다. 값과 그 값을 다루는 코드가 한 덩어리에 들어 있다. b.print() 는 "b 라는 책아, 네 정보를 찍어라"로 읽힌다.

discounted 에서 price * (100 - percent) / 100 이 정수 나눗셈이라는 것도 눈여겨보자. 5강에서 본 그대로다. 20000 * 90 / 100 은 딱 떨어져서 다행이지만, 순서를 price / 100 * (100 - percent) 로 바꾸면 값이 어긋날 수 있다.

객체를 비교하면

책 두 권을 똑같이 만들어 비교해보자.

Book c = new Book("자바", 20000);
Book d = new Book("자바", 20000);

System.out.println(c == d);
System.out.println(c.equals(d));
콘솔
  • false
  • false

==false 인 건 이제 놀랍지 않다. new 를 두 번 했으니 자리가 둘이고 주소가 다르다.

그런데 .equals()false 다. 9강에서 배열도 그랬다.

우리는 Book.equals() 를 만든 적이 없다. 그런데 부를 수 있다. 어디서 왔을까. 그리고 왜 String 은 내용을 비교하는데 우리 Book 은 주소를 비교할까.

14강에서 답한다. 6강과 9강에서 두 번 미뤄둔 그 질문이다. 이제 세 번째다.

null 인 객체를 건드리면

Book b = null;
b.print();
터미널
  • Exception in thread "main" java.lang.NullPointerException

4강에서 본 그 예외다. b 상자에는 주소가 없는데 따라가라고 시켰다.

객체를 다루기 시작하면 null 을 만날 자리가 훨씬 많아진다. 필드의 기본값도 null 이니, 생성자에서 채우지 않은 참조형 필드는 전부 지뢰다.

정리하면

클래스는 설계도, 객체는 실물. 값과 그 값을 다루는 코드를 한 덩어리로 묶는다.

다시 짚어보기
  1. 01

    클래스에 필드를 모은다

    흩어진 배열 대신 한 덩어리로. 제목과 가격이 어긋날 수 없다.

    class Book {
        String title;
        int price;
    }
  2. 02

    new 로 객체를 만들면 주소를 돌려준다

    출력하면 Book@681a9515 처럼 주소가 나온다. 필드는 기본값으로 채워진다.

  3. 03

    생성자는 이름이 클래스와 같고 반환 타입이 없다

    안 만들면 컴파일러가 기본 생성자를 넣어준다. 하나라도 만들면 넣어주지 않는다.

    Book(String title, int price) {
        this.title = title;
        this.price = price;
    }
  4. 04

    this 는 지금 이 객체다

    이름이 겹치면 가까운 쪽(매개변수)이 이긴다. this 를 빼면 조용히 null 이 남는다.

  5. 05

    메서드는 필드를 그냥 쓴다

    static 이 없다. 객체 하나하나가 자기 값을 갖고 있기 때문이다.

그런데 지금 Book 에는 구멍이 있다. 누구나 이렇게 할 수 있다.

Book b = new Book("자바", 20000);
b.price = -50000;

가격이 마이너스인 책이 생겼다. 생성자를 아무리 잘 만들어놔도, 만든 뒤에 밖에서 마음대로 바꿀 수 있다면 소용이 없다.

다음 강의에서 필드에 자물쇠를 채운다. 2강에서 미뤄둔 public 이 드디어 나온다.

클래스와 객체 — 설계도와 실물 — 디코드랩(DCODELAB)