1. 파편화된 데이터 관리의 위험성

게임 서버를 개발한다고 가정해보자. 유저의 HP나 레벨 같은 민감한 데이터가 보호받지 못하고 있다면 어떤 일이 벌어질까? 누구나 public 변수에 접근해 값을 조작할 수 있다면, 이는 버그가 아니라 보안 사고다.

Open Field (Bad Case)

아래 코드는 모든 필드가 public으로 열려 있어 무결성이 깨지기 쉬운 상태다.

public class GameCharacter {
    public String name;
    public int hp;
    public int level;
}

// 사용부: 누구나 데이터를 오염시킬 수 있다.
GameCharacter user = new GameCharacter();
user.hp = -9999; // 논리적으로 불가능한 값 주입
user.level = 999; // 부정 행위 가능

단순히 문법적인 문제가 아니다. 데이터의 주권이 객체 자신이 아닌 외부(호출자)에 있다는 것이 문제의 핵심이다.


2. 접근 제어와 데이터 무결성 (Encapsulation)

데이터를 보호하는 첫 번째 원칙은 "남에게 맡기지 말고 직접 관리하라"는 것이다. private 접근 제어자로 외부 접근을 차단하고, 정해진 메서드(Getter/Setter)를 통해서만 데이터를 조작하게 강제해야 한다.

Setter의 역할: 단순 대입이 아닌 '검증'

public class GameCharacter {
    private int hp; // 1. 외부 접근 차단

    // 2. 검증 로직을 통과해야만 값 변경 허용
    public void takeDamage(int damage) {
        if (damage < 0) return; // 유효성 검사
        this.hp -= damage;
    }
}

이제 user.hp = -500과 같은 코드는 컴파일조차 되지 않는다. 객체의 상태는 객체 스스로가 정의한 규칙 안에서만 변화한다.


3. 불변 객체와 빌더 패턴 (Builder Pattern)

데이터 무결성을 극대화하려면, 애초에 수정할 수 없는(Immutable) 상태로 만드는 것이 가장 안전하다. 생성 시점에만 값을 주입하고, Setter를 아예 제공하지 않는 방식이다.

하지만 필드가 많아지면 생성자의 매개변수 순서를 일일이 기억하기 어렵다(Constructor Hell). 이를 해결하기 위해 빌더 패턴을 사용한다.

Builder를 통한 직관적인 생성

// 생성: 메서드 체이닝을 통해 가독성 확보
User session = User.builder()
    .id("faker01")
    .server("KR")
    .level(10)
    .build(); // 이 시점에 비로소 완전한 객체가 생성됨

수정 메커니즘: toBuilder()

"불변 객체인데 값을 어떻게 바꾸나요?"라는 질문이 생길 수 있다. 답은 "바꾸지 않고, 새로 만든다"이다. 기존 객체는 그대로 두고, 변경된 부분만 반영하여 새로운 객체를 복제(Copy)해 리턴한다.

// user.setLevel(11); // (X) 불변 객체이므로 Setter 없음

// 기존 정보를 복제하되, 레벨만 11로 변경하여 '새 객체' 생성
User nextSession = session.toBuilder()
    .level(11)
    .build();

// session과 nextSession은 완전히 다른 메모리 주소를 가진 별개의 객체다.

이 방식은 멀티스레드 환경에서도 데이터가 꼬일 걱정 없이 안전하게 공유될 수 있다.


4. 싱글턴: 유일한 관리자 (Singleton)

개별 유저(User)는 수천, 수만 명이 될 수 있지만, 이들을 관리하는 서버(Manager)는 단 하나여야 한다. 만약 관리자가 여러 명이라면, A 관리자에게 로그인했는데 B 관리자는 이를 모르는 사태가 발생한다.

Global State Manager

public class ServerManager {
    // 1. 자신의 인스턴스를 static 변수로 딱 하나만 보유 (Method Area)
    private static ServerManager instance = new ServerManager();

    // 2. 생성자를 private으로 막아 외부 생성을 차단
    private ServerManager() { }

    // 3. 오직 이 메서드를 통해서만 접근 허용
    public static ServerManager getInstance() {
        return instance;
    }
}

5. 실전 시나리오: 로그인부터 레벨업까지

지금까지 배운 싱글턴(관리자), 빌더(생성), 불변 객체(무결성)가 실제 코드에서 어떻게 맞물려 돌아가는지 순서대로 확인해보자.

Step-by-Step Execution

public static void main(String[] args) {
    // 1. 서버 관리자 소환 (Singleton)
    // - "야, 매니저 나와봐." (새로 만드는 게 아님)
    ServerManager server = ServerManager.getInstance();

    // 2. 유저 접속 (Builder)
    // - "신규 유저 'Faker' 입장합니다."
    User gamer = User.builder()
        .id("Faker")
        .level(1)
        .hp(100)
        .build();

    // 3. 게임 플레이 및 레벨업 (Immutability -> toBuilder)
    // - "레벨 1 올랐네? 기존 정보 복사해서 레벨만 2로 바꾼 새 객체 만들어."
    // - gamer.level++; (X) 직접 수정 불가
    User levelUpGamer = gamer.toBuilder()
        .level(2)
        .build();

    // 4. 데이터 저장 (Memory Management)
    // - "매니저야, 이 유저 정보 네 장부(Main Memory)에 적어놔."
    server.saveUser(levelUpGamer);
}

이 흐름을 통해 데이터는 생성(Builder) -> 수정(toBuilder) -> 관리(Singleton)의 파이프라인을 안전하게 통과한다.


6. Deep Dive: 메모리에서의 생존 방식

작성한 코드가 실제 JVM 메모리 위에서 어떻게 움직이는지 이해해야 한다. 재시작 시 데이터가 사라지는 이유도 여기에 있다.

[Method Area] (고정 영역)
- ServerManager class
- static instance (참조 변수) ────┐
                                           │
                                           │ (가리킴)
[Heap Area] (동적 영역)                    │
- new ServerManager() (실체) ◀─────┘
- new User("Faker") (유저 1)
- new User("Chovy") (유저 2)

영속성(Persistence)의 필요성

  • Method AreaHeap은 모두 RAM(휘발성 메모리)에 존재한다.
  • "게임 끄면 데이터 날아가나요?": 맞다. RAM은 전원이 차단되면 데이터가 증발한다.
  • 그래서 서버는 유저 정보를 서버 컴퓨터의 영구 저장소(HDD/SSD)DB(데이터베이스)에 기록해둔다.
  • 재시작 시, 이 저장소에서 데이터를 다시 읽어와(Load) 메모리(Heap)에 띄워줘야 한다. 이를 '영속성'이라고 한다.