개요/방향성 등의 서론

직전 게시글 참고

Adapter(어댑터) / Wrapper(래퍼) Pattern

서비스 개발을 한다면 인터페이스 활용은 거의 필수이다.
인터페이스의 버전이 올라가거나, 인터페이스가 레거시에서 차세대로 눈에 띄게 바뀌는 경우
인터페이스의 규격도 바뀌기 마련이다.

개인 개발, 소규모 개발에 있어서는 클라이언트의 코드를 바꿔주면 되겠지만
큰 규모의 프로젝트라면 클라이언트 코드를 바꿔버리기에는 위험성이 있다.
또 신규 인터페이스가 문제가 생긴다면 언제든 롤백을 할 준비도 돼있어야 한다.

실무에서 경험을 바탕으로 생각해본다면
A인터페이스를 사용하기 위해 공통 유틸에 a_using이라는 메서드를 두었다.
만약 업그레이드 버전인 B인터페이스로 변경해야 하는 상황이 생겼다면
b_using메서드를 새로 만드는 것이 아니라
a_using메서드 안에 연동방식 환경변수(레거시/차세대)를 두고
B인터페이스를 필요에 따라 사용할 수 있도록 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//서버 실행 환경 변수
const env = {
event_check_api: "B",
//event_check_api : "A"
};

// (구) 인터페이스
const a_interface = function ({ id, classes, made_date }) {
//id, 직업, 생성일을 input으로
const input = { id, classes, made_date };

//......
//어떤 계산이나 DB/네트워크 작업이 있다고 가정함
//......

//이벤트 대상인지 판단하여 return
return { event_target: false };
};

// (신) 인터페이스
const b_interface = function ({ id, classes, made }) {
//id, 직업, 생성일을 input으로
const input = { id, classes, made };

//......
//어떤 계산이나 DB/네트워크 작업이 있다고 가정함
//......

//이벤트 대상인지 판단하여 return
return { event_planned: false };
};

const a_using = ({ id, classes, made_date }) => {
if (env.event_check_api === "A") return a_interface();
else if (env.event_check_api === "B") {
//input을 신규 인터페이스에서 요구하는 규격에 맞추었다.
//이 과정이 Adapt라는 추상적인 표현으로 불림
const b_instance = b_interface({ id, classes, made: made_date });

//output 역시 신규 인터페이스에서 제공한 규격을 기존 규격으로 변경해주었다.
//이 과정도 Adapt라는 추상적인 표현으로 불림
return { event_target: b_instance.event_planned };
}
};

//a_using에는 Adapt 기능이 있으므로
//클라이언트 내 a_using을 호출하는 수십 군데 이상에서는 기존과 같이 사용하면 된다.
const { event_target } = a_using({
id: "5324234124235324",
classes: "hero",
made_date: "20201214",
});

console.log(event_target);

Composite(컴포지트) Pattern

객체가 여러 개 있다고 할 때, 각 객체 간 연관이 있을 수도 있고 없을 수도 있다.
연관이 있다면 그 사이에 어떻게 의미 부여를 할지 코드로써 나타낼 수 있다.

공장의 파이프라인과 같이 일의 순서가 중요한 관계라면 연결리스트(linked list)의 방향으로 설정해야 할 것이고
조직도와 같이 상하관계가 명백하면서 1대N으로 퍼지는 관계라면 트리로써 개발해야 할 것이다

컴포지트 패턴은 그 중 트리 자료구조를 활용하는 패턴이다.
아래 예시에서는 메이플 내에 지도(맵)을 활용하려고 한다.

맵 구성도 : ROOT => 각 차원 => 각 차원 내의 마을들 => 마을 안의 20~30개 정도 사냥터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//최상위 노드
function Root() {
this.dimensions = [];
}

//전체 맵을 구분하는 각 차원
function Dimension(name_dim) {
this.towns = [];
this.name_dim = name_dim;
}

// 각 차원 내에서 마을
function Town(name_town) {
this.grounds = [];
this.name_town = name_town;
}

// 각 마을 내에서 사냥터
function Ground(name_ground) {
this.towns = [];
this.name_ground = name_ground;
}

// 메서드 체인을 할 수 있는 prototype 설정
Root.prototype = {
add: function (child) {
this.dimensions.push(child);
return this;
},
};
Dimension.prototype = {
add: function (child) {
this.towns.push(child);
return this;
},
};
Town.prototype = {
add: function (child) {
this.grounds.push(child);
return this;
},
};

const root = new Root();
const [maple_word, grandis, arcane_river] = [
new Dimension("maple_word"),
new Dimension("grandis"),
new Dimension("arcane_river"),
];
const [sleepywood, ellinia] = [new Town("sleepywood"), new Town("ellinia")];
const [ground_1, ground_2, ground_3] = [
new Ground("ground_1"),
new Ground("ground_2"),
new Ground("ground_3"),
];
root.add(maple_word).add(grandis).add(arcane_river);
maple_word.add(sleepywood).add(ellinia);
sleepywood.add(ground_1).add(ground_2).add(ground_3);

console.log(root);
console.log(root.dimensions[0]);
console.log(root.dimensions[0].towns[0]);

Module(모듈) Pattern

생성자 패턴에서 사용한 방법들은 this를 통해 숨기고자 하는 변수들도 접근할 수 있는 경우가 있다.
모듈 패턴은 이보다 더 강력하게 변수, 함수들을 숨길 수 있는 패턴이다.
메이플 초기 접속 시 캐릭터 선택창을 간략하게 구현했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 1장 생성자 패턴에서 사용한 캐릭터 생성자 함수
class Character {
constructor(name, level, classes, guild) {
this.name = name;
this.level = level;
this.classes = classes;
this.guild = guild;
}
}

const userApi = () => {
//지금은 하드코딩 해두었지만, 캐릭터 목록을 서버 접속 시 DB에서 끌어온다고 가정함
const hero_1 = new Character("히어로짱짱", 270, "hero", null);
const deven_1 = new Character("데벤짱짱", 250, "demon avenger", null);
let characters = [hero_1, deven_1];

const addUser = (name) => {
characters.push(name);
};

const getAllUsers = () => {
return characters;
};

const deleteUser = ({ name }) => {
let deleted = false;
const filtered = characters.filter((character) => {
if (character.name !== name) {
return true;
} else {
deleted = true;
return false;
}
});

if (!deleted) {
throw new Error("User not found");
}

characters = filtered;
};

return {
add: addUser,
get: getAllUsers,
del: deleteUser,
};
};

//API 연동 준비
const api = userApi();
console.log(api.get());

//캐릭터 생성
api.add(new Character("루미짱짱", 10, "luminous", null));
console.log(api.get());

//캐릭터 삭제
api.del({ name: "데벤짱짱" });
console.log(api.get());

Decorator(데코레이터) Pattern

인스턴스에 추가적인 속성/메서드가 필요할 때가 있다.
보통의 경우에는 인스턴스를 찍어내는 생성자에 해당 속성/메서드를 추가하겠지만,
특정 인스터스에 아주 한정되어 사용하면 인스턴스 단에 속성/메서드를 직접 부여해야 한다.

즉 지금까지 정리한 JS 패턴에는 속성/메서드를 입력하기 위한 3가지 방법이 있다.

  1. 일반적인 방법으로 생성자 안에 선언
  2. 공통 성향이 강하다면 static하게 메모리 덜 잡아먹도록 프로토타입으로 선언
  3. 인스턴스에서만 쓰인다면 인스턴스에 직접 선언

아래 코드에 전사 속성의 공통 스킬은 prototype, 매크로 등록은 인스턴스 decorator로 표현했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Character {
constructor(name, level, classes, guild) {
this.name = name;
this.level = level;
this.classes = classes;
this.guild = guild;
}
}

Character.prototype.warrior = {
common_skill_1: () => {
console.log("body of steel");
},
};

const deven_1 = new Character("데벤짱짱", 250, "demon avenger", null);
const hero_1 = new Character("히어로짱짱", 270, "hero", null);

//인스턴스에 추가 함수 등록
deven_1.macro_slot_1 = function () {
console.log("dimension_sword");
console.log("shield_chasing");
};

//전사 공통 스킬(prototype)은 잘 실행함
hero_1.warrior.common_skill_1();
//히어로는 매크로 등록을 안해서 아래 실행시 TypeError 발생(is not a function`)
//hero_1.macro_slot_1();

deven_1.warrior.common_skill_1();
deven_1.macro_slot_1();

Facade(퍼사드) Pattern

시스템 내 1개 이상의 인터페이스를 수정/보완/결합하여 새로운 인터페이스를 만들고
공통으로 사용할 수 있는 제공하는 패턴이다.

어떤 수준의 인터페이스를 다뤄야 퍼사드라고 부를지는 프로젝트마다 다를 것이다.
(소셜 로그인 API, 자체 API, 스크롤 옵저버, modal 창 이벤트, 버튼 클릭 이벤트까지
인터페이스는 다양하기 마련이다.)

프론트엔드 실무를 하며 가장 효율적이라고 느낀 퍼사드 패턴은
서버와의 네트워킹을 표준화한 것이었다.

유플러스 web 개발에서는 일반적인 FE프로젝트와 같이 axios모듈을 사용했으나,
대규모 프로젝트인만큼 클라이언트-서버 간 통신에서 엄연히 지켜야 할 규격이 존재했다.
특히 다른 시스템에서는 아예 사용하지 않는 유플러스만의 규칙도 존재했다.
(자세한 것은 내부 정보라 말할 수 없음)

이것을 매번 숙지하여 header, parameter를 적절히 암호화하고, form에 딱 맞게 변환하는 작업들은 비생산적이다.
모두 같은 규칙의 작업이 이루어 진다면 공통으로 한번만 작업하는 것이 효율적이고, 후에 유지보수하기도 좋다.

아래 예시에서 메이플 이벤트 정보를 받아오는 서버를 표현했다.
header에서 페이징 정보는 base64 encode 상태로 서버와 통신한다.
이것의 encode, decode가 퍼사드 내에서 이루어지도록 구현해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//홈페이지에 노출되는 이벤트
const getEvent = async () => {
return await facade.get({
url: "maple-test-api-this-is-not-existing-url",
header: { paging: { pageNo: 1, pageSize: 10 } },
params: { platform: "homePage" },
});
};

const facade = {
get: function ({ url, header, params }) {
return new Promise((resolve) => {
//header에서 페이징 정보를 base64 encode함
const header_encoded = {
paging: btoa(JSON.stringify(header.paging)),
};
console.log("[GET] event data from API");
console.log("[url] ", url);
console.log("[header] ", header);
console.log("[header_encoded] ", header_encoded);
console.log("[params] ", params);

//보통 node.js에서는 axios모듈로 서버로부터 땡겨옴
//완전 클라이언트 사이드 코딩이라면 fetch메서드 혹은 jquery ajax사용
//지금은 연동할 수 있는 API가 없으니 setTimeout으로 대체
const result = {
header: {
paging: "eyJwYWdlTm8iOjEsInBhZ2VTaXplIjoxMCwidG90YWxQYWdlIjo0fQ==",
},
body: [
{
name: "진의 월드와이드 핸섬!",
start_date: "20220901",
end_date: "20220906",
},
{
name: "썬데이 메이플",
start_date: "20220904",
end_date: "20220904",
},
],
};
setTimeout(() => {
resolve({
//base64 encoded된 페이징 정보를 decode하여 전달
header: atob(result.header.paging),
body: result.body,
});
}, 2000);
});
},
};

async function getData() {
try {
const { header, body } = await getEvent();
console.log("\n2000ms passed\n");
console.log(header);
console.log(body);
} catch (error) {
console.log(error);
}
}

getData();

Proxy(프록시) Pattern

클라이언트 개발은 서버나 DBMS에 비해, 다루는 데이터의 양이 많지는 않다.
개인적인 의견으로는 FE에서 신경써야할 부분은 “서버로 요청 - 서버로부터 응답받음”에 소모되는 자원과
그렇게 받아온 데이터의 상태를 관리하는 영역이라고 생각한다.

프록시 패턴은 다른 객체/함수(A)에 직접 접근을 막기 위해 한번 우회하는 객체/함수(B)를 사용하는 패턴이다.
이 때 B를 잘 활용한다면 앞서 말한 네트워킹에 소모되는 자원을 줄일 수 있다.

PS(problem-solving)에서 Dynamic Programming(메모이제이션)과 비슷한 개념이다.

아래 예제에는 스킬들이 특정 쿨타임이 지나지 않으면 발동할 수 없도록 Proxy 함수를 구현했다.
예제에서 GUI나 사용자 입력은 구현이 번거로우므로 랜덤 스킬을 선택해 발동한다.
Proxy 함수 내 캐시에는 최근 스킬 발동 시간을 저장한다.
스킬 호출 시간과 캐시에 저장된 시간의 차이가 200ms보다 클 때만 스킬을 사용한다.
200ms 쿨타임이 지나지 않으면 사용할 수 없음 문구를 출력한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 쿨타임이 있는 스킬 사용하는 API
function SkillUsingAPI() {
this.getValue = function (coin) {
switch (coin) {
case "frenzy":
return new Date().getTime();
case "execution":
return new Date().getTime();
case "shield_chasing":
return new Date().getTime();
}
};
}

// 스킬 사용 API를 우회하여 접근하는 함수
function SkillUsingAPIProxy() {
this.api = new SkillUsingAPI();
this.cache = {};

this.getValue = function (skill) {
//쿨타임은 실제로 모두 다르겠으나, 테스트를 위해 편의상 200ms로 통일해둠
if (this.cache[skill] && new Date().getTime() - this.cache[skill] < 200) {
console.log(`...... ${skill} 쿨타임이 지나지 않아 사용할 수 없습니다.`);
} else {
this.cache[skill] = this.api.getValue(skill);
console.log(
`++++++ ${skill} 사용완료, 최근 사용 시간 ${this.cache[skill]}`
);
}

return this.cache[skill];
};
}

const proxyAPI = new SkillUsingAPIProxy();

//3가지 중 랜덤하여 스킬 선택
const getRandomSkill = () => {
const skill_array = ["frenzy", "execution", "shield_chasing"];
const idx = Math.floor(Math.random() * skill_array.length);
return skill_array[idx];
};

//스킬 사용을 위해 50ms로 매크로를 걸어두었다.
let skillUsingMacroTest = setInterval(() => {
proxyAPI.getValue(getRandomSkill());
}, 50);

//1초 후 매크로 해제(매크로 20회 발동)
setTimeout(() => {
clearInterval(skillUsingMacroTest);
}, 1000);