2011년 6월 23일 목요일

Cassandra (3/12)

3. 카산드라 데이터 모델

이 장에서 카산드라의 디자인 목표, 데이터 모델, 그리고 일반적인 동작의 성질들을 알아보겠다.
관계형 세상에서 온 개발자들과 관리자들은 처음에는 카산드라의 데이터 모델은 매우 이해하기 어려울 것이다. Keyspace와 같은 용어는 완전히 새로운 것이고 column 같은 것은 양쪽에서 사용은 하고 있는 말이지만 그 의미는 양쪽에서 다르다. Dynamo나 Bigtable과 같은 것에 대한 문서를 보고 있어도 혼동이 될 수있는데 그것은 카산드라가 그것들에 기반을 두고는 있지만 자신만의 독특한 모델을 가지고 있기 때문이다.
그래서 여기서는 공통된 기반에서 시작하여 익숙하지 않은 용어들로 이동한다. 그리고 카산드라의 세상과 관계형의 세상의 사이 간극을 메우기 위해서 다리를 놓도록 한다.

3.1. 관계형 데이터 모델

관계형 데이터베이스에서 우리는 한 개의 애플리케이션에 응답하는 어떤 컨테이너가 되는 데이터베이스를 가지고 있다. 데이터베이스는 테이블을 가지고 있다. 테이블은 이름과 몇 개의 컬럼을 가지고 있다. 이 또한 이름을 가지고 있다. 우리는 여기 테이블에 데이터를 추가한다. 그리고 각 컬럼에 값을 지정한다. 어떤 칼럼에 값이 없으면 null을 사용한다. 이는 새로운 행을 테이블에 추가한다. 이후에 우리가 이의 primary key 라는 고유의 식별자를 알면 읽어볼 수 있다. 테이블 안의 값을 업데이트하고 싶으면 행 전체를 업데이트하거나 일부분만 할 수도 있다. 이것은 우리가 SQL 문의 where절에 사용하는 filter 에 따라서다.
카산드라를 배우기 위해 관계형 세상에서 알던 것은 잠시 접어두는 것이 유용할 것이다.

3.2. 간단한 소개

이 섹션에서 카산드라의 데이터 모델을 이해하기 위해 바텀 업 방식을 쓴다.
당신이 지금 필요한 가장 간단한 데이터 저장은 array나 list이며 다음과 같이 보인다.

그림3-1. 값들의 list
당신이 추후에 쿼리해볼 수 있는 이 리스트의 값들이지만 당신은 각 값이 무엇을 표현하는지 알기 위해 자세히 보아야하며 외부적으로 문서 같은 것을 통해 어떤 셀이 어떤 값을 가지고 있는지 등을 참조할 수 있다. 이는 옵셔널한 값들이 없을 때 채우지는 않고 null을 사용함으로써 크기는 유지할 수 있다. Array는 매우 유용한 데이터 구조이지만 문법적으로 아주 풍부하지는 않다.
우리는 값과 이름을 맞추기 위해 두번째 차원을 추가한다. 우리는 각 셀에 이름을 추가하고 지도 같은 구조를 가지며 아래 그림과 같다.

그림3-2. 이름/값 쌍의 지도
우리가 각 값의 이름을 알 수 있기 때문에 분명한 발전이다. 만약 사용자 정보를 가진다면 컬럼이름을 first Name, lastName, phone, email 등등 으로 가질 수 있다. 이것은 처리하고 다루기에 약간 더 풍부한 정보이다.
그러나 우리가 여태까지 살펴본 구조는 한 엔티티당 한 인스턴스를 가지고 있을 때만 작동한다. 이것은 마치 한 사람이나 사용자 혹은 호텔이나 트윗과 같다. 이것이 같은 구조에 여러개의 엔티티를 저장하고자 하면 잘 되지 않는다. 이름/값의 짝의 모임을 통일하거나 같은 컬럼의 이름을 되풀이 하는 방법은 없다. 그래서 우리는 컬럼값을 모으고 다른 주소들로 매길수 있는 것이 필요하다. 우리는 다같이 set으로 다루어 질수 있는 컬럼의 그룹을 레퍼런스할 수 있는 key가 필요하다. 우리는 행이 필요하다. 그리고 우리가 한 행을 갖는다면 우리는 모든 이름/값 쌍을 한 엔티티에서 가질수 있고 이름에 대한 값만 가질 수 있다. 우리는 이 이름/값 쌍을 column이라고 부를수 있다. 우리는 각 colum 행을 가지고 각 별개의 엔티티를 row라고 부를수 있다. 그리고 고유한 식별자를 row key라고 부른다.
카산드라는 비슷한 데이터를 관계짓는 논리적인 차원을 컬럼군이라고 정의한다. 예를 들어 User 라는 컬럼군을 가질수 있고 Hotel, Address Book 등등을 가질 수 있다. 이런 식으로 컬럼군은 관계형 세상의 테이블과 비슷하다.
모두 볼 때, 컬럼이라는 카산드라 데이터 구조의 기본을 보았으며 이름/값 쌍이며, 컬럼 군 즉 비슷한 하지만 같지는 않은 컬럼 셋을 가진 것이다.
관계형 데이터베이스에서는 컬럼 이름을 스트링으로만 가질 수 있었다. 하지만 카산드라는 그런 제한 사항이 없다. 행의 키와 컬럼의 이름 다 스트링이 될 수 있다 하지만 long integer나 UUID 등 어떤 형태의 바이트 array가 될 수 있다. 그래서 키 이름을 정하는데 좀 더 다양성을 둘 수 있다.
이것은 카산드라의 컬럼의 흥미로운 특징을 보여준다. 미리 정한 이름/값 쌍처럼 간단할 필요도 없고 값에 뿐 아니라 키에다가 직접 어떤 유용한 값을 저장할 수 있다. 이는 카산드라에서 인덱스를 만들 때 일반적이다. 여기서는 자세히 보지 않는다.
우리는 모든 새로운 엔티티를 저장할 때 마다 모든 컬럼의 값을 모두 저장할 필요는 없다. 아마 주어진 엔티티에 대해서 모든 컬럼의 값을 알지도 못한다. 예를 들어 어떤 사람들은 두번째 전화번호를 가지고 있고 어떤 사람들은 그렇지 않다 그리고 카산드라에 의해 지지되는 온라인 폼에서 어떤 것은 옵셔날이고 어떤 것은 필수이다. 좋다. 공간의 낭비처럼 우리가 모르는 값에 대해 null 값을 채우는 것 대신에 지금 당장은 그 컬럼을 저장하지 않는다. 그래서 여러 차원의 array 구조를 다음과 같이 가지고 있다.

