자바의 객체지향을 이해하려면 class 문법부터 외우는 것보다 관련된 데이터와 동작을 한곳에 묶는 이유를 생각해 보는 것이 좋다. 예를 들어 회원의 포인트를 변경하는 작업이 프로그램 곳곳에 흩어져 있다면 정책이 바뀔 때마다 여러 파일을 고쳐야 한다.
회원 포인트를 숫자 하나로 다루기보다 다음과 같이 클래스로 표현해 보자.
// MemberPointExample.java
public class MemberPointExample {
public static void main(String[] args) {
MemberPoint account = new MemberPoint("user-21", 100);
account.earn(40);
account.use(30);
System.out.println(account.getBalance()); // 110
}
}
class MemberPoint {
private final String memberId;
private int balance;
MemberPoint(String memberId, int startingBalance) {
if (startingBalance < 0) {
throw new IllegalArgumentException("초기 포인트는 음수일 수 없습니다.");
}
this.memberId = memberId;
this.balance = startingBalance;
}
void earn(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("적립 포인트는 양수여야 합니다.");
}
balance += amount;
}
void use(int amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("사용 가능한 포인트를 확인하세요.");
}
balance -= amount;
}
int getBalance() {
return balance;
}
}
MemberPoint가 클래스, new MemberPoint(...)로 만들어진 account가 객체(인스턴스)다. memberId, balance는 객체의 상태이고 earn, use는 행동이다.
생성자는 new로 객체를 만들 때 호출된다. 클래스와 이름이 같고 반환 타입을 쓰지 않는다. 위 코드의 this.balance는 현재 객체의 필드를 뜻한다.
private 필드는 외부에서 직접 바꾸지 못한다. 값을 수정하는 메서드에서 조건을 검사하면 음수 잔액 같은 잘못된 상태를 막을 수 있다. 이를 캡슐화라고 한다.
final String memberId는 생성 시 정해진 참조를 이후 재대입할 수 없게 한다. 반면 balance는 서비스 요구에 따라 값이 변하므로 final이 아니다.
static int calculateFee(int unitPrice, int count) {
return unitPrice * count;
}
메서드의 int는 반환 타입이다. void는 반환값이 없을 때 사용한다. 매개변수는 외부에서 입력받는 값이다. 같은 이름으로 매개변수 구성을 달리하는 오버로딩도 가능하다.
static String displayName(String name) {
return name;
}
static String displayName(String name, String team) {
return team + " / " + name;
}
반환 타입만 바꾼 것으로는 오버로딩할 수 없다.
자주 오해하는 부분이다. 자바는 원시 타입뿐 아니라 객체 참조도 항상 값으로 전달한다. 객체를 전달할 때는 참조값의 복사본이 넘어간다.
// ReferenceExample.java
public class ReferenceExample {
static class Box {
int value = 10;
}
static void changeField(Box box) {
box.value = 20; // 같은 객체의 내용을 바꾼다.
}
static void replaceReference(Box box) {
box = new Box(); // 지역 매개변수만 새 객체를 가리킨다.
box.value = 99;
}
public static void main(String[] args) {
Box box = new Box();
changeField(box);
replaceReference(box);
System.out.println(box.value); // 20
}
}
changeField()는 복사된 참조를 통해 같은 객체의 필드를 바꾸기 때문에 호출한 쪽에서도 변경이 보인다. 그러나 replaceReference() 안에서 매개변수에 다른 객체를 대입해도 호출한 쪽의 box가 바뀌지는 않는다.
그래서 자바를 객체를 참조에 의해 전달하는 언어(pass-by-reference)라고 설명하면 부정확하다.
class OrderIdGenerator {
private static int sequence = 0;
static int nextId() {
return ++sequence;
}
}
static 필드와 메서드는 특정 인스턴스가 아니라 클래스 차원에서 공유된다. 위 코드는 개념 설명용이며 멀티스레드 환경에서 안전한 ID 생성기는 아니다. 실제 서비스에서는 DB 시퀀스, UUID, 원자적 카운터 등 목적에 맞는 식별자 생성 방법을 사용한다.
static final은 재대입할 수 없는 클래스 수준 값을 나타낼 때 자주 사용한다.
클래스를 많이 만드는 것보다 책임이 잘 모여 있는지가 중요하다. 포인트 잔액을 변경하는 규칙은 포인트 객체에, 데이터베이스 조회는 별도의 저장소 계층에 두면 변화에 대응하기 쉽다. 객체가 무조건 모든 일을 할 필요는 없다.
static 멤버는 인스턴스가 아닌 클래스 차원에서 관리된다.