ZooKeeper Java API를 활용한 데이터 갱신, 노드 확인 및 권한 제어

setData로 노드 데이터 갱신

ZooKeeper Java 클라이언트는 기존 Znode의 내용을 변경할 때 setData 계열 메서드를 사용한다. 동기와 비동기 두 가지 형태가 제공되며, 메서드 시그니처는 다음과 같다.

public Stat setData(String path, byte[] data, int version)

public void setData(String path, byte[] data, int version,
                    AsyncCallback.StatCallback cb, Object ctx)

여기서 version은 낙관적 금을 위한 값이다. CAS(Compare And Swap)와 같은 개념으로, 클라이언트가 갱신을 시도할 때 이전에 조회한 데이터 버전을 전달하면 서버는 현재 버전과 일치하는지 확인한다. 다른 클라이언트가 먼저 데이터를 수정해 버전이 달라졌다면 갱신은 실패하며, 이를 통해 분산 환경에서의 동시 쓰기 충돌을 방지할 수 있다.

버전을 -1로 지정하면 서버는 "가장 최근 버전"을 대상으로 갱신한다. 이는 동시성 제약이 필요 없는 단순한 덮어쓰기 작업에 유용하다.

동기 방식 예제

package zkexample;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;

public class SyncUpdateDemo implements Watcher {
    private static final CountDownLatch latch = new CountDownLatch(1);

    @Override
    public void process(WatchedEvent event) {
        if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
            latch.countDown();
        }
    }

    public static void main(String[] args) throws Exception {
        String node = "/sample-node";
        ZooKeeper client = new ZooKeeper("localhost:2181", 5000, new SyncUpdateDemo());
        latch.await();

        client.create(node, "initial".getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

        Stat stat = client.setData(node, "modified".getBytes(), -1);
        System.out.println("current version: " + stat.getVersion());
    }
}

비동기 방식 예제

package zkexample;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.concurrent.CountDownLatch;

public class AsyncUpdateDemo implements Watcher {
    private static final CountDownLatch latch = new CountDownLatch(1);

    @Override
    public void process(WatchedEvent event) {
        if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
            latch.countDown();
        }
    }

    public static void main(String[] args) throws Exception {
        ZooKeeper client = new ZooKeeper("localhost:2181", 5000, new AsyncUpdateDemo());
        latch.await();

        String node = "/async-node";
        client.create(node, "old".getBytes(),
                ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

        client.setData(node, "new".getBytes(), -1, new AsyncCallback.StatCallback() {
            @Override
            public void processResult(int rc, String path, Object ctx, Stat stat) {
                if (rc == 0) {
                    System.out.println("async update success, version: " + stat.getVersion());
                } else {
                    System.out.println("async update failed, rc: " + rc);
                }
            }
        }, null);
    }
}

exists로 노드 존재 여부 확인

클라이언트는 exists 메서드로 특정 경로에 Znode가 재하는지 확인할 수 있다. 존재하면 Stat 객체를 반환하고, 존재하지 않으면 null을 반환한다. Watcher를 등록하면 해당 노드가 생성·삭제·데이터 변경될 때 이벤트를 받을 수 있다.

package zkexample;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

public class ExistsDemo implements Watcher {
    public static void main(String[] args) throws Exception {
        ZooKeeper client = new ZooKeeper("localhost:2181", 5000, new ExistsDemo());
        String node = "/watch-target";

        Stat stat = client.exists(node, true);
        if (stat == null) {
            System.out.println("node does not exist");
        } else {
            System.out.println("node exists, version: " + stat.getVersion());
        }
    }

    @Override
    public void process(WatchedEvent event) {
        System.out.println("event: " + event.getType() + " on " + event.getPath());
    }
}

addAuthInfo와 ACL을 이용한 권한 제어

여러 애플리케이션이 하나의 ZooKeeper 클러스터를 공유할 때 데이터 침해를 막기 위해 ACL(Access Control List)을 사용한다. ZooKeeper는 world, auth, digest, ip, super 등의 권한 체계를 지원하며, 여기서는 가장 일반적인 digest 방식을 살펴본다.

세션에 인증 정보를 추가하려면 addAuthInfo를 사용한다. 이후 해당 세션에서 수행하는 모든 작업은 이 인증 정보를 함께 전달한다.

인증 세션으로 보호된 노드 생성

package zkauth;

import org.apache.zookeeper.*;

public class AuthCreateDemo {
    public static void main(String[] args) throws Exception {
        ZooKeeper client = new ZooKeeper("localhost:2181", 5000, null);
        client.addAuthInfo("digest", "admin:secret".getBytes());

        client.create("/secure-node", "data".getBytes(),
                ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
    }
}

인증 없는 세션의 접근 제한

package zkauth;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

public class UnauthorizedReadDemo {
    public static void main(String[] args) throws Exception {
        ZooKeeper owner = new ZooKeeper("localhost:2181", 5000, null);
        owner.addAuthInfo("digest", "admin:secret".getBytes());
        owner.create("/secure-node", "data".getBytes(),
                ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);

        ZooKeeper guest = new ZooKeeper("localhost:2181", 5000, null);
        try {
            guest.getData("/secure-node", false, null);
        } catch (KeeperException.NoAuthException e) {
            System.out.println("access denied: " + e.getMessage());
        }
    }
}

delete 권한의 특징

ZooKeeper에서 어떤 노드를 삭제할 수 있으려면 부모 노드에 대한 삭제 권한이 있어야 한다. 따라서 인증이 설정된 노드 아래의 자식 노드를 삭제하려면 해당 인증 정보가 필요하지만, 인증이 설정된 노드 자체는 상위 경로의 권한만 있으면 삭제할 수 있다.

package zkauth;

import org.apache.zookeeper.*;

public class DeleteAuthDemo {
    public static void main(String[] args) throws Exception {
        ZooKeeper owner = new ZooKeeper("localhost:2181", 5000, null);
        owner.addAuthInfo("digest", "admin:secret".getBytes());

        owner.create("/protected", "init".getBytes(),
                ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
        owner.create("/protected/child", "init".getBytes(),
                ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);

        ZooKeeper guest = new ZooKeeper("localhost:2181", 5000, null);
        try {
            guest.delete("/protected/child", -1);
        } catch (KeeperException.NoAuthException e) {
            System.out.println("child deletion requires auth");
        }

        guest.delete("/protected", -1);
        System.out.println("parent node deleted without auth");
    }
}

위 예제에서 /protected/child를 삭제하려면 /protected에 대한 권한이 필요해 실패하지만, /protected 자체를 삭제할 때는 상위 경로인 루트 노드의 권한만으로 제가 가능하다. 이를 실무에서 설계할 때는 주의가 필요하다.

태그: ZooKeeper ZooKeeper-ACL ZooKeeper-Watcher ZooKeeper-API Java-client

7월 29일 03:42에 게시됨