ZooKeeper API

주키퍼에서 Java API에 대해서 설명한다.

ZooKeeper는 Java와 C를 위한 공식적인 API를 제공하고 있고, 주키퍼 커뮤니티에서 .NET, python 등 여러 언어의 비공식적인 API를 제공하고 있다.

ZooKeeper API 개요

Znode는 주키퍼 앙상블의 핵심 컴포넌트이고 주키퍼 API는 주키퍼 앙상블과 znode의 세세히 조작할 수 있는 간단한 메소드를 제공한다.

주키퍼 앙상블에 접속하면 주키퍼 앙상블은 클라이언트에게 하나의 Session ID를 발급한다. 주기적으로 서버에 heartbeats를 전송한다. 그렇지 않으면 주키퍼 앙상블은 Session ID를 만료하고 클라이언트는 재연결해야한다. znode Get/Set은 session ID가 존재하는 한 활성화된다. 모든 작업이 완료되었다면 주키퍼 앙상블과의 연결을 종료한다. 클라이언트가 오랫동안 비활성화 되면, 주키퍼 앙상블은 자동적으로 클라이언트와의 연결을 종료한다.

ZooKeeper API 라이브러리 추가

먼저, ZooKeeper API를 사용하기 위해 아래 라이브러리를 추가한다.

dependencies {
    implementation 'org.apache.zookeeper:zookeeper:3.8.1'
}

Java 바인딩

ZooKeeper API의 중점적인 부분은 ZooKeeper 클래스이다. 이는 ZooKeeper 앙상블과의 연결을 위해 클래스의 생성자와 메소드를 통해 옵션들을 제공한다.

  • connect : ZooKeeper 앙상블에 연결
  • create : Znode 생성
  • exists : Znode 존재하는지 여부와 그 정보를 확인
  • getData : 특정 Znode에서 데이터를 가져오기
  • setData : 특정 Znode에서 데이터를 설정
  • getChildren : 특정 Znode에서 사용 가능한 모든 하위 노드 가져오기
  • delete : 특정 znode와 모든 자식을 삭제
  • close : 연결을 끊다

connect : ZooKeeper 앙상블에 연결

ZooKeeper 클래스는 생성자를 통해 접속할 수 있는 기능을 제공한다.

ZooKeeper(String connectionString, int sessionTimeout, Watcher watcher)
  • connectionString : ZooKeeper 앙상블 host
  • sessionTimeout : 세션 타임아웃(milliseconds)
  • watcher : Watcher 인터페이스를 구현하고 있는 하나의 객체. ZooKeeper 앙상블은 watcher 객체를 통해 접속 상태를 반환한다.

새로운 헬퍼 클래스인 ZooKeeperConnection을 생성하고 connect 메서드를 추가해보자. connect 메서드는 하나의 ZooKeeper 객체를 생성하고 주키퍼 앙상블로 연결하고 나서 객체를 반환한다.

여기에서는 CountDownLatch는 클라이언트가 연결될 때 까지 메인 프로세스를 대기하는데 await 호출한다.

주키퍼 앙상블은 Watcher callback을 통해 커넥션 상태를 받는다. Watcher callback은 주키퍼 앙상블로 클라이언트가 연결 될 때 한번 호출될 것이고 Watcher callback은 메인 프로세스의 await lock을 해제하기 위해 CountDownLatchcountDown 메서드를 호출한다.

package com.devkuma.zookeeper;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class ZooKeeperConnection {

    private ZooKeeper zoo;
    final CountDownLatch connectedSignal = new CountDownLatch(1);

    public ZooKeeper connect(String host) throws IOException, InterruptedException {
        zoo = new ZooKeeper(host, 5000, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getState() == Event.KeeperState.SyncConnected) {
                    connectedSignal.countDown();
                }
            }
        });

        connectedSignal.await();
        return zoo;
    }

    public void close() throws InterruptedException {
        zoo.close();
    }
}

create : Znode 생성

ZooKeeper 클래스는 주키퍼 앙상블에 새로운 znode를 생성하기 위한 create 메서드를 제공한다.

create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
  • path : Znode의 경로.
    • 예를 들어, /myapp1,/myapp2, /myapp1/mydata1, myapp2/mydata1/myanothersubdata
  • data : 특정 znode 경로에 저장된 데이터
  • acl : 생성된 노드의 접근 제어 목록. ZooKeeper API는 기본 acl 리스트를 얻기 위한 ZooDefs.Ids static 인터페이스를 제공한다.
    • 예를 들어, ZooDefs.Ids.OPEN_ACL_UNSAFE는 열려있는 znode를 위한 acl의 리스트를 반환한다.
  • createMode : node의 타입이다. ephemeral이 될 수도 있고 sequential이 될 수도 있고 둘 다가 될 수도 있다. 이는 하나의 enum 값이다.

create 메서드의 기능을 확인해보기 위해 새로운 자바 어플리케이션 생성해 보겠다. 메인 메서드에서는 ZooKeeperConnection 타입의 객체 하나를 생성하고 주키퍼 앙상블로 connect 메서드를 통해 접속한다.
connect 메서드는 ZooKeeper 객체인 zk를 반환할 것이다. custom path와 데이터와 함께 zk 객체의 create 메서드를 호출한다.

package com.devkuma.zookeeper;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;

public class ZKCreate {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static void create(String path, byte[] data) throws KeeperException, InterruptedException {
        zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";

        byte[] data = "My first zookeeper app".getBytes();

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            create(path, data);
            conn.close();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

exists : Znode의 존재 확인

ZooKeeper 클래스는 Znode의 존재 여부를 체크하는 exists 메서드를 제공한다. 찾으려는 znode가 존재하면 해당 znode의 메타데이터를 반환한다.

exists(String path, boolean watcher)
  • path : Znode 경로
  • watcher : 지정된 znode를 감시할지 여부를 지정하는 부울 값
package com.devkuma.zookeeper;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class ZKExist {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static Stat znode_exists(String path) throws KeeperException, InterruptedException {
        return zk.exists(path, true);
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            Stat stat = znode_exists(path);

            if (stat != null) {
                System.out.println("Node exists and the node version is " + stat.getVersion());
            } else {
                System.out.println("Node does not exists.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

getData : Znode 데이터 조회

ZooKeeper 클래스는 지정된 znode 내에 저장된 데이터와 status 정보를 얻기 위한 getData 메서드를 제공한다.

getData(String path, Watcher watcher, Stat stat)
  • path : znode의 경로.
  • watcher : Watcher 타입의 콜백 함수. 주키퍼 앙상블은 지정된 znode의 데이터가 변경될 때 이 Watcher 콜백을 통해 알리게 된다.
  • stat : znode의 메타데이터를 반환한다.
package com.devkuma.zookeeper;

import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class ZKGetData {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static Stat znode_exists(String path) throws KeeperException, InterruptedException {
        return zk.exists(path, true);
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";
        final CountDownLatch connectedSignal = new CountDownLatch(1);

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            Stat stat = znode_exists(path);

            if (stat != null) {
                byte[] b = zk.getData(path, event -> {
                    if (event.getType() == Watcher.Event.EventType.None) {
                        switch (event.getState()) {
                            case Expired:
                                connectedSignal.countDown();
                                break;
                        }
                    } else {
                        String path1 = "/MyFirstZnode";

                        try {
                            byte[] bn = zk.getData(path1, false, null);
                            String data = new String(bn, "UTF-8");
                            System.out.println(data);
                            connectedSignal.countDown();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }, null);

                String data = new String(b, "UTF-8");
                System.out.println(data);

                connectedSignal.await();
            } else {
                System.out.println("Node does not exists.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

어플리케이션을 실행하면 현재 셋팅된 data가 콘솔창에 출력되고 await 상태로 대기하고 있다. Watcher에 대한 콜백 함수도 지정해 놓았으므로 zkCli를 사용하여 데이터를 set해보면 Watcher 콜백함수가 실행될 것이다.

bin/zkCli.sh
>>> set /MyFirstZnode Hello

그러면 콘솔창에 콜백함수가 호출되어 Hello가 출력되고, countDown 메서드를 통해 await 상태의 lock을 해제하므로 어플리케이션이 종료된다.

Hello

setData : Znode 데이터 설정

ZooKeeper 클래스는 지정한 znode에 data를 변경하여 추가하기 위한 setData 메서드를 제공한다.

setData(String path, byte[] data, int version)
  • path : znode의 경로.
  • data : 지정된 znode 경로에 저장된 데이터.
  • version : znode의 현재 버전. ZooKeeper는 data에 변화가 있으면 znode의 버전 번호를 갱신한다.
package com.devkuma.zookeeper;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class ZKSetData {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static void update(String path, byte[] data) throws KeeperException, InterruptedException {
        zk.setData(path, data, zk.exists(path, true).getVersion());
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";
        byte[] data = "Success".getBytes();

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            update(path, data);
            System.out.println("Done.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

애플리케이션이 실행되면 지정된 znode의 데이터가 변경되며 ZooKeeper CLI인 zkCli.sh를 사용하여 확인할 수 있다.

cd /path/to/zookeeper
bin/zkCli.sh
>>> get /MyFirstZnode

getChildren : 하위 노드 데이터 설정

ZooKeeper 클래스는 특정 znode의 모든 하위 노드들을 가져오는 getChildren 메서드를 제공한다.

getChildren(String path, Watcher watcher)
  • path : znode의 경로.
  • watcher : Watcher 유혀의 콜백 함수. ZooKeeper 앙상블은 지정된 znode의 데이터가 삭제되거나 znode 아래의 자식이 생성/삭제이 변경될 때 이 Watcher 콜백을 통해 알리게 된다.
package com.devkuma.zookeeper;

import java.util.List;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

public class ZKGetChildren {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static Stat znode_exists(String path) throws KeeperException, InterruptedException {
        return zk.exists(path, true);
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            Stat stat = znode_exists(path);

            if (stat != null) {
                List<String> children = zk.getChildren(path, false);
                children.forEach(System.out::println);
            } else {
                System.out.println("Node does not exists.");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

하위 노드가 존재하지 않는다면, zkCli를 사용하여 추가한다.

bin/zkCli.sh
>>> create /MyFirstZnode/myfirstsubnode Hi
>>> create /MyFirstZnode/mysecondsubnode Hi

delete: Znode 삭제

ZooKeeper 클래스는 지정된 znode를 제거하기 위한 delete 메서드를 제공한다.

delete(String path, int version)
  • path : znode의 경로.
  • version : znode의 현재 버전.
package com.devkuma.zookeeper;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class ZKDelete {
    private static ZooKeeper zk;
    private static ZooKeeperConnection conn;

    public static void delete(String path) throws KeeperException, InterruptedException {
        zk.delete(path, zk.exists(path, true).getVersion());
    }

    public static void main(String[] args) {
        String path = "/MyFirstZnode";

        try {
            conn = new ZooKeeperConnection();
            zk = conn.connect("localhost");
            delete(path);
            System.out.println("Done.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

이 때, 지정된 노드 내에 하위 노드가 존재하면 Exception이 발생한다. 그러므로 하위 노드들을 전부 제거 후 delete를 수행해야 한다.

org.apache.zookeeper.KeeperException$NotEmptyException: KeeperErrorCode = Directory not empty for /MyFirstZnode
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:125)
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
	at org.apache.zookeeper.ZooKeeper.delete(ZooKeeper.java:873)
	at ZKDelete.delete(ZKDelete.java:11)
	at ZKDelete.main(ZKDelete.java:20)

참조




최종 수정 : 2024-01-18