자바 클래스와 객체 — 설계도를 그리고 객체를 찍어내는 원리

여기까지는 변수와 제어문만으로도 프로그램을 만들 수 있었습니다. 그런데 다루는 대상이 늘어나면 그 방식은 곧 한계에 부딪힙니다. 이번 편은 그 한계를 먼저 보여 준 뒤, 클래스가 무엇을 해결해 주는지 코드를 고쳐 가며 확인합니다.

변수만으로 버티면 어디서 무너집니까

강좌 글 한 편을 표현한다면 제목, 조회수, 공개 여부가 필요합니다.

String noteTitle = "자바 입문";
int noteView = 320;
boolean notePublished = true;

글이 한 편일 때는 멀쩡합니다. 그런데 세 편이 되면 변수가 아홉 개, 백 편이 되면 손을 쓸 수 없습니다. 게다가 이 셋이 같은 글의 정보라는 사실은 이름 앞부분이 같다는 약속에만 기대고 있어서, 다른 글의 조회수를 잘못 대입해도 프로그램은 아무 문제를 느끼지 못합니다.

흩어진 값을 한 덩어리로 묶고 그 덩어리가 할 일까지 함께 넣어 두는 것, 이것이 클래스가 존재하는 이유입니다.

클래스는 설계도, 객체는 찍어낸 물건입니다

클래스에는 데이터를 담는 필드와 그 데이터로 하는 일을 정의한 메서드가 들어갑니다.

public class CardNote {
    String title;        // 필드
    int viewCount;

    void addView() {     // 메서드
        viewCount++;
    }

    void describe() {
        System.out.println(title + " / 조회수 " + viewCount);
    }
}

이 클래스는 설계도일 뿐입니다. 실제로 쓰려면 new객체를 만들어야 합니다.

CardNote note = new CardNote();
note.title = "자바 입문";
note.addView();
note.describe();          // 자바 입문 / 조회수 1

설계도 하나로 객체는 얼마든지 찍어낼 수 있고, 각 객체는 자기만의 titleviewCount를 따로 가집니다. 여기서 흔한 실수 하나. new 없이 CardNote note;라고만 선언한 뒤 note.title에 접근하면 가리키는 객체가 없어 NullPointerException이 납니다. 선언은 이름표를 만드는 일, new가 실제 물건을 만드는 일입니다.

생성자와 this — 태어날 때 값을 채워 넣기

객체를 만든 뒤 필드를 하나씩 대입하는 것은 번거롭고 빠뜨리기도 쉽습니다. 생성자는 객체가 만들어지는 순간 자동 실행되는 특별한 메서드로, 이름은 클래스와 똑같이 짓고 반환형은 쓰지 않습니다.

public class CardNote {
    String title;
    int viewCount;

    public CardNote(String title) {
        this.title = title;
        this.viewCount = 0;
    }
}
CardNote note = new CardNote("자바 클래스와 객체");

this지금 이 객체 자신을 가리킵니다. 위 생성자는 매개변수와 필드 이름이 똑같이 title이라, 그냥 title = title;이라고 쓰면 매개변수가 필드를 가려 매개변수에 매개변수를 넣는 무의미한 코드가 됩니다. 오류도 나지 않고 필드만 조용히 비어 있게 되는 고약한 실수입니다. this.title이라고 붙여야 필드 쪽을 정확히 가리킵니다.

접근 제어자와 캡슐화 — 아무나 못 만지게 막기

지금 구조에서는 밖에서 note.viewCount = -500;처럼 말이 안 되는 값을 넣어도 막을 수 없습니다. 그래서 필드는 private으로 잠그고 정해진 통로로만 드나들게 하는데, 이것이 캡슐화입니다.

접근 제어자는 네 가지입니다. private은 같은 클래스 안에서만, 아무것도 붙이지 않으면 같은 패키지 안에서만, protected는 여기에 상속받은 클래스까지, public은 어디서든 접근할 수 있습니다.

public class CardNote {
    private String title;
    private int viewCount;

    public CardNote(String title) {
        this.title = title;
        this.viewCount = 0;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        if (title.length() > 0 && title.length() <= 40) {
            this.title = title;
        }
    }

    public void addView() {
        viewCount++;
    }

    public void describe() {
        System.out.println(title + " / 조회수 " + viewCount);
    }
}

이제 조회수는 addView()로만 늘어나고, 제목도 조건문 검사를 통과해야 바뀝니다. 값을 꺼내는 메서드를 게터, 넣는 메서드를 세터라고 부릅니다.

상속과 오버라이딩 맛보기

공지 글은 일반 글과 같지만 표시 방식만 다르다고 해 봅시다. 다시 만들 것 없이 기존 클래스를 물려받으면 됩니다.

public class NoticeNote extends CardNote {

    public NoticeNote(String title) {
        super(title);          // 부모 생성자 호출
    }

    @Override
    public void describe() {
        System.out.println("[공지] " + getTitle());
    }
}

extends로 부모의 필드와 메서드를 물려받고, super(...)로 부모 생성자를 먼저 실행합니다. 물려받은 메서드의 내용만 바꿔 쓰는 것을 오버라이딩이라 합니다. @Override는 필수가 아니지만, 이름을 잘못 써서 새 메서드를 만들어 버렸을 때 컴파일러가 잡아 주므로 붙이는 습관을 들이십시오.

다음 편에서는 이렇게 만든 객체를 여러 개 담는 방법을 다룹니다. 자바 배열과 컬렉션으로 이어집니다.

핵심 정리

  • 클래스와 객체 — 클래스는 설계도, new로 만든 것이 객체입니다
  • 필드와 메서드 — 데이터와 그것으로 하는 일을 한 덩어리로 묶습니다
  • 생성자와 this — 생성 시 자동 실행되며 this는 자기 자신을 가리킵니다
  • 캡슐화 — 필드는 private, 통로는 public 메서드로 엽니다
  • 상속과 오버라이딩extends로 물려받고 필요한 메서드만 바꿔 씁니다