캡슐화 — private 을 왜 붙이나
Article
11강 마지막에 남긴 구멍이다.
Book b = new Book("자바", 20000);
b.price = -50000;
b.print();
- 자바 / -50000
가격이 마이너스인 책이 생겼다. 아무도 막지 않았다.
생성자에서 아무리 값을 잘 검사해봐야 소용없다. 만든 뒤에 밖에서 마음대로 바꿀 수 있다면 그 검사는 한순간만 유효하다.
문을 잠근다
필드 앞에 private 을 붙인다.
class Book {
private String title;
private int price;
}
b.price = -50000;
- error: price has private access in Book
- b.price = -50000;
- ^
컴파일러가 막았다. private 은 "이 클래스 안에서만 쓸 수 있다" 는 뜻이다.
그런데 이제 밖에서 가격을 읽지도 못한다. 잠그기만 하면 쓸모가 없다. 문을 만들어줘야 한다.
getter 와 setter
값을 꺼내주는 메서드를 getter, 넣어주는 메서드를 setter 라고 부른다. 그냥 관례적인 이름이지 자바 문법은 아니다.
class Book {
private String title;
private int price;
public Book(String title, int price) {
this.title = title;
setPrice(price);
}
public String getTitle() {
return title;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
if (price < 0) {
throw new IllegalArgumentException("가격은 음수일 수 없다: " + price);
}
this.price = price;
}
}
이제 가격은 오직 setPrice 를 통해서만 바뀐다. 문이 하나뿐이니 그 문에 검사대를 놓으면 된다.
Book b = new Book("자바", -50000);
- Exception in thread "main" java.lang.IllegalArgumentException:
- 가격은 음수일 수 없다: -50000
생성자가 setPrice 를 부르고 있어서, 만들 때도 검사를 통과해야 한다.
throw 는 "문제가 생겼다"고 알리며 프로그램을 중단시키는 명령이다. 18강에서 제대로 다룬다. 지금은 잘못된 값이 조용히 들어가는 것보다 시끄럽게 죽는 게 낫다는 것만 챙겨두자.
public 필드
b.price = -50000
- 누구나 언제든 바꾼다
- 검사할 자리가 없다
- 잘못된 값이 조용히 들어간다
- 나중에 필드 이름을 못 바꾼다
private 필드 + 메서드
b.setPrice(-50000)
- 들어오는 문이 하나뿐이다
- 그 문에 검사대를 놓는다
- 잘못된 값은 그 자리에서 막힌다
- 안쪽은 마음대로 바꿔도 밖이 안 깨진다
2강의 public
2강에서 public static void main 의 다섯 단어 중 첫 번째를 미뤄뒀다. 이제 답할 수 있다.
public 은 private 의 반대다. 누구나 부를 수 있다는 뜻이다.
main 은 우리 코드 밖에 있는 JVM 이 부른다. private 이면 JVM 이 부를 수 없다. 그래서 public 이어야 한다.
자바에는 이런 표시가 네 개 있다. 이걸 접근 제어자라고 부른다.
| 표시 | 누가 쓸 수 있나 | 주로 쓰는 곳 |
|---|---|---|
public | 누구나 | 메서드, 클래스 |
protected | 같은 묶음 + 자식 클래스 | 14강에서 다시 본다 |
| (안 적음) | 같은 묶음 안에서만 | 묶음은 21강에서 |
private | 이 클래스 안에서만 | 필드 |
아무것도 안 적은 상태도 하나의 선택지다. 이걸 default 접근이라고 부르는데, 7강의 switch 에 나온 default 키워드와는 아무 관계가 없다.
외울 것은 두 줄이다. 필드는 private, 메서드는 public. 나머지는 필요해질 때 배운다.
감추면 나중에 바꿀 수 있다
캡슐화의 진짜 이득은 검사대가 아니다. 안쪽을 마음대로 뜯어고칠 수 있다는 것이다.
가격을 원 단위 int 로 저장하다가, 나중에 세금 계산 때문에 다른 방식으로 바꿔야 한다고 하자.
필드가 public 이었다면 b.price 를 쓰는 모든 코드가 깨진다. 그 코드가 몇 군데인지 알 수도 없다.
private 이었다면 getPrice() 안쪽만 고치면 된다. 밖에서는 getPrice() 를 부른다는 사실만 알 뿐, 안이 어떻게 생겼는지 모른다.
public int getPrice() {
return basePrice + tax; // 안쪽이 바뀌어도
} // 밖은 그대로 getPrice() 를 부른다
밖에 드러낸 것과 안에 감춘 것을 나누는 일. 이게 캡슐화다.
이 생각은 15강 인터페이스에서 한 번 더 크게 확장된다. 거기서는 클래스 통째로 감춘다.
record — getter 를 손으로 안 쓴다
값을 담아 나르기만 하는 클래스는 코드가 뻔하다. 필드, 생성자, getter, 그리고 나중에 배울 equals 까지.
자바 16 부터는 한 줄로 된다.
record Point(int x, int y) { }
Point p = new Point(1, 2);
System.out.println(p);
System.out.println(p.x());
System.out.println(p.equals(new Point(1, 2)));
- Point[x=1, y=2]
- 1
- true
세 가지가 눈에 띈다.
첫째, 출력이 Point@681a9515 가 아니라 내용이다. 둘째, 값을 꺼내는 메서드가 getX() 가 아니라 x() 다. 셋째, equals 가 true 다.
11강에서 Book 두 권을 비교했을 때는 false 였다. record 는 다르다. 컴파일러가 equals 를 대신 만들어주는데, 내용을 비교하도록 만들어준다.
record 의 필드는 전부 private 이고 한 번 정해지면 바뀌지 않는다. 6강의 String 처럼 불변이다. 그래서 setter 가 없다.
검사대가 필요하면 이렇게 붙인다.
record Book(String title, int price) {
Book {
if (price < 0) throw new IllegalArgumentException("음수 불가");
}
}
괄호도 매개변수도 없는 이상한 생성자다. 값을 필드에 넣기 직전에 끼어드는 자리라 이렇게 생겼다.
정리하면
필드는 잠그고, 필요한 문만 낸다. 그래야 안쪽을 바꿀 자유가 생긴다.
- 01
필드는
private밖에서 직접 못 바꾼다. 컴파일 에러로 막힌다.
- 02
문은 메서드로 낸다
들어오는 문이 하나면 그 문에 검사대를 놓을 수 있다.
public void setPrice(int price) { if (price < 0) throw new IllegalArgumentException("..."); this.price = price; } - 03
필요한 문만 낸다
getter/setter 를 전부 만들면 필드를 public 으로 여는 것과 다르지 않다.
- 04
public은 누구나 부를 수 있다는 뜻main 이 public 인 이유. 우리 코드 밖의 JVM 이 불러야 하니까.
- 05
값만 나르는 불변 클래스는
record생성자·접근자·equals·toString 을 컴파일러가 대신 써준다.
record Point(int x, int y) { }
main 의 다섯 단어 중 이제 static 만 남았다. void 는 10강에서, String[] args 는 9강에서, public 은 방금 답했다.
다음 강의가 static 이다. 그리고 이 단어를 이해하면 두 가지가 한꺼번에 풀린다. 왜 System.out 을 new 없이 쓸 수 있었는지, 그리고 10강에서 만난 non-static method cannot be referenced from a static context 가 무슨 소리였는지.