Java 23

캡슐화, 단순 숨기기가 아닌 객체의 보호

필드에 직접 값을 넣으면 편한데 왜 private으로 막고 getter/setter를 따로 만들어야 하는지 납득이 어렵다.

근거 · 교안 p158-p166

캡슐화, 단순 숨기기가 아닌 객체의 보호 대표 이미지

1장: 편리함 너머의 위험: 객체의 무결성

솔라의 손가락이 경쾌하게 키보드를 두드렸다. 모니터 화면에는 방금 완성한 짧은 코드가 반짝이고 있었다.

class Car {
    public String name;
    public int speed;
}

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.name = "씽씽이";
        myCar.speed = 80;

        System.out.println(myCar.name + "의 현재 속도는 " + myCar.speed + "km/h 입니다.");
    }
}

“언니, 이것 좀 봐.”

자신감이 가득한 솔라의 목소리에 소파에서 책을 읽던 루나가 고개를 들었다. 솔라는 의자를 빙글 돌려 루나를 향해 자랑스럽게 말했다.

“자동차 클래스를 만들었는데, 코드가 엄청 간단하고 명료해. myCar.speed = 80; 이렇게 필요한 값에 바로 접근해서 값을 넣어주면 끝이야. 어떤 책에서는 이걸 못하게 막고, 값을 넣는 복잡한 메서드를 따로 만들던데, 왜 그래야 하는지 모르겠어. 이게 훨씬 편하잖아?”

솔라의 말대로였다. 코드는 군더더기 없이 깔끔했고, 실행 결과도 ‘씽씽이의 현재 속도는 80km/h 입니다.’라고 정확히 출력되었다. 직접 필드에 접근하는 방식은 직관적이고 편리해 보였다.

루나는 잠시 솔라의 코드를 들여다보았다. 비난도, 칭찬도 없는 차분한 눈길이었다.

“음, myCar가 지금 시속 80km로 달리고 있네. 잘 작동하는구나.”

“응! 간단하지?”

“정말 편해 보이긴 한다.” 루나는 고개를 끄덕이며 말했다. “그럼 이 myCar는 한 시간 뒤에 어디쯤 가 있을까?”

“그거야 간단하지!”

솔라는 자신의 코드가 시험대에 오른 것 같아 신이 났다. 재빨리 main 메서드 아래에 새로운 코드를 추가했다.

// 한 시간 뒤의 이동 거리를 계산하는 로직
int distance = myCar.speed * 1;
System.out.println(myCar.name + "은(는) 한 시간 뒤 " + distance + "km 지점에 있습니다.");

실행 버튼을 누르자, 콘솔에 새로운 문장이 나타났다.

씽씽이는 한 시간 뒤 80km 지점에 있습니다.

솔라는 만족스러운 미소를 지었다. “봐, 문제없지? speed 값을 바로 가져와서 계산하니까 얼마나 편해.”

“잘 되네.” 루나가 말했다. 그러고는 잠시 뜸을 들인 뒤, 마치 스쳐 지나가는 생각인 듯 가볍게 물었다. “혹시 코드를 짜다가… 실수로 이렇게 값을 넣으면 어떻게 될까?”

루나가 가리킨 곳은 myCar.speed = 80; 이라는 부분이었다. 솔라의 머릿속에 루나가 제안하는 새로운 값이 그려졌다.

myCar.speed = -100;

“마이너스 100?” 솔라는 미간을 찌푸렸다. “자동차 속도가 음수일 수는 없잖아. 후진을 말하는 거라도 속도는 양수 값으로 표현하고 방향을 바꾸는 거지. 이건… 말이 안 되는데.”

솔라 스스로도 이상하다는 걸 알았지만, ‘컴퓨터는 시키는 대로 할 뿐이니까’라고 생각하며 코드를 수정했다. 호기심 반, 의심 반이었다.

Car myCar = new Car();
myCar.name = "씽씽이";
myCar.speed = -100; // 실수로 잘못된 값을 넣은 상황

System.out.println(myCar.name + "의 현재 속도는 " + myCar.speed + "km/h 입니다.");

