CodeGym /행동 /JAVA 25 SELF /캡슐화의 원칙과 필요한 이유

캡슐화의 원칙과 필요한 이유

JAVA 25 SELF
레벨 15, 레슨 0
사용 가능

1. 캡슐화의 정의

캡슐화는 객체 지향 프로그래밍(OOP)의 근본 원칙 중 하나입니다. 쉽게 말해, 캡슐화는 객체의 내부(그 “속내”)를 감추고, 미리 마련된 “문” — 공개 메서드 — 를 통해서만 접근하게 하는 능력입니다.

현대 커피머신을 떠올려 보세요. 사용자는 버튼과 디스플레이만 볼 수 있고, 보일러나 펌프, 내부 배관이 어떻게 생겼는지는 알 필요가 없습니다. 그는 “카푸치노”를 누르고 결과를 받습니다. 내부는 전부 숨겨져 있죠. 이것이 바로 캡슐화입니다!

Java(및 다른 OOP 언어)에서 캡슐화는 다음을 통해 달성됩니다:

  • 데이터 은닉 — 클래스의 필드를 private(최소한 public은 아님)으로 선언합니다.
  • 공개 인터페이스 — 객체 사용자에게 정말 필요한 메서드만 “외부로 노출”합니다.

도식: 캡슐화는 이렇게 보입니다

+-------------------------------+
|         클래스 Student         |
|-------------------------------|
| - name: String                |  // private 필드
| - age: int                    |  // private 필드
|-------------------------------|
| + getName(): String           |  // public 메서드
| + setName(String): void       |  // public 메서드
| + getAge(): int               |  // public 메서드
| + setAge(int): void           |  // public 메서드
+-------------------------------+

여기서 - 기호는 private(숨김), +는 public(외부에서 접근 가능)을 의미합니다.

게터와 세터란?

캡슐화가 왜 필요한지 살펴보기 전에, 먼저 게터와 세터를 빠르게 알아봅시다. 이들은 클래스의 private 필드와 “소통”하도록 도와주는 특별한 메서드입니다.

게터(getter) — private 필드의 값을 가져오는 메서드입니다. 보통 getImyaPolya()처럼 이름을 짓습니다.

세터(setter) — private 필드의 값을 설정하는 메서드입니다. 보통 setImyaPolya(znachenie)처럼 이름을 짓습니다.

간단한 예:

public class Student {
    private String name; // private 필드 - 외부에서 보이지 않음
    
    // 게터 - '학생의 이름을 주세요'
    public String getName() {
        return name;
    }
    
    // 세터 - '학생의 이름을 설정해'
    public void setName(String name) {
        this.name = name;
    }
}

작동 방식:

Student student = new Student();
student.setName("바샤");           // 세터로 이름 설정
String name = student.getName();   // 게터로 이름 가져오기

게터와 세터를 객체에게 하는 “정중한 요청”으로 생각하세요. (student.name = "바샤"처럼) 주머니를 뒤지는 대신, 정중히 “이름을 설정해 주세요”라고 부탁합니다(student.setName("바샤")).

미리 이야기하자면: 몇 강 뒤에 게터와 세터를 자세히 배우고, 그 비밀과 제대로 활용하는 법을 익힐 것입니다. 지금은 핵심 아이디어만 이해하면 충분합니다!

2. 왜 캡슐화가 필요한가?

데이터를 잘못 사용하는 것을 방지

만약 클래스의 모든 필드가 public이라면, 외부 코드는 그것들의 값을 마음대로 직접 바꿀 수 있습니다:

Student s = new Student();
s.age = -1000; // 어이쿠, 뱀파이어 학생!

이는 위험합니다! 프로그램이 예측 불가능하게 동작할 수 있고, 버그는 가장 예상치 못한 곳에서 나타날 수 있습니다.

내부 구현을 바꿔도 외부 코드에 영향이 없음

캡슐화를 사용하면 클래스를 사용하는 코드를 망가뜨리지 않고 내부 구조를 변경할 수 있습니다. 예를 들어, 데이터 저장 방식을 바꾸거나 메서드에 검증 로직을 추가하더라도, 클래스 사용자는 이를 눈치채지 못합니다 — 여전히 같은 메서드를 호출할 뿐입니다.

가독성과 유지보수성 향상

내부 세부사항이 모두 숨겨지면, 외부 인터페이스가 더 깔끔하고 명확해집니다. 클래스를 사용하는 개발자는 내부 동작을 파헤칠 필요가 없습니다 — 어떤 메서드가 제공되고 무엇을 하는지만 알면 됩니다.

생활 속 예

