공공부하자개발 · 영어 학습 노트
자바
초급자바의 뼈대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정리‹ 이전다음 ›

2. 핵심 원리

2.1 접근 제어자 4종

접근 제어자(access modifier)는 클래스, 필드, 메서드, 생성자에 붙어 "누가 이것에 접근할 수 있는가"를 정합니다.

제어자 같은 클래스 같은 패키지 다른 패키지의 자식 클래스 그 외 모든 곳
public ✅ ✅ ✅ ✅
protected ✅ ✅ ✅ ❌
(없음, default / package-private) ✅ ✅ ❌ ❌
private ✅ ❌ ❌ ❌

위에서 아래로 갈수록 좁아집니다. 초보 단계의 실전 규칙은 단순합니다.

대상 기본 선택 이유
필드 private 외부에서 직접 수정 금지. 예외 거의 없음
생성자 public (또는 private + 정적 팩토리)
외부에 제공할 메서드 public 클래스의 "API"
내부 도우미 메서드 private 구현 세부사항. 나중에 바꿔도 외부 영향 없음
상수 public static final 공유용
클래스 public (파일당 1개) 또는 default

원칙: 가장 좁은 범위에서 시작해서 필요할 때만 넓힌다. public을 한 번 열면 누군가 쓰기 시작하고, 그 뒤로는 좁힐 수 없습니다(그 코드가 깨지므로). 반대로 private을 public으로 넓히는 건 언제든 가능합니다.

protected는 상속(중급 레슨)에서 다룹니다. default(아무것도 안 붙임)는 "같은 패키지 안에서만"이며, 패키지 내부 협력용 클래스에 씁니다.

클래스에는 public과 default만 붙습니다. private class는 최상위에서 불가능합니다(다른 클래스 안에 중첩된 경우만 가능).

2.2 캡슐화의 원리 — 상태와 통로

java
public class BankAccount {
    private long balance;                 // 숨김

    public void deposit(long amount) {    // 통로 1
        if (amount <= 0) throw new IllegalArgumentException("입금액은 양수");
        balance += amount;
    }

    public void withdraw(long amount) {   // 통로 2
        if (amount > balance) throw new IllegalStateException("잔액 부족");
        balance -= amount;
    }

    public long getBalance() {            // 읽기 통로
        return balance;
    }
}

balance를 바꾸는 방법은 deposit과 withdraw 둘뿐이고, 둘 다 검증을 거칩니다. 따라서 balance는 절대 음수가 될 수 없습니다. 이것을 불변식(invariant, 객체가 항상 지켜야 하는 조건)이라고 합니다. 캡슐화의 목적은 불변식을 클래스 안에서 보장하는 것입니다. 외부 코드가 아무리 엉망이어도 BankAccount의 잔액은 음수가 안 됩니다.

만약 balance가 public이면 불변식을 보장할 방법이 없습니다. 어디선가 account.balance = -1;을 해 버리면 끝입니다. 그 코드를 찾으려면 프로젝트 전체를 뒤져야 합니다.

캡슐화가 주는 것:

이득 설명
유효성 보장 잘못된 값은 들어오는 문에서 차단
변경 격리 내부 표현(타입, 계산 방식)을 바꿔도 외부 코드 무변경
디버깅 용이 값이 바뀌는 곳이 메서드 몇 개뿐 → 브레이크포인트 몇 개면 추적 끝
의도 표현 withdraw()는 "출금"이라는 업무 의미를 갖지만 balance -= x는 그냥 뺄셈

2.3 getter와 setter

private 필드를 읽고 쓰기 위한 관례적 메서드입니다.

종류 이름 규칙 예
getter get + 필드명(첫 글자 대문자) getName(), getBalance()
boolean getter is + 필드명 isActive(), isPaid()
setter set + 필드명, 매개변수 1개, 반환 void setName(String name)

이 이름 규칙은 단순한 취향이 아닙니다. 스프링, JPA, Jackson(JSON 변환), 각종 프레임워크가 이 이름을 보고 필드를 찾습니다(리플렉션). getName이 아니라 fetchName이라 지으면 JSON 변환에서 name 필드가 사라집니다.

모든 필드에 getter/setter를 기계적으로 만들면 안 됩니다. 그건 public 필드와 다를 게 없습니다. 질문은 이것입니다.

  • 이 값을 외부에서 읽어야 하는가? → 필요할 때만 getter
  • 이 값을 외부에서 바꿔야 하는가? → 대부분 아니오. 바꿔야 한다면 setter가 아니라 업무 의미가 있는 메서드(deposit, cancel, changeAddress)

