자바 캡슐화란? private과 getter·setter로 이해하는 객체지향 캡슐화
JAVA를 사용하여 개발하다 보면 반드시 알아야 하는 개념들이 있다. 그중 하나가 OOP, 즉 객체지향 프로그래밍이다. 시리즈로 정리하고 있으니 자바 추상화 글도 함께 보면 좋다. 객체지향 프로그래밍은 캡슐화, 상속, 추상화, 다형성이라는 네 가지 특징으로 이루어지는데, 이번 글에서는 그 첫 번째인 자바 캡슐화에 대해 알아보려고 한다.
거창한 이론보다는 실제 현장에서 캡슐화가 어떻게 쓰이는지를 예제 코드로 살펴보고, 접근 제어자와 getter/setter를 쓸 때 주의할 점까지 함께 정리해 보았다. 이 글이 부디 도움이 되었으면 좋겠다.

자바 캡슐화 개념: 무엇을 묶고 무엇을 숨기나
캡슐화는 보통 다음과 같이 정의한다.
데이터(변수, variable)와 그 데이터를 처리하는 코드(메서드, method)를 하나의 단위(Single Unit)로 묶는(wrapping) 원리 또는 구조
조금 더 실무적으로 말하면, 클래스 안의 필드는 private으로 숨기고 외부에서는 정해진 메서드를 통해서만 값을 읽고 바꾸게 만드는 것이다. 우리가 DTO 클래스에서 흔히 쓰는 getter와 setter가 바로 캡슐화를 사용하고 있는 대표적인 예다.
캡슐화를 하면 클래스가 자기 필드에 누가 어떻게 접근할 수 있는지를 직접 정할 수 있다. 외부에서 변수에 바로 접근하지 못하게 막아 두면, 엉뚱한 값이 들어와서 프로그램에 문제를 일으키는 일을 미리 차단할 수 있다. 필자는 이 부분이 캡슐화에서 무엇보다 중요하다고 생각한다.
자바 캡슐화 예제: private과 setter로 값 지키기
아래 소스 코드를 통해 같이 보도록 하자. 결재 단계를 표현하는 Approval 클래스다.
public class Approval {
private String step_no;
private String step_name;
private String step;
public String getStep() {
return step;
}
public void setStep(String step) {
this.step = step;
}
public String getStep_no() {
return step_no;
}
public void setStep_no(String step_no) {
// 숫자가 아니면 0으로 저장한다
if (ComUtil.validateInt(step_no)) {
this.step_no = step_no;
} else {
this.step_no = String.valueOf(0);
}
}
public String getStep_name() {
return step_name;
}
public void setStep_name(String step_name) {
this.step_name = step_name;
}
}
여기서 핵심은 setStep_no 메서드다. step_no 필드는 private이라 외부에서 직접 건드릴 수 없고, 값을 넣으려면 반드시 setStep_no를 거쳐야 한다. 그리고 setStep_no 안에서 숫자인지 검사하기 때문에 잘못된 값은 0으로 바뀌어 저장된다. 참고로 ComUtil.validateInt는 숫자인지 확인하는 사내 유틸 메서드라고 생각하면 된다.
캡슐화가 없으면 생기는 문제
반대로 step_no가 private이 아니라 public String step_no처럼 어디서나 접근할 수 있게 열려 있다고 생각해 보자. 그러면 외부에서 아래처럼 아무 값이나 넣을 수 있게 된다.
Approval app = new Approval();
app.step_no = "가나다"; // 숫자가 아닌 값이 그대로 들어간다
숫자가 아닌 값이 들어온 이상, 이 값을 숫자로 바꿔 쓰는 곳에서 오류가 나면서 프로그램이 멈추는 불상사가 생길 것이다. 하지만 위 예제처럼 필드를 private으로 막고 setter에서 값을 검사하면 외부에서 들어오는 값을 통제할 수 있다.
이 클래스를 사용하는 쪽에서는 따로 검사 코드를 짤 필요가 없다. 변수에 대해 고민할 것도 없이 setStep_no에 값만 넣어 주면 클래스가 알아서 처리해 준다. 내부 구현은 드러내지 않고 필요한 기능만 밖으로 보여 주는 것, 이것이 캡슐화의 기본인 정보 은닉이다.
자바 접근 제어자 정리
자바 캡슐화 구현의 핵심은 접근 제어자다. 네 가지 접근 제어자가 어디까지 접근을 허용하는지 정리하면 다음과 같다.
| 접근 제어자 | 같은 클래스 | 같은 패키지 | 자식 클래스 | 전체 |
|---|---|---|---|---|
public | O | O | O | O |
protected | O | O | O | X |
| (default) | O | O | X | X |
private | O | X | X | X |
자세한 범위는 Oracle 공식 문서에도 같은 표로 정리되어 있다. 필드는 기본적으로 private으로 두고, 꼭 외부에 열어야 하는 기능만 public 메서드로 제공하는 것이 일반적인 원칙이다.
getter/setter만 만든다고 캡슐화일까?
물론 실제 현장에서 개발을 하다 보면 setStep_no 같은 메서드보다 setStep_name처럼 아무 검사 없이 값을 그대로 넣는 setter를 훨씬 많이 보게 된다. 시간이나 여건상 IDE로 getter/setter만 자동 생성해서 쓰는 경우가 대부분이기 때문이다.
하지만 모든 필드에 검사 없는 setter를 열어 두면 사실상 필드를 public으로 연 것과 크게 다르지 않다. 조금씩이라도 이 변수가 어떻게 쓰여야 하는지 정의하고 그에 맞게 setter를 만든다면, 다음 사람이 이어서 개발할 때 훨씬 편해질 것이다. 실무에서 도움이 되는 원칙을 몇 가지 정리하면 다음과 같다.
- 바뀔 일이 없는 값에는 setter를 만들지 않고 생성자에서 한 번만 받는다.
- setter 대신
approve(),reject()처럼 의미가 드러나는 메서드를 만든다. - 값 검사는 setter나 생성자 한 곳에서만 해서 규칙이 흩어지지 않게 한다.
- 리스트 같은 컬렉션 필드는 getter에서 원본 대신 복사본이나 읽기 전용 목록을 돌려준다.
Java 16 이후라면 record도 고려해 보자
값을 담아 전달하기만 하는 DTO라면 Java 16부터 정식으로 들어온 record를 쓰는 것도 좋은 방법이다. record는 필드가 자동으로 private final이 되고 setter가 없기 때문에 한 번 만든 값은 바뀌지 않는다. 검사 로직은 생성자 안에 넣으면 된다.
public record Approval(String stepNo, String stepName) {
public Approval {
if (!ComUtil.validateInt(stepNo)) {
stepNo = "0";
}
}
}자바 캡슐화 장점 정리
- 데이터 보호: 잘못된 값이 필드에 들어오는 것을 막을 수 있다.
- 유지보수: 내부 구현을 바꿔도 외부에 공개한 메서드만 그대로면 사용하는 쪽 코드를 고칠 필요가 없다.
- 사용 편의: 사용하는 쪽은 내부 사정을 몰라도 메서드만 호출하면 된다.
- 디버깅: 값이 바뀌는 길이 setter 하나뿐이라 문제가 생겼을 때 추적하기 쉽다.
마무리
여기까지 자바 캡슐화 개념과 예제, 접근 제어자, 그리고 실무에서 getter/setter를 쓸 때 생각해 볼 점을 정리해 보았다. 정리하면 필드는 숨기고, 값이 바뀌는 길은 메서드로 통제하는 것이 캡슐화의 핵심이다.
다음 편에서는 객체지향의 또 다른 특징인 자바 추상화에 대해 알아보도록 하겠다. 접근 제어자에 대한 공식 설명은 Oracle Java 튜토리얼에서도 확인할 수 있다.
첫 댓글을 남겨보세요