Erlang : ETS Table
- 프로그래밍 언어/erlang
- 2023. 12. 31.
참고
1. ETS가 필요한 이유
ETS(Erlang Term Stroage)가 필요한 이유는 다음과 같다.
- 프로세스가 내부 스택 영역에 데이터를 저장하는 것은 일반적으로 좋은 선택임.
- 프로세스 간에 데이터를 공유해야 할 때는, 프로세스의 스택 영역에 데이터를 저장하는 것으로는 해결할 수 없음.
이런 이유 때문에 모든 프로세스가 전역적으로 접근할 수 있는 것이 필요한데, 이런 기능을 제공해주는 것이 ETS(Erlang Term Storage)다.
1.1 ETS란?
ETS(Erlang Team Storage)에 대해서 간단히 알아보자.
- ETS는 Erlang VM에 포함된 인메모리 DB임.
- 업데이트 허용되고, GC가 접근할 수 없는 VM에 존재함.
- 인메모리 DB이기 때문에 속도가 빠름.
ETS 테이블은 여러 프로세스의 동시 읽기 / 쓰기에 대한 제어가 가능하기 때문에 유용하게 사용할 수 있다.
* 주의
ETS 테이블은 기본적으로 1400개까지 생성할 수 있다. erl - env ERL_MAX_ETS_TABLES 수로 수정할 수 있지만, 많은 ETS 테이블을 사용하지 않도록 제한하기 위해 1400개로 제한한다.
2. ETS의 컨셉
- ETS는 BIF로 구현됨.
- ETS 테이블은 erlang 튜플을 저장함.
- 튜플 중 하나의 요소가 PK로 사용됨. PK를 이용해 ets 테이블에서 데이터를 읽어올 수 있음.
2.1 ETS 테이블 타입
ETS 테이블은 다음 타입을 지원한다.
- set : 각 PK가 유니크한 자료구조로 저장함.
- ordered_set : 각 PK가 유니크한 자료구조. 각 PK에 대해 오름차순으로 정렬된 자료구조
- bag : PK가 같지만, 튜플 자체는 다른 튜플을 여러개 저장할 수 있음 (1,2), (1,3) 같은 것들
- duplicate_bag : 같은 튜플을 여러개 저장할 수 있음.
2.2 ETS 테이블의 제어 프로세스
ETS 테이블은 제어 프로세스를 가진다.
- 특정 프로세스가 ETS 테이블을 시작하면, 해당 프로세스는 ETS 테이블의 소유자가 된다.
- 기본적으로 ETS 테이블 소유자만 테이블에 접근 가능함. 옵션에 따라 public, private하게 쓸 수 있음.
- ETS 제어 프로세스가 죽으면, ETS 테이블도 종료됨.
- ETS 제어 프로세스가 죽는 경우, 소유권을 다른 프로세스에 양도해서 ETS 테이블을 유지할 수도 있음.
3. ETS 옵션
ets:new/2를 이용해 ETS에 테이블을 생성한다. 이 때 옵션을 전달할 수 있는데, 각 옵션을 참고해보면 다음과 같다. ets:new/2 API는 다음과 같이 사용한다.
ets:new(table_name, [OPT1, OPT2, ...]).
3.1 Type
- set
- ordered_set
- bag
- duplicate_bag
생성된 ETS Table이 어떤 타입으로 동작하는지를 정한다.
3.2 Access
- public
- private
public을 선택하면 모든 프로세스가 접근 가능하다. private을 선택하면 특정 프로세스만 접근 가능하다.
3.3 named_table
옵션으로 named_table을 주게 되면, ets_table에 이름으로 접근할 수 있다. 그렇지 않은 경우에는 Ref를 직접 전달해서 접근해야한다. 아래 코드를 참고하자.
1> ets:new(hello1, []).
#Ref<0.151984113.2570190849.95343>
2> ets:new(hello2, [named_table]).
hello2
3.4 {keypos, Position}
ETS 테이블은 튜플을 저장하고, 튜플의 특정 레코드를 PK로 삼는다. 위 옵션은 튜플에서 어떤 요소를 PK로 사용할지를 결정한다.
1> ets:new(hello, [set, named_table, {keypos, 2}]).
hello4
2> ets:insert(hello, {hello, ballo}).
[{hello,ballo}]
% hello table에 hello로 색인된 레코드 없음.
3> ets:lookup(hello, hello).
[]
% hello table에 ballo로 색인된 레코드 있음.
3> ets:lookup(hello, ballo).
[{hello,ballo}]
3.5 {heir, Pid, Data} | {heir, none}
이 옵션은 ETS Table의 부모 프로세스가 죽는 경우 상속 여부를 결정하는 옵션이다.
- 기본적으로 ETS Table의 상속자는 설정되지 않는다.
- 필요한 경우 ets:setopts(Table, {heir, Pid, Data})를 이용해 상속자를 정의하거나 변경할 수 있음.
- 테이블 소유권 양도할 경우 ets:give_away/3를 호출하면 됨.
3.6 {read_concurrency, true | false}
여러 프로세스가 ETS Table을 동시에 읽도록 하고 싶을 때 줄 수 있는 옵션이다.
- 읽기 작업은 많고, 쓰기 작업은 적은 경우이면서 성능 향상이 필요한 경우 사용함.
- 이 옵션을 켰을 때, Read / Write의 인터리빙(교차 실행)이 빈번히 발생할 경우 성능 감소가 수반됨.
3.7 {write_concurrency, true | false}
일반적으로 쓰기 작업을 하면 lock을 걸고 쓰기를 하고, 이를 통해 다른 프로세스가 접근할 수 없게 된다. 이 옵션을 사용하게 되면 동시에 쓰기가 가능하도록 하면서 쓰기 성능이 향상된다.
3.8 compressed
이 옵션을 사용하면 PK를 제외한 대부분의 필드를 압축할 수 있음.
4. ETS Table 사용 예시
1> ets:new(ingredients, [set, named_table]).
ingredients
2> ets:insert(ingredients, {bacon, great}).
true
3> ets:lookup(ingredients, bacon).
[{bacon,great}]
4> ets:insert(ingredients, [{bacon, awesome}, {cabbage, alright}]).
true
5> ets:lookup(ingredients, bacon).
[{bacon,awesome}]
6> ets:lookup(ingredients, cabbage).
[{cabbage,alright}]
7> ets:delete(ingredients, cabbage).
true
8> ets:lookup(ingredients, cabbage).
[]
ETS Table은 이런 식으로 사용할 수 있다.
- Insert: ETS Table에 데이터를 넣음. 튜플 형식만 가능함.
- lookup: ETS Table에서 데이터 조회. 반환 값은 항상 List임. 해당 Key로 여러 데이터가 존재할 수도 있기 때문임.
4.1 ETS Table 또 다른 사용 예시 → ETS Table 순회
14> ets:new(ingredients, [ordered_set, named_table]).
ingredients
15> ets:insert(ingredients, [{ketchup, "not much"}, {mustard, "a lot"}, {cheese, "yes", "goat"}, {patty, "moose"}, {onions, "a lot", "caramelized"}]).
true
16> Res1 = ets:first(ingredients).
cheese
17> Res2 = ets:next(ingredients, Res1).
ketchup
18> Res3 = ets:next(ingredients, Res2).
mustard
19> ets:last(ingredients).
patty
20> ets:prev(ingredients, ets:last(ingredients)).
onions
- ETS Table이 ordered_set으로 되어있어서 저장된 데이터는 PK 값을 기준으로 정렬됨.
- ets:first()를 이용해 가장 첫번째 요소를 찾을 수 있음.
- ets:next()를 이용해 다음 요소를 찾을 수 있음. 기준 값을 줘야함.
- ets:prev()를 이용해 이전 요소를 찾을 수 있음. 기준 값을 줘야함.
- ets:last()를 이용해 마지막 요소를 찾을 수 있음.
21> ets:next(ingredients, ets:last(ingredients)).
'$end_of_table'
22> ets:prev(ingredients, ets:first(ingredients)).
'$end_of_table'
경계 조건에서는 $를 앞에 붙인 $end_of_table을 반환해준다.
5. 패턴 매칭으로 ETS 검색
erlang에서는 패턴 매칭을 이용해 ETS에 저장된 튜플을 검색하는 방법을 제공한다. 패턴 매칭은 다음을 이용해서 할 수 있다.
- $숫자 : 조회된 레코드에서 숫자값이 작은 녀석이 앞에 오도록 처리됨.
- _ : 상관없음
- atom
사용법을 살펴보면 다음과 같다.
% 패턴
% 이것과 유사 : {items, C, A, _, C}
{items, '$3', '$1', '_', '$3'}
% 매칭 결과
{items, 20, 1, 2, 20}
{items, 10, 1, 2, 10}
패턴 매칭을 이용해 ETS에서 검색하기 위해서는 ets:match/2, ets:match_object/2를 이용해야한다.
1> ets:new(table, [named_table, bag]).
table
2> ets:insert(table, [{items, a, b, c, d}, {items, a, b, c, a}, {cat, brown, soft, loveable, selfish}, {friends, [jenn,jeff,etc]}, {items, 1, 2, 3, 1}]).
true
3> ets:match(table, {items, '$1', '$2', '_', '$1'}).
[[a,b],[1,2]]
4> ets:match(table, {items, '$114', '$212', '_', '$6'}).
[[d,a,b],[a,a,b],[1,1,2]]
5> ets:match_object(table, {items, '$1', '$2', '_', '$1'}).
[{items,a,b,c,a},{items,1,2,3,1}]
6> ets:delete(table).
true
- match/2는 필요한 레코드만 반환한다. 예를 들어 '_'로 표현된 값은 필요없기 때문에 조회 결과에 포함되지 않음.
- match/2는 레코드를 반환할 때 $숫자에 적힌 숫자값을 기준으로 컬럼을 정렬해서 반환한다.
- match_object/2는 전체 레코드를 반환한다.
6. 함수 가드 절을 이용한 패턴매칭 조회
ETS는 함수 가드 절을 이용해 패턴매칭으로 레코드를 검색할 수 있다.
[{{'$1','$2',<<1>>,'$3','$4'},
[{'andalso',{'>','$4',150},{'<','$4',500}},
{'orelse',{'==','$2',meat},{'==','$2',dairy}}],
['$1']},
{{'$1','$2',<<1>>,'$3','$4'},
[{'<','$3',4.0},{is_float,'$3'}],
['$1']}]
% 읽는 방식
[{InitialPattern1, Guards1, ReturnedValue1},
{InitialPattern2, Guards2, ReturnedValue2}].
% 고차원
[Clause1,
Clause2]
위 코드가 함수 가드 절을 이용한 패턴매칭 조회 문법이다. 그런데 이 문법은 사실상 읽을 수 없다. 가장 Raw한 내용은 읽을 수 없는데, 좀 더 위에서 보면 각 패턴 매칭을 이용한 Clause를 제공하는 방식이다.
% Guard1
[{'andalso',{'>','$4',150},{'<','$4',500}},
{'orelse',{'==','$2',meat},{'==','$2',dairy}}]
%% == ... when Var4 > 150 andalso Var4 < 500, Var2 == meat orelse Var2 == dairy -> ...
구체적인 예시를 살펴보면 위의 패턴 매칭 문법을 실제 함수 가드절로 바꿔보면 다음과 같다.
% Return Value -> 레코드 첫번째값 반환.
['$1']
% Return Value -> 레코드 전체 반환.
['$_']
마지막으로 ReturnValue를 설정해야한다. 앞서 사용했던 것처럼 $+숫자를 이용해서 반환할 값을 정하는데, $1은 레코드의 첫번째 컬럼을 반환한다.
6.1 함수 가드절 생성
위의 함수 가드절 패턴매칭 구문은 이해하기 어렵다. 이런 패턴매칭 구문을 쉽게 생성해주기 위해서 erlang은 Parse Transform 기능을 제공해준다. Parse Transform 자체는 얼랭 코드를 컴파일할 때 AST에 접근해서 모듈의 코드를 새로운 형식으로 수정할 수 있도록 해준다.
-module(SomeModule).
-include_lib("stdlib/include/ms_transform.hrl").
...
some_function() ->
ets:fun2ms(fun(X) when X > 4 -> X end).
모듈에서는 다음과 같이 사용할 수 있다.
- ets:fun2ms()를 호출 → fun(X)에 있는 가드절과 리턴값을 분석해서 위의 ETS 패턴매칭 형식으로 변환해준다.
1> ets:fun2ms(fun(X) -> X end).
[{'$1',[],['$1']}]
2> ets:fun2ms(fun({X,Y}) -> X+Y end).
[{{'$1','$2'},[],[{'+','$1','$2'}]}]
3> ets:fun2ms(fun({X,Y}) when X < Y -> X+Y end).
[{{'$1','$2'},[{'<','$1','$2'}],[{'+','$1','$2'}]}]
4> ets:fun2ms(fun({X,Y}) when X < Y, X rem 2 == 0 -> X+Y end).
[{{'$1','$2'},
[{'<','$1','$2'},{'==',{'rem','$1',2},0}],
[{'+','$1','$2'}]}]
5> ets:fun2ms(fun({X,Y}) when X < Y, X rem 2 == 0; Y == 0 -> X end).
[{{'$1','$2'},
[{'<','$1','$2'},{'==',{'rem','$1',2},0}],
['$1']},
{{'$1','$2'},[{'==','$2',0}],['$1']}]
얼랭 쉘에서는 다음과 같이 사용할 수 있다.
6.2 이전 코드 적용
[{{'$1','$2',<<1>>,'$3','$4'},
[{'andalso',{'>','$4',150},{'<','$4',500}},
{'orelse',{'==','$2',meat},{'==','$2',dairy}}],
['$1']},
{{'$1','$2',<<1>>,'$3','$4'},
[{'<','$3',4.0},{is_float,'$3'}],
['$1']}]
섹션 초반에 이런 패턴이 있다는 것을 이야기 했다. 이 패턴은 fun2ms() 메서드를 적용해서 읽기 쉽게 만들 수 있다.
1> ets:fun2ms(
fun({Food, Type, <<1>>, Price, Calories})
when Calories > 150 andalso
Calories < 500,
Type == meat orelse
Type == dairy; Price < 4.00,
is_float(Price) ->
Food end).
이 코드를 실행하면 위의 ETS 패턴 매칭에 대한 값을 생성해준다. $1은 Food, $2는 Type인데, 여기서 Food를 반환하기 때문에 ['$1']를 반환값으로 사용하는 것을 볼 수 있다.
6.3 사용 시, 유의사항
- fun()의 파라메터는 단일 파라메터 / 튜플 1개만 올 수 있음.
- 바이너리 내에서 값을 할당할 수 없는 파라메터를 받을 수 없음.
6.4 fun2Ms 사용 실습 → 일치하는 조건 검색
11> rd(food, {name, calories, price, group}).
food
12> ets:new(food, [ordered_set, {keypos,#food.name}, named_table]).
food
13> ets:insert(food, [#food{name=salmon, calories=88, price=4.00, group=meat},
13> #food{name=cereals, calories=178, price=2.79, group=bread},
13> #food{name=milk, calories=150, price=3.23, group=dairy},
13> #food{name=cake, calories=650, price=7.21, group=delicious},
13> #food{name=bacon, calories=800, price=6.32, group=meat},
13> #food{name=sandwich, calories=550, price=5.78, group=whatever}]).
true
- 레코드 food를 컴파일한다.
- 새로운 ETS Table인 food를 생성한다.
- food에 5개의 레코드를 삽입한다.
14> ets:select(food, ets:fun2ms(fun(N = #food{calories=C}) when C < 600 -> N end)).
[#food{name = cereals,calories = 178,price = 2.79,group = bread},
#food{name = milk,calories = 150,price = 3.23,group = dairy},
#food{name = salmon,calories = 88,price = 4.0,group = meat},
#food{name = sandwich,calories = 550,price = 5.78,group = whatever}]
15> ets:select_reverse(food, ets:fun2ms(fun(N = #food{calories=C}) when C < 600 -> N end)).
[#food{name = sandwich,calories = 550,price = 5.78,group = whatever},
#food{name = salmon,calories = 88,price = 4.0,group = meat},
#food{name = milk,calories = 150,price = 3.23,group = dairy},
#food{name = cereals,calories = 178,price = 2.79,group = bread}]
- ets:fun2ms()를 이용해 가드절을 ETS용 패턴매칭으로 수정한다.
- 1에서 만든 값을 이용해 select()를 실행한다.
6.4 fun2Ms 사용 실습 → 일치하는 조건 삭제
17> ets:select_delete(food, ets:fun2ms(fun(#food{price=P}) when P > 5 -> true end)).
3
18> ets:select_reverse(food, ets:fun2ms(fun(N = #food{calories=C}) when C < 600 -> N end)).
[#food{name = salmon,calories = 88,price = 4.0,group = meat},
#food{name = milk,calories = 150,price = 3.23,group = dairy},
#food{name = cereals,calories = 178,price = 2.79,group = bread}]
- 일치하는 조건을 삭제하고 싶다면, 이 때는 반환값으로 true를 반환해야한다.
- 일치하는 조건을 검색할 때는 반환할 컬럼을 선택했었다.
- 이 때는 ets:select_delete()를 이용해서 제거해야한다.
'프로그래밍 언어 > erlang' 카테고리의 다른 글
Erlang 공부 : Distribunomicon 2부 (0) | 2024.01.07 |
---|---|
Erlang 공부 : Distribunomicon (0) | 2024.01.02 |
erlang 공부 : Building OTP Applications (1) | 2023.12.29 |
인텔리제이 erlang 디버그 모드 설정 (0) | 2023.12.29 |
erlang 공부 : Rage Against The Finite-State Machines 2부 (0) | 2023.12.27 |