setBalance(long)이 있으면 deposit의 검증을 우회할 수 있습니다. 계좌에는 setBalance가 없어야 합니다.

2.4 setter에 유효성 검증 넣기 — 실무 패턴

setter가 정말 필요한 경우(이름, 주소, 이메일처럼 단순 속성)에는 검증을 setter 안에 둡니다. 생성자도 같은 setter를 호출하면 검증이 한 곳에 모입니다.

java
public class Member {
    private String email;
    private int age;

    public Member(String email, int age) {
        setEmail(email);      // 생성자도 setter를 통과 → 검증 한 곳
        setAge(age);
    }

    public void setEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("잘못된 이메일: " + email);
        }
        this.email = email.trim().toLowerCase();     // 정규화도 여기서
    }

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("나이 범위 오류: " + age);
        }
        this.age = age;
    }
}

검증 실패 시 예외를 던지는 이유: return false로 조용히 실패하면 호출자가 확인을 빼먹기 쉽고, 잘못된 상태가 "실패한 줄도 모르고" 지나갑니다. IllegalArgumentException(인자가 잘못됨), IllegalStateException(지금 상태에서 그 작업 불가)은 "프로그래머의 실수"를 알리는 표준 예외입니다. 예외는 중급 레슨에서 깊게 다룹니다.

방어적 복사(defensive copy): 배열이나 컬렉션 필드는 getter가 내부 배열을 그대로 돌려주면 외부가 그것을 수정할 수 있습니다(03 레슨의 주소 복사).

java
private int[] scores;

public int[] getScores() {
    return scores.clone();       // 복사본 반환. 외부가 바꿔도 내부는 안전
}

public void setScores(int[] scores) {
    this.scores = scores.clone(); // 들어올 때도 복사. 호출자가 나중에 바꿔도 안전
}

2.5 불변 객체 — 바꿀 수 없으면 안전하다

불변 객체(immutable object)는 생성된 뒤 상태가 절대 바뀌지 않는 객체입니다. String, Integer, LocalDate, BigDecimal이 모두 불변입니다.

만드는 규칙:

  1. 모든 필드를 private final
  2. setter 없음
  3. 생성자에서 모든 필드 초기화 (검증 포함)
  4. 가변 필드(배열, 컬렉션)는 방어적 복사
  5. 클래스를 final로 (상속으로 깨지 못하게) — 선택적
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("음수 불가");
        this.amount = amount;
        this.currency = currency;
    }

    public Money plus(Money other) {           // 바꾸지 않고 새 객체를 반환
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("통화 불일치");
        return new Money(amount + other.amount, currency);
    }

    public long getAmount() { return amount; }
    public String getCurrency() { return currency; }
}

왜 불변이 안전한가:

상황 가변 객체 불변 객체
메서드에 넘김 메서드가 몰래 바꿀 수 있음 (04 call by value) 바꿀 방법이 없음
여러 곳에서 공유 한 곳의 변경이 모두에게 전파 안전하게 공유
멀티스레드 동기화 필요 동기화 불필요
HashMap의 키 키가 바뀌면 못 찾음 항상 안전
디버깅 "누가 바꿨지?" 만든 곳만 보면 됨

final 필드 ≠ 불변 객체: private final int[] scores는 scores가 다른 배열을 가리키지 못한다는 뜻이지 배열 내용이 안 바뀐다는 뜻이 아닙니다. scores[0] = 99는 됩니다. 그래서 규칙 4(방어적 복사)가 필요합니다.

"값을 바꾸고 싶으면?" 새 객체를 만듭니다. money.plus(other)가 Money를 반환하듯이. String의 toUpperCase()가 새 문자열을 반환하는 것과 같은 방식입니다. 처음에는 낭비 같지만 JVM은 작은 객체 생성에 매우 최적화되어 있고, 버그 감소 효과가 훨씬 큽니다.

2.6 record — 불변 데이터 클래스를 한 줄로 (JDK 16+, 21 표준)

위의 Money 클래스는 필드 2개인데 20줄입니다. 실무에서는 필드가 10개인 DTO(Data Transfer Object, 데이터를 나르기만 하는 객체)가 흔하고, 그때마다 생성자, getter, equals, hashCode, toString을 쓰면 100줄이 넘습니다. record는 이 보일러플레이트를 없앱니다.

java
public record Money(long amount, String currency) { }

이 한 줄이 자동으로 만드는 것:

자동 생성 내용
private final 필드 amount, currency
생성자 Money(long amount, String currency) (정규 생성자)
접근자 amount(), currency() — get 접두사 없음
equals 모든 필드가 같으면 같은 객체
hashCode 필드 기반
toString Money[amount=1000, currency=KRW]

record의 특징:

  • 필드는 무조건 private final — setter를 만들 수 없음. 불변.
  • 다른 클래스를 상속할 수 없음 (extends 불가). 인터페이스 구현은 가능.
  • 인스턴스 필드를 추가로 선언할 수 없음 (헤더의 컴포넌트만). static 필드는 가능.
  • 메서드는 자유롭게 추가 가능.

검증은 컴팩트 생성자(compact constructor)에 씁니다.

java
public record Money(long amount, String currency) {
    public Money {                                // 매개변수 목록 없음 = 컴팩트 생성자
        if (amount < 0) throw new IllegalArgumentException("음수 불가");
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("통화 필수");
        currency = currency.toUpperCase();        // 필드 대입 전 값 정규화 (this. 안 붙임)
    }                                             // 끝나면 자동으로 this.amount = amount; ... 실행

    public Money plus(Money other) {
        return new Money(amount + other.amount, currency);
    }
}

record vs 일반 클래스:

상황 선택
데이터를 담아 나르기만 (DTO, API 응답, 좌표, 금액) record
불변이어야 함 record
상태가 변해야 함 (계좌, 장바구니, 게임 캐릭터) 일반 클래스 + 캡슐화
상속이 필요 일반 클래스
JPA 엔티티 (기본 생성자 + 가변 필요) 일반 클래스

2.7 패키지 구조 — 접근 제어의 단위

패키지(package)는 관련 클래스를 묶는 폴더이자 이름 공간이며, default 접근 제어의 경계입니다.

text
com.shop
├── member
│   ├── Member.java           (public)
│   ├── MemberService.java    (public)
│   └── MemberValidator.java  (default — 패키지 안에서만 씀)
├── order
│   ├── Order.java
│   └── OrderService.java
└── common
    └── Money.java
java
package com.shop.order;

import com.shop.common.Money;          // 다른 패키지 클래스 사용 선언
import com.shop.member.Member;
import java.util.List;                 // java.lang(String, System 등)은 import 불필요

public class Order {
    private Member buyer;
    private Money total;
}

패키지 설계 원칙:

원칙 설명
도메인(업무 영역)별로 나눔 member, order, payment. 종류별(controller, dto)보다 응집도 높음
패키지 내부 협력 클래스는 default 외부에 노출할 것만 public
순환 의존 금지 order가 member를 쓰고 member가 order를 쓰면 얽힘
이름은 소문자, 도메인 역순 com.company.project.domain

import는 "이 파일에서 Money라고 쓰면 com.shop.common.Money를 뜻한다"는 선언입니다. import com.shop.common.*;(와일드카드)는 패키지의 모든 클래스를 가져오지만, 어떤 클래스를 쓰는지 불명확해지고 이름 충돌 위험이 있어 실무에서는 IDE가 개별 import를 자동으로 씁니다.

이 커리큘럼의 실습 코드는 패키지 없이 한 폴더에 둡니다. 패키지 없는 클래스는 "기본 패키지(default package)"에 속하며 학습용으로만 씁니다. 실무 코드는 반드시 패키지를 씁니다.

2.8 캡슐화 체크리스트

클래스를 쓸 때마다 확인할 것:

  1. 모든 필드가 private인가?
  2. 각 필드에 setter가 정말 필요한가? 업무 메서드로 대체할 수 없는가?
  3. setter/생성자에 유효성 검증이 있는가?
  4. 배열/컬렉션 필드를 getter로 그대로 노출하지 않는가?
  5. 바뀔 필요가 없는 클래스라면 불변(record)으로 만들 수 있는가?
  6. public 메서드는 이 클래스가 외부에 약속하는 API인가, 아니면 내부 구현이 새어 나간 것인가?
핵심 원리
  • 2.1 접근 제어자 4종
  • 2.2 캡슐화의 원리 — 상태와 통로
  • 2.3 getter와 setter
  • 2.4 setter에 유효성 검증 넣기 — 실무 패턴
  • 2.5 불변 객체 — 바꿀 수 없으면 안전하다
  • 2.6 record — 불변 데이터 클래스를 한 줄로 (JDK 16+, 21 표준)
  • 2.7 패키지 구조 — 접근 제어의 단위
  • 2.8 캡슐화 체크리스트
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제