그림3-3. 컬럼 군
이것을 그림으로만 보지 않고 JavaScript Object Notation (JSON) 의 용어로 생각하는 것도 도움이 된다.
Musician: ColumnFamily 1
bootsy: RowKey
email: bootsy@pfunk.com, ColumnName:Value
instrument: bass ColumnName:Value
george: RowKey
email: george@pfunk.com ColumnName:Value
Band: ColumnFamily 2
george: RowKey
pfunk: 1968-2010 ColumnName:Value
우리는 여기 두개의 컬럼군이 있다. Musician과 Band이다. Musician 컬럼은 두개의 행이 있다. “bootsy”와 “George”이다. 이 두개의 행은 관련한 두개의 컬럼이 있다. Bootsy 기록은 두개의 컬럼(email과 instrument)을 가지고 George 컬럼은 한 개의 컬럼을 가지고 있다. 카산드라에서 이것은 좋다. 두번째 컬럼군은 Band이다. 마찬가지로 George 행과 “pfunk”라는 이름의 컬럼을 가지고 있다.
컬럼은 카산드라에서 세번째 측면을 가지고 있다. 타임스탬프, 컬럼이 마지막으로 업데이트된 시간을 기록한다. 이것은 자동적인 메타데이터 프로퍼티가 아니라 클라이언트는 쓰기를 실행할때마다 타임스탬프를 제공해야 한다. 타임스탬프로 퀴리를 할 수는 없지만 서버에서 순수하게 conflict resolution하기 위해 사용된다.
행은 타임스탬프를 가지지 않고, 각 컬럼만 가지고 있다.
만약 우리가 새로운 차원을 추가하는 관련 컬럼끼리의 그룹을 만든다면 어떻겠는가? 카산드라는 super column family 라고 불리는 것을 가지고 우리가 이것을 할 수 있게 한다. Super colomn family 지도의 지도라고 생각할 수 있다. Super column family가 그림 3-4에 있다.

그림3-4. A super column family
컬럼 군에서의 행은 이름/값 쌍의 모음들을 가지고 있고, super column family는 부컬럼을 가지고 있으며, 부컬럼은 컬럼들의 모임이다. 그래서 일반적인 컬럼군에서 값의 주소는 값을 가르키고 있는 컬럼 이름을 가리키고 있는 row key 이다. Super로 분류되는 컬럼군에서 값의 주소는 값을 가리키는 부컬럼을 가리키는 컬럼 이름을 가리키는 row key이다. 조금 다르게 보아서, super column family에서 행은 컬럼을 가지고 있고 각각은 부컬럼을 가지고 있다.
그래서 이것은 카산드라 데이터 모델을 보는 버텀 업 접근 방법이다. 이제 기본적인 이해를 가지고 있는 상태에서 좀 넓게 보아 좀 더 상위 레벨을 보고 탑 다운 접근 방법을 가져보자. 완전히 이해하기 위해서 여러 번 다시 말해야 할만큼 이 주제에 대해서는 여러가지 혼란 사항이 있다.

3.3. 클러스터

카산드라는 한 개의 노드만을 운영한다면 좋은 해결책은 아니다. 이미 언급되었듯이 카산드라 데이터베이스는 사용자에게는 한 개의 기기처럼 보이게 여러 개의 기기를 분산 환경에서 운영할수 있도록 특별히 디자인 되어 있다. 그래서 가장 바깥에서 보이는 카산드라의 구조는 클러스터이다. ring이라고 불리기도 한다. 이는 카산드라가 ring에 어레인지하여 데이터를 노드들에 배분하기 때문이다.
노드는 다른 데이터들의 복사를 가진다. 첫번째 노드가 다운되면 복사본이 대신 쿼리에 대한 대답을 할 수 있다. Peer-to-peer 프로토콜이 사용자에게 투명하게 노드들 상에 복사되도록 하고 replication factor는 클러스터 상에서 같은 데이터의 복사본을 받게 되는 기기의 숫자이다. 6장에서 자세히 살펴볼 것이다.

3.4. Keyspaces
클러스터는 keyspace를 담고 있고 보통은 한 개를 담고 있다. Keyspace는 카산드라에서 가장 바깥쪽의 데이터 저장소이며 관계형 데이터베이스에게 잘 응답한다. 관계형 데이터베이스처럼 keyspace는 여러가지 행동을 정의하는 이름과 어트리뷰트 모음들을 가지고 있다. 애클리케이션당 한 개의 keyspace를 가지는 것이 좋다고 사람들이 조언하지만 실제적인 기반이 있는 사실은 아닌것같다. 실제적으로 받아들일 만한 조언이지만 애플리케이션이 필요로 하는 만큼 keyspace를 만들어도 된다. 하지만 애플리케이션 당 수천개의 keyspace를 만든다면 문제가 생길 것이다.
당신의 시큐리티 제한에 따라 한 클러스터에 여러 개의 keyspace를 돌리는 것이 가능하다. 예를 들어 당신의 애플리케이션이 Twitter라고 불린다면 당신은 Twitter-Cluster라고 불리는 클러스터를 가지고 Twitter라고 불리는 keyspace를 갖게 될 것이다. 내가 아는한은 카산드라에는 그런 것을 위한 naming 컨벤션이 없다.
카산드라에서 keyspace 당 설정할 수 있는 기본 어트리뷰트는
- Replication factor
가장 간단하게 말해서 replication factor는 각 데이터에대해 몇 개의 복사본이 되는지 노드의 개수다. 만약 당신의 replication factor가 3이라면 링안에 3개의 노드가 복사본을 모두 가지고 있다는 것이다.
replication factor 는 더 많은 일관성을 얻기위해 당신이 퍼포먼스에는 어느 정도 희생하는지 결정할 수 있도록 해준다. 그것은 당신의 읽기와 쓰기에서의 일관성은 replication factor에 기반을 둠을 의미한다.
- Replica placement strategy
replica placement 는 링안에서 어떻게 복사본들이 위치될 것인지를 말한다. 카산드라에 어떤 노드가 어떤 key의 복사본을 가질것인지 다른 전략이 있다. 이것들은 SimpleStrategy(예전에는 RackUnawareStrategy로 알려졌다.) OldNetworkTopologyStrategy(예전에는 RackAwareStrategy로 알려졌다.) 그리고 NetworkTopologyStrategy(예전에는 DatacenterShardStrategy로 알려졌다.)가 있다.
- Column families
데이터베이스가 테이블의 저장소인 것 처럼 keyspace는 한 개나 그 이상의 컬럼군의 리스트의 저장소이다. 컬럼 군은 대략 관계형 모델의 테이블과 비슷하다. 그리고 행들의 집합의 저장소이다. 각 행은 정렬된 컬럼을 가지고 있다. 컬럼군은 데이터의 구조를 나타낸다. 각 keyspace는 적어도 한 개 그리고 종종 많은 수의 컬럼군을 가진다.
나는 replication factor와 replica placement strategy 를 언급했는데 왜냐하면 keyspace당 셋이기 때문이다. 그러나 데이터 모델에 당장 직접적인 영향을 가지고 있지는 않다.
애플리케이션당 많은 수의 keyspace를 생성하는 것은 가능은 하지만 일반적으로 추천사항은 아니다. 애플리케이션을 여러 개의 keyspace로 나누는 것은 다른 replication factor나 replica placement strategy 를 어떤 컬럼 군에 원할 때 뿐이다. 예를 들어 만약 당신이 중요도가 낮은 데이터를 가지고 있다면 카산드라가 복사를 위해 열심히 일하지 않도록 이것을 고유한 keyspace에 낮은 replication factor를 가지고 둘 수 있다. 그러나 이것은 많이 복잡할 수도 있다. 아마도 한 개의 keyspace로 시작해서 모든 레벨에 조정이 필요한지 보는 것이 더 나은 아이디어일것이다.

