공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대0/8 완료
  • 00개발 환경과 첫 프로그램
  • 01변수, 데이터 타입, 형변환
  • 02연산자와 제어문
  • 031차원 · 2차원 배열
  • 04클래스, 객체, 생성자, 오버로딩
  • 05접근 제어자와 캡슐화
  • 06메서드 활용 패턴 (초급)
  • 07enum, 패키지와 import, static 과 final
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 초급 › 05 / 8

접근 제어자와 캡슐화

섹션 7진행 0 / 8
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: BankAccount — 캡슐화의 표준 형태

잔액은 private, 변경은 deposit/withdraw만, 거래 내역은 방어적 복사로 노출합니다.

java
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
    }
}

예제 2: Member — setter 검증과 정규화

이메일과 나이를 setter에서 검증하고, 생성자가 같은 setter를 재사용해 검증 로직을 한 곳에 모읍니다.

java
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);                     // 출력: 민수 <kim.minsu@shop.com> 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
    }
}

예제 3: Money — 불변 객체와 연산

값을 바꾸는 대신 새 객체를 반환하는 불변 클래스입니다. 공유해도 안전함을 확인합니다.

java
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
        }
    }
}

예제 4: record — 같은 것을 한 줄로

예제 3의 Money를 record로 다시 쓰고, 자동 생성된 equals/toString을 확인합니다. DTO 용도의 record도 함께 봅니다.

java
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");  → 그런 메서드 없음
    }
}

예제 5: ShoppingCart — 가변 객체지만 캡슐화된 컬렉션

장바구니는 상태가 변해야 하므로 일반 클래스이지만, 내부 배열을 노출하지 않고 업무 메서드로만 조작합니다. record인 Item을 요소로 씁니다.

java
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 이상
        }
    }
}

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\beginner\05_encapsulation
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: BankAccount — 캡슐화의 표준 형태
  • 예제 2: Member — setter 검증과 정규화
  • 예제 3: Money — 불변 객체와 연산
  • 예제 4: record — 같은 것을 한 줄로
  • 예제 5: ShoppingCart — 가변 객체지만 캡슐화된 컬렉션
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제