// 한 시간 뒤의 이동 거리를 계산하는 로직
int distance = myCar.speed * 1;
System.out.println(myCar.name + "은(는) 한 시간 뒤 " + distance + "km 지점에 있습니다.");

엔터 키를 누르자, 프로그램은 아무런 불평 없이 순순히 결과를 내놓았다.

씽씽이의 현재 속도는 -100km/h 입니다. 씽씽이는 한 시간 뒤 -100km 지점에 있습니다.

솔라는 잠시 화면을 멍하니 바라보았다. 마이너스 100km 지점. 그게 대체 어디란 말인가. 자동차가 시간을 거슬러 과거로라도 갔다는 뜻일까? 코드는 아무런 오류 없이 실행되었지만, 그 결과는 현실에서는 결코 존재할 수 없는, 완전히 망가진 상태를 보여주고 있었다.

편리하다고 생각했던 방식이 만들어 낸 황당한 결과였다.

“아…”

낮은 탄식이 솔라의 입에서 흘러나왔다. 고개가 절로 끄덕여졌다.

“알겠다. speed에 직접 값을 넣는 게 편하기는 한데… 이렇게 말도 안 되는 값이 아무런 제지 없이 들어갈 수 있는 거였구나. 나는 당연히 양수만 넣을 거라고 생각했지만, 만약 다른 사람이 이 Car 클래스를 가져다 쓰거나, 내가 나중에 정신없이 코드를 짜다가 실수라도 하면… 이렇게 이상한 유령 자동차가 만들어질 수도 있겠네.”

솔라는 방금 전까지 자랑스러웠던 자신의 코드를 다시 보았다. 이제는 다르게 보였다. 외부의 어떤 공격에도 무방비로 노출된, 너무나 허술한 설계였다. 편리함이라는 장점 뒤에 ‘객체의 상태가 언제든 망가질 수 있다’는 치명적인 위험이 숨어 있었다.

루나는 그저 조용히 솔라의 깨달음을 지켜볼 뿐이었다.

생각을 정리한 솔라가 루나를 보며 물었다.

“그럼 이런 위험한 접근을 어떻게 막아야 해? speed처럼 객체의 핵심적인 상태는 아무나 마음대로 건드릴 수 없게 단단히 지켜야 할 것 같은데… 방법이 있는 거야?”

2장: 객체의 문지기: private과 안전한 통로

솔라의 커서는 어제 작성했던 Car 클래스의 코드 위를 맴돌고 있었다. 어제까지만 해도 간단해서 자랑스러웠던 두 줄의 코드가 이제는 불안하게 느껴졌다.

public String name;
public int speed;

public. ‘공공의’, ‘모두에게 열려있는’. 솔라는 이 단어에 시선을 고정했다. 속도가 음수가 되는 말도 안 되는 상황은 바로 이 ‘모두에게 열려있는’ 속성 때문이었다. 마치 대문도, 자물쇠도 없이 귀중품을 둔 방과 같았다. 누구나 들어와 마음대로 값을 바꾸고 망가뜨릴 수 있었다.

“어떻게 막아야 할까… 이 위험한 접근을.”

스스로에게 던진 질문에 대한 답을 찾으려는 듯, 솔라는 public이라는 단어를 지우고 그 자리에 반대 의미를 가질 법한 단어를 입력했다.

private. ‘사적인’, ‘개인의’.

class Car {
    private String name;
    private int speed;
}

단어를 바꾸자마자, 어제 작성했던 main 메서드의 코드에 즉시 붉은 밑줄이 그어졌다. 이클립스가 보내는 명백한 경고 신호였다.

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        myCar.name = "씽씽이"; // Error: The field Car.name is not visible
        myCar.speed = -100;   // Error: The field Car.speed is not visible
        ...
    }
}

“오! 막혔다!”

솔라는 작은 탄성을 내뱉었다. 외부에서 myCar.speed에 직접 접근하려는 시도가 컴파일러 수준에서 차단된 것이다. 이제 그 누구도, 심지어 코드를 작성하는 자기 자신조차도, speed 필드에 마음대로 값을 할당할 수 없게 되었다. 위험한 음수 값을 넣는 실수는 원천적으로 불가능해졌다. 튼튼한 벽을 세워 객체의 중요한 데이터를 보호하는 데 성공한 것 같았다.