3.5. 컬럼 군
컬럼 군은 행들의 정렬된 집합의 저장소이고 그것은 각각 정렬된 컬럼들의 집합이다. 관계형 세상에서 당신이 물리적으로 모델로부터 데이터베이스를 만들 때 데이터베이스(keyspace)의 이름을 정하고 테이블의 이름을 정하고 (컬럼 군과 테이블은 같지 않다.) 그리고 각 테이블에 있는 컬럼의 이름을 정의한다.
컬럼군이 관계형 테이블과 같다는 생각을 너무 깊게 가지지 말아야 할 이유들이 있다. 첫째로 카산드라는 스키마에 자유롭게 여겨지는데 그것은 컬럼군이 정의되었더라도 컬럼은 아니기 때문이다. 당신은 당신의 필요에 따라서 컬럼군에 컬럼을 언제든 추가 할 수 있다. 둘 째로 컬럼군은 두개의 이름과 비교자라는 애트리뷰트를 가지고 있다. 비교자는 컬럼이 쿼리에서 반환되었을 때 어떻게 소트되는지 long, byte, UTF8 이나 다른 순서에 따라 보여준다.
관계형 데이터베이스에서 테이블이 디스크에 어떻게 저장되는지는 사용자에게 자주 투명하게 보여지며 RDBMS 가 디스크에 테이블을 저장하는지 데이터 모델링은 그에 대한 추천사항을 듣기 어렵다. 이는 컬럼군이 테이블이 것을 기억해야할 또 다른 이유이다. 컬럼 군이 각각 다른 파일로 디스크에 저장되는 때문에 같은 컬럼군에서 저장되는 관련있는 컬럼은 같이 정의하는 것이 중요하다.
컬럼군이 관계형 테이블과 다른 점은 관계형 테이블은 컬럼만을 정의하고 사용자는 행의 값만을 제공한다는 것이다. 하지만 카산드라에서는 테이블은 컬럼을 가지거나 super 컬럼 군으로 정의 될 수 있다. Super 컬럼군을 사용하는 이점은 nesting 할 수 있는 것이다.
디폴트인 표준 컬럼군에서는 표준을 정하고 super 컬럼군에서는 super를 정한다.
당신이 카산드라에서 컬럼군에 데이터를 쓸 때, 한 개나 그 이상의 컬럼에 값을 정해준다. 그 고유한 식별자를 가진 값들의 모임을 행이라고 부른다. 행은 row key라고 불리는 고유한 key를 가지며 그것은 primary key 처럼 작동한다. 그래서 column-oriented 또는 columnar 라고 부르는 것이 틀리지는 않으며 행을 컬럼의 컨테이너로 생각하는 것이 모델을 이해하기 쉬운 방법이다. 이것이 사람들이 카산드라 컬럼 군을 사차원 해쉬와 비슷하다고 하는 이유이기도 하다.
[Keyspace][ColumnFamily][Key][Column]
우리는 아래에 보는바와 같이 Hotel 컬럼군을 나타내기위해 JSON 같은 노테이션을 쓸 수 있다.
Hotel {
key: AZC_043 { name: Cambria Suites Hayden, phone: 480-444-4444,
address: 400 N. Hayden Rd., city: Scottsdale, state: AZ, zip: 85255}
key: AZS_011 { name: Clarion Scottsdale Peak, phone: 480-333-3333,
address: 3000 N. Scottsdale Rd, city: Scottsdale, state: AZ, zip: 85255}
key: CAS_021 { name: W Hotel, phone: 415-222-2222,
address: 181 3rd Street, city: San Francisco, state: CA, zip: 94103}
key: NYN_042 { name: Waldorf Hotel, phone: 212-555-5555,
address: 301 Park Ave, city: New York, state: NY, zip: 10019}
}
이 예에서 행의 key는 hotal의 고유한 primary key이고 컬럼은 이름, 전화번호, 주소, 도시, 주, 우편번호 이다. 행이 우연히 모든 같은 컬럼의 값을 정의해도 당신은 쉽게 4개 컬럼의 한 행을 가질수 있고 같은 컬럼군에서 400컬럼짜리 다른 행을 가질수도 있으며 아무것도 오버랩되지는 않을 것이다.
우리는 CLI를 사용해서 아래처럼 한 컬럼군을 쿼리할 수 있다.
cassandra> get Hotelier.Hotel['NYN_042']
=> (column=zip, value=10019, timestamp=3894166157031651)
=> (column=state, value=NY, timestamp=3894166157031651)
=> (column=phone, value=212-555-5555, timestamp=3894166157031651)
=> (column=name, value=The Waldorf=Astoria, timestamp=3894166157031651)
=> (column=city, value=New York, timestamp=3894166157031651)
=> (column=address, value=301 Park Ave, timestamp=3894166157031651)
Returned 6 results.
이것은 컬럼군에서 6개의 컬럼이 있고 결과는 column-oriented 여서 6개의 결과를 보며 New York에 한 개의 hotel을 가졌음을 보여준다. 그 행에 6개의 컬럼을 가졌지만 다른 행에는 더 적거나 더 많은 컬럼을 가질 수 있다.

