코드 한 줄의 기록

Java 캡슐화와 접근 제어자 완벽 가이드: 불변 객체 패턴 기초부터 활용까지 본문

JAVA

Java 캡슐화와 접근 제어자 완벽 가이드: 불변 객체 패턴 기초부터 활용까지

CodeByJin 2025. 10. 8. 08:24
반응형

Java 개발자가 반드시 알아야 할 캡슐화(encapsulation) 개념과 접근 제어자(access modifier), 그리고 불변 객체(immutable object) 패턴의 기초를 함께 살펴봅니다.

캡슐화(Encapsulation)란 무엇인가?

캡슐화는 객체 지향 프로그래밍의 핵심 원칙 중 하나로, 객체 내부의 상태(필드)와 행위(메소드)를 하나로 묶고, 외부로부터 직접 접근을 제한하여 데이터 무결성유지보수성을 높이는 기법입니다.

  • 내부 구현 변경 시 외부 영향 최소화
  • 잘못된 값 설정 방지
  • 코드 재사용성과 가독성 향상

캡슐화 예제

public class User {
    // 필드는 private로 선언해 외부 직접 접근 방지
    private String name;
    private int age;

    // 생성자를 통해 초기화
    public User(String name, int age) {
        this.name = name;
        setAge(age);  // 검증 로직 적용
    }

    // name 필드 getter
    public String getName() {
        return name;
    }

    // name 필드 setter (필요 시 검증 로직 추가 가능)
    public void setName(String name) {
        if(name == null || name.isEmpty()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
        }
        this.name = name;
    }

    // age 필드 getter
    public int getAge() {
        return age;
    }

    // age 필드 setter (유효성 검증)
    public void setAge(int age) {
        if(age < 0 || age > 150) {
            throw new IllegalArgumentException("나이는 0 이상 150 이하여야 합니다.");
        }
        this.age = age;
    }
}

 

위 예제에서 User 클래스는 private 필드를 갖고, public getter/setter를 통해 값 설정 및 검증을 수행합니다.
이를 통해 외부에서 age에 잘못된 값이 들어가는 것을 방지할 수 있습니다.

접근 제어자(Access Modifier) 정리

Java에서는 멤버(필드, 메소드, 생성자) 접근성을 제어하기 위해 4가지 접근 제어자를 제공합니다.

접근 제어자 동일 클래스 동일 패키지 하위 클래스 전체 공개
private O X X X
(default) O O X X
protected O O O X
public O O O O
  • private: 오직 같은 클래스 내부에서만 접근 가능
  • default (package-private): 같은 패키지 내부에서만 접근 가능
  • protected: 같은 패키지 + 다른 패키지의 하위 클래스에서 접근 가능
  • public: 어디에서나 접근 가능

예시 코드

package com.example;

public class Parent {
    private String secret;           // 같은 클래스만 접근 가능
    int packageInfo;                 // default: 같은 패키지에서만 접근 가능
    protected String talkToChild;    // 하위 클래스에서 접근 가능
    public String greet;             // 누구나 접근 가능

    public Parent() {
        this.secret = "비밀";
        this.packageInfo = 42;
        this.talkToChild = "안녕";
        this.greet = "Hello";
    }
}

package com.example.child;

import com.example.Parent;

public class Child extends Parent {
    public void demo() {
        // secret -> 접근 불가
        // int x = secret;          // 컴파일 에러

        // packageInfo -> 접근 불가 (다른 패키지)
        // int y = packageInfo;     // 컴파일 에러
        
        // talkToChild -> 접근 가능 (protected)
        String msg = talkToChild;
        
        // greet -> 접근 가능 (public)
        String hi = greet;
    }
}

불변 객체(Immutable Object) 패턴

불변 객체란, 생성 이후 상태가 변하지 않는 객체를 말합니다. 불변 객체는 스레드 안정성(thread-safety)을 보장하고, 디버깅·테스트·캐싱을 쉽게 해 줍니다.
 

불변 객체 설계 원칙

  1. 클래스는 final로 선언하거나, 적어도 상속 금지
  2. 모든 필드는 private final로 선언
  3. 생성자에서 모든 필드를 완전히 초기화
  4. setter 메소드를 제공하지 않음
  5. 내부 가변 객체는 방어적 복사(defensive copy) 적용

간단한 불변 객체 예제

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    // 이동 후 새로운 Point 인스턴스 반환
    public Point move(int dx, int dy) {
        return new Point(this.x + dx, this.y + dy);
    }
}
  • Point 객체는 생성 이후 x, y 값이 변경될 수 없습니다.
  • 이동 시 기존 객체를 변경하지 않고, 새로운 객체를 반환합니다.

컬렉션을 포함한 불변 객체

public final class Team {
    private final String name;
    private final List<String> members;

    public Team(String name, List<String> members) {
        this.name = name;
        // 방어적 복사
        this.members = new ArrayList<>(members);
    }

    public String getName() {
        return name;
    }

    public List<String> getMembers() {
        // 리스트 수정 방지를 위한 읽기 전용 뷰 반환
        return Collections.unmodifiableList(members);
    }
}

 
방어적 복사와 읽기 전용 뷰를 통해 외부에서 내부 리스트를 변경하지 못하도록 설계했습니다.

캡슐화와 불변 객체 패턴 활용 사례

설정(Configuration) 객체

애플리케이션 설정 정보를 담은 객체에 불변 패턴을 적용하면, 전체 라이프사이클 동안 설정 값이 변하지 않아 안전하게 공유할 수 있습니다.

public final class AppConfig {
    private final String dbUrl;
    private final int poolSize;

    public AppConfig(String dbUrl, int poolSize) {
        this.dbUrl = dbUrl;
        this.poolSize = poolSize;
    }

    // getters만 제공
    public String getDbUrl() { return dbUrl; }
    public int getPoolSize() { return poolSize; }
}

 
도메인 모델(Entity) 설계

도메인 계층에서도 핵심 비즈니스 객체는 불변으로 설계하면, 단위 테스트버그 추적이 쉬워집니다.

public final class Money {
    private final BigDecimal amount;
    private final Currency currency;

    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("통화 단위 불일치");
        }
        return new Money(this.amount.add(other.amount), currency);
    }
    // getters...
}

 
Java에서 캡슐화는 객체 내부 구현을 숨기고, 접근 제어자를 통해 외부 접근을 엄격히 관리함으로써 코드 품질을 높입니다. 또한 불변 객체 패턴은 상태 변경을 막아 스레드 안전성과 유지보수성을 크게 개선합니다.

학습을 위한 주요 포인트

  • private 필드 + public/protected getter·setter로 캡슐화 구현
  • default, protected, public 접근 범위의 차이 이해
  • final 클래스와 private final 필드로 불변 객체 설계
  • 내부 컬렉션 방어적 복사 및 읽기 전용 뷰 적용
  • 실무 도메인 및 설정 객체에 불변 패턴 활용

위 개념들을 일상 프로젝트에 적용해 보면서, 코드 품질과 유지보수성을 체감해 보세요. Java의 객체 지향 설계 원칙을 바탕으로 더 견고한 애플리케이션을 만들 수 있습니다.

Java 클래스와 객체 완벽 정리 - 초보자도 이해하는 객체지향 프로그래밍 핵심 개념

Java를 배우면서 가장 먼저 접하게 되는 개념이 바로 클래스와 객체입니다. 처음에는 "왜 이런 걸 배워야 하지?" 싶었는데, 지금 돌이켜보니 Java 프로그래밍의 모든 것이 여기서 시작된다는 걸 깨

byteandbit.tistory.com

반응형