WebRTC 코드 정리

WebRTC는 P2P이며 웹에서 1:1로 연결하여 서로 데이터를 실시간으로 주고 받는 웹 표준기술입니다. 추후 적용을 위해 관련 코드를 정리합니다.

먼저 개념도는 다음과 같습니다. (아래 그림은 WebRTC에 대한 이해를 돕기 위해 제가 작성한 것으로 잘못 표기된 부분이 있을 수 있습니다.)

일단 쭉 말로 설명하면.. 먼저 데이터를 주고 받을 채널을 생성하고 채널을 생성한 Peer가 제안(Offer) 객체를 만들어 다른 Peer에게 전달합니다. 그럼 피어는 제안과 함께 받은 채널을 확인하고 응답(Answer) 객체를 생성해 전달합니다. 그러면 이 두 Peer들은 icecandidate라는 이벤트가 발생하게되고 Candidate 객체를 서로 주고 받아 설정함으로써 두 Peer 간의 연결이 이루어 집니다. 이렇게 연결이 이루어지면 앞서 언급한 채널을 통해 메세지를 실시간으로 주고 받을 수 있습니다. 위의 그림을 보면 Peer간의 연결을 위해 Offer, Answer, Candidate 정보를 교환하기 위해 별도의 서버가 필요한 것을 볼 수 있습니다. 하지만 WebRTC의 핵심은 서버없이 Peer 간의 데이터 전송이므로 일단 연결이 되면 더 이상 서버는 필요치 않습니다.

웹 표준 기술이 지향하는 매우 심플한 API 구조에 맞게 코드가 단순한데요. 서버, 클라이언트 코드가 매우 짧으므로 전체 코드를 남깁니다. 먼저 클라이언트에 대한 코드는 다음과 같습니다.

const socket = io();
const roomName = "room1";

let peerConnection;
let dataChannel;

document.querySelector("form").addEventListener("submit", (event) => {
    event.preventDefault();

    if(dataChannel) {
        const value = document.querySelector("form input").value
        dataChannel.send(value);
    }
});

function onReady() {
    peerConnection = new RTCPeerConnection({
        iceServers: [{ urls: [
            "stun:stun.l.google.com:19302",
            "stun:stun1.l.google.com:19302",
            "stun:stun2.l.google.com:19302",
            "stun:stun3.l.google.com:19302",
            "stun:stun4.l.google.com:19302",
        ]
    }]});

    peerConnection.addEventListener("icecandidate", (event) => {
        socket.emit("ice", event.candidate, roomName);
    });
}

window.onload = () => {
    socket.emit("join", roomName, onReady);
}

function onMessage(msg) {
    const ul = document.querySelector("ul");
    const li = document.createElement("li");
    li.innerText = msg;
    ul.append(li);
}

socket.on("welcome", async () => {
    dataChannel = peerConnection.createDataChannel("chat");
    dataChannel.addEventListener("message", (event) => {
        onMessage(event.data);
    });

    const offer = await peerConnection.createOffer();
    peerConnection.setLocalDescription(offer);
    socket.emit("offer", offer, roomName);
});

socket.on("offer", async (offer) => { 
    peerConnection.addEventListener("datachannel", (event) => {
        dataChannel = event.channel;
        dataChannel.addEventListener("message", (event)=> {
            onMessage(event.data);
        });
    })

    peerConnection.setRemoteDescription(offer);
    const answer = await peerConnection.createAnswer();
    peerConnection.setLocalDescription(answer);
    socket.emit("answer", answer, roomName);
});

socket.on("answer", (answer) => {
    peerConnection.setRemoteDescription(answer);
});

socket.on("ice", (ice) => {
    peerConnection.addIceCandidate(ice);
});

서버단의 코드는 다음과 같습니다.

import http from "http"
import express from "express";
import SocketIO from "socket.io";

const app = express();

app.set("view engine", "pug");
app.set("views", __dirname + "/views");
app.use("/public", express.static(__dirname + "/public"));
app.get("/", (_, res) => res.render("home"));

const httpServer = http.createServer(app);
const wsServer = SocketIO(httpServer);

const port = 3000;
const handleListen = () => console.log(`Listening on http://localhost:${port}`)
httpServer.listen(port, handleListen);

wsServer.on("connection", socket => {
    socket.on("join", (roomName, done) => {
        socket.join(roomName);
        done();
        socket.to(roomName).emit("welcome");
    });

    socket.on("offer", (offer, roomName) => {
        socket.to(roomName).emit("offer", offer);
    });

    socket.on("answer", (answer, roomName) => {
        socket.to(roomName).emit("answer", answer);
    });

    socket.on("ice", (ice, roomName) => {
        socket.to(roomName).emit("ice", ice);
    })
});

이용한 기술은 JS, Node.JS, Express, Socket.IO, Pug입니다.

IT 서적, “JAVA 언어로 배우는 디자인 패턴 입문” 추천

이 글은 영진닷컴 출판사에서 도서 리뷰를 요청하여 작성한 글입니다. 비록 출판사 리뷰 요청에 의해 작성한 글이지만 저는 이미 예전에 이 책을 수차례 완독하고 OOP 학습과 깊은 이해에 큰 도움일 받았던 책이였다는 점에서 개인적으로도 충분히 추천드리고 싶은 책이기도 합니다.

OOP 개념을 이제 막 배운 개발자라면 GoF의 디자인패턴 23가지를 반드시 학습해야 하는 이유는 OOP를 실제 코드에 효과적으로 적용한 체계적인 방법이 바로 디자인패턴이기 때문입니다. 디자인패턴에 대한 서적은 많지만 장황한 설명으로 난해하거나 이해하기 어려운데.. 하지만 “Java 언어로 배우는 디자인 패턴 입문”은 다자인패턴을 최대한 알기 쉽게 설명한 책 중 단연 으뜸입니다.

이 책은 2001년 1판을 시작으로 2022년에 3판으로 새롭게 업데이트 되어 출간되었습니다. 이 책은 디자인패턴을 Java라는 언어를 통해 설명하지만 Java에 특화된 OOP 코드 없이 일반적인 OOP 코드를 사용한 구체적인 예제의 구현을 중심으로 설명합니다. 결국 Java가 아니더라도 OOP의 기본을 완전하게 지원하는 다른 언어를 이용해서도 이 책에서 제시한 예제를 그대로 작성할 수 있습니다.

디자인패턴을 학습함에 있어서 반드시 알고 있어어 하는 것은 UML입니다. 즉, 소프트웨어의 설계도인데요. UML 중 클래스 다이어그램과 시퀀스 다이어그램은 소프트웨어의 설계를 위해 매우 핵심적인 표기를 위한 다이어그램입니다. 클래스 다이어그램은 클래스들 간의 관계도를 효과적으로 나타내며 시퀀스 다이어그램은 실행 순서를 클래스들 간의 소통과 함께 효과적으로 나타냅니다.

저 같은 경우 프로그램을 개발하고 유지보수 등을 이유로 오랜 시간이 흐른 뒤에 다시 해당 프로그램의 코드를 수정해야 할때 가장 먼저 살펴보는 것이 클래스 다이어그램입니다. 그만큼 클래스 다이어그램은 소프트웨어를 큰 그림으로 매우 효과적으로 이해하고 분석할 수 있는 설계도입니다. 그리고 좀더 구체적이고 세세한 부분을 분석할때는 시퀀스 다이어그램을 살펴봅니다.

이 책의 저자는 디자인패턴에 대한 구체적인 사례를 클래스다이어그램과 시퀀스 다이어그램으로 빠짐없이 제시하고 이들을 중심으로 한가지 명확한 사례를 실행 가능한 구체적인 코드로 작성합니다. 이 책에서 제시하는 23개의 패턴 중 State 패턴을 하나로 예로 들어보면 먼저 클래스 다이어그램은 다음과 같습니다. (원래 서적보다 좀더 간결하게 표기하였습니다)

이 책에서 언급하는 State 패턴은 어떤 사물의 상태를 클래스로 표현하여 각 상태에 따른 처리를 매우 효과적으로 구분하여 구현 할 수 있는 패턴입니다. 이 패턴을 사용하면 상태에 대한 처리를 if 문으로 분기하지 않고 클래스로 나눠 구현함으로써 코드가 매우 효과적으로 분리되어 관리될 수 있어 새로운 상태의 추가가 발생할 경우 이 State 패턴의 가치가 극대화 됩니다. 위의 클래스다이어그램은 이 State 패턴을 금고 경비 시스템의 구현 사례를 타나낸 것이고 금고를 사용함에 있어 낯일 경우(DayState 클래스)와 밤일 경우(NightState)에 따라 그 처리를 분리한 것입니다.

이 책에서는 이 금고 경비 시스템을 사례로 State 패턴으로 효과적으로 설명하기 위해 사용한 언어는 Java 코드이지만 앞어 언급했던 것처럼 OOP의 기본적인 내용을 충실하게 제공하는 언어라면 이 책의 코드를 자신이 원하는 언어로 바꿔 표현하는 것도 충분히 가능합니다. 저 같은 경우 Java가 아닌 이제는 대세로 자리잡은 웹 개발 언어인 TypeScript로 완전히 동일하게 구현해 봤는데.. 이 책의 Java 코드를 전혀 다른 언어로 동일한 시스템을 구현해 보면서 재미는 물론 해당 언어의 OOP에 대한 더욱 깊이 있는 이해가 가능했습니다.

이 책에서 언급한 State 패턴에 대한 구현 클래스를 TypeScript로 표현한 코드를 모두 언급해 보면 다음과 같습니다.

먼저 상태를 나타내는 인터페이스 코드는 다음과 같습니다. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { Context } from "./Context";

export interface State {
    doClock(context: Context, hour: number): void; 
    doUse(context: Context): void;
    doAlarm(context: Context): void;
    doPhone(context: Context): void;
}

그리고 이 State 인터페이스를 통해 낯 상태를 나타내는 DayState 클래스는 다음과 같습니다. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { Context } from "./Context";
import { State } from "./State";
import { NightState } from "./NightState";

export class DayState implements State {
    private static singleton: DayState = new DayState();
    private constructor() {}
    public static getInstance():State {
        return this.singleton;
    }

    doClock(context: Context, hour: number): void {
        if(hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance());
        }
    }

    doUse(context: Context): void {
        context.recordLog("금고 사용(주간)");
    }

    doAlarm(context: Context): void {
        context.callSecurityCenter("비상벨(주간)");
    }

    doPhone(context: Context): void {
        context.callSecurityCenter("일반 통화(주간)");
    }

    public toString(): String {
        return "[주간]";
    }
}

밤 상태는 다음과 같구요. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { Context } from "./Context";
import { State } from "./State";
import { DayState } from "./DayState";

export class NightState implements State {
    private static singleton: NightState = new NightState();
    private constructor() {}

    doClock(context: Context, hour: number): void {
        if (9 <= hour && hour < 17) {
            context.changeState(DayState.getInstance());
        }
    }

    doUse(context: Context): void {
        context.callSecurityCenter("비상: 야간 금고 사용!");
    }

    doAlarm(context: Context): void {
        context.callSecurityCenter("비상벨(야간)");
    }

    doPhone(context: Context): void {
        context.recordLog("야간 통화 녹음");
    }

    public static getInstance(): NightState {
        return this.singleton;
    }

    public toString(): String {
        return "[야간]";
    }
}

상태를 관리하고 전체적인 제어를 위한 Context 인터페이스는 다음과 같습니다. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { State } from "./State";

export interface Context {
    setClock(hour: number): void;
    changeState(state: State): void;
    callSecurityCenter(msg: String): void;
    recordLog(msg: String): void;
}

그리고 Context 인터페이스로 구체적인 금고시스템을 구현한 SafeFrame은 다음과 같습니다. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { Context } from "./Context";
import { DayState } from "./DayState";
import { State } from "./State";

export class SafeFrame implements Context {
    private state: State = DayState.getInstance();

    public constructor(title: string) {
        (document.querySelector(".title") as HTMLElement).innerText = title;

        document.querySelector(".use").addEventListener("click", this.onClickEvent.bind(this));
        document.querySelector(".alram").addEventListener("click", this.onClickEvent.bind(this));
        document.querySelector(".phone").addEventListener("click", this.onClickEvent.bind(this));
        document.querySelector(".exit").addEventListener("click", this.onClickEvent.bind(this));
    }

    onClickEvent(evt: Event) {
        const text = (evt.currentTarget as HTMLElement).innerText;
        if(text === "금고사용") {
            this.state.doUse(this);
        } else if(text == "비상벨") {
            this.state.doAlarm(this);
        } else if(text == "일반통화") {
            this.state.doPhone(this);
        } else if(text == "종료") {
            alert("시스템 종료");
            document.body.innerHTML = "";
        }
    }

    setClock(hour: number): void {
        const clockString = `현재 시간은 ${hour}시`;
        console.log(clockString);
        const domTime = document.querySelector(".time");
        if(domTime) domTime.innerHTML = clockString;
        this.state.doClock(this, hour);
    }

    changeState(state: State): void {
        console.log(`${this.state}에서 ${state}로 상태가 변화했습니다.`);
        this.state = state;
    }