3.6. 컬럼 군 옵션
각 컬럼 군을 정의할 수 있는 몇 개 더 추가적인 파라미터가 있다. 이것은 :
- keys_cached
SSTable 당 캐쉬하는 위치의 개수다. 이것은 컬럼의 이름과 값은 참조하지 않고 key의 숫자를 컬럼군당 행의 위치로 참조하며 가장 최근에 사용된 순서로 메모리에 저장된다.
- rows_cached
행의 모든 컨텐츠가 메모리에 캐쉬된다.
- comment
당신의 컬럼 군의 정의에 필요한 중요한 사항을 기억할 수 있도록 해주는 표준 커멘트이다.
- Read_repair_chance
이것은 0과 1사이의 값으로서 특정값을 지정하지 않은채 쿼리를 수행해서 두 개나 더 많은 복사본을 같은 행에서 반환하고 한 개 이상의 복사본이 시간이 너무 지난 것이거나 해서 read repair 동작이 수행이 될 확률이다. 쓰기보다 읽기 동작을 많이 수행한다면 이 값을 낮추고 싶을 수 있다.
- Preload_row_cache
서버 시작시에 행 캐쉬를 미리 이동시킬지 말지 정해준다.
나는 이것들이 데이터 모델에 대한 것이기 보다는 설정과 서버 행동에 대한 것이기 때문에 이것을 최대한 단순화 시키려고 하였다. 6장에서 더 자세히 다룬다.

3.7. 컬럼
컬럼은 카산드라의 데이터 모델에서 가장 기본적인 단위이다. 컬럼은 이름,값,시간의 세 쌍이며 여기서 타임 스탬프를 생각할 수 있다. 다시 한 번 관계형 세계에서 우리는 컬럼이라는 용어에 익숙하지만 카산드라에서는 좀 다르므로 혼돈될 수 있다. 무엇보다 관계형 데이터베이스를 설계할 때 테이블 이름에서 모든 컬럼의 이름을 정해서 테이블의 구조를 정하고 후에 데이터를 쓸때는 단지 미리 정의된 구조에 값을 제공한다.
하지만 카산드라에서는 컬럼을 미리 정의하지 않고 keyspace의 컬럼군을 정의하고 컬럼을 정의하지 않은채 데이터를 아무곳에나 쓸 수 있다. 그것은 카산드라에서 모든 컬럼의 이름은 클라이언트에 의해서 제공되기 때문이다. 이는 당신의 애플리케이션의 데이터 와의 동작에 상당한 유연함을 더하여 시간이 지남에 따라 발전하게 한다.
이름과 값을 위한 데이터 타입은 종종 스트링처럼 제공되는 자바 바이트 array이다. 이름과 값이 바이트 타입이라서 그 길이는 자유롭다. 시간을 위한 데이터 타입은 org.apache.cassandra.db.IClock 이다. 하지만 0.7 버전에서 타임 스탬프는 백워드 컴패터빌리티가 있다. 이 컬럼의 구조는 그림 3-5에 보여진다.

그림3-5. 컬럼의 구조
여기에 당신이 정의할 만한 컬럼의 예제가 구조의 깨끗한 정리를 위해 JSON 노테이션으로 나타내져 있다.
{
"name": "email",
"value: "me@example.com",
"timestamp": 1274654183103300
}
이 예제에서 컬럼은 email 이라고 정해져 있는데 좀 더 정확히 이름의 이라는 애트리뷰트의 값이 email이다. 한 개의 컬럼군은 여러 개의 key를 가지고 있으며 그것은 이 컬럼을 가지고 있을 수도 있는 다른 행을 나타낸다. 이는 관계형 테이블이 모든 행에 같은 셋의 컬럼을 가지고 있다고 생각하기 때문에 관계형 모델에서 나오기 힘든 것이다. 그러나 카산드라에서 컬럼군은 많은 행을 가지고 있고 각각 같거나 다른 셋의 컬럼을 가질 수 있다.
서버 측에서 컬럼은 멀티 스레딩 이슈를 피하기 위해 변화불가능하다. 컬럼은 카산드라에서 org.apache.cassandra.db.IColumn 인터페이스에 의해서 정의되며 super 컬럼의 경우에 그 서브 컬럼의 값을 Collection 으로 가져오고 가장 최근의 변화를 가져오는 등 컬럼의 값을 바이트 array로 가져오는 동작을 포함하여 여러 동작을 가능하게 한다.
관계형 데이터베이스에서 행은 함께 저장된다. 이것은 카산드라의 초기 버전에서는 이렇지 않았다. 0.6버전에서 행은 같은 컬럼군과 함께 디스트에 저장된다.

3.8. 넓은 행, 좁은 행

전통적인 관계형 데이터베이스에서 테이블을 디자인할 때 당신은 보통 엔티티를 다루게 되거나 Hotel, User, Product 등 특정한 명사를 묘사하기 위해 애트리뷰트들을 다루게 된다. 당신의 테이블이 어떤 명사를 나타내는지 결정한 후에는 행의 크기는 조절할 수 없기 때문에 행의 크기 자체는 별로 고려대상이 아니다. 그러나 카산드라를 다룰때는 행의 크기를 결정해야 한다. 행이 가지고 있는 컬럼의 개수에 따라 넓거나 좁을 수도 있다.
넓은 행은 아마 몇 천이나 몇 백만의 컬럼을 가진 행을 의미한다. 보통은 많은 수의 컬럼을 가지는 불과 몇 개의 행이 있을 수 있다. 반대로 관계형 모델과 비슷하게 적은 수의 컬럼을 가진 많은 행을 정의할 수 있는데 이것은 좁은 모델이다.
넓은 행은 보통 자동으로 생성된 이름 (즉, UUID나 타임스탬프) 같은 것을 담고 있고 이것은 어떤 것들의 리스트를 저장하는데 사용된다. 모니터링 애플리케이션을 예로서 고려해보면 당신은 행의 key에 수정된 타임스탬프를 사용하는 시간을 잘게 나눈 것을 행으로 가질 수도 있고 인터벌 사이에 당신의 애플리케이션에 접근하는 IP 주소를 가진 컬럼을 저장할 수도 있다. 시간이 지나면 새로운 행의 key를 만들수 있다.
좁은 행은 좀 더 전통적인 RDBMS의 행과 같아서 각 행은 비슷한 컬럼 이름을 가진 셋이다. RDBMS 행과는 다르지만 모든 컬럼은 근본적으로 옵셔날하다.
넓은 행과 좁은 행의 다른 차이점은 넓은 행만이 컬럼 이름의 소팅 순서에 신경을 쓴다는 것이다. 다음 절에서 보자.

