위 json파일을 보고 어지러운가? 아니면 어느정도 이해할만 한가? 아래 표 먼저 보게되면 어지러울 수 있기 때문에, 생김새 먼저 보여준 점 이해 부탁한다.
Controller 속성
이름
설명
값
name
컨트롤 이름
컨트롤러마다 Unique한 이름을 가진다.
last_uri
Page URI의 마지막 세그먼트
dir이 같다면, 이 값은 서로 달라야한다.
debug
MkLogger의 기록 레벨. MkLogger의 기본 설정보다 낮은 값이면 기록되지 않는다.
debug, info, warn, error
api
RESTful API 컨트롤러인가?
yes, no
device
접속 기기, 그리고 언어마다 서로 다른 jsp 파일, 그리고 uri를 설정할 수 있다. 접속 기기는 우측과 같으며, 각 기기마다 JSONObject를 가진다.
desktop, android, ios
device 속성
default
기본 언어에 대한 페이지. device마다 default는 반드시 가져야 하며, 만약 추가적인 언어로 만들어진 페이지를 지정할 경우, 언어의 공식 두글자를 사용하여야 한다.
default가 아니라면, ko, en 등 언어의 2글자 표현
path
JSP View가 위치할 경로. /WEB-INF 아래에 위치하여야 한다.
file
JSP View 파일의 이름
uri
Dispatching을 위한 Page URI. 여기에 last_uri가 더해져 최종 URI가 된다.
Service 속성
이름
설명
값
page_static
서비스가 client의 요청과 무관하게 load될 때 실행되는지를 설정한다.
true, false
type
서비스의 종류와 이름을 설정하는 JSONObject. type 아래에는 "kind"와 "id"가 있다.
이 때, "id"는 내가 사용하고자 하는 service가 controller급이라면, 해당 controller 설정 json 파일에 같은 id를 가지는 서비스가 존재하여야 한다.
"kind":sql, jwt "id": unique한 값을 가지며, 서비스를 구분하는 이름.
method
서비스를 사용하기 위해 어떤 HTTP method를 사용하여야 하는가를 설정한다.
GET, POST
obj
서비스를 통해 받아오거나 생성한 데이터를 어떤 자료구조로 표현할지를 설정한다.
list, map
parameter_name
서비스를 사용하기 위해 필수 parameter를 client로 부터 받을 때, 데이터의 앞첨자를 가리킨다. 각 서비스마다 유니크한 앞첨자를 가져야 한다. page_static의 경우 아무 값도 가지지 않는다.
unique한 name
value
서비스를 사용하기 위해 필수 parameter를 client로부터 받을 때, 데이터를 받을 parameter 뒷첨자 이름이다. parameter_name과 함께 쓰인다. 특히 SQL 서비스의 경우, 이 값들은 SQL 서비스에도 똑같은 이름이어야 하며, SQL controller 내에서는 @로 감싸져야 한다.
내가 이 글을 쓰면서 느낀건데,, 나는 MkWEb의 사용법을 여기에 올리려 한게 아니라 개발일지를 적으려 했다..
흠.. 어떻게 블로그 글을 써야할까?
MKWeb은 정말 내가 json파일만 수정하면 웹서버가 동작하는, 그런 기능이 필요했다. 처음에는 모든 웹 개발자가 접근하는 HTML, 마크업 언어와 비슷한 XML로 MkWeb의 모든 설정을 하였다.
하지만 이는 기능이 늘어감에 따라 복잡해지는 XML과 그로 인한 떨어지는 가독성 때문에 JSON 으로 대체하였다.
다행인점은, 컨트롤러와 서비스를 읽어오는것과 그 기능이 하게 하는것은 쉽게 프로그래밍이 가능하다는 것이다.
내가 이때까지 FTP 서비스나, 세션 등 많은 컨트롤러를 개발하면서 느낀 점은 다음과 같았다.
1. 이렇게 많은 서비스를 만든다 해서, 프론트 개발자가 모두 사용할까? 2. 내가 지정한 규칙에 맞춰(hard) 사용한다면, 프론트 개발자가 어려움을 느끼지 않을까?
그래서 나는 정말 기본적인 Logger, SQL, RESTful API, JWT와 같은 기능만 구현하여 사용자에게 제공하고, 나머지 추가 컨트롤러나 서비스(FTP와 같은)는 개발하여 MkWeb에 적재시키도록 할 예정이다.
사실 프론트 개발자는 React나 vue를 많이 사용하는데, 요즘 fresh한 느낌과는 거리가 먼 jsp를 굳이 사용할까? 하는 느낌도 있었기 때문에, 최대한 가볍게, 그러나 편리하게 쓸 수 있는 방향으로 살짝 방향을 전환하였다. (RESTful API만을 이용해서 React의 데이터 서버로 사용한다든지 등.. 근데, node가 아닌 tomcat을 따로 설치해야 한다는게 좀.. 이걸 굳이 프론트 개발자가 하려나..?)
MkWeb은 Minwhoan-Kihyeon's Webserver framework의 약자이다. 이름에서 볼 수 있듯이 최초 시작은 나와 내 친구인 hyeonic 둘이서 개발을 시작하였다.
MkWeb은 앞장에서 언급한 바 있지만,프론트 개발자가 더 쉽고 간편하게, 내가 알지 못하는 서버에 대한 고민 없이웹 서비스를 할 수 있게 도와주는 프레임 워크이다.
사실https://mkweb.dev-whoan.xyz이 페이지에 들어가면 MkWeb에 대한 설명이 조금이나마 더 문서에 맞는 어투를 사용하여 작성하였으나, 처음 공식문서를 작성하다보니 그 어색함이 있고, 표현하기에도 애매한 바가 있고, 편하게 그 내용을 정리하고 싶어서 여기에도 글을 쓴다.
MkWeb은 MVC 패턴에 맞춰 디자인된 웹서버 프레임워크이다. 그렇다고 해서 Spring Project는 아니다. Tomcat과 같은 WAS에서 동작하는 JSP 기반 웹 서버이다. (JSP라 쓰고 HTML/Javascript 라 읽는다. ㅋㅋ)그럼에도 불구하고 MVC에 맞춰 디자인한 이유는 다음과 같다.
1. 프론트 개발자가 웹서버를 쉽게 정의할 수 있어야 한다. 2. 프론트 개발자가 웹서버를 쉽게 유지/보수 할 수 있어야 한다. 3. 설계만 잘 한다면, 그에 맞게 웹서버가 동작할 수 있어야 한다.
지금이야 기본 기능이 구현된 후에서야 쓰는 글이지만, 위 3가지를 바꿔 말하면,
1. 프론트 개발자가 사용하고자 하는 View를 정의하고 (JSP), 미리 짜여진 Controller의 설계에 맞춰 기능을 잘 만든다면 웹서버가 동작하여야 한다.
2. 유지보수를 쉽게 하기 위해 프론트 개발자는 일절 Server-side Programming 지식이 없어도 되야 하므로, 프론트 개발자가 잘 사용하는 json/xml 방식을 통해 유지/보수 할 수 있어야 한다.
와 같다. 어느정도 그림이 그려 지는가?
따라서 우리 MkWeb는 MVC를 다음과 같이 정의했다.
Model: 서버의 자원 (DB, File Server 등)
View: Client가 볼 웹 페이지
Controller: 모델과 뷰 사이의 관계
Service: 컨트롤러 내에 존재하여 실제 동작하는 워커
정리해보면, MkWeb은 프론트 개발자가 json으로 정의된 컨트롤러들을 잘 정의하고, 사용하고자 하는 Service를 잘 등록해주면 웹서버가 돌아간다!
지금 지원하는 Service(Controller)는 다음과 같다.
1. RDBMS 2. RESTful API 3. MkLogger 4. FTP
개발 연혁(?) 2020.05: dev.whoan과 hyeonic의 웹서버 공부를 목적으로 MkWeb 개발 시작
2020.08: RESTful API 기능을 추가하고자 하였으나, 우리 둘의 지식 부족으로 인해 MkWeb 개발 중단
2020.09: 학기 시작과 hyeonic의 4학년 준비로 인해 MkWeb의 공동개발 중단, dev.whoan의 독자개발 진행
그럼에도 불구하고 위 사진처럼 너 와 나 사이에 공간이 생기는걸 볼 수 있는데, 이는 버그가 아니라 HTML 문서에서 줄바꿈이 인식되어 공간이 발생한 것이다. 즉 이를 해결하기 위해서는 빈공간에 대한 사이즈를 줄여야 하는데, 다음과 같은 방법이 있다.
1. <p> 태그를 작성하면서 엔터(줄바꿈)을 하지 않는다. 2. p-wrapper내부에 있는 공간을 인식하는 것 이기 때문에, p-wrapper내의 글자 크기를 줄인다. 3. <p>태그의 닫힘 문자를 건너뛴다. (</p>를 쓰지 않는다. 이는 문제가 될 수 있어서 비추천...)
그러나 이 역시 p태그의 요소와 내용이 간결하기 때문에 보기 쉽지만, 클래스가 여러 개 들어가거나, 내용이 길어질 경우 꽤 tricky 해질 수 있기 때문에 비추천 한다.
그래서 내가 주로 사용하는 방법은 2번 방법이다. css에서 p-wrapper의 글자 크기(줄바꿈을 없애기 위해)를 0으로 설정하고, p-wrapper내에 직접적인 글자를 작성하지 않는 것이다.
#p-wrapper{
float: left;
background-color: #f1cc82;
font-size: 0; /* 여기서 p-wrapper의 글자를 지우고 */
}
.p-tag{
display: inline-block;
background-color: #ccc;
font-size: 16px; /* 여기서 p tag의 글자 크기를 설정한다. */
}
만약 p-tag에서 font-size를 지정해주지 않으면, 다음과 같은 문제가 발생할 수 있으니, 꼭 p-wrapper 내부의 어떤 요소에 글자를 작성한다면, 해당 요소의 css에서 글자 크기를반드시지정해 줘야 한다.
아무것도 없다!
정상적으로 모든 요소에 글자 크기 설정을 진행하고 나면, 아래와 같이 빈 공간이 사라진 채로 요소를 붙일 수 있다.
이번에 갤럭시 버즈 프로가 생겼다. 기존에 버즈 1세대를 사용하고 있었는데, 음질 차이가 얼마나 클까 하는 생각이 있었고, 큰 기대는 하지 않았다. (애니콜 케이스만 너무 기대됐다.)
1월 30일에 삼성 갤캠스에서 주문 했는데, 2월 9일이 되어서야 배달을 받았다. (삼성 물류 노조 파업 때문에 꽤 오래 걸리는 것 같다.)
버즈 프로와 애니콜 박스
배송 받았을 때 버즈 프로보다 애니콜 케이스에 눈길이 더 갔고, 정말 너무 귀엽게 잘 만든 케이스인 것 같다.
버즈 프로 내부
그래도 버즈가 애니콜보다 더 귀한 몸이기 때문에, 버즈 프로의 박스를 먼저 열었고, 위 사진처럼 생겼다. 구성품은 버즈 프로 본체와 오른쪽 검정 박스에 USB Type C 선, 그리고 여분의 이어팁이 들어있다. 나는 귓구멍이 작기 때문에, 제일 먼저 버즈 프로의 이어팁을 S사이즈로 교체해주었다.
버즈 프로
이번 버즈 프로는 애니콜 케이스 뿐 아니라 색감도 정말 잘 뽑힌 것 같다. 원래 IT 기기는 떄 타면 안된다는 생각에 항상 검정색을 선호하는데, 이번에는 바이올렛이 예쁘게 나온 것 같아 바이올렛으로 선택했고, 후회할 일 없을 것 같다.
애니콜 케이스 장착
애니콜 케이스를 끼웠는데!! 너무 귀엽다!!! 이건 진짜 없는 사람은 따로 주문해서라도 끼워야 한다!!! 너무 귀엽잖아!!
사실 둘 다 귀에 끼우고 있을 때는 별 차이가 없다. 그런데 내가 큰 차이를 느낀 때는, 버즈를 귀에서 뺐을 때 귀의 어색함, 통증이 버즈 프로에서 꽤 크게 느껴졌다. 버즈 1세대에 적응해서 그런가? 버즈 프로를 귀에서 빼면 일단 귀가 "살것같아!"라고 하는 듯한 느낌이 든다. 이어팁을 사제로 구매해서 바꾸면 괜찮아진다는 말이 있기에, 구매 후 교체할 예정이다.
음질
버즈 1세대 << 버즈 프로
사실 별 기대하지 않은 음질이지만, 꽤 큰 차이가 느껴졌다. 버즈의 ANC를 끄고 들어도, 해상력이 꽤 좋아졌으며 버즈 1세대에서는 크게 들리지 않았던 여러 채널을 버즈 프로에선 들을수 있었다. 예를들어 드럼의 킥을 버즈에서는 '그냥 "킥"이구나'. 하고 느껴졌다면, 버즈 프로에서는 '오 "킥" 괜찮은데?' 하는 감이 느껴질 정도였으니.
배터리
버즈 1세대 > 버즈 프로 (19년부터 사용한 1세대의 수명 단축을 고려함)
프로에 더 많은 기능이 들어가서 그런가? 실제론 버즈 1세대의 사용 시간이 더 긴 것 같다. 사용했을 때는 둘의 차이를 크게 느끼지 못했지만, 수명이 줄어 든 버즈 1세대를 생각하면 프로가 더 짧은 것 같다.
통화 음질
버즈 1세대 <<~||넘사벽||~<< 버즈 프로
통화 음질은 말할 것 없다. 버즈 1세대는 통화할 때 무조건 버즈를 빼고 핸드폰으로 통화해야 했는데, 버즈 프로는 그냥 끼고 통화해도 사람들이 모른다. 심지어 여자친구는 핸드폰으로 통화하는 것 같다고 한다. 정말 정말 정말 정말 나는 이 통화 음질만으로도 정말 만족한다.
기타 기능
버즈 1세대 <<<< 버즈 프로
기타 기능은 ANC, 주변 소음 듣기, 대화 감지 등 버즈 프로에만 있는 기능들을 생각했을 때 당연히 버즈 프로의 압승이다. ANC는 정말 충격 받은게, 내 방에 컴퓨터 2대가 있는데 버즈 프로를 끼고 ANC 기능을 켜면 감쪽같이 컴퓨터의 팬 소리가 안들린다! 충격이었다. 처음에 ANC를 키고 꼈을때는 몰랐는데, ANC에서 주변 소음 듣기 모드를 처음 켰을 때 팬 소리를 듣고 헐! 대박인데? 하고 느껴졌다. 버즈 프로를 빼니, 컴퓨터의 팬 소리가 정말 크게 느껴지는게 아닌가!
주변 소음 듣기 또한 대박이다. 버즈 1세대의 주변 소음 듣기(비슷한 기능이 있는데, 이름이 기억 안난다. 똑같은가?)는 켜도 화이트 노이즈 그리고 떨어지는 기능으로 인해 별 의미 없는 기능이었는데, 버즈 프로의 그것은 정말 대박이다. 주변 사람과 자연스러운 대화가 가능하고, 그렇다 해서 버즈 프로가 재생하던 미디어가 안들리는 것도 아니다! 이것 정말 대박인데?
대화 감지 기능도 좋기는 한데, 항상 켜놓을만한 기능은 아닌 것 같다. 버즈 프로가 조금이라도 내가 말한다는 듯한 낌새를 파악하면 (작은 노래 흥얼거림이라던지, 기침이라던지, 기타 등등 마이크가 인식만 한다면) 곧바로 주변감지 기능이 켜지기 때문에, 미디어에 집중하긴 어렵기 때문이다. 그래서 나는 버즈 프로의 터치 기능을 ANC <-> 주변 소음 듣기 모드 변환으로 설정해 놓고, 대화 감지 기능은 꺼놨다.
Queue는 Stack과는 반대로, FIFO(First In First Out)성질을 갖는다. 즉 먼저 들어온 녀석이 먼저 나간다.
Queue의 가장 흔한 예시로는 프린트 출력 대기열을 들 수 있는데, 먼저 출력 요청한 문서를 먼저 출력해야 하기 때문이다. 비슷한 예로 입장 대기줄 등이 있다.
큐는 탐색에서도 스택과 함께 많이 쓰이므로 잘 알아두면 좋다. 특히 큐는 우선순위 큐, 원형 큐 ... 등 많은 종류가 있기 때문에, 기본 큐는 반드시 알아야 한다.
Queue에는 어떤게 필요할까?
Queue는 Stack과 비슷한 자료구조로, 데이터를 삽입하고 반환할 수 있어야 한다. Stack처럼 제일 위에 삽입한다고 생각해도 된다. 다만 Stack과의 차이점은 시작점과 끝점이 있어야 한다는 것이다. Stack은 top이라는 시작점만 존재했지만, Queue는 끝점도 가져야하는 이유는 다음과 같다.
1. 제일 먼저 들어온 녀석을 반환하기 위함 2. 큐가 가득 찼는지 확인하기 위함.
즉 Queue에도 Top만 존재했다면, 어느 녀석을 먼저 반환해야 할지, 새로 들어온 녀석은 어디로 들어와야 할지를 동시에 수행할 수 없기 때문에 끝점도 존재해야 하는 것이다.
그렇다면 rear는 어떻게 활용할 수 있을까? Queue에 데이터가 삽입되면 rear가 하나씩 증가하게 된다. 이를 조금 더 생각 해 보면, front와 rear가 같다면, Queue가 비어있는 상황이 되고, rear가 capacity와 같아지게 되면 Queue가 가득차게 되는 것이다.
따라서 Queue는 front, rear, capacity 그리고 queue를 담는 배열이 필요하다.
Queue 구현
Queue의 기본은 다음과 같다.
public class Queue {
int front;
int rear;
int capacity;
Object[] queue;
Queue(int capacity){
this.front = -1;
this.rear = -1;
this.capacity = capacity;
queue = new Object[this.capacity];
}
public boolean isFull() {
/*
capacity가 10이면 배열은 0~9를 갖고, rear도 0부터 시작하기 때문에 -1을 해야한다.
*/
return (this.rear == this.capacity-1);
}
public boolean isEmpty() {
/*
front와 rear가 같으면 비어있는 상태이기 때문에, 편의를 위해 front와 rear를 -1로 초기화 시켜준다.
*/
if(front == rear) {
front = -1;
rear = -1;
}
return (this.front == this.rear);
}
}
다음으로 Queue에 데이터를 삽입하고 반환하는 함수가 필요한데, Stack에서는 이를 pop, push라고 했다면 Queue에서는 enqueue, dequeue라고 한다.
데이터를 반환하기 위해서는 front에 있는 먼저 들어온 녀석을 반환해줘야 하는데, 반환 해줄 때 마다 front의 위치를 옮겨주어야 한다.
왼쪽이 간편해 보인다.
위 그림을 보면, 다음 출력 문서의 위치 front는 고정되어 있고, 남아있는 문서들을 한칸씩 옮기는 오른쪽 그림과 다음 출력 문서의 위치 front를 이동시키고 남아있는 문서들은 고정되어 있는 왼쪽 그림중 어느 것이 더 편해 보이겠는가?
정답은 당연히 왼쪽 그림인 front를 옮기는게 훨씬 간단하다.
이렇게 dequeue에서 front를 옮기기 때문에, rear의 위치 또한 단순히 배열의 index를 옮겨서는 안되고, 한가지 트릭을 써야 한다. (만약 n칸짜리 queue에서 n개를 넣고 dequeue를 n번 하면, front와 rear의 크기는 같아지게 된다. 즉 queue는 비어있게 되는데, 문제는 rear의 위치가 capacity와 같아지기 때문에 동시에 가득찬 상황이 된다. 이는 Queue의 모순이지 않는가?)
rear에 사용할 트릭은, 단순히 index를 지정하는 게 아니라 Queue의 capacity로 나누어서 나온 몫이 rear의 index가 되게 해야 한다. 위 괄호안에서 든 예시로 말하자면, capacity가 10이라고 가정 했을 때, rear와 front는 모두 9가 되는 상황이다. 이 때, (rear+1) % (capacity = 10) 을 하면 삽입할 다음 index는 0이 되고, rear와 front의 차이는 다시금 10이 되기 때문에 처음부터 넣는것과 같게 된다. (+1을 해주는 이유는, 삽입할 element는 다음 번 index를 가져야 하기 때문이다.)
이를 바탕으로 enqueue를 설계하면 다음과 같다.
public class Queue {
...
public void enqueue(Object element) {
if(isFull()) {
System.out.println("Queue is Full!");
return;
}
rear = (rear+1) % this.capacity;
queue[rear] = element;
}
...
}
dequeue 또한 enqueue와 같은 원리로 Queue의 capacity로 나눈 몫을 index로 가지면 된다. 이유는 위에서 rear에 대한 설명한 것과 같다.
public class Queue {
...
public Object dequeue() {
if(isEmpty()) {
System.out.println("Queue is empty");
return null;
}
front = (front+1) % this.capacity;
return queue[front];
}
...
}
Queue도 Stack과 같이 element를 반환하지 않고 엿보는 peek을 가질 수 있는데, 코드는 다음과 같다.
public class Queue {
...
public Object peek() {
if(isEmpty()) {
System.out.println("Queue is empty");
return null;
}
return queue[front+1];
}
...
}
이제 queue의 size와 초기화를 시켜주는 함수를 만들면 된다.
size는 rear - front를 통해 구할 수 있다. 이는 내가 마지막으로 삽입한 index - 제일 먼저 삽입한 index가 되고, 5개 입력했으면 다음과 같기 때문이다 (5-0) = 5. 그런데 배열은 0부터 시작하기 때문에 front와 rear에 각각 1씩 더해주면 된다. ( (4+1) - (-1+1) ) = 5. 또한 Queue는 front와 rear의 index가 역전될 수 있으니, 절대값 으로 반환하면 된다.
초기화는 간단하게 front, rear, 모두 -1로, 그리고 queue 배열을 새로 선언하면 된다.
코드는 다음과 같다.
public class Queue {
...
public int size() {
return Math.abs( (rear+1) - (front+1) );
}
public void clear() {
if(isEmpty()) {
System.out.println("Queue is already empty!");
}
else {
front = -1;
rear = -1;
queue = new Object[this.capacity];
System.out.println("Queue has cleared!");
}
}
}
Queue 코드 전체 및 실행결과
public class Queue {
int front;
int rear;
int capacity;
Object[] queue;
Queue(int capacity){
this.front = -1;
this.rear = -1;
this.capacity = capacity;
queue = new Object[this.capacity];
}
public boolean isFull() {
return (this.rear == this.capacity-1);
}
public boolean isEmpty() {
if(front == rear) {
front = -1;
rear = -1;
}
return (this.front == this.rear);
}
public void enqueue(Object element) {
if(isFull()) {
System.out.println("Queue is Full!");
return;
}
rear = (rear+1) % this.capacity;
queue[rear] = element;
}
public Object dequeue() {
if(isEmpty()) {
System.out.println("Queue is empty");
return null;
}
front = (front+1) % this.capacity;
return queue[front];
}
public Object peek() {
if(isEmpty()) {
System.out.println("Queue is empty");
return null;
}
return queue[front+1];
}
public int size() {
return Math.abs( (rear+1) - (front+1) );
}
public void clear() {
if(isEmpty()) {
System.out.println("Queue is already empty!");
}
else {
front = -1;
rear = -1;
queue = new Object[this.capacity];
System.out.println("Queue has cleared!");
}
}
}