이번 포스팅에서는 Chapter11의 null 대신 Optional 클래스에 대해 진행하도록 하겠습니다.
2. 값이 없는 상황을 어떻게 처리할까?
책에서는 null 관련하여 아래 예제를 말합니다.
public class Person {
private Car car;
public Car getCar() {
return car;
}
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() {
return insurance;
}
}
public class Insurance {
private String name;
public String getName() {
return name;
}
}
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
위의 코드는 Person이 차를 가지고 있지 않는 경우에는 NullPointerException 이 발생하게 됩니다.
1) 보수적인 자세로 NullPointerException 줄이기
위 예제에서 null를 예방하기 위해 if-else 를 사용하게 된다면 아래와 같은 코드가 될 것입니다.
public String getCarInsuranceName(Person person) {
if (person != null) {
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) {
return insurance.getName();
}
}
}
return "Unknown";
}
한눈에 봐도 코드가 난잡하며, 객체의 연관도가 깊을수록 if의 깊이는 증가됩니다.
가끔 깊이가 너무 깊어져 이를 예방하기 위해 아래와 같은 코드가 나올수도 있습니다.
public String getCarInsuranceName(Person person) {
if (person == null) {
return "Unknown";
}
Car car = person.getCar();
if (car == null) {
return "Unknown";
}
Insurance insurance = car.getInsurance();
if (insurance == null) {
return "Unknown";
}
return insurance.getName();
}
이 코드의 경우에는 깊이는 깊어지지 않지만 너무 많은 return 출구가 있어 유지보수가 어렵다는 단점을 가지게 됩니다.
2) null 때문에 발생하는 문제
위와 같이 null로 인해서 발생하는 문제로는 아래와 같습니다.
에러의 근원 : NullPointerException 은 자바에서 가장 흔히 발생하는 에러입니다.
코드를 어지럽힘 : null 체크를 통해 코드가 난잡하고 어지럽게 됩니다.
아무 의미가 없음 : null은 아무 의미도 표현하지 않으며, 이는 값이 없음을 표현하기에 부적합 합니다.
자바 철학에 위배된다 : 자바의 경우, 개발자에게 모든 포인터를 숨겼지만 null은 유일하게 포인터를 숨길 수 없었습니다.
형식 시스템에 구멍을 만든다 : null은 무형식이며 정보를 포함하고 있지 않으므로 모든 참조 형식에 null을 할당 할 수 있고, 이는 결국 위험한 코드를 만들게 됩니다.
반응형
3. Optional 클래스 소개
자바 8 에서는 null 문제를 해결하기 위해 Optional<T> 라는 클래스를 제공합니다.
이는 사실상 단순히 null를 위한 Wrapper 클래스입니다.
아래는 Optional 를 간단히 보여주는 그림입니다.
모든 null 참조를 Optional로 대치하는것은 바람직하지 않습니다.
Optional은 객체에 대해서 null 체크, null 인 경우 대처를 어떻게 할지를 도와주는 역할입니다.
때문에, Optional를 사용한다고 NullPointerException이 나지 않는것은 아닙니다.
4. Optional 적용 패턴
1) Optional 객체 만들기
Optional 도 클래스이기 때문에 객체를 생성해서 사용해야 합니다.
1. 빈 Optional
아래와 같이 빈 Optional 객체를 생성할 수 있습니다.
Optional<Car> optCar = Optional.empty();
2. null이 아닌 값으로 Optional 만들기
아래와 같은 기존에 있는 객체로 Optional 객체를 만들 수도 있습니다.
Optional<Car> optCar = Optional.of(car);
단, 이 경우 인자인 car가 null인 경우 NullPointerException이 발생합니다.
3. null 값으로 Optional 만들기
아래와 같은 방법으로도 Optional 객체를 만들 수 있습니다.
Optional<Car> optCar = Optional.ofNullable(car);
2번의 of 메서드와의 차이점으로는 car가 null인 경우 NullPointerException 가 아닌 빈 Optional을 반환한다는 점입니다.
2) 맵으로 Optional의 값을 추출하고 변환하기
Optional 클래스는 스트림 메서드와 비슷한 map 메서드를 지원합니다.
아래와 같이 map 을 사용할 수 있습니다.
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
만약, Optional이 비어있으면 아무일도 일어나지 않습니다.
3) flatMap으로 Optional 객체 연결
스트림 메서드의 flatMap 비슷한 기능의 메서드도 제공하고 있습니다.
Optional의 flatMap도 스트림 메서드와 동일하게 Optional<Optional<T>> 와 같은 depth 가 생기는 것을 Optional<T> 로 평준화 해주는 메서드입니다.
4) Optional로 자동차의 보험회사 이름 찾기
그럼 이제 배운 Optional를 사용하여 예제를 해결하게 되면 아래와 같은 코드가 나오게 됩니다.
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
public class Car {
private Insurance insurance;
public Optional<Insurance> getInsuranceAsOptional() {
return Optional.ofNullable(insurance);
}
}
public class Insurance {
private String name;
public String getName() {
return name;
}
}
public String getCarInsuranceName(Person person) {
return Optional.of(person)
.flatMap(Person::getCarAsOptional)
.flatMap(Car::getInsuranceAsOptional)
.map(Insurance::getName)
.orElse("other value");
}
Optional 클래스는 필드 형식으로 사용할 것을 가정하지 않아, Serializable 인터페이스를 구현하지 않습니다. 때문에, 위와 같이 get 메서드에만 Optional 를 사용하는것을 권장합니다.
5) Optional 스트림 조작
자바 9 에서는 Optional 의 스트림 처리를 제공하기 위해 Optional에 stream() 메서드를 제공합니다.
Apache Zeppelin이란 Spark를 Web 기반 NoteBook으로 간편히 수행할 수 있게 제공하는 어플리케이션입니다.
ide를 통해 할 수도 있지만 Web 기반으로 어디서든 접근하여 간편히 Spark 로직을 생산할 때 많이 사용하는 도구입니다.
3. 설치 및 CDH와 연동
CDH ( = Cloudera's Distribution for Hadoop )에서 사용하는 CM (= Cloudera Manager)에서는 안타깝게도 Apache Zeppelin 설치를 제공하고 있지 않습니다.
HDP ( = Hortonworks Data Platform ) 에서 사용하는 Ambari 에서는 지원하고 있습니다. Ambari에서 설치 법은 아래 URL에 나와있습니다. https://docs.cloudera.com/HDPDocuments/HDP3/HDP-3.1.5/installing-zeppelin/content/installing_apache_zeppelin.html
때문에, 수동으로 설치하여 CDH와 연동하여 사용하여야 합니다.
1) 다운로드
저는 CDH 중 호스트 한개를 선택하여 설치를 진행하였습니다.
먼저 아래와 같이 zeppelin 을 다운받습니다.
( 현재 포스팅 시점에서는 zeppelin-0.9.0-preview1 가 최신 버전이지만 많이 사용하는 0.8.1 버전으로 진행하도록 하겠습니다. )
이번 포스팅에서는 Hbase가 제공하는 클라이언트 API 중 고급 기능에 대해 알아보겠습니다.
2. 필터
Hbase에서는 Get 말고도 필터를 이용하여 데이터를 조회하여 가져올 수 있습니다.
1) 필터 소개
Hbase 클라이언트는 Filter라는 추상클래스와 여러가지 구현클래스를 제공하고 있습니다.
또한, 개발자는 직접 Filter 추상 클래스를 구현하여 사용할 수 있습니다.
모든 필터는 실제로 서버측에 적용이 되어 수행되어 집니다.
클라이언트에서 적용되는 경우에는 많은 데이터를 가져와 필터링해야 하기 때문에 대규모 환경에서는 적합하지 않습니다.
아래는 필터가 실제로 어떻게 적용되는지 보여줍니다.
클라이언트에서 필터 생성 -> RPC로 직렬화한 필터 서버로 전송 -> 서버에서 역직렬화하여 사용
2) 필터의 계층 구조
Filter 추상클래스가 최상단이며 추상클래스를 상속받아 뼈대를 제공하는 추상클래스로 FilterBase가 있습니다.
Hbase에서 제공하는 구현 클래스들은 FilterBase를 상속하고 있는 형태입니다.
3) 비교 필터
Hbase가 제공하는 필터 중 비교연산을 지원하는 CompareFilter가 있습니다.
생성자 형식은 아래와 같습니다.
CompareFilter(final CompareOperator op, final ByteArrayComparable comparator)
CompareOperator 에는 [LESS, LESS_OR_EQUAL, EQUAL, NOT_EQUAL, GREATER_OR_EQUAL, GREATER, NO_OP] 가 있습니다.
ByteArrayComparable 는 추상 클래스로 compareTo 추상 메서드를 가지고 있습니다.
4) 로우 필터 - RowFilter
로우 필터는 로우 키 기반으로 데이터를 필터링 할 수 있도록 제공하고 있습니다.
아래는 예제 코드입니다.
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Table hTable = connection.getTable(TableName.valueOf("testtable"));
Scan scan = new Scan();
scan.addColumn(Bytes.toBytes("colfam1"), Bytes.toBytes("col-0"));
Filter filter1 = new RowFilter(CompareFilter.CompareOp.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes("row-22")));
scan.setFilter(filter1);
ResultScanner scanner1 = hTable.getScanner(scan);
scanner1.forEach(System.out::println);
scanner1.close();
Filter filter2 = new RowFilter(CompareFilter.CompareOp.EQUAL, new RegexStringComparator(".*-.5"));
scan.setFilter(filter2);
ResultScanner scanner2 = hTable.getScanner(scan);
scanner2.forEach(System.out::println);
scanner2.close();
Filter filter3 = new RowFilter(CompareFilter.CompareOp.EQUAL, new SubstringComparator("-5"));
scan.setFilter(filter3);
ResultScanner scanner3 = hTable.getScanner(scan);
scanner3.forEach(System.out::println);
scanner3.close();
위 예제에서는 아래 필터들을 사용하는 것을 볼 수 있습니다.
filter1 : 지정한 로우키에 대해서 사전편찬식으로 저장되는 로우들을 이용하여 필터
filter2 : 정규표현식을 이용하여 필터
filter3 : 부분 문자열을 이용하여 필터.
5) 패밀리 필터 - FamilyFilter
패밀리 필터의 경우 로우 필터와 동작방식이 비슷하지만 로우 키가 아닌 로우 안의 컬럼패밀리를 대상으로 비교합니다.
아래는 예제 코드입니다.
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Table hTable = connection.getTable(TableName.valueOf("testtable"));
Filter filter1 = new FamilyFilter(CompareFilter.CompareOp.LESS, new BinaryComparator(Bytes.toBytes("colfam3")));
Scan scan = new Scan();
scan.setFilter(filter1);
ResultScanner scanner = hTable.getScanner(scan);
scanner.forEach(System.out::println);
scanner.close();
Get get1 = new Get(Bytes.toBytes("row-5"));
get1.setFilter(filter1);
Result result1 = hTable.get(get1);
System.out.println("Result of get(): " + result1);
Filter filter2 = new FamilyFilter(CompareFilter.CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("colfam3")));
Get get2 = new Get(Bytes.toBytes("row-5"));
get2.addFamily(Bytes.toBytes("colfam1"));
get2.setFilter(filter2);
Result result2 = hTable.get(get2);
System.out.println("Result of get(): " + result2);
위 예제 볼 수 있듯이 Filter는 Get, Scan 두개에 setFilter 메서드를 통해 적용 가능합니다.
또한, 컬럼 패밀리도 사전 편찬식으로 저장되는것을 이용하는것을 볼 수 있습니다.
6) 퀄리파이어 필터 - QualifierFilter
퀄리파이어 필터는 말 그대로 퀄리파이어를 대상으로 비교하는 필터입니다.
아래는 예제 코드입니다.
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Table hTable = connection.getTable(TableName.valueOf("testtable"));
Filter filter = new QualifierFilter(CompareFilter.CompareOp.LESS_OR_EQUAL, new BinaryComparator(Bytes.toBytes("col-2")));
Scan scan = new Scan();
scan.setFilter(filter);
ResultScanner scanner = hTable.getScanner(scan);
scanner.forEach(System.out::println);
scanner.close();
Get get = new Get(Bytes.toBytes("row-5"));
get.setFilter(filter);
Result result = hTable.get(get);
System.out.println("Result of get() : " + result);
7) 값 필터
이번에는 값을 대상으로 비교하는 필터입니다.
아래는 예제입니다.
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Table hTable = connection.getTable(TableName.valueOf("testtable"));
Filter filter = new ValueFilter(CompareFilter.CompareOp.EQUAL, new SubstringComparator(".4"));
Scan scan = new Scan();
scan.setFilter(filter);
ResultScanner scanner = hTable.getScanner(scan);
scanner.forEach(result -> {
Arrays.asList(result.rawCells()).forEach(System.out::println);
});
scanner.close();
Get get = new Get(Bytes.toBytes("row-5"));
get.setFilter(filter);
Result result = hTable.get(get);
Arrays.asList(result.rawCells()).forEach(System.out::println);
8) 의존 컬럼 필터 - DependentColumnFilter
의존 컬럼 필터의 경우 단순 필터링이 아닌 더 복잡한 필터 기능을 제공합니다.
이 필터는 다른 컬럼이 필터링될지 여부를 결정하는 의존 컬럼을 지정합니다.
아래는 의존 컬럼 필터의 생성자 입니다.
DependentColumnFilter(
final byte [] family,
final byte[] qualifier,
final boolean dropDependentColumn,
final CompareOperator op,
final ByteArrayComparable valueComparator
)
9) 단일 컬럼값 필터 - SingleColumnValueFilter
단일 컬럼값 필터는 특정 컬럼과 값에 대해 비교 필터로 사용합니다.
생성자는 아래와 같습니다.
SingleColumnValueFilter(
final byte [] family,
final byte [] qualifier,
final CompareOperator op,
final byte[] value
)
SingleColumnValueFilter(
final byte [] family,
final byte [] qualifier,
final CompareOperator op,
final ByteArrayComparable comparator
)
하지만, 제공하는 필터가 아닌 사용자가 만든 Custom 한 필터가 필요한 경우가 있습니다.
이런경우에는 Filter 혹은 FilterBase 추상 클래스를 상속받아 만들 수 있습니다.
반응형
3. 카운터
Hbase에서는 고급 기능인 카운터 기능도 제공하고 있습니다.
1) 카운터 소개
카운터 기능이 없다면 개발자는 수동으로 아래와 같은 절차를 만들어야 합니다.
로우 lock
값 조회
값 증가 후 write
lock 해제
하지만 이러한 절차는 과도한 경합상황을 야기하며,
lock이 잠긴상태로 어플리케이션이 죽게되면 lock 정책에 따른 타임아웃이 끝나기 전까지는 다른 어플리케이션인 접근할 수 없게 됩니다.
Hbase에서는 클라이언트 측 호출을 통해서 이러한 절차를 원자적으로 처리할 수 있도록 제공하고 있습니다.
더불어, 한 호출로 여러개의 카운터 갱신 기능도 제공하고 있습니다.
아래는 hbase shell 에서 카운터 예제를 수행한 사진입니다.
incr 은 주어진 인자 만큼 카운터의 값을 증가 시킨 후 반환 받습니다.
get_counter는 현재 카운터 값을 반환합니다.
아래는 카운터도 보통의 컬럼 중에 하나라는것을 증명하는 사진입니다.
추가로 incr의 경우 음수를 인자로 주어 카운터를 감소시킬수도 있습니다.
2) 단일 카운터
Hbase 클라이언트는 단일 카운터만을 대상으로 처리할 수 있도록 제공하고 있습니다.
이때, 정확한 카운터 컬럼을 지정해야 합니다.
아래는 HTable 클래스에서 제공하는 메서드입니다.
long incrementColumnValue(byte[] row, byte[] family, byte[] qualifier, long amount)
long incrementColumnValue(byte[] row, byte[] family, byte[] qualifier, long amount, Durability durability)
카운터를 수행할 좌표값(row, family, qualifier) 와 카운터 증감 값을 인자로 받는것을 알 수 있습니다.
단, Durability 인자로 오버로딩이 되어 있습니다.
이 Durability 는 WAL에 반영을 어떻게 할건지에 대한 설정입니다.
Durability 는 enum으로 아래와 같은 값들이 정의되어 있습니다.
public enum Durability {
USE_DEFAULT,
SKIP_WAL,
ASYNC_WAL,
SYNC_WAL,
FSYNC_WAL
}
3) 복수 카운터
카운터를 증가시키는 방법으로는 HTable의 increment 메서드가 있습니다.
아래는 increment 메서드 명세입니다.
Result increment(final Increment increment)
이 메서드를 사용하기 위해서는 Increment 인스턴스를 만들어야 합니다.
이 Increment 인스턴스에는 카운터 컬럼의 좌표값과 적절한 세부 사항을 넣어야 합니다.
아래는 Increment의 생성자입니다.
Increment(byte [] row)
생성자로 로우키를 먼저 필수로 지정한 뒤 범위를 줄이고 싶을때는 아래와 같은 메서드로 가능합니다.
Increment addColumn(byte [] family, byte [] qualifier, long amount)
카운터의 경우 버전은 내부적으로 처리하기 때문에 인자에 timestamp를 받는 부분이 없습니다.
또한, addFamily 메서드도 없는데 그 이유는 카운터는 특정 컬럼이기 때문입니다.
추가로 시간 범위를 지정하여 읽기 연산의 이점을 얻는 메서드도 제공하고 있습니다.
Increment setTimeRange(long minStamp, long maxStamp)
먼저 들어가기에 앞서 DSL은 특정 도메인 모델을 위한 언어이므로 아래와 같은 도메인을 설정하고 시작하겠습니다.
@Getter
@Setter
public class Stock {
private String symbol;
private String merket;
}
@Getter
@Setter
public class Trade {
public enum Type {BUY, SELL};
private Type type;
private Stock stock;
private int quantity;
private double price;
public double getValue() {
return quantity * price;
}
}
public class Order {
@Getter
private String customer;
private List<Trade> trades = new ArrayList<>();
public void addTrade(Trade trade) {
trades.add(trade);
}
public double getValue() {
return trades.stream().mapToDouble(Trade::getValue).sum();
}
}
아래는 위 도메인 모델을 이용하여 BigBank라는 고객이 요청한 두 거래를 포함하는 주문을 만들은 예제입니다.
Order order = new Order();
order.setCustomer("BigBank");
Trade trade1 = new Trade();
trade1.setType(Trade.Type.BUY);
Stock stock1 = new Stock();
stock1.setSymbol("IBM");
stock1.setMerket("NYSE");
trade1.setStock(stock1);
trade1.setPrice(125.00);
trade1.setQuantity(80);
order.addTrade(trade1);
Trade trade2 = new Trade();
trade2.setType(Trade.Type.SELL);
Stock stock2 = new Stock();
stock2.setSymbol("GOOGLE");
stock2.setMerket("NASDAQ");
trade2.setStock(stock2);
trade2.setPrice(375.00);
trade2.setQuantity(50);
order.addTrade(trade2);
위 코드는 한눈에 봐도 장황하며 비개발자가 이해하기는 더욱 불가능한 코드입니다.
직관적으로 도메인 모델을 반영할 수 있는 DSL이 필요한 시점입니다.
1) 메서드 체인
DSL에서 가장 흔한 방식 중 하나가 메서드 체인 방식입니다.
메서드 체인을 사용하면 위에서 도메인 객체를 만들기 위한 장황한 코드는 아래와 같이 바뀔수 있습니다.
Order order = forCustomer("BigBank")
.buy(80)
.stock("IBM")
.on("NYSE")
.at(125.00)
.buy(50)
.stock("GOOGLE")
.on("NASDAQ")
.at(375.00)
.end();
위와 같은 DSL을 제공하기 위해서는 몇개의 빌더 클래스를 만들어야 합니다.
public class MethodChainingOrderBuilder {
public final Order order = new Order();
private MethodChainingOrderBuilder(String customer) {
order.setCustomer(customer);
}
public static MethodChainingOrderBuilder forCustomer(String customer) {
return new MethodChainingOrderBuilder(customer);
}
public TradeBuilder buy(int quantity) {
return new TradeBuilder(this, Trade.Type.BUY, quantity);
}
public TradeBuilder sell(int quantity) {
return new TradeBuilder(this, Trade.Type.SELL, quantity);
}
public MethodChainingOrderBuilder addTrade(Trade trade) {
order.addTrade(trade);
return this;
}
public Order end() {
return order;
}
}
public class TradeBuilder {
private final MethodChainingOrderBuilder builder;
public final Trade trade = new Trade();
public TradeBuilder(MethodChainingOrderBuilder builder, Trade.Type type, int quantity) {
this.builder = builder;
trade.setType(type);
trade.setQuantity(quantity);
}
public StockBuilder stock(String symbol) {
return new StockBuilder(builder, trade, symbol);
}
}
public class StockBuilder {
private final MethodChainingOrderBuilder builder;
private final Trade trade;
private final Stock stock = new Stock();
public StockBuilder(MethodChainingOrderBuilder builder, Trade trade, String symbol) {
this.builder = builder;
this.trade = trade;
stock.setSymbol(symbol);
}
public TradeBuilderWithStock on(String market) {
stock.setMerket(market);
trade.setStock(stock);
return new TradeBuilderWithStock(builder, trade);
}
}
@AllArgsConstructor
public class TradeBuilderWithStock {
private final MethodChainingOrderBuilder builder;
private final Trade trade;
public MethodChainingOrderBuilder at(double price) {
trade.setPrice(price);
return builder.addTrade(trade);
}
}
이러한 빌드 클래스를 이용하여 좀 더 직관적인 도메인 객체를 생성하는 DSL을 만들 수 있게 되었습니다.
하지만, 복잡한 빌드 클래스를 만들어야 한다는 단점이 있습니다.
2) 중첩된 함수 이용
중첩된 함수 DSL 패턴은 다른 함수안에 함수를 이용해 도메인 모델을 만듭니다.
아래는 중첩 함수 DSL를 사용하여 도메인 객체를 만드는 예제 입니다.
Order order = order("BigBank",
buy(80,
stock("IBM", on("NYSE")), at(125.00)),
sell(50,
stock("GOOGLE", on("NASDAQ")), at(375.00))
);
public class NestedFunctionOrderBuilder {
public static Order order(String customer, Trade... trades) {
Order order = new Order();
order.setCustomer(customer);
Stream.of(trades).forEach(order::addTrade);
return order;
}
public static Trade buy(int quantity, Stock stock, double price) {
return buildTrade(quantity, stock, price, Trade.Type.BUY);
}
public static Trade sell(int quantity, Stock stock, double price) {
return buildTrade(quantity, stock, price, Trade.Type.SELL);
}
private static Trade buildTrade(int quantity, Stock stock, double price, Trade.Type buy) {
Trade trade = new Trade();
trade.setQuantity(quantity);
trade.setType(buy);
trade.setStock(stock);
trade.setPrice(price);
return trade;
}
public static double at(double price) {
return price;
}
public static Stock stock(String symbol, String market) {
Stock stock = new Stock();
stock.setSymbol(symbol);
stock.setMerket(market);
return stock;
}
public static String on(String market) {
return market;
}
}
메서드 체인에 비해 함수의 중첩 방식이 도메인 객체 계층 구조에 그대로 반영된다는 것이 장점입니다.
하지만 이러한 DSL은 사용시 괄호가 많아진다는 단점이 있습니다.
3) 람다 표현식을 이용한 함수 시퀀싱
위 중첩함수 DSL을 이번에는 람다를 사용하여 더욱 깔끔하게 만들 수 있습니다.
아래는 예제입니다.
Order order = order( o-> {
o.forCustomer("BigBank");
o.buy( t -> {
t.quantity(80);
t.price(125.00);
t.stock( s -> {
s.symbol("IBM");
s.market("NYSE");
});
});
o.sell(t -> {
t.quantity(50);
t.price(375.00);
t.stock(s -> {
s.symbol("GOOGLE");
s.market("NASDAQ");
});
});
});
public class LambdaOrderBuilder {
private Order order = new Order();
public static Order order(Consumer<LambdaOrderBuilder> consumer) {
LambdaOrderBuilder builder = new LambdaOrderBuilder();
consumer.accept(builder);
return builder.order;
}
public void forCustomer(String customer) {
order.setCustomer(customer);
}
public void buy(Consumer<LambdaTradeBuilder> consumer) {
trade(consumer, Trade.Type.BUY);
}
public void sell(Consumer<LambdaTradeBuilder> consumer) {
trade(consumer, Trade.Type.SELL);
}
private void trade(Consumer<LambdaTradeBuilder> consumer, Trade.Type type) {
LambdaTradeBuilder builder = new LambdaTradeBuilder();
builder.trade.setType(type);
consumer.accept(builder);
order.addTrade(builder.trade);
}
}
public class LambdaTradeBuilder {
public Trade trade = new Trade();
public void quantity(int quantity) {
trade.setQuantity(quantity);
}
public void price(double price) {
trade.setPrice(price);
}
public void stock(Consumer<LambdaStockBuilder> consumer) {
LambdaStockBuilder builder = new LambdaStockBuilder();
consumer.accept(builder);
trade.setStock(builder.stock);
}
}
public class LambdaStockBuilder {
public Stock stock = new Stock();
public void symbol(String symbol) {
stock.setSymbol(symbol);
}
public void market(String market) {
stock.setMerket(market);
}
}
위 방법은 앞서 봤던 2가지 방법의 장점을 모두 가지고 있습니다.
위 3가지 방법은 모두 장단점을 가지고 있으며 선호에 따라 3가지의 방법을 조합하여 사용해도 됩니다.
4) DSL에 메서드 참조 사용하기
DSL을 만들때 메서드 참조를 사용하여 더욱 직관적으로 만들 수 있습니다.
아래는 위 주식 거래 도메인에서 계산을 하는 DSL을 만드는 예제입니다.
double value = new TaxCalculator()
.withTaxRegional()
.withTaxSurcharge()
.calculate(order);
public class TaxCalculator {
private boolean useRegional;
private boolean useGeneral;
private boolean useSurcharge;
public TaxCalculator withTaxRegional() {
useRegional = true;
return this;
}
public TaxCalculator withTaxGeneral() {
useGeneral = true;
return this;
}
public TaxCalculator withTaxSurcharge() {
useSurcharge = true;
return this;
}
public double calculate(Order order) {
return calculate(order, useRegional, useGeneral, useSurcharge);
}
public static double calculate(Order order, boolean useRegional, boolean useGeneral,
boolean useSurcharge) {
double value = order.getValue();
if(useRegional) value = Tax.regional(value);
if(useGeneral) value = Tax.general(value);
if(useSurcharge) value = Tax.surcharge(value);
return value;
}
}
위의 TaxCalculator 클래스는 각 boolean 값을 사용하여 확장성 부분에서 미약합니다.
하지만, 메서드 참조를 통해 리팩터링이 가능합니다.
아래는 메서드 참조를 통해 리팩터링한 코드입니다.
double value = new TaxCalculator()
.with(Tax::regional)
.with(Tax::surcharge)
.calculate(order);
public class TaxCalculator {
public DoubleUnaryOperator taxFunction = d -> d;
public TaxCalculator with(DoubleUnaryOperator f) {
taxFunction = taxFunction.andThen(f);
return this;
}
public double calculate(Order order) {
return taxFunction.applyAsDouble(order.getValue());
}
}
이번 포스팅에서는 Chapter9의리팩터링, 테스팅, 디버깅에 대해 진행하도록 하겠습니다.
2. 가독성과 유연성을 개선하는 리팩터링
람다를 사용한 코드는 동작 파라미터화를 통해 다양한 요구사항에 대응할 수 있습니다.
1) 코드 가독성 개선
코드 가독성이란 "어떤 코드를 다른 사람도 쉽게 이해할 수 있음" 을 의미합니다.
람다를 사용하게되면 이 코드 가독성을 높일 수 있습니다.
2) 익명 클래스를 람다 표현식으로 리팩터링하기
아래는 익명클래스를 람도로 리팩터링한 예제입니다.
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
Runnable r2 = () -> System.out.println("Hello");
한눈에 봐도 코드가 간결해지면서 가독성이 올라간것을 볼 수 있습니다.
하지만 모든 익명 클래스를 람다로 바꿀수 있는것은 아닙니다.
1. 익명 클래스에서 사용한 this, super는 람다와 다릅니다.
익명클래스의 this는 익명클래스 자신을 가리키는 반면,
람다는 this의 경우 람다를 감사는 클래스를 가리킵니다.
2. 익명 클래스는 감싸고 있는 클래스의 변수를 가릴수 있지만 람다는 가릴수 없습니다.
아래는 예제입니다.
int a = 10;
Runnable r1 = new Runnable() {
@Override
public void run() {
int a =2; // 정상 동작
System.out.println(a)
}
};
Runnable r2 = () -> {
int a= 2; // 컴파일 에러
System.out.println(a);
};
3. 익명클래스는 람다 표현식으로 바꿀시 컨텍스트 오버로딩에 따른 모호함이 발생합니다.
이런 경우, 명시적 형변환을 통해 모호함을 제거할 수 있습니다.
아래는 예제입니다.
interface Task {
void execute();
}
public static void doSomething(Task a) {a.execute();};
public static void doSomething(Runnable r) {r.run();};
public static void main(String[] args) {
doSomething((Task)() -> System.out.println("Danger Danger!!"));
}
3) 람다 표현식을 메서드 참조로 리팩터링하기
람다에서 메서드 참조를 통해 더욱 가독성을 높일 수 있습니다.
아래는 예제입니다.
enum CaloricLevel {
DIET, NORMAL, FAT
}
@Getter
@RequiredArgsConstructor
private static class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private final Type type;
enum Type {
MEAT, FISH, OTHER
}
public CaloricLevel getCaloricLevel() {
if (this.getCalories() <= 400) return CaloricLevel.DIET;
else if (this.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
}
}
public static void main(String[] args) {
List<Dish> menu = Arrays.asList(
new Dish("pork", false, 800, Dish.Type.MEAT),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("season fruit", true, 120, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("salmon", false, 450, Dish.Type.FISH)
;
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
menu.stream().collect(groupingBy(Dish::getCaloricLevel));
}
이번 포스팅에서는 Chapter7의병렬 데이터 처리와 성능 에 대해 진행하도록 하겠습니다.
2. 병렬 스트림
java 8 에서는 병렬 처리를 간편하게 제공하고 있습니다.
예로 컬렉션에서 parallelStream를 통해 간편히 병렬 스트림 처리가 가능합니다.
1) 순차 스트림을 병렬 스트림으로 변환하기
순차 스트림에 parallel 메서드를 통해 병렬스트림으로 변경이 가능합니다.
아래는 그 예제입니다.
public long parallelSum(long n) {
return Stream.iterate(1L, i -> i +1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}
parallel 처리 동작방식은 각 쓰레드에게 분할한 청크를 전달하여 병렬로 수행하도록 하는 것입니다.
아래는 그림으로 수행과정을 나타낸 것입니다.
추가로, 병렬에서 순차 스트림으로 다시 바꿀때에는 sequential 메서드를 사용하면 됩니다.
만약, parallel과 sequential 두개를 모두 사용했을 때에는 최종적으로 호출된 메서드로 전체 스트림 파이프라인에 영향이 미치게 됩니다.
병렬스트림으로 사용하는 쓰레드는 ForkJoinPool 을 사용하며 갯수는 Runtime.getRuntime().availableProcessors() 의 반환값으로 결정됩니다. 전역적으로 쓰레드수를 변경하고 싶을때는 아래와 같이 시스템 설정을 해주시면 됩니다. System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");
2) 병렬 스트림 효과적으로 사용하기
병렬 프로그래밍은 항상 유심히 문제점이 없는지 파악한 후 개발해야 합니다.
java에서 제공해주는 병렬 스트림도 마찬가지입니다.
parallel을 사용하는 부분은 올바른 처리가 이루어지는지와 성능은 올라갔는지 테스트를 해야합니다.
java의 스트림은 내부로직이 복잡하기 때문에, 무분별하게 사용하게 되면 성능이 더욱 악화가 될 수 있습니다.
Collectors를 통해 리듀싱 작업을 수행하는 경우에는 toConcurrentMap과 같이 ConcurrentMap 클래스가 리턴타입일때만 병렬로 수행이 됩니다. 결국, 다른 toList와 같은 리턴타입은 병렬로 수행되지 않으므로 parallelStream으로 처리할 경우 성능이 악화 될 수 있습니다.
병렬로 처리해야하는지에 대한 결정은 아래와 같은 부분이 도움이 될 수 있습니다.
확신이 서지 않는다면 직접 성능 측정.
박싱 타입 유의
순차스트림보다 병렬 스트림에서 성능이 떨어지는 연산 고려 - ex) limit과 같이 순서에 의존하는 연산.
스트림 전체 파이프라인 연산 비용 고려
소량 데이터의 경우 병렬 스트림 제외
스트림으로 구성하는 자료구조가 적절한지 고려
최종 연산의 병합 과정 비용 고려
반응형
3. 포크/조인 프레임워크
포크/조인 프레임워크는 자바 7에서 추가되었으며, 자바 8의 병렬스트림에서는 내부적으로 포크/조인 프레임워크로 처리하고 있습니다.
포크/조인 프레임워크는 재귀를 통해 병렬화 작업을 작게 분할한 다음 ForkJoinPool의 작업자 스레드에 분산 할당하는 방식입니다.
1) RecursiveTask 활용
스레드 풀을 이용하려면 RecursiveTask<R>의 서브클래스를 만들어야 합니다.
여기서 R은 제네릭으로 결과타입 또는 결과가 없을때의 타입을 의미합니다.
RecursiveTask를 구현하기 위해서는 추상메서드인 compute메서드를 구현해야합니다.
compute 메서드는 태스크를 서브태스크로 분할하는 로직과 더 이상 분할할 수 없을 때 개별 서브태스크의 결과를 생산할 알고리즘을 정의해야 합니다.
아래는 compute 메서드의 의사코드입니다.
if (태스크가 충분히 작거나 더 이상 분할 할 수 없으면) {
순차적으로 태스크 계산
} else {
태스크를 두 서브태스크로 분할
태스크가 다시 서브태스크로 분할되도록 이 메서드를 재귀적으로 호출함
모든 서브태스크의 연산이 완료될 때까지 기다림
각 서브태스크의 결과를 합침
}
아래는 long[] 의 sum을 구하는 것을 포크/조인 프레임워크를 사용하는 예제입니다.
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
private final long[] numbers;
private final int start;
private final int end;
public static final long THRESHOLD = 10_000;
public ForkJoinSumCalculator(long[] numbers) {
this(numbers, 0, numbers.length);
}
@Override
protected Long compute() {
int length = end - start;
if(length <= THRESHOLD) {
return computeSequentially();
}
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length/2);
leftTask.fork();
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length/2, end);
long rightResult = rightTask.compute();
long leftResult = leftTask.join();
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for(int i = start; i< end; i++) {
sum += numbers[i];
}
return sum;
}
}
위 ForkJoinSumCalculator의 경우에는 배열의 길이가 10,000 보다 큰것은 반으로 자르면서 분할시키고 있습니다.
그리고 분할을 재귀로 계속 수행 후 결과를 모아서 반환하고 있습니다.
재귀를 통해 분할된 작업들은 ForkJoinPool에 넘겨져 병렬로 수행되어 집니다.
위 예제와 같이, 병렬로 수행 시 결과에 영향이 가지 않는 연산에서만 사용해야 합니다.
2) 포크/조인 프레임워크를 제대로 사용하는 방법
아래는 포크/조인 프레임워크를 효과적으로 사용하기 위해 알아야 할 점입니다.
두 서브 태스크가 모두 시작된 다음에 join을 호출해야 합니다.
RecursiveTask 내에서는 ForkJoinPool의 invoke메서드 대신 fork나 compute 메서드를 직접 호출해야 합니다.
서브 태스크에서 fork나 compute를 통해 ForkJoinPool의 일정을 조절할 수 있습니다.
포크/조인 프레임워크를 이용하는 병렬계산은 디버깅하기 어렵습니다.
멀티코어에 포크/조인 프레임워크를 사용하는 것이 순차 처리보다 무조건 빠르지는 않습니다.
3) 작업 훔치기
멀티 쓰레드로 처리를 하다보면 고루 처리량을 할당했더라도, 각 쓰레드마다 완료 시점이 다릅니다.
이 경우, 노는 쓰레드가 발생하게되며 성능이 생각한것만큼 좋아지지 않게됩니다.
이를 위해, 포크/조인 프레임워크는 작업훔치기라는 기법을 사용하고 있습니다.
간단히 말해서, task들을 큐가 아닌 덱에 넣고 노는 쓰레드는 일하는 쓰레드의 덱의 꼬리에서 task가 있다면 훔쳐와 동작하는 것입니다.
그렇기 때문에, task는 적절히 작은 양으로 분배가 되도록 해야합니다.
4. Spliterator 인터페이스
자바 8은 Spliterator라는 새로운 인터페이스를 제공합니다.
Spliterator는 스트림을 분할하여 처리할때 사용하며,
자바 8은 컬렉션 프레임워크에 포함된 모든 자료구조에 사용할 수 있는 디폴트 Spliterator 구현을 제공하고 있습니다.
아래는 Spliterator 인터페이스에서 필수로 구현해야하는 메서드만을 모아놓은 명세입니다.
public interface Spliterator<T> {
boolean tryAdvance(Consumer<? super T> action);
Spliterator<T> trySplit();
long estimateSize();
int characteristics();
}
tryAdvance 는 Spliterator의 요소를 하나씩 순차적으로 소비하면서 탐색해야 할 요소가 남아 있으면 참을 반환합니다.
trySplit 는 Spliterator의 일부 요소를 분할해서 두 번째 Spliterator를 생성하는 메서드입니다.
estimateSize 는 메서드로 탐색해야 할 요소 수 정보를 제공할 수 있습니다.
characteristics 는 Spliterator에서 정의한 int형으로 각 값은 의미하고 있는것이 있습니다. - ex) 16 = Spliterator.ORDERED
1) 분할 과정
Spliterator는 trySplit 메서드를 통해 스트림 요소를 재귀적으로 분할합니다.
아래는 Spliterator를 통해 분할하는 과정의 그림입니다.
이 과정은 characteristics 메서드로 정의하는 Spliterator의 특성에 영향을 받습니다.
2) Spliterator 특성
characteristics 메서드는 Spliterator 자체의 특성 집합을 포함하는 int를 반환합니다.
아래는 Spliterator 특성에 대한 표입니다.
특성
의미
ORDERED
리스트처럼 요소에 정해진 순서가 있으므로 순서에 유의해야 함.
DISTINCT
x, y 두 요소를 방문했을 시 x.equals(y) 는 항상 false를 반환해야 함.
SORTED
탐색된 요소는 미리 정의된 정렬 순서를 따라야 함.
SIZED
크기가 알려진 소스로 Spliterator를 생성했으므로 estimateSize는 정확한 값을 반환함.
NON-NULL
탐색하는 모든 요소는 null이 아님.
IMMUTABLE
이 Spliterator는 불변. 즉 탐색 중간에 추가, 삭제가 불가능함.
CONCURRENT
동기화 없이 Spliterator의 소스를 여러 쓰레드에서 동시에 고칠 수 없음.
SUBSIZED
이 Spliterator 그리고 분할되는 모든 Spliterator는 SIZED 특성을 갖고 있음.
public class PutExample {
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
Put put = new Put(Bytes.toBytes("row1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"), Bytes.toBytes("val2"));
hTable.put(put);
}
}
위의 Configuration은 org.apache.hadoop.conf 패키지에 존재하며 설정 정보를 주입하는 역할입니다.
기존 설정정보는 hbase-site.xml에 기입해야하지만, 동적으로 변경해야 할때는 Configuration를 사용하면 됩니다.
아래는 주키퍼 쿼럼 정보를 동적으로 Configuration를 사용하여 세팅하는 예제입니다.
부여하지 않을 시에는 로우가 저장소에 추가되는 순간에 해당 리전서버의 시각으로 자동 부여가 됩니다.
Hbase의 경우, 셀은 타임스탬프값을 기준으로 정렬되어져 저장됩니다.
2. KeyValue 클래스
코드상에서 KeyValue 인스턴스를 처리해야 하는 경우가 종종 있습니다.
때문에, KeyValue 클래스에 대해 간단히 살펴보겠습니다.
우선, KeyValue 클래스는 특정 셀의 정보를 가지고 있습니다.
특정 셀의 정보는 로우 키, 컬럼패밀리, 컬럼 퀄리파이어, 타임스탬프를 의미합니다.
메서드로는 아래와 같이 있습니다.
// 아래 3개는 KeyValue 인스턴스에 저장되어 있는 전체 바이트 배열에 관한 메서드입니다.
byte[] getBuffer()
int getOffset()
int getLength()
// 아래 2개는 로우키와 데이터가 저장된 워시 좌표 정보의 바이트 배열을 반환하는 메서드입니다.
byte[] getRow()
byte[] getKey()
또한, KeyValue 클래스는 내부적으로 Comparator를 통해 값에 대해서 커스텀하게 정렬을 할 수 있도록 제공합니다.
아래는 KeyValue 클래스에서 제공하는Comparator 종류입니다.
비교 연산자
설명
KeyComparator
KeyValue 2개의 키를 비교합니다. 즉, getKey 메서드를 통해 원시 바이트 배열을 비교합니다.
KVComparator
원시 형태의 KeyComparator를 감싼 형으로서, KeyValue 2개를 비교할 때 사용합니다.
RowComparator
getRow 로 얻은 값으로 비교합니다.
KeyValue 클래스는 type이라는 필드를 가지고 있습니다.
이 type은 KeyValue에 하나의 차원이 더 추가되는것과 같습니다.
type 에 사용가능한 값은 아래와 같습니다.
유형
설명
Put
해당 KeyValue 인스턴스가 일반적인 Put 연산임을 의미합니다.
Delete
해당 KeyValue 인스턴스가 일반적인 Delete 연산임을 의미합니다.
DeleteColumn
Delete와 같지만, 더 광범위하게 전체 컬럼을 삭제한다는 의미입니다.
DeleteFamily
Delete와 같지만, 더 광범위하게 전체 컬럼패밀리 및 그에 속한 모든 컬럼을 삭제한다는 의미입니다.
위와 같이 false로 설정된 후부터는 put 메서드를 호출하더라도 클라이언트측 메모리 버퍼에 저장되고 서버로는 실제 전송이 이루어 지지 않습니다.
서버로 RPC를 날리기 위해서는 아래와 같은 메서드를 호출하면 됩니다.
void flushCommits() throws IOException
쓰기 버퍼의 경우 클라이언트에서 내부적으로 알아서 비워주기 때문에 크게 고려하지 않아도 됩니다.
추가로, 쓰기 버퍼의 크기도 조정이 가능한데 방법은 아래와 같습니다.
long getWriteBufferSize()
long setWriteBufferSize(long writeBufferSize) throws IOException
물론, 명시적으로 버퍼를 비울수도 있습니다.
개발자가 작성한 코드로 인해서 버퍼가 비워지는 경우는 아래와 같습니다.
flushCommits 호출 시
autoFlush가 true의 경우 put 메서드 호출 시
setWriteBufferSize 호출 시
아래는 쓰기 버퍼를 사용한 예제 입니다.
public static void main(String[] args) {
Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "testtable");
System.out.println("Auto flush: " + hTable.isAutoFlush());
hTable.setAutoFlush(false);
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
hTable.put(put1);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val2"));
hTable.put(put2);
Put put3 = new Put(Bytes.toBytes("row3"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val3"));
hTable.put(put3);
Get get = new Get(Bytes.toBytes("row1"));
Result res1 = hTable.get(get);
System.out.println("Result: " + res1);
hTable.flushCommits();
Result res2 = hTable.get(get);
System.out.println("Result: " + res2);
}
위 결과 출력은 아래와 같습니다.
Auto flush: true
Result: keyvalues=None
Result: keyvalues={ro1/colfam1/qual1/123412358/Put/vlen=4}
위에서 설명한것과 같이 버퍼에 쓰기만 하고 서버로 PRC를 하지 않았기 때문에
첫번째 Get에서는 None이 나온것을 볼 수 있습니다.
만약 버퍼에 있는 데이터를 보기 위해서는 아래 메서드를 사용하면 됩니다.
ArrayList<Put> getWriteBuffer()
하지만 이 방법은 멀티쓰레드 환경에서 조심해야 합니다.
이유로는 List에 접근 시 힙 크기를 확인하지 않고 접근하며, 또 버퍼 비우기가 진행되는 도중 다른 쓰레드가 값을 변경할 수 있기 때문입니다.
4. Put 리스트
클라이언트 API는 단일 put이 아닌 List<put>도 처리 가능하도록 제공합니다.
void put(List<Put> puts) throws IOException
위 메서드를 사용하면 List<Put>으로 데이터 적재가 가능합니다.
다만, List에 있는 모든 Put이 성공하지 않을 수 있습니다.
성공하지않은 Put이 있다면 클라이언트는 IOException을 받게됩니다.
하지만, Hbase는 List에 있는 put을 이터레이트돌며 적용하기 때문에 하나가 실패한다고 안에 있는것이 모두 실패하지는 않습니다.
실패한 Put은 쓰기버퍼에 남아있게 되고 다음 flush 작업에서 재수행하게 됩니다.
만약 데이터가 잘못되어 실패되는 케이스라면 계속 버퍼에 남게되어 재수행을 반복하게 될 것입니다.
이를 방지하기 위해서는 수동을 버퍼를 비워줘야 합니다.
아래는 일괄 put을 날린 후 try-catch를 통해 실패가 발생하게 있다면 명시적으로 버퍼를 비우는 예제 코드입니다.
List<Put> puts = new ArrayList<>();
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"), Bytes.toBytes("val1"));
puts.add(put1)
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("BOGUS"), Bytes.toBytes("qual1"), Bytes.toBytes("val2"));
puts.add(put2)
Put put3 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"), Bytes.toBytes("val3"));
puts.add(put3)
Put put4 = new Put(Bytes.toBytes("row2"));
puts.add(put4)
try {
hTable.put(puts);
} catch (Exception e) {
hTable.flushCommits();
}
추가로, 리스트 기반의 입력시에는 Hbase 서버에서 입력 연산의 순서가 보장되지 않습니다.
5. 원자적 확인 후 입력 연산
Hbase에서는 원자적 확인 후 입력이라는 특별한 기능을 제공합니다.
이 기능은 특정한 조건에 만족하는 경우 put 연산을 수행할 수 있도록합니다.
반환값으로는 boolean 값으로 put 연산이 수행되었는지의 여부를 의미합니다.
사용법으로는 아래와 같습니다.
최신 버전에서는 deprecated 되어 있습니다.
boolean checkAndPut(byte[] row, byte[] family, byte[] qualifier, byte[] value, Put put) throws IOException
boolean checkAndPut(final byte [] row, final byte [] family, final byte [] qualifier, final CompareOp compareOp, final byte [] value, final Put put) throws IOException
boolean checkAndPut(final byte [] row, final byte [] family, final byte [] qualifier, final CompareOp compareOp, final byte [] value, final Put put) throws IOException
2) Get 메서드
Hbase 클라이언트는 데이터를 읽어오는 Get 메서드를 제공합니다.
1. 단일 Get
특정 값을 반환받는데 사용하는 메서드입니다.
Result get(Get get) throws IOException
put과 유사하게 get 메서드도 전용 Get 클래스를 인자로 받고 있습니다.
Get 클래스의 생성자 메서드는 아래와 같습니다.
Get(byte[] row)
Get(byte[] row, RowLock rowLock)
아래는 한 로우에대해서 읽는 데이터 범위를 줄이기위해 필요한 보조적인 메서드들입니다.
Get addFamily(byte[] family)
Get addColumn(byte[] family, byte[] qualifier)
Get setTimeRange(long minStamp, long maxStamp) throws IOException
Get setTimeStamp(long timestamp)
Get setMaxVersions()
Get setMaxVersions(int maxVersions) throws IOException
읽어올때는 위에서 소개한 Bytes 헬퍼 클래스를 통해 byte[]을 원하는 데이터 타입으로 변환이 가능합니다.
static String toString(byte[] b)
static boolean toBoolean(byte[] b)
static long toLong(byte[] b)
static float toFloat(byte[] b)
static int toInt(byte[] b)
반응형
2. Result 클래스
get 메서드의 반환 타입은 Result 클래스입니다.
Result 클래스는 Hbase 데이터인 컬럼패밀리, 퀄리파이어, 타임스탬프, 값 등을 모두 가지고 있으며 내부 메서드로 제공하고 있습니다.
byte[] getValue(byte[] family, byte[] qualifier) // 특정 셀 값
byte[] value() // 사전 편찬식으로 정렬된 KeyValue 중에 첫번째 값
byte[] getRow() // 로우키
int size() // KeyValue 갯수
boolean isEmpty() // KeyValue 존재 여부
KeyValue[] raw() // KeyValue 접근을 위한 메서드
List<KeyValue> list() // raw에서 반환되는 KeyValue 배열을 단순히 list 형식으로 바꿔서 반환하는 메서드
delete()는 인수로 전달했던 List<Delete> 에서 실패한 Delete 객체만이 남게됩니다.
결국, 모두 성공했을때는 인자로 받은 List<Delete>는 텅비게 되어집니다.
3. 원자적 확인 후 삭제 연산
delete에서도 put과 동일하게 원자적으로 확인 후 삭제 연산을 할 수 있도록 제공하고 있습니다.
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final byte[] value, final Delete delete) throws IOException
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final CompareOp compareOp, final byte[] value, final Delete delete) throws IOException
boolean checkAndDelete(final byte[] row, final byte[] family, final byte[] qualifier, final CompareOperator op, final byte[] value, final Delete delete) throws IOException
value 파라미터에 null을 넣어서 수행하게 되면 해당 컬럼 값이 존재하는지 여부를 검사합니다.
4. 일괄처리 연산
지금까지는 단일 또는 리스트 기반의 연산을 알아봤습니다.
이번에는 여러개의 로우에 대해서 다양한 연산을 일괄처리하는 API에 대해서 알아보겠습니다.
클라이언트는 이 일괄처리를 위해 Put, Get, Delete의 조상 클래스인 Row 클래스를 제공합니다.
메서드는 아래와 같습니다.
void batch(final List<? extends Row> actions, final Object[] results)
여러 Row 하위 클래스들을 인자로 받아 처리 후 2번째 인자로 받은 Objects 배열에 결과를 담습니다.