3.9. 컬럼 정렬

컬럼은 그 정의에서 다른 면이 있다. 카산드라에서 결과값이 클라이언트에게 반환 되었을 때 컬럼의 이름이 어떻게 정렬 순서와 비교되는지 정하게 된다. 컬럼은 닫히는 컬럼군에 정의된 대로 “Compare With” 타입대로 정렬된다. :AsciiType, BytesType, LexicalUUIDType, IntegerType, LongType, TimeUUIDType, 이나 UTF8Type이다.
- AsciiType
이것은 입력값이 US-ASCII로 파싱될 수 있다는 것을 검증하며 직접적으로 바이트값을 비교하는 것이다. US_ASCII는 영어 알파벳 순서에 기반한 문자 인코딩 메커니즘이다. 128 문자를 정의하고 94문자는 프린트 할 수 있다.
- BytesType
이것은 디폴트로서 직접적인 바이트값 비교에 의한 정렬이다. 하지만 검증하는 단계는 넘어간다. BytesType은 디폴트인 이유가 있는데 이는 거의 모든 타입의 데이터에 올바른 정렬방식을 제공한다. (UTF-8과 ASCII를 포함한다.)
- LexicalUUIDType
16바이트(128비트) Universally Unique Identifier (UUID) 문자적으로 비교된다.
- LongType
이 정렬은 8 바이트 (64 비트) 긴 숫자형이다.
- IntegerType
0.7 버전에서 소개되었는데 이것은 LongType보다 빠르고 LongType에서 제공되는 64비트의 수보다 더 작거나 더 큰 정수값을 허가한다.
- TimeUUIDType
이것은 16바이트의 타임스탬프값으로 정렬한다. 공통적인 버전을 타임스탬프 UUID로 생성하는 방법은 5가지가 있다. 카산드라가 사용하는 스키마는 버전 첫번째의 UUID로서 컴퓨터의 MAC주소에 기반하고 Gregorian 달력의 시작점에서 100 나노초의 인터벌로 수를 매긴 것이다.
- UTF8Type
UTF-8 을 캐릭터 인코더로 사용한 스트링이다. 이것이 좋은 디폴트 값으로 보일지몰라도 그것은 아마 XML이나 다른 데이터 교환 메커니즘을 사용하는 개발자들은 공동 인코딩을 사용하는데 더 편안함을 느끼기 때문일 것이다. 카산드라에서는 그러나 당신은 UTF8Type을 당신의 데이터가 검증되기 바라면 사용해야 한다.
- Custom
만약 당신이 원한다면 당신의 고유한 컬럼 정렬 방식을 만들수 있다. 카산드라의 많은 것들처럼 pluggable 하다. 당신이 해야 하는 일은 org.apache.cassandra.db.marshal.AbstractType 을 확장하고 클래스 이름을 정하는 것이다.
컬럼 이름은 compare_with 값에 따라 정렬된다. 행은 한 편, partitioner에 의해 정의된 대로 정렬된다 (예를 들면 RandomPartitioner 는 무작위 정렬순서 이다). 6장에서 partitioner를 살펴본다.
카산드라에서는 관계형 데이터베이스에서 했던 것처럼 값으로 정렬할 수는 없다. 이것은 좀 이상한 제한처럼 보이지만 카산드라는 모든 행을 메모리에 넣지 않은 채 많은 행을 각각의 컬럼에 펫칭하지 않도록 각 컬럼의 이름으로 정렬해야 한다. 퍼포먼스는 카산드라의 중요한 장점이어서 읽는 시점에 정렬하는 것은 퍼포먼스에 중요한 안좋은 영향을 미친다는 점은 주목할 만하다.

3.10. Super 컬럼들
Super 컬럼은 컬럼의 한 특별한 종류이다. 양 종류의 컬럼은 이름 값 쌍이고 보통의 일반적인 컬럼은 바이트 array를 저장하며 super 컬럼의 값은 서브컬럼의 지도이어서 바이트 array를 저장한다. 이것은 단지 컬럼의 지도를 저장한다는 것을 기억하고 다른 super컬럼의 지도를 저장하는 super 컬럼을 정의할 수는 없다. 그래서 super 컬럼의 개념은 한 레벨의 깊이만 갈수 있고, 제한없는 수의 컬럼은 가질 수 있다.
기본적인 구조의 super 컬럼은 이름이고 바이트 array이다. (보통 컬럼과 같다). 그 컬럼은 그 key는 컬럼 이름 그리고 값은 컬럼과 같은 지도와 같이 담겨져 있다.

그림3-6. Super 컬럼의 기본적 구조
각 컬럼 군은 각 고유한 파일들로 디스크에 저장이 된다. 그래서 퍼포먼스를 최적화 하기위해서 같이 쿼리할 것 같은 것은 같은 컬럼 군에 두는 것이 중요하고 super 컬럼은 이것을 위해 유용할 수 있다.
SuperColumn 클래스는 IColumn 과 IColumnContainer 를 구현하며 둘 다 org.apache.cassandra.db 패키지에 있다. Thrift API 는 카산드라에 원격 동작을 할 수 있는 기반 RPC 메커니즘이다. Thrift API가 상속 개념이 없어서 가끔은 데이터 구조가 이 타입을 사용할 때 기반이 되는 컬럼 군이 Super 나 Standard 타입인지 아닌지 알기를 예상하며 ColumnOrSupercolumn 으로의 API 참조를 보게된다.
여기서 데이터 모델에서의 좀 더 풍부한 점을 보게된다. 일반적인 컬럼을 사용할 때 앞에서 보았듯이 카산드라는 4차원 해쉬테이블 처럼 보이게 된다. 그러나 super 컬럼에서는 5차원 해쉬같이 된다.
[Keyspace][ColumnFamily][Key][SuperColumn][SubColumn]
Super 컬럼을 사용하기 위해 당신의 컬럼 군을 타입 Super로 정의한다. 그리고 나서 당신은 일반적인 컬럼 군에서 하듯이 행의 key를 가지고 있다. 그러나 Super 컬럼을 참조하고 있고 이는 단순히 일반적인 컬럼들의 지도의 리스트를 가리키는 이름이다. (때때로 서브컬럼이라고 불린다).
여기 PointOfInterest 라고 불리는 super 컬럼 군 정의의 예가 있다. 호텔 관련 도메인에서 “point of interest” 는 여행자들이 방문하고 싶은 공원, 박물관, 동물원 등과 같은 호텔 근처의 위치이다.
PointOfInterest (SCF)
SCkey: Cambria Suites Hayden
{
key: Phoenix Zoo
{
phone: 480-555-9999,
desc: They have animals here.
},
key: Spring Training
{
phone: 623-333-3333,
desc: Fun for baseball fans.
},
}, //end of Cambria row
SCkey: (UTF8) Waldorf=Astoria
{
key: Central Park
desc: Walk around. It's pretty.
},
key: Empire State Building
{
phone: 212-777-7777,
desc: Great view from the 102nd floor.
}
}
}
PointOfInterest라는 super 컬럼 군은 두 개의 super 컬럼을 가지고 있으며 각각은 Cambria Suites Hayden과 Waldorf=Astoria 라는 이름을 가진 다른 호텔이다. 행의 key는 각각 다른 point of interest의 이름으로서 “Phoenix Zoo” 그리고 “Central Park” 같은 것이다. 각 행은 묘사를 하기 위한 컬럼들을 가지고 있으며 (“desc” 컬럼이다.) 어떤 행은 전화번호를 자기고 어떤 것은 그렇지 않다. 같은 구조의 행을 묶는 관계형 테이블 같지 않게, 컬럼군과 super 컬럼군은 단지 비슷한 기록들을 묶는다.
CLI를 사용하여 우리는 super 컬럼 군을 다음과 같이 쿼리할 수 있다.
cassandra> get PointOfInterest['Central Park']['The Waldorf=Astoria']['desc']
=> (column=desc, value=Walk around in the park. It's pretty., timestamp=1281301988847)
이 쿼리는 어쩌다 Super 타입으로 정의된 PointOfInterest 컬럼군에서 행의 key “Central Park”라는 행의 key를 사용하며 묻고 있다. “Waldorf=Astoria” 라는 super 컬럼에서 “desc” 컬럼의 값을 가져다 준다. (이는 point of interest를 묘사하는 단순한 언어 텍스트이다).

3.11. 종합적 Keys
Super 컬럼을 모델링할 때 중요하게 고려해야 할 것이 있다. 카산드라는 서브컬럼을 인덱스 하지 않는다. 그래서 super 컬럼을 메모리로 로드할 때 그 모든 컬럼도 로드된다.
당신은 당신의 고유한 디자인에서 종합적인 key를 사용하여 쿼리를 할 때 도움이 될 수 있다. 종합적인 key는 과 같은 것이다.
이것은 당신이 모델링을 할 때 고려하는 것이 될 수 있으며, 이후에 하드웨어 크기를 정할 때 다시 확인 해야 할 것이다. 그러나 당신의 데이터 모델이 몇 천개의 서브컬럼을 필요로 한다면 당신은 다른 접근 방법을 택해서 super 컬럼을 사용하지 않을수도 있다. 대안으로는 종합적 key를 만드는 것이있다. Super 컬럼으로 컬럼을 나타내는 대신 종합적 key 접근방법은 당신이 보통 컬럼군을 보통 컬럼과 사용하여 custom delimiter를 키 이름에 사용하고 클라인언트 정보검색에 파싱하도록 하는 것이다.
여기 종합적인 key 패턴의 예가 있다. 카산드라 디자인 패턴의 예와 함께 사용되어 나는 Materialized View 라고 부르며 공통 카산드라 디자인 패턴과 함께 Valueless 컬럼이라고 부른다.
HotelByCity (CF) Key: city:state {
key: Phoenix:AZ {AZC_043: -, AZS_011: -}
key: San Francisco:CA {CAS_021: -}
key: New York:NY {NYN_042: -}
}
여기서 세가지 일이 발생한다. 첫째로 우리는 Hotel 이라는 다른 컬럼 군에 이미 호텔 정보를 정의하였다. 그러나 호텔 데이터를 정규화하지 않은 HotelByCity 라는 두번째 컬럼군을 만들수 있다. 우리는 이미 가지고 있는 정보를 되풀이 하게 되는데 그러나 그것을 RDBMS의 view 와 유사하게 저장을 한다. 이는 그것이 빠르고 직접적인 퀴리 쓰기를 하게 해주기 때문이다. 우리가 호텔을 도시로 찾는다는 것을 알았을 때 우리는 그 검색을 위한 행의 key를 정의하는 테이블을 만들 수 있다. 그러나 같은 이름의 도시를 가진 많은 주가 있기 때문에 단지 도시 만으로 행의 key를 이름짓지 않고 주와 함께 해야한다.
그리고 우리는 Valueless Column 이라고 불리는 다른 패턴을 사용한다. 우리가 알아야 할 것은 도시안에 어떤 호텔들이 있는가 이다. 그리고 더 이상 비정규화 할 필요는 없다. 그래서 컬럼의 이름을 값으로 사용한다. 그리고 컬럼은 해당하는 값을 갖지 않는다. 컬럼이 삽입되어지면 우리는 단지 비어있는 바이트 array를 저장한다.

3.12. RDBMS와 카산드라의 차이점을 디자인하기

카산드라의 모델과 RDBMS에서 가능한 쿼리 방법간에는 몇 가지 다른점이 있어서 이를 알아두는 것이 중요하다.

3.12.1. No Query 언어
SQL은 관계형 데이터베이스에서 쓰이는 표준 Query 언어이다. 카산드라는 쿼리 언어가 없다. RPC 직렬화 메커니즘을 통해 접근할 수 있는 Thrift API가 있을 뿐이다.

3.12.2. 비참고용의 진실성
카산드라는 참고용 진실성이라는 개념이 없어서 join이라는 개념도 없다. 관계형 데이터베이스에서 다른 테이블의 기록의 primary key를 테이블안의 외래 key로서 명시할 수 있다. 그러나 카산드라에서는 이런 것을 할 수 없다. 테이블에서 다른 엔티티에 관련된 ID를 저장하는 것이 아직 공통적인 디자인 요구사항이라해도 순차적 삭제가 아직 가능하지 않다.

3.12.3. 두번째 Index

여기서 당신이 호텔 프라퍼티를 위한 고유한 ID를 찾기 원한다고 할 때 두번째 인덱스가 왜 필요한지 살펴보자. 관계형 데이터베이스에서 아래와 같은 쿼리를 사용할 수 있다.
SELECT hotelID FROM Hotel WHERE name = 'Clarion Midtown';
이것은 당신이 찾는 호텔의 이름은 알지만 고유한 ID는 모를 때 사용해야할 쿼리이다. 이러한 퀴리를 전달받았을 때 관계형 데이터베이스는 당신이 찾는 값을 찾기 위해 각 행 열을 뒤지며 전체 테이블 스캔을 할 것이다. 하지만 이것은 당신의 테이블이 매우 크다면 매우 느릴 수 있다. 그래서 관계형 에서 이에대한 해결은 name 컬럼에 인덱스를 만들어 관계형 데이터베이스가 빠르게 찾아볼 수 있는 데이터의 복사본을 만드는 것이다. hotelID가 이미 고유한 primary key이기 때문에 자동으로 인덱스되고 그것이 primary index이다. 이름 컬럼에 또 다른 인덱스를 만드는 것은 두번째 인덱스를 만든다. 카산드라는 현재 이것을 지원하지 않는다.
카산드라에서 같은 것을 수행하기 위해서는 찾는 데이터를 가지고 있는 두번째 컬럼군을 만드는 것이다. 호텔 이름을 저장하기 위해 한 개의 컬럼군을 만들고 각 ID에 매핑한다. 두번째 컬럼군은 분명한 두번째 인덱스처럼 동작할 것이다.
두번째 인덱스를 지원하는 것은 카산드라 7.0에 추가되었다. 이것은 당신이 컬럼 값에 인덱스를 만드는 것을 허용한다. 그래서 만약 당신이 주어진 도시에 사는 사용자를 모두 보고 싶다면 두번째 인덱스 지원은 처음부터 다 하는 일을 하지 않도록 해줄것이다.

3.12.4. 정렬은 디자인 결정사항
RDBMS에서는 당신의 쿼리에서 ORDER BY 를 사용하므로써 기록이 반환되는 순서를 쉽게 바꿀수 있었다. 디폴트 정렬 순서는 설정할수 없다. 디폴트로 기록은 그것들이 쓰여진 순서대로 반환된다. 당신이 그 순서를 바꾸고 싶으면 쿼리를 바꾸어 어느 컬럼의 리스트이던지 정렬할 수 있다. 카산드라에서는 그러나 정렬은 다르게 다루어진다. 그것은 디자인 결정사항이다. 컬럼군 정의는 CompareWith 인자를 포함한다. 그것은 읽기에서 당신의 행을 정렬하는 순서를 말해주지만 쿼리당 설정가능한 것은 아니다.
RDBMS가 당신이 컬럼에 저장된 데이터 타입에 따라 정렬하는 것을 제한하는 반면, 카산드라는 바이트 array만을 저장하여 접근방법가 의미가 없게한다. 하지만 당신이 할 수 있는것은 컬럼이 몇가지 다른 타입(ASCII, Longinteger, TimestampUUID, lexicographically, etc.) 의 하나인것 처럼 정렬하는 것이다. 당신 자신의 플러그인할수 있는 비교자를 써서 정렬을 할수도 있다.
그밖에 SQL에 있는 것처럼 ORDER BY나 GROUP BY 같은 문장의 지원은 없다. SliceRange라고 하는 쿼리 타입이 있어서 4장에서 자세히 보겠지만 반전을 가능하게 한다는 점에서 ORDER BY 와 유사하다.

3.12.5. 비정규화
관계형 데이터베이스 디자인에서 정규화의 중요성을 가르침받는다. 이는 카산드라로 할 때는 데이터 모델이 비정규화 되었을 때 가장 좋은 퍼포먼스를 내므로 별로 장점이 아니다. 관계형 데이터베이스에서 어떤 회사가 비정규화된 데이터 상태로 끝내는 것은 자주 있는 일이다. 여기에는 두가지 공통적인 이유가 있다. 하나는 퍼포먼스이다. 회사들은 많은 join를 일년동안 수행하므로써만 그들이 원하는 퍼포먼스를 얻을 수는 없다. 그래서 알고 있는 쿼리에서 비정규화를 한다. 이것은 작동하게 된다. 하지만 관계형 데이터베이스가 디자인 되었던 것처럼 세밀한 동작은 하지 못한다. 그래서 궁극적으로는 관계형 데이터베이스를 사용하는 것이 이러한 환경에서 가장 좋은 것이 아닐까 질문하게 된다.
관계형 데이터베이스가 의도적으로 비정규화되는 두번째 이유는 비즈니스 문서구조가 기억을 필요로 하기 때문이다. 그것은 당신이 데이터가 시간이 지남에 따라 변할 수 있는 외부의 많은 테이블을 참조하는 둘러싸는 테이블을 가지고 있기 때문이다. 그러나 당신은 둘러싸는 문서들을 역사속의 스냅샷으로 보존할 필요가 있다. 여기서 공통적인 예로는 인보이스가 있다. 당신은 이미 고객과 제품 테이블이 있다. 그래서 그 테이블들을 참조하는 인보이스를 만들수 있다고 생각한다. 그러나 이것은 실제로는 안된다. 고객이나 가격 정보는 바뀔 수 있다. 그리고 당신은 인보이스의 진실성을 잊어버리게 된다.
관계형 세상에서 비정규화는 Codd의 정규화 폼을 위반한다. 그리고 우리는 그것을 피하려고 노력한다. 그러나 카산드라에서는 비정규화는 정상적인 것이다. 만약 데이터 모델이 간단하다면 필요하지도 않다. 그러나 그것을 두려워하지는 말라.
중요한 점은 데이터를 모델링하고 나서 쿼리를 쓰는 대신에 카산드라에서는 당신은 쿼리를 모델하고 데이터가 그 주위에서 정돈되도록 한다. 당신의 애플리케이션이 사용할 가장 공통적인 쿼리 패스를 생각하자. 그리고 그것을 지원하기 위한 컬럼 군을 만들자.
비방하는 사람들은 이것이 문제라고 한다. 그러나 당신이 관계형 도메인에서 그랬던 것처럼 당신의 애플리케이션에서 쿼리에 대해 열심히 생각하는 것은 너무 당연한 일이다. 당신이 잘못해서 양쪽 세계에서 문제를 가질 수 있다. 혹은 당신의 쿼리가 시간이 지남에 따라 바뀌고 당신의 데이터 셋을 업데이트 하기위해 일해야 할 것이다. 하지만 이것은 잘못된 테이블을 정하거나 RDBMS에서 추가적인 테이블을 원하는 것과 아무 다를게 없다.
Cloudkick이 카산드라에서 저장하고 데이터를 모니터링하는 흥미로운 기사를 보려면 http://www.cloudkick.com/blog/2010/mar/02/4_months_with_cassandra 를 보라.

3.13. 디자인 패턴

사람들이 카산드라를 디자인 패턴이라고 묘사되어질 말로 카산드라를 사용하는 공통적인 경우가 몇 가지 있다. 나는 이 공통적 패턴에 이름을 붙이겠다. Materialized View, Valueless Column, and Aggregate Key.

3.13.1. Materialized View

추가적인 쿼리를 나타내는 두번째 인덱스를 만드는 것은 일반적이다. SQL의 WHERE 문이 없기 때문에 당신의 데이터를 두번째 컬럼군에 씀으로써 그 쿼리를 나타내어 같은 효과를 볼 수 있다.
예를 들어 당신이 User 컬럼군을 가지고 있고 어떤 특정한 시에서 사용자를 찾고자 한다면 당신은 UserCity라는 두번째 컬럼군을 만들수 있고 거기에 사용자 데이터와 더불어 시를 key(사용자 이름 대신) 그리고 그 시에사는 사용자이름을 딴 컬럼을 가질 수 있다. 이것은 비정규화 테크닉이며 쿼리의 속도를 향상시키고 특별히 당신의 쿼리 주위에 데이터를 디자인하는 예이다. 이러한 사용법은 카산드라 세계에서는 일반적인 것이다. 당신이 어떤 한 도시의 사용자를 위하여 쿼리를 하려고 할 때, 당신은 User컬럼군을 쿼리하여 클라이언트에 많은 데이터 작업을 하지 않고 단지 UserCity 컬럼군을 쿼리하면 된다.
여기서 “materialized” 는 당신이 쿼리에 대답하기위해 필요한 것이 거기에 있고 원래 데이터를 찾아보지 않게 원래 데이터의 모든 복사본을 저장하는 것을 의미한다. 당신이 사용중인 컬럼 이름을 저장하기 위해 두번째 쿼리를 수행한다면 그것은 두번째 인덱스이다.
0.7 버전에서 카산드라는 두번째 인덱스를 위한 네이티브를 지원한다.

3.13.2. Valueless Column

우리의 User/UserCity 예제에 더 추가해보자. 우리가 User 컬럼군에 참조 데이터를 저장하기 때문에 두가지 일이 일어난다. 첫째, 당신은 고유하고 사려깊은 key를 가져야 할 필요가 있다. 그것은 참조적 진실성을 더하게 한다. 둘째, UserCity 컬럼군안의 컬럼 값을 필요로 하지는 않는다. 만약 당신이 Boise의 행의 key를 가진다면 컬럼 이름은 그 도시의 사용자의 이름이 될 수 있다. 당신의 참조 데이터가 User 컬럼군에 있어서 컬럼은 아무런 의미있는 값을 갖지 않는다. 당신은 단지 조립식의 리스트와 같이 그것을 사용할 뿐이며 하지만 당신은 참조 컬럼군에서 추가적인 데이터를 그 리스트에서 얻기 원할 수도 있다.

3.13.3. Aggregate Key

당신이 Valueless Column 패턴을 사용할 때, Aggregate Key 패턴을 또한 필요로 할 수도 있다. 이 패턴은 두개의 스칼라 값을 모으기 위한 구분자와 함께 함께 융합한다. 우리의 예제를 더 진행하기 위해, 도시 이름은 일반적으로 고유하지 않다. US안의 많은 주가 Springfield라는 도시가 있고, Paris, Texas, Paris, Tenessee가 있다. 그래서 여기서는 우리의 Materialized View 에서 사용될Aggregate Kye를 만들기 위해 주이름과 도시이름을 융합하는 것이 좋다. 이 키는 TX:Paris나 TN:Paris 같은 모양일 것이다. 많은 카산드라 사용자는 colon을 보통 구분자로 사용한다. 그러나 키에서 의미가 없는 다른 문자를 사용해도 된다.
3.14. 명심해야 할 것들
당신이 관계형 정신세계에서 카산드라 데이터 모델로 옮겨오기 위해서 노력할 때는 몇 가지 주의해야 할 것이 있다. 당신이 관계형 데이터베이스와 오랜 기간 일했다면 그리 쉬운 일은 아니다. 여기 몇 가지 포인트가 있다.
 당신의 쿼리를 시작하라. 당신의 애플리케이션이 어떤 쿼리를 필요로 하는지 묻고 당신이 관계형 세상에서 그랬던 것 처럼 데이터 모델링을 처음으로 하지 말고 그 주위의 데이터를 먼저 모델하라. 이것은 어떤 사람들에게는 충격적이다. 어떤 스마트한 사람들은 새로운 쿼리를 할 때 이 접근 방법은 문제를 일으키기도 한다고 말했다. 나의 대답은 그들이 왜 그들의 데이터 타입이 그들의 쿼리보다 더 고정되어 있다고 가정하는가 질문하는 것이다.
 당신은 매 쿼리에 타임스탬프를 제공해야 하기 때문에 많은 클라이언트와 싱크를 하는 전략이 필요하다. 이것은 카산드라가 가장 최근에 쓰기한 값을 결정하기 위해 타임스탬프를 사용하는 것이 중요하다. 여기서 Network Time Protocol (NPT) 서버를 사용하는 것이 한 개의 좋은 방법이다. 다시 한 번, 어떤 스마트한 사람들이 내게 물었다. 왜 서버가 시간을 관리하게 하지 않느냐고. 나의 대답은 대칭적인 분산 데이터베이스에서 서버측은 같은 문제를 가지고 있다는 것이다.

댓글 없음:

댓글 쓰기