버티 프레이시(Bertie Prayc)가 우리 서버에서 중요한 데이터를 훔쳐서 비트토렌트(BitTorrent) 시스템을 통해 공유하고 있다. 버티의 가짜 계정을 만들어 그의 평판을 떨어뜨려야 한다.
페드로: 비트토렌트 시스템은 .torrent 파일에 기반하고 있어서 Bencode 인코더를 만들어야 해요.
이브: 네, 그렇다면 먼저 Bencode 포맷에 대해 알아야겠네요.
다음은 Bencode 인코딩 규칙이다.
페드로: 쉬워 보이네요
이브: 그럴 수도 있지만, 값들은 중첩될 수 있다는 점을 고려해야 해요. 예를 들면 리스트 안의 리스트 같은.
페드로: 물론이죠. bencode 인코딩에는 해석자 패턴을 사용할 수 있을 것 같네요.
이브: 한번 해 보시죠.
페드로: 모든 bencode 요소들을 위한 인터페이스부터 시작해 보죠.
interface BencodeElement {
String interpret();
}
페드로: 그리고 나서 각각의 데이터 타입과 데이터 컨데이너에서 위의 인터페이스를 구현합니다.
class IntegerElement implements BencodeElement {
private int value;
public IntegerElement(int value) {
this.value = value;
}
@Override
public String interpret() {
return "i" + value + "e";
}
}
class StringElement implements BencodeElement {
private String value;
StringElement(String value) {
this.value = value;
}
@Override
public String interpret() {
return value.length() + ":" + value;
}
}
class ListElement implements BencodeElement {
private List<? extends BencodeElement> list;
ListElement(List<? extends BencodeElement> list) {
this.list = list;
}
@Override
public String interpret() {
String content = "";
for (BencodeElement e : list) {
content += e.interpret();
}
return "l" + content + "e";
}
}
class DictionaryElement implements BencodeElement {
private Map<StringElement, BencodeElement> map;
DictionaryElement(Map<StringElement, BencodeElement> map) {
this.map = map;
}
@Override
public String interpret() {
String content = "";
for (Map.Entry<StringElement, BencodeElement> kv : map.entrySet()) {
content += kv.getKey().interpret() + kv.getValue().interpret();
}
return "d" + content + "e";
}
}
페드로: 마지막으로 bencode 인코딩 문자열을 만들 수 있어요.
// discredit user
Map<StringElement, BencodeElement> mainStructure = new HashMap<StringElement, BencodeElement>();
// our victim
mainStructure.put(new StringElement("user"), new StringElement("Bertie"));
// just downloads files
mainStructure.put(new StringElement("number_of_downloaded_torrents"), new IntegerElement(623));
// and nothing uploads
mainStructure.put(new StringElement("number_of_uploaded_torrents"), new IntegerElement(0));
// and nothing donates
mainStructure.put(new StringElement("donation_in_dollars"), new IntegerElement(0));
// prefer dirty categories
mainStructure.put(new StringElement("preffered_categories"),
new ListElement(Arrays.asList(
new StringElement("porn"),
new StringElement("murder"),
new StringElement("scala"),
new StringElement("pokemons")
)));
BencodeElement top = new DictionaryElement(mainStructure);
// let's totally discredit him
String bencodedString = top.interpret();
BitTorrent.send(bencodedString);
이브: 재미있네요, 그런데 코드량이 너무 많네요!
페드로: 기능이 많다 보니 읽기 어려워졌어요.
이브: 코드가 곧 데이터(Code is Data)라는 말을 들어 보신 적 있을 거예요. 그래서 클로저에서는 훨씬 수월해지죠.
;; multimethod to handle bencode structure
(defmulti interpret class)
;; implementation of bencode handler for each type
(defmethod interpret java.lang.Long [n]
(str "i" n "e"))
(defmethod interpret java.lang.String [s]
(str (count s) ":" s))
(defmethod interpret clojure.lang.PersistentVector [v]
(str "l"
(apply str (map interpret v))
"e"))
(defmethod interpret clojure.lang.PersistentArrayMap [m]
(str "d"
(apply str (map (fn [[k v]]
(str (interpret k)
(interpret v))) m))
"e"))
;; usage
(interpret {"user" "Bertie"
"number_of_downloaded_torrents" 623
"number_of_uploaded_torrent" 0
"donation_in_dollars" 0
"preffered_categories" ["porn"
"murder"
"scala"
"pokemons"]})
이브: 데이터를 정의하는 것이 얼마나 쉬운지 보이세요?
페드로: 그러네요.interpret은 bencode 자료형 당 단순히 하나의 함수네요. 별도의 클래스가 아니라.
이브: 맞아요, 해석자 패턴은 단순히 트리를 처리하는 함수들일 뿐인거죠.