    callSecurityCenter(msg: String): void {
        const domOutput = document.querySelector(".output");
        const domItem = document.createElement("div");
        domItem.innerText = `call! ${msg}`;
        domOutput.appendChild(domItem);
    }

    recordLog(msg: String): void {
        const domOutput = document.querySelector(".output");
        const domItem = document.createElement("div");
        domItem.innerText = `recording ... ${msg}`;
        domOutput.appendChild(domItem);
    }
}

위의 모든 클래스를 활용하여 실행 가능하도록 한 코드는 다음과 같구요. (책에서는 Java로 되어 있으나 이를 Typescript로 변환하였습니다)

import { SafeFrame } from "./SafeFrame";

class Main {
    public static run(): void {
        let frame: SafeFrame = new SafeFrame("State 패턴 샘플");

        let hour = 0;

        setInterval(() => {
            frame.setClock(hour);
            hour++;

            if(hour > 23) hour = 0;
        }, 1000);
    }
}

Main.run();

실행 결과는 다음과 같습니다. (책에서는 Java의 UI로 실행되나 이를 웹브라우저에서 실행되도록 하였습니다)

이미 언급했지만 이 책은 디자인패턴을 매우 쉽게 효과적으로 설명하고 있습니다. 또한 GoF의 23개의 디자인패턴은 특정 언어에 종속적이지 않으므로 이 책에서 제시하는 Java언어가 아닌 다른 객체지향 언어를 통해 코드를 작성해보면 디자인패턴 뿐만 아니라 해당 언어의 OOP에 대한 깊이 있는 이해도 가능하다는 점이 이 책의 또 다른 장점입니다. 중급 이전의 개발자로써 소프트웨어 설계에 관심이 있고 디자인패턴을 학습하고자 하시는 분들에게 이 책을 추천드립니다. 특히 OOP의 개념을 이제 막 학습한 분들에게 실제 OOP를 실무나 실제 코드에 어떻게 적용하는지 깊은 이해를 돕는 책으로써 강력히 추천드립니다.

VWorld와 호환성을 가진 (동일한 형식을 가진) 타일맵 가공시 주의할 점

기관에서 주로 사용하는 배경지도가 VWorld이다 보니 VWorld을 기본도로 해서 배경맵을 더 강화(Enrichment)시켜주는 작업이 필요할 때가 있습니다.

해당 기관은 VWorld 배경맵을 사용했는데 여러가지 문제로 관리하는 건물이나 시설물이 상당 부분 누락된 VWorld을 보강해야 할 필요가 있었고 이에 대한 작업을 수행하게 되었습니다. 예전에도 이런 비슷한 작업을 진행했는데.. 매번 할때마다 그 전의 작업 내역을 잊어 버리고 다시 VWorld의 구조를 분석해 작업 흐름을 추론해 진행했습니다. 해서.. 이번에는 작업 내역을 정리해 둡니다.

먼저 VWorld는 EPSG:3857 좌표계를 사용합니다. 레벨이 1~19까지 존재합니다. 각 단계의 축척에 대한 픽셀 당 논리단위 거리는 다음과 같습니다. 1레벨부터 시작합니다.

78000,39000,19600,9800,4900,2400,1222.9924523925781,611.4962261962891,305.74811309814453,152.87405654907226,76.43702827453613,38.218514137268066,19.109257068634033,9.554628534317017,4.777314267158508,2.388657133579254,1.194328566789627,0.5971642833948135,0.29858214169740677

그리고 타일맵 좌표의 전체 범위는 다음과 같습니다.

-20037508.342789244,-20037508.342789244,20037508.342789244,20037508.342789244

1레벨은 전세계의 지도가 표시됩니다. 반대로 마지막 레벨인 19레벨은 최대한으로 확대된 건물이 표시됩니다. 그리고 타일맵이 저정되는 디렉토리 구조가 Level/Column/Row 순서로 저장됩니다. 좀더 일반적인 타일맵의 디렉토리 구조는 Level/Row/Column 입니다. 끝으로 Row의 인덱스가 역순이라는 점도 주의해야 합니다.

작업시 초점을 맞춰야할 것들을 정리해서 요약하면 다음과 같습니다. 1) 타일맵 좌표의 전체 범위 2) 각 축척 단계의 픽셀 당 논리 단위 거리값 3) 디렉토리의 구조(Level/Row/Column) 4) Row 인덱스가 역순임