잔액은 private, 변경은 deposit/withdraw만, 거래 내역은 방어적 복사로 노출합니다.
import java.util.Arrays;
public class BankAccount {
private final String accountNo;
private final String owner;
private long balance;
private long[] history = new long[0]; // 입출금 내역 (+입금, -출금)
public BankAccount(String accountNo, String owner, long initial) {
if (accountNo == null || accountNo.isBlank()) throw new IllegalArgumentException("계좌번호 필수");
if (initial < 0) throw new IllegalArgumentException("초기 잔액은 0 이상");
this.accountNo = accountNo;
this.owner = owner;
this.balance = initial;
}
public void deposit(long amount) {
if (amount <= 0) throw new IllegalArgumentException("입금액은 양수: " + amount);
balance += amount;
record(amount);
}
public void withdraw(long amount) {
if (amount <= 0) throw new IllegalArgumentException("출금액은 양수: " + amount);
if (amount > balance) throw new IllegalStateException("잔액 부족. 잔액=" + balance + " 요청=" + amount);
balance -= amount;
record(-amount);
}
private void record(long delta) { // 내부 도우미: private
history = Arrays.copyOf(history, history.length + 1);
history[history.length - 1] = delta;
}
public long getBalance() { return balance; }
public String getOwner() { return owner; }
public String getAccountNo() { return accountNo; }
public long[] getHistory() { return history.clone(); } // 방어적 복사
// setBalance 없음! 잔액은 deposit/withdraw로만 변한다.
public static void main(String[] args) {
BankAccount acc = new BankAccount("110-123", "김민수", 10_000);
acc.deposit(5_000);
acc.withdraw(3_000);
System.out.println(acc.getOwner() + " 잔액: " + acc.getBalance());
// 출력: 김민수 잔액: 12000
try {
acc.withdraw(50_000);
} catch (IllegalStateException e) {
System.out.println("실패: " + e.getMessage());
}
// 출력: 실패: 잔액 부족. 잔액=12000 요청=50000
long[] h = acc.getHistory();
h[0] = 999_999_999; // 복사본만 바뀜
System.out.println(Arrays.toString(acc.getHistory()));
// 출력: [5000, -3000]
// acc.balance = -1; → 컴파일 오류: balance has private access
}
}이메일과 나이를 setter에서 검증하고, 생성자가 같은 setter를 재사용해 검증 로직을 한 곳에 모읍니다.
public class Member {
private String email;
private String nickname;
private int age;
private boolean active = true;
public Member(String email, String nickname, int age) {
setEmail(email);
setNickname(nickname);
setAge(age);
}
public void setEmail(String email) {
if (email == null || !email.trim().matches("^[\\w.+-]+@[\\w-]+\\.[\\w.]+$")) {
throw new IllegalArgumentException("이메일 형식 오류: " + email);
}
this.email = email.trim().toLowerCase();
}
public void setNickname(String nickname) {
if (nickname == null || nickname.length() < 2 || nickname.length() > 12) {
throw new IllegalArgumentException("닉네임은 2~12자: " + nickname);
}
this.nickname = nickname.trim();
}
public void setAge(int age) {
if (age < 14) throw new IllegalArgumentException("14세 이상만 가입 가능: " + age);
this.age = age;
}
public void deactivate() { this.active = false; } // setActive(boolean) 대신 업무 메서드
public String getEmail() { return email; }
public String getNickname() { return nickname; }
public int getAge() { return age; }
public boolean isActive() { return active; }
@Override
public String toString() {
return nickname + " <" + email + "> " + age + "세 " + (active ? "활성" : "탈퇴");
}
public static void main(String[] args) {
Member m = new Member(" Kim.Minsu@Shop.COM ", "민수", 25);
System.out.println(m); // 출력: 민수 <[email protected]> 25세 활성
String[][] bad = {{"not-an-email", "민수", "25"}, {"a@b.com", "김", "25"}, {"a@b.com", "민수", "10"}};
for (String[] b : bad) {
try {
new Member(b[0], b[1], Integer.parseInt(b[2]));
} catch (IllegalArgumentException e) {
System.out.println("거부: " + e.getMessage());
}
}
// 출력: 거부: 이메일 형식 오류: not-an-email
// 출력: 거부: 닉네임은 2~12자: 김
// 출력: 거부: 14세 이상만 가입 가능: 10
m.deactivate();
System.out.println(m.isActive()); // 출력: false
}
}값을 바꾸는 대신 새 객체를 반환하는 불변 클래스입니다. 공유해도 안전함을 확인합니다.
public final class Money {
private final long amount;
private final String currency;
public Money(long amount, String currency) {
if (amount < 0) throw new IllegalArgumentException("음수 금액 불가: " + amount);
if (currency == null || currency.length() != 3) throw new IllegalArgumentException("통화 코드 3자리");
this.amount = amount;
this.currency = currency.toUpperCase();
}
public Money plus(Money o) {
checkCurrency(o);
return new Money(amount + o.amount, currency);
}
public Money minus(Money o) {
checkCurrency(o);
return new Money(amount - o.amount, currency); // 음수면 생성자가 막음
}
public Money times(int n) {
return new Money(amount * n, currency);
}
private void checkCurrency(Money o) {
if (!currency.equals(o.currency)) throw new IllegalArgumentException("통화 불일치: " + currency + " vs " + o.currency);
}
public long getAmount() { return amount; }
public String getCurrency() { return currency; }
@Override
public String toString() { return String.format("%,d %s", amount, currency); }
public static void main(String[] args) {
Money price = new Money(12_900, "krw");
Money shipping = new Money(3_000, "KRW");
Money total = price.times(3).plus(shipping);
System.out.println(price); // 출력: 12,900 KRW (원본 그대로)
System.out.println(total); // 출력: 41,700 KRW
Money shared = price; // 공유해도 안전 — 아무도 못 바꿈
Money discounted = shared.minus(new Money(2_900, "KRW"));
System.out.println(shared + " / " + discounted); // 출력: 12,900 KRW / 10,000 KRW
try {
price.minus(new Money(20_000, "KRW"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 출력: 음수 금액 불가: -7100
}
try {
price.plus(new Money(10, "USD"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 출력: 통화 불일치: KRW vs USD
}
}
}예제 3의 Money를 record로 다시 쓰고, 자동 생성된 equals/toString을 확인합니다. DTO 용도의 record도 함께 봅니다.
public class RecordDemo {
record Money(long amount, String currency) {
Money { // 컴팩트 생성자: 검증 + 정규화
if (amount < 0) throw new IllegalArgumentException("음수 금액 불가: " + amount);
currency = currency.toUpperCase();
}
Money plus(Money o) { return new Money(amount + o.amount, currency); }
}
record OrderSummary(long orderId, String customer, Money total, int itemCount) { }
record Point(int x, int y) {
static final Point ORIGIN = new Point(0, 0); // static 필드는 가능
double distanceTo(Point o) {
return Math.hypot(x - o.x, y - o.y);
}
}
public static void main(String[] args) {
Money a = new Money(1000, "krw");
Money b = new Money(1000, "KRW");
System.out.println(a); // 출력: Money[amount=1000, currency=KRW]
System.out.println(a.amount()); // 출력: 1000 (getAmount 아님)
System.out.println(a.equals(b)); // 출력: true (필드 값 비교 자동 구현)
System.out.println(a == b); // 출력: false (다른 객체)
System.out.println(a.hashCode() == b.hashCode()); // 출력: true
OrderSummary s = new OrderSummary(1001L, "Kim", a.plus(b), 2);
System.out.println(s);
// 출력: OrderSummary[orderId=1001, customer=Kim, total=Money[amount=2000, currency=KRW], itemCount=2]
System.out.println(s.total().amount()); // 출력: 2000
Point p = new Point(3, 4);
System.out.println(p.distanceTo(Point.ORIGIN)); // 출력: 5.0
// p.x = 10; → 컴파일 오류: record 필드는 final
// s.setCustomer("Lee"); → 그런 메서드 없음
}
}장바구니는 상태가 변해야 하므로 일반 클래스이지만, 내부 배열을 노출하지 않고 업무 메서드로만 조작합니다. record인 Item을 요소로 씁니다.
import java.util.Arrays;
public class ShoppingCart {
record Item(String name, int price, int qty) {
Item {
if (qty <= 0) throw new IllegalArgumentException("수량은 1 이상");
if (price < 0) throw new IllegalArgumentException("가격은 0 이상");
}
int subtotal() { return price * qty; }
}
private static final int MAX_ITEMS = 20;
private Item[] items = new Item[0];
public void add(String name, int price, int qty) {
if (items.length >= MAX_ITEMS) throw new IllegalStateException("장바구니 최대 " + MAX_ITEMS + "종");
for (int i = 0; i < items.length; i++) {
if (items[i].name().equals(name)) { // 이미 있으면 수량 합침
items[i] = new Item(name, price, items[i].qty() + qty); // record는 불변 → 새로 생성
return;
}
}
items = Arrays.copyOf(items, items.length + 1);
items[items.length - 1] = new Item(name, price, qty);
}
public void remove(String name) {
Item[] next = new Item[items.length];
int n = 0;
for (Item it : items) if (!it.name().equals(name)) next[n++] = it;
items = Arrays.copyOf(next, n);
}
public int total() {
int sum = 0;
for (Item it : items) sum += it.subtotal();
return sum;
}
public int itemCount() { return items.length; }
public Item[] getItems() { return items.clone(); } // 방어적 복사 (요소는 불변 record라 안전)
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.add("노트북", 1_200_000, 1);
cart.add("마우스", 25_000, 2);
cart.add("마우스", 25_000, 1); // 합쳐서 3개
System.out.println(cart.itemCount() + "종, 합계 " + cart.total());
// 출력: 2종, 합계 1275000
for (Item it : cart.getItems()) System.out.println(" " + it + " = " + it.subtotal());
// 출력: Item[name=노트북, price=1200000, qty=1] = 1200000
// 출력: Item[name=마우스, price=25000, qty=3] = 75000
cart.remove("노트북");
System.out.println(cart.itemCount() + "종, 합계 " + cart.total());
// 출력: 1종, 합계 75000
try {
cart.add("불량", 100, 0);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 출력: 수량은 1 이상
}
}
}