하지만 기쁨은 잠시였다.

“어? 잠깐만…”

솔라는 다시 붉은 밑줄이 그어진 코드를 보았다. 위험한 값인 -100 뿐만 아니라, 올바른 값인 80을 넣으려고 해도 똑같은 오류가 발생할 터였다. 심지어 speed 값을 읽어와서 이동 거리를 계산하는 것조차 불가능해졌다.

int distance = myCar.speed * 1; // Error: The field Car.speed is not visible

“이건… 그냥 감옥이잖아?”

솔라는 허탈한 목소리로 중얼거렸다. 도둑을 막으려고 철문을 굳게 닫아걸었는데, 정작 집주인인 자신까지 드나들 수 없게 된 꼴이었다. 데이터를 보호하려다 아예 쓸모없게 만들어 버린 것이다. private은 그저 정보를 숨기고 틀어막는, 답답한 벽일 뿐인 걸까.

그때, 조용히 솔라의 화면을 지켜보던 루나가 입을 열었다.

“견고한 벽을 세우는 데는 성공했네. 이제 자동차의 엔진(speed)은 외부의 어떤 충격에도 안전해졌어.”

“안전해지긴 했는데, 이제 시동도 걸 수 없게 됐어. 속도를 바꿀 방법이 아예 사라졌잖아. 이건 그냥 고철 덩어리야.” 솔라가 투덜거렸다.

루나는 고개를 저었다. “아니. 차의 정체성은 바뀌지 않았어. 행동 방식만 바꾸면 돼. 외부에서 엔진을 직접 만지는 대신, 운전석에 앉아서 페달을 밟는 것처럼 말이야.”

“페달?”

“응. Car 클래스 외부에서는 이제 speed를 직접 건드릴 수 없지. 하지만 Car 클래스 내부에서는 자기 자신의 private 필드에 접근할 수 있어. 그렇다면 Car 스스로가 외부의 요청을 받아 speed를 안전하게 변경할 수 있는 ‘공식적인 통로’를 열어주면 어떨까?”

‘공식적인 통로’. 솔라는 그 말을 되뇌었다. 외부에서 벽을 부수고 들어오는 대신, 주인이 만들어 둔 문을 통해 정중하게 요청하는 이미지. 솔라는 즉시 Car 클래스 안으로 돌아가 새로운 코드를 작성하기 시작했다. 외부(public)에 공개된, 값을 설정하는(set) 메서드였다.

class Car {
    private String name;
    private int speed;

    public void setSpeed(int newSpeed) {
        // 외부에서 받은 값을 내부에 있는 private 필드에 할당한다.
        this.speed = newSpeed;
    }

    public int getSpeed() {
        // 내부에 있는 private 필드의 값을 외부로 반환한다.
        return this.speed;
    }
}

그리고 main 메서드의 코드를 새롭게 만든 통로를 사용하도록 수정했다.

public class Main {
    public static void main(String[] args) {
        Car myCar = new Car();
        // myCar.name = "씽씽이";  // 직접 접근 불가!
        myCar.setSpeed(80);      // 메서드를 통한 안전한 접근

        System.out.println("현재 속도는 " + myCar.getSpeed() + "km/h 입니다.");

        int distance = myCar.getSpeed() * 1;
        System.out.println("한 시간 뒤 " + distance + "km 지점에 있습니다.");
    }
}

오류를 표시하던 붉은 밑줄이 사라졌다. 실행 버튼을 누르자, 콘솔에 깔끔한 결과가 출력되었다.

현재 속도는 80km/h 입니다. 한 시간 뒤 80km 지점에 있습니다.

“됐다!” 솔라의 얼굴에 다시 화색이 돌았다. private이라는 벽으로 일단 모든 직접 접근을 막아버리고, setSpeedgetSpeed라는 이름의 공식적인 문(메서드)을 만들어 통제된 접근을 허용하는 방식. 이제 외부에서는 오직 이 문을 통해서만 speed의 값을 바꾸거나 조회할 수 있었다. private은 무작정 숨기기 위한 기능이 아니라, 외부의 무분별한 접근으로부터 객체를 보호하는 첫 번째 방어선이었던 것이다.