스마트폰을 어떻게 사용하는지 떠올려 보세요. 화면 터치가 어떻게 처리되는지, 배터리가 어떻게 구성되어 있는지, 통신 모듈이 어떻게 동작하는지 고민하지 않습니다. 아이콘과 버튼 같은 이해하기 쉬운 인터페이스를 통해 필요한 기능을 호출할 뿐입니다. 제조사가 내부 구현을 바꿔도 사용자는 알아차리지 못합니다.

코드에서의 실제 예

BankAccount라는 클래스가 있다고 가정해 봅시다. 예전 버전의 프로그램에서는 잔액을 점으로 구분된 문자열 형태, 예를 들어 "1.000.50"로 저장했습니다. 이후 개발자들이 잔액을 double 숫자로 저장하기로 했습니다. 만약 필드가 public이었다면, account.balance에 직접 접근하던 기존 코드는 전부 깨졌을 것입니다.

하지만 캡슐화를 사용해 필드를 숨기고 deposit()과 getBalance() 메서드만 제공한다면, 외부 코드는 변화가 있었는지도 모릅니다:

public class BankAccount {
    private double balance; // 필드는 숨김

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    public double getBalance() {
        return balance;
    }
}

이제 내일이라도 잔액을 센트 단위(long)로 저장하고 싶다면, 클래스의 내부 구현만 바꾸면 됩니다. deposit()과 getBalance()를 호출하는 나머지 코드는 예전처럼 계속 동작합니다.

3. 나쁜/좋은 캡슐화 예

나쁜 예: public 필드

public class Student {
    public String name;
    public int age;
}

이 접근의 문제점:

  • 아무 코드나 필드에 어떤 값이든(심지어 잘못된 값도) 넣을 수 있습니다.
  • 데이터 검증을 추가할 방법이 없습니다.
  • 필드의 타입이나 구조를 바꾸면, 그 필드를 사용하는 모든 코드를 수정해야 합니다.

좋은 예: private 필드와 public 메서드

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        // 검증을 추가할 수 있음!
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("이름은 비어 있을 수 없습니다");
        }
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        // 나이가 음수가 아닌지 확인
        if (age < 0) {
            throw new IllegalArgumentException("나이는 음수일 수 없습니다");
        }
        this.age = age;
    }
}

장점:

  • 외부 코드는 필드를 직접 바꿀 수 없고 — 오직 메서드를 통해서만 변경할 수 있습니다.
  • 검증, 로깅, 자동 동작(예: 통계 업데이트)을 추가할 수 있습니다.
  • 내부 표현이 바뀌어도(예: 나이를 다른 형식으로 저장) 외부 인터페이스는 그대로 유지됩니다.

사용 시 모습

Student s = new Student();
s.setName("알리사");
s.setAge(20);

System.out.println(s.getName() + ", 나이: " + s.getAge());

음수 나이를 할당해 보세요 — 실행 시점에 바로 오류가 납니다! 프로그램이 어이없는 실수로부터 보호됩니다.

4. OOP의 다른 원칙들과의 관계

캡슐화는 다른 OOP 원칙들의 “엄마”와 같습니다. 캡슐화가 없다면 상속도, 다형성도, 추상화도 존재할 수 없습니다. 이들은 나중에 자세히 배우겠지만, 여기서는 간단히만 언급하겠습니다:

  • 상속(extends)은 기존 클래스를 기반으로 새로운 클래스를 만들고, 그 동작을 확장하거나 변경하게 해 줍니다. 만약 클래스의 내부가 모두 열려 있다면, 자식 클래스가 실수로 중요한 것을 망가뜨릴 수 있습니다.
  • 다형성(서로 다른 클래스의 객체가 같은 메시지에 서로 다르게 반응하는 능력)은 내부 구현과 외부 인터페이스의 명확한 구분 없이는 불가능합니다.
  • 추상화는 객체의 본질적인 특성만 드러내고 세부사항을 숨기는 것입니다. 캡슐화는 추상화를 실무에서 구현하는 데 도움을 줍니다.

비유

자동차를 생각해 보세요. 운전자에게 보이는 것은 핸들, 페달, 레버 — 이것이 인터페이스입니다. 나머지(엔진, 변속기, 전자장치)는 보닛 아래에 숨겨져 있습니다. 운전자가 엔진의 모든 나사를 직접 조작할 수 있었다면, 사고는 훨씬 더 자주 일어났을 것입니다!

5. 실전 예: 우리 애플리케이션에서의 캡슐화

우리의 학습용 애플리케이션을 이어서 발전시켜 봅시다 — 예를 들어 “주소록”. Contact라는 클래스가 있고, 이름과 전화번호를 저장한다고 합시다.

캡슐화 없음(안티 패턴):

public class Contact {
    public String name;
    public String phone;
}

사용:

Contact contact = new Contact();
contact.name = ""; // 어이쿠! 이름이 비어 있음
contact.phone = null; // 전화번호가 지정되지 않음

캡슐화 적용(올바른 접근):

public class Contact {
    private String name;
    private String phone;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("연락처 이름은 비어 있을 수 없습니다");
        }
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        if (phone == null || phone.isBlank()) {
            throw new IllegalArgumentException("전화번호는 비어 있을 수 없습니다");
        }
        this.phone = phone;
    }
}

이제 외부 코드는 빈 이름이나 전화번호를 남겨둘 수 없습니다:

Contact contact = new Contact();
contact.setName("이반");
contact.setPhone("+1-999-123-45-67");

빈 이름을 지정하려고 하면 — 프로그램이 오류를 던집니다.

6. 유용한 포인트

캡슐화와 장기 유지보수

작은 학습용 프로젝트에서는 “신사협정”만으로도 충분하다고 느낄 수 있습니다. 누가 음수 나이나 빈 이름을 넣겠어? 하지만 프로젝트가 커지고 다른 개발자들이 참여하며, 심지어 본인도 몇 달 뒤면 구현 세부사항을 잊어버리게 되면 — 바로 여기서 캡슐화가 혼란을 막아줍니다.

  • 클래스 내부를 쉽게 바꿀 수 있음 — 예를 들어, 전화번호를 문자열이 아니라 PhoneNumber 타입의 객체로 보관해야 한다면 구현만 바꾸면 되고, 외부 코드는 건드릴 필요가 없습니다.
  • 테스트가 더 쉬움 — 변경이 메서드를 통해서만 일어나므로, 어떤 데이터가 언제 바뀌는지 쉽게 추적할 수 있습니다.
  • 버그 감소 — 잘못된 값과 우발적 변경으로부터 보호합니다.

질문: 항상 게터와 세터가 필요할까요?

초보자는 흔히 이렇게 생각합니다. “캡슐화 = private 필드 + public 게터/세터니까, 모든 필드에 대해 게터와 세터를 만들어야지!” 꼭 그렇지는 않습니다.

  • 어떤 필드는 읽기 전용이어야 할 때가 있습니다(예: 객체의 유일한 식별자). 이때는 게터만 만듭니다.
  • 어떤 필드는 아예 “외부로 노출”할 필요가 없습니다 — 그러면 게터/세터를 만들지 않습니다.
  • 필드 값을 클래스 내부에서만 바꿀 수 있어야 한다면, 세터를 private으로 만들 수 있습니다.

황금률: 외부 코드에 정말 필요한 데이터와 메서드만 공개하세요.

시각화: 접근법 비교

접근법 필드 접근 예 제어 가능성 보안성
public 필드
obj.field = value;
아니오 낮음
private 필드 + 메서드
obj.setField(value);
예 높음

7. 캡슐화 작업 시 흔한 실수

실수 № 1: 클래스의 모든 필드가 public으로 선언됨. 초보자에게서 가장 흔한 실수입니다. 이런 코드는 금세 통제 불능이 됩니다. 누구나 당신 모르게 어떤 데이터든 바꿀 수 있기 때문입니다. 시간을 아끼고 싶더라도 이렇게 하지 마세요!

실수 № 2: 검증과 로직 없는 게터/세터. 접근자 메서드를 만든다면, 이를 검증에 활용하세요. 잘못된 값의 할당을 막아야 합니다. 단순히 파라미터 값을 필드에 “복사”하는 것이 항상 최선은 아닙니다.

실수 № 3: 내부 구조의 성급한 공개. “혹시 모르니” 하고 모든 필드에 대한 게터/세터를 미리 만들어 두면, 너무 많은 세부사항을 공개하게 되고 나중에 바꾸기 어려워질 수 있습니다.

실수 № 4: 변경 가능한 객체를 그대로 반환. 필드가 변경 가능한 객체(예: 리스트)라면, 게터로 그것을 그대로 반환하지 마세요. 사본을 반환하거나, 그 객체를 불변으로 만드세요.

1
과제
JAVA 25 SELF, 레벨 15, 레슨 0
잠금
클럽 회원의 비밀 데이터 🕵️‍♀️
클럽 회원의 비밀 데이터 🕵️‍♀️
1
과제
JAVA 25 SELF, 레벨 15, 레슨 0
잠금
클럽 회원 소개 🤝
클럽 회원 소개 🤝
1
과제
JAVA 25 SELF, 레벨 15, 레슨 0
잠금
데이터 수정 및 오류 방지 🚫
데이터 수정 및 오류 방지 🚫
1
과제
JAVA 25 SELF, 레벨 15, 레슨 0
잠금
창고의 변경 불가능한 상품 라벨 🏷️
창고의 변경 불가능한 상품 라벨 🏷️
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION