무엇인가
정해진 풀이 절차가 없는 문제를 위한 구조다. 공용 저장소(칠판)에 지금까지 알아낸 것을 적어 두면, 여러 전문가 모듈이 칠판을 들여다보다가 자기가 기여할 수 있을 때 나서서 새 사실을 적는다. 답이 나올 때까지 반복한다.
파이프-필터가 “순서가 정해진” 처리라면, 블랙보드는 순서를 미리 정할 수 없을 때 쓴다. 누가 언제 쓸모 있을지는 칠판의 현재 내용에 달렸다.
구조
graph TD KS1[지식원: 문자 인식] -->|기여| BB[(칠판)] KS2[지식원: 단어 사전] -->|기여| BB KS3[지식원: 문법 검사] -->|기여| BB BB -->|현재 상태 조회| KS1 BB -->|조회| KS2 BB -->|조회| KS3 CTL{제어: 누가 나설 차례인가} --> BB
C++로 보기
각 지식원은 내가 지금 기여할 수 있나를 스스로 판단한다. 제어부는 기여할 수 있는 것을 계속 돌리다가, 아무도 못 하면 멈춘다.
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
// 칠판: 공용 사실 저장소다
class Blackboard {
std::unordered_map<std::string, std::string> facts_;
public:
bool has(const std::string& k) const { return facts_.count(k) > 0; }
std::string get(const std::string& k) const {
auto it = facts_.find(k);
return it == facts_.end() ? std::string{} : it->second;
}
void put(const std::string& k, const std::string& v) {
facts_[k] = v;
std::cout << " 칠판에 기록: " << k << " = " << v << "\n";
}
};
class KnowledgeSource {
public:
virtual ~KnowledgeSource() = default;
virtual const char* name() const = 0;
virtual bool canContribute(const Blackboard&) const = 0;
virtual void contribute(Blackboard&) = 0;
};
// 생년이 있으면 나이를 계산할 수 있다
class AgeCalculator : public KnowledgeSource {
public:
const char* name() const override { return "나이 계산"; }
bool canContribute(const Blackboard& bb) const override {
return bb.has("birthYear") && !bb.has("age");
}
void contribute(Blackboard& bb) override {
int born = std::stoi(bb.get("birthYear"));
bb.put("age", std::to_string(2026 - born));
}
};
// 나이가 있어야 등급을 정한다 — 위가 먼저 돌아야 한다
class GradeAssigner : public KnowledgeSource {
public:
const char* name() const override { return "등급 판정"; }
bool canContribute(const Blackboard& bb) const override {
return bb.has("age") && !bb.has("grade");
}
void contribute(Blackboard& bb) override {
int age = std::stoi(bb.get("age"));
bb.put("grade", age >= 19 ? "성인" : "미성년");
}
};
// 이름과 등급이 모두 있어야 인사말이 된다
class Greeter : public KnowledgeSource {
public:
const char* name() const override { return "인사말 생성"; }
bool canContribute(const Blackboard& bb) const override {
return bb.has("name") && bb.has("grade") && !bb.has("greeting");
}
void contribute(Blackboard& bb) override {
bb.put("greeting", bb.get("name") + "님(" + bb.get("grade") + ") 환영합니다");
}
};
int main() {
Blackboard bb;
bb.put("name", "수원");
bb.put("birthYear", "1990");
std::vector<std::unique_ptr<KnowledgeSource>> sources;
sources.push_back(std::make_unique<Greeter>()); // 일부러 순서를 섞는다
sources.push_back(std::make_unique<GradeAssigner>());
sources.push_back(std::make_unique<AgeCalculator>());
// 제어: 기여할 수 있는 놈이 없을 때까지 돈다
for (int round = 1; ; ++round) {
bool progressed = false;
std::cout << "-- " << round << "회차\n";
for (auto& ks : sources) {
if (ks->canContribute(bb)) {
std::cout << " " << ks->name() << " 나섬\n";
ks->contribute(bb);
progressed = true;
}
}
if (!progressed) break;
}
std::cout << "결과: " << bb.get("greeting") << "\n";
}지식원을 일부러 뒤섞어 등록했는데도 답이 나온다. 순서를 코드가 정하지 않고 칠판의 상태가 정하기 때문이다. 새 지식원을 넣을 때 어디에 끼울지 고민할 필요가 없는 것이 이 패턴의 값이다.
대신 종료 조건이 늘 문제다. 위 코드는 “아무도 기여 못 하면 종료”인데, 지식원끼리 사실을 계속 고쳐 쓰면 영원히 돌 수 있다.
언제 쓰나
- 정해진 알고리즘이 없는 문제. 음성 인식, 영상 이해, 계획 수립처럼 부분 단서를 모아 가는 일.
- 여러 접근법을 섞어야 하고, 어느 것이 언제 통할지 모를 때.
- 새로운 해법 모듈을 계속 붙여야 할 때.
대가
- 종료와 수렴을 보장하기 어렵다. 위에서 본 그대로다.
- 모든 것이 칠판을 거치므로 결합은 느슨하지만 추적이 어렵다. 누가 무엇을 왜 적었는지 안 남는다.
- 제어 전략이 성능을 좌우한다. 매번 전부 물어보는 방식은 지식원이 늘면 비싸진다.
- 칠판이 공유 상태라 병렬화하면 곧바로 동시성 문제가 된다.
실제로 만나는 곳
음성 인식 시스템(이 패턴의 원조가 Hearsay-II였다), 규칙 기반 전문가 시스템, 일부 자율주행·로보틱스의 상황 인지 모듈.