자신의 설계에 만족한 솔라는 잠시 생각에 잠겼다. 이 구조라면 이제 안전할까? 문득, 어제의 그 끔찍한 실수가 떠올랐다. 솔라는 시험 삼아 코드 한 줄을 고쳐보았다.

myCar.setSpeed(-100);

코드는 아무런 오류 없이 실행되었다. 그리고 콘솔에는 어제와 똑같은, 기괴한 문장이 나타났다.

현재 속도는 -100km/h 입니다.

솔라의 미소가 굳어졌다. 벽을 세우고 문까지 만들었는데, 어째서 결과는 똑같은 걸까. 그녀는 자신이 만든 setSpeed 메서드를 멍하니 들여다보았다. 그곳엔 외부에서 어떤 값이 들어오든 그대로 speed에 전달하는 코드만 덩그러니 놓여 있었다.

“언니… private으로 직접 접근은 막았는데, 이 setSpeed라는 문이 아무나 다 통과시켜주고 있어. 문지기가 졸고 있는 것 같아. 이러면 벽을 세운 의미가 없는 거 아니야?”

3장: 안전한 통로의 핵심: 값 검증

솔라의 책상 위, 모니터 불빛 옆으로 작은 메모지 한 장이 놓여 있었다. 키보드를 두드리는 대신, 솔라는 펜으로 무언가를 그리고 있었다. 단순한 그림이었다. 커다란 사각형 상자에는 Car 객체라고 쓰여 있었다. 상자 둘레에는 굵은 선으로 벽이 그려져 있었고, 그 위에는 private이라는 팻말이 붙어 있었다. 벽의 한쪽에는 setSpeed()라는 이름의 문이 달려 있었다.

문제는 그 다음이었다. 솔라는 -100이라는 숫자가 적힌 졸라맨 하나가 그 문을 아무런 제지 없이 통과해 상자 안으로 들어가는 모습을 그렸다. 그 옆에는 시무룩한 표정의 이모티콘까지 그려 넣었다. 완벽한 방어벽을 세웠다고 생각했는데, 정작 문지기가 아무나 들여보내고 있는 상황을 한눈에 보여주는 그림이었다.

마침 방에 들어온 루나는 솔라의 책상 위 그림을 잠시 들여다보았다.

“그림으로 보니까 문제가 더 명확하게 보이네.”

루나의 말에 솔라가 고개를 들었다.

“언니, 이게 지금 내 코드 상태야. private으로 벽을 세워서 아무나 못 들어오게 막았더니, 이번엔 setSpeed라는 문이 문제야. 어떤 값이든 그냥 다 통과시켜주고 있어. 이러면 벽을 세운 의미가 없잖아. 문지기가 졸고 있는 것 같아.”

솔라는 자신이 만든 setSpeed 메서드를 다시 쏘아보았다.

public void setSpeed(int newSpeed) {
    this.speed = newSpeed;
}

외부에서 어떤 값이 들어오든, 그 값을 그대로 내부의 speed 필드에 전달하는 코드. 너무나 성실해서 문제인 문지기였다.

“문지기라… 좋은 비유네.” 루나가 고개를 끄덕였다. “그럼 솔라 네가 생각하는 진짜 문지기는 어떤 일을 해야 해? 그냥 오는 사람마다 문만 열어주는 게 다일까?”

“아니지! 당연히 신원을 확인해야지. 출입증이 있는지, 방문 목적이 뭔지, 위험한 물건은 없는지… 다 검사해야 진짜 문지기지.”

말을 내뱉는 순간, 솔라의 눈이 반짝 빛났다. ‘검사해야 한다.’ 스스로 내뱉은 말 한마디가 안개 속에 가려져 있던 해답을 환하게 비췄다. 그녀는 재빨리 키보드로 손을 옮겼다.

“맞아, 검사! 이 setSpeed 메서드가 문지기라면, 들어오는 값을 검사해야 해!”

솔라는 setSpeed 메서드 안으로 커서를 옮겼다. 자동차의 속도는 음수일 수 없다. 이것이 바로 Car 객체가 스스로 지켜야 할 가장 중요한 규칙이었다.

public void setSpeed(int newSpeed) {
    // 문지기가 들어온 값을 검사하는 로직!
    if (newSpeed >= 0) {
        this.speed = newSpeed; // 유효한 값일 때만 상태를 변경한다.
    } else {
        // 유효하지 않은 값이 들어왔을 경우
        System.out.println("속도는 음수일 수 없습니다. 요청이 무시됩니다.");
        // 혹은 this.speed = 0; 으로 안전한 기본값을 설정할 수도 있다.
    }
}

새로운 코드는 마치 깐깐한 경비원 같았다. 0 이상의 유효한 속도 값이 들어올 때만 문을 열어 speed 필드를 변경하고, 만약 음수 같은 이상한 값이 들어오면 문을 굳게 닫고 “속도는 음수일 수 없습니다.”라는 경고 메시지를 내보내도록 했다.

심장이 두근거렸다. 솔라는 마지막 테스트를 위해 main 메서드에서 다시 한번 -100이라는 값을 setSpeed에 넘겨주었다.

myCar.setSpeed(-100);

그리고 getSpeed()를 통해 현재 속도를 확인하는 코드를 실행했다. 엔터 키를 누르자, 콘솔에 나타난 결과는 어제와 완전히 달랐다.

속도는 음수일 수 없습니다. 요청이 무시됩니다. 현재 속도는 0km/h 입니다.

초기값 그대로인 0km/h. 마이너스 속도라는 유령 같은 상태는 더 이상 나타나지 않았다. setSpeed라는 문지기가 마침내 자신의 임무를 완벽하게 수행한 것이다.

“됐다! 막았어!”

솔라는 자신도 모르게 작은 환호성을 내질렀다. 이제야 모든 조각이 맞춰지는 기분이었다. private으로 외부의 직접적인 공격을 막아내고, 유일한 통로인 setter 메서드에 유효성 검증이라는 든든한 방어 로직을 심어두는 것. 이 두 가지가 결합되었을 때 비로소 객체는 외부의 어떤 실수나 공격에도 흔들리지 않는 견고한 성이 될 수 있었다.

“이제야 알겠어.” 솔라는 모니터에서 눈을 떼고 루나를 보며 말했다. “캡슐화는 단순히 데이터를 감추는 게 아니었어. 이렇게 객체가 스스로를 보호하도록, 언제나 ‘말이 되는’ 상태를 유지하도록 만드는 안전장치였구나. 특히 이 setter 안의 값 검증이 진짜 핵심이었네.”

더 이상 setter는 귀찮고 형식적인 코드가 아니었다. 객체의 심장을 지키는 가장 중요한 보호막이었다.

솔라는 잠시 생각에 잠기더니, 이클립스 창을 잠시 내리고 깨끗한 워드 프로세서를 열었다. 그리고 망설임 없이 제목을 입력했다.

‘Car 객체 설계 보고서’

그녀는 방금 얻은 깨달음을 바탕으로, 완벽한 자동차 객체를 설계하기 위한 원칙들을 하나씩 정리하기 시작했다.

  1. 데이터 보호: speedname 같은 핵심 데이터는 반드시 private으로 선언하여 외부로부터의 직접적인 접근을 원천 차단한다. 객체의 상태는 오직 객체 스스로만 변경할 수 있어야 한다.

  2. 통제된 통로: 데이터의 조회 및 변경은 반드시 public으로 공개된 gettersetter 메서드를 통해서만 이루어져야 한다. 이는 유일하고 공식적인 소통 창구 역할을 한다.

  3. 상태 무결성 보장: setSpeed() 메서드는 전달받은 값이 0 이상인 경우에만 speed 필드를 변경한다. 유효하지 않은 값(음수)이 들어올 경우, 변경 요청을 무시하고 현재 상태를 그대로 유지함으로써 객체가 비정상적인 상태에 빠지는 것을 능동적으로 방어한다.

자신이 직접 작성한 세 가지 원칙을 보며 솔라는 만족스럽게 고개를 끄덕였다. 이것이 바로 ‘객체의 보호’를 위한 설계도였다. 이제 어떤 객체를 만들든, 이 원칙을 가장 먼저 떠올리게 될 것 같았다.