관리 메뉴

Mini

24.10.3. 개발일지 // MemberSaveController 구현, 프론트컨트롤러 v4구현, 유연한 컨트롤러 구현, 어댑터패턴, instanceof interface 본문

JS/boostCamp

24.10.3. 개발일지 // MemberSaveController 구현, 프론트컨트롤러 v4구현, 유연한 컨트롤러 구현, 어댑터패턴, instanceof interface

Mini_96 2024. 10. 3. 23:53

* MemberSaveController 구현

  • 먼저 domain > member 구현이 필요.
export class Member {

    private id: number; // DB의 AutoIncrement Id
    private userId: string;
    private name: string;
    private password: string;
    private email: string;

    constructor(id: number, userId : string ,name : string , password: string, email: string) {
        this.id = id;
        this.userId = userId;
        this.name = name;
        this.password = password;
        this.email = email;
    }

    public setPassword(password: string) : void {
        this.password = password;
    }

    public setUsername(name : string) : void {
        this.name = name;
    }

    public setId(id: number) : void {
        this.id = id;
    }

    public getId() {
        return this.id;
    }
}
  • 저장소 구현
import { Member } from "./Member";

export class MemberRepository {
    private static instance: MemberRepository | null = null;
    private store: Map<number, Member> = new Map();
    private sequence: number = 0;

    /**
     * 생성자를 private으로 선언하여 외부에서 직접 인스턴스를 생성할 수 없게 합니다.
     */
    private constructor() {}

    /**
     * MemberRepository의 유일한 인스턴스를 반환합니다.
     * 인스턴스가 없으면 새로 생성하여 반환합니다.
     * @returns {MemberRepository} MemberRepository의 유일한 인스턴스
     */
    public static getInstance(): MemberRepository {
        if (MemberRepository.instance === null) {
            MemberRepository.instance = new MemberRepository();
        }
        return MemberRepository.instance;
    }

    /**
     * 새로운 Member를 저장소에 추가합니다.
     * @param {Member} member - 저장할 Member 객체
     * @returns {number} 저장된 Member
     */
    public save(member: Member): Member {
        member.setId(++this.sequence);
        this.store.set(member.getId(), member);
        return member;
    }

    /**
     * ID로 Member를 조회합니다.
     * @param {number} id - 조회할 Member의 ID
     * @returns {Member | undefined} 조회된 Member 객체 또는 undefined
     */
    public findById(id: number): Member | undefined {
        return this.store.get(id);
    }

    /**
     * 모든 Member를 반환합니다.
     * @returns {Member[]} 저장된 모든 Member 객체의 배열
     */
    public findAll(): Member[] {
        return Array.from(this.store.values());
    }
}
  • save controller 구현
import {ControllerV3} from "../ControllerV3";
import {ModelView} from "../../ModelView";
import {Member} from "../../../domain/member/Member";
import {MemberRepository} from "../../../domain/member/MemberRepository";

export class MemberSaveController implements ControllerV3{

    private memberRepository: MemberRepository = MemberRepository.getInstance();

    process(paramMap: Map<string, string>): ModelView {
        const userId: string = paramMap.get('userId');
        const name: string = paramMap.get('username');
        const email : string = paramMap.get('email');
        const password: string = paramMap.get('password');

        const member = new Member(0, userId, password , name, email);
        this.memberRepository.save(member);

        const mv : ModelView = new ModelView("save-result");
        mv.getModel().set("member",member);
        return mv;
    }
}

 

* paramMap 분석

  • 이번의 핵심은 paramMap이다.
  • 역할 : request의 key, value를 저장해서 controller에 넘겨주기
  • controller에서는 이를 이용해서 회원가입등 비즈니스 로직을 처리한다.

body에는 object형태로 값이 들어있다.

  • obj 형태를 map으로 바꾸면 될듯?
export function objectToMap<T = any>(obj: { [key: string]: T }): Map<string, T> {
    return new Map(Object.entries(obj));
}
  • 프론트 컨트롤러 코드수정
//컨트롤러가 req를 몰라도 되도록 Map에 req 정보를 담아서 넘김
const paramMap : Map<string, string> = objectToMap(req.body); //여기에 body같은것들 다 넘겨야함!
const mv: ModelView = controller.process(paramMap);

결과

 

* 회원가입 폼 수정

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>회원 가입</title>
</head>
<body>
<div class="container">
  <h1>회원 가입</h1>
  <h2>회원 정보 입력</h2>
  <form action="save" method="POST">
    <div class="form-group">
      <label for="name">userId</label>
      <input type="text" id="userId" name="userId" required>
    </div>
    <div class="form-group">
      <label for="name">name</label>
      <input type="text" id="name" name="name" required>
    </div>
    <div class="form-group">
      <label for="password">비밀번호</label>
      <input type="password" id="password" name="password" required>
    </div>
    <div class="form-group">
      <label for="email">이메일</label>
      <input type="text" id="email" name="email" required>
    </div>

    <div class="button-group">
      <button type="submit" class="btn-primary">회원 가입</button>
      <button type="button" class="btn-secondary" onclick="location.href='/login/'">취소</button>
    </div>
  </form>
</div>
</body>
</html>

 

* memberList도 실제로 repository에서 data를 가져오도록 수정해보자

export class MemberListControllerV3 implements ControllerV3{

    private memberRepository: MemberRepository = MemberRepository.getInstance();

    process(paramMap: Map<string, string>): ModelView {
        const members = this.memberRepository.findAll();

        const mv : ModelView = new ModelView("members");
        mv.getModel().set("members",members);

        return mv;
    }
}
  • members.ejs
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>회원 목록</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            line-height: 1.6;
            margin: 0;
            padding: 20px;
            background-color: #f4f4f4;
        }
        .container {
            max-width: 800px;
            margin: auto;
            background: white;
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        h1 {
            color: #333;
            text-align: center;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        th {
            background-color: #f2f2f2;
            font-weight: bold;
        }
        tr:hover {
            background-color: #f5f5f5;
        }
        .no-members {
            text-align: center;
            color: #666;
            margin-top: 20px;
        }
    </style>
</head>
<body>
<h1>회원목록</h1>
<ul>
    <table>
        <thead>
        <tr>
            <th>ID</th>
            <th>이름</th>
            <th>email</th>
        </tr>
        </thead>
        <tbody>
    <% members.forEach(function(member) { %>
            <tr>
                <td><%= member.userId %> </td>
                <td><%= member.name %></td>
                <td><%= member.email %></td>
            </tr>
    <% }); %>
        </tbody>
    </table>
</ul>
</body>
</html>

결과

 

* 프론트컨트롤러 v4구현

  • 기존의문제 : Controller를 구현하는 개발자가 ModelView객체를 만들어서 Front Controller에 줘야함.
  • 해결 : 프론트 컨트롤러에서 model의 참조값을넘기고, controller에서는 model에 내용만 채운후, viewname만 반환하면된다.

  • model은 프론트 컨트롤러에서 넘겨받은 model 이다. 여기에 값을 채운다.
  • viewname만 리턴
export interface ControllerV4{
    /**
     * @param paramMap
     * @param model
     * @return viewName
     */
    process (paramMap : Map<string, string> , model : Map<string, object> ): string;
}
  • controller 매우간단.
import {ControllerV4} from "../ControllerV4";

export class MemberFormControllerV4 implements ControllerV4{

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        return "new-form";
    }
}

여기만 수정하면 된다.

import {MemberRepository} from "../../../domain/member/MemberRepository";
import {ControllerV4} from "../ControllerV4";

export class MemberListControllerV4 implements ControllerV4{

    private memberRepository: MemberRepository = MemberRepository.getInstance();

    process(paramMap: Map<string, string>, model: Map<string, object>): string{
        const members = this.memberRepository.findAll();

        model.set("members",members);

        return "members";
    }
}
  • 프론트 컨트롤러 수정
    • controller에서 viewname을 반환해줌, model 도 채워줌
    • 그걸 그대로 사용하면된다.
    • 뭔가 프레임워크와 개발자 모두 편해진 느낌이다.

주석부분 : 기존코드
결과

* 유연한 컨트롤러 구현

  • 기존 문제 : ControllerV4와 같은 특정 컨트롤러 타입에 의존적이다.
  • 해결 : 어댑터 패턴을 통해 어떤 유형의 핸들러(컨트롤러) 이던지 처리하도록 개선.

설계

  • 핸들러어댑터 설계
    • support => 지원하는 컨트롤러 인지 type 확인
    • handle => 유사 process, ModelView return
import {ModelView} from "../ModelView";
import {Response} from "../../was/response";
import {Request} from "../../was/request";

export interface MyHandlerAdapter {
    supports(handler : Object): boolean;

    handle(req : Request, res : Response) : ModelView
}
  • ts는 최상위객체가 any이다.
import {ModelView} from "../ModelView";
import {Response} from "../../was/response";
import {Request} from "../../was/request";

export interface MyHandlerAdapter {
    supports(handler : any): boolean;

    handle(req : Request, res : Response) : ModelView
}
  • 인터페이스에는 instanceof도 지원이안된다.
  • -> 구체적으로 비교필요..
export class ControllerV3HandleAdapter implements MyHandlerAdapter {
    /**
     * interface에는 instanceof 사용 불가능
     * -> 세부비교 필요
     * @param handler
     */
    supports(handler : any ): boolean {
        return (
            typeof handler === "object" &&
            handler !== null &&
            "process" in handler &&
            typeof (handler as ControllerV3).process === "function"
        );
    }
  • 강제형변환은 (type) 대신 as 키워드를 사용하면된다.

  • 핸들러 어댑터 구현
    • paramMap을 컨트롤러에게 넘김
    • controller는 modelview를 반환.
    • 그걸 프론트컨트롤러에게 리턴 
    • V3를 지원하는 어댑터!
    • 역할 : 어댑터 : any의 handler(컨트롤러)를 V3컨트롤러로 바꿔줌 && controller.process 실행 , V4전용 등...
import {MyHandlerAdapter} from "../MyHandlerAdapter";
import {ModelView} from "../../ModelView";
import {ControllerV3} from "../../v3/ControllerV3";
import {objectToMap} from "../../../utils/utils";
import {Response} from "../../was/response";
import {Request} from "../../was/request";

export class ControllerV3HandleAdapter implements MyHandlerAdapter {
    /**
     * interface에는 instanceof 사용 불가능
     * -> 세부비교 필요
     * @param handler
     */
    supports(handler : any ): boolean {
        return (
            typeof handler === "object" &&
            handler !== null &&
            "process" in handler &&
            typeof (handler as ControllerV3).process === "function"
        );
    }

    handle(req: Request, res: Response , handler : any): ModelView {
        const controller  = handler as ControllerV3;
        req.body;
        //컨트롤러가 req를 몰라도 되도록 Map에 req 정보를 담아서 넘김
        const paramMap : Map<string, string> = objectToMap(req.body);
        const mv = controller.process(paramMap);

        return mv;
    }
}
  • 프론트 컨트롤러 수정
    • controllerMap -> handlerMappingMap으로 명칭변경
    • Adapters 목록을 배열로 관리
    • // 어댑터 : any의 handler(컨트롤러)를 V3컨트롤러로 바꿔줌 && controller 실행 , V4전용 등...
    •  
import {Response} from "../../was/response";
import {Request} from "../../was/request";
import {ModelView} from "../ModelView";
import {MyView} from "../MyView";
import * as path from "path";
import {objectToMap} from "../../utils/utils";
import {ControllerV4} from "./ControllerV4";
import {MyHandlerAdapter} from "./MyHandlerAdapter";
import {MemberFormControllerV3} from "../v3/controller/MemberFormControllerV3";
import {MemberListControllerV3} from "../v3/controller/MemberListControllerV3";
import {ControllerV3HandleAdapter} from "./adapter/ControllerV3HandleAdapter";
import {MemberSaveControllerV3} from "../v3/controller/MemberSaveControllerV3";



export class FrontControllerServletV5 {

    private readonly urlPatterns:string;
    private handlerMappingMap : Map<String,any> = new Map<String, ControllerV4>; //모든타입(any)의 핸들러(컨트롤러)지원
    private handlerAdapters : MyHandlerAdapter[]= []; // 핸들러 어댑터들 저장. 핸들러 어댑터 목록

    constructor() {
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members/save", new MemberSaveControllerV3());
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members/new-form", new MemberFormControllerV3());
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members", new MemberListControllerV3());

        this.handlerAdapters.push(new ControllerV3HandleAdapter());
    }

    public service(req : Request, res : Response){
        // if(!this.handlerMappingMap.has(reqURI)){
        //     res.status(404).send();
        //     return;
        // }

        /**
         * 일단 any로 가져온다. (어떤 컨트롤러가 올지모름)
         */
        const reqURI : string = req.path;
        const handler : any = this.handlerMappingMap.get(reqURI);
        if(!handler){
            res.status(404).send();
            return;
        }

        const adapter = this.getHandlerAdapter(handler);
        const mv = adapter.handle(req,res,handler);

        const viewName = mv.getViewName();
        const view: MyView = this.viewResolver(viewName); //물리이름이 들어간 MyView 객체 만들기

        view.renderEjs(mv.getModel(),req,res);
    }

    /**
     * 핸들러 어댑터 목록을 완탐하면서
     * 지금 들어온 핸들러가 지원 목록에 있는지 확인,
     * 그것 리턴.
     * @param handler
     * @private
     */
    private getHandlerAdapter(handler: any) : MyHandlerAdapter {
        for (const adapter of this.handlerAdapters) {{
            if (adapter.supports(handler)) {
                return adapter;
            }
        }}
        throw new Error("handler adapter를 찾을수 없습니다. handler =" + handler);
    }

    /**
     * 논리이름을 (members)
     * 물리이름으로 변환 (~~~/dist/views/members.html)
     * @param viewName
     * @private
     */
    private viewResolver(viewName: string):MyView {
        const viewPath: string = path.join(process.cwd(), 'dist', 'views',viewName+'.html');
        const view = new MyView(viewPath);
        return view;
    }
}

 

 

* forEach의 문제

  • 의도 : 맞는 핸들러를 찾은경우 찾은 어댑터를 리턴
  • 문제 : forEach는 return 을 무시함 (???!!!)

??? 레전드

private getHandlerAdapter(handler: any) : MyHandlerAdapter {
    let find = 0;
    this.handlerAdapters.forEach(adapter => {
        /**
         * 목록에있는경우,
         * 각자의 인터페이스 또한 지원하는지 확인
         * ex : MemberListControllerV3는 ControllerV3를 support
         */

        if (adapter.supports(handler)) {
            find = 1;
            return adapter;
        }
    })
  • 해결 : for of 혹은 find, 혹은 고차함수 사용.
private getHandlerAdapter(handler: any) : MyHandlerAdapter {
    for (const adapter of this.handlerAdapters) {{
        if (adapter.supports(handler)) {
            return adapter;
        }
    }}
    throw new Error("handler adapter를 찾을수 없습니다. handler =" + handler);
}

 

* flow chart

  • 회원등록폼 호출 가정
  • request의 url에 맞는 MemberFormControllerV3 반환
  • 어댑터 목록을 완탐 -> 찾음 (V3HandlerAdapter) 반환
  • V3HandlerAdapter.handle 호출, (매개변수로 어댑터 넘김)
  • any type의 MemberFormControllerV3 를 강제형변환
  • MemberFormControllerV3 . process 호출
  • render (new-form)

 

* V4버전의 컨트롤러도 추가

  • V4어댑터를 만들면된다.
    • V4의 프론트 컨트롤러에서 했던 역할을 여기서 일부 해주면된다!
    • V4버전도 프론트 컨트롤러에 modelView를 만들어서라도 넣어줘야한다.
    • V4에서 했던것 : model을 인자로넘겨서, controller에서 값넣기 && viewName return
@@ -0,0 +1,40 @@
import {MyHandlerAdapter} from "../MyHandlerAdapter";
import {Response} from "../../../was/response";
import {Request} from "../../../was/request";
import {ControllerV3} from "../../v3/ControllerV3";
import {ModelView} from "../../ModelView";
import {objectToMap} from "../../../utils/utils";
import {ControllerV4} from "../../v4/ControllerV4";
import {MyView} from "../../MyView";

export class ControllerV4HandleAdapter implements MyHandlerAdapter {

    /**
     * Controller V4의 구현체인지 확인
     * @param handler
     */
    supports(handler : any): boolean {
        return (
            handler !== null &&
            "process" in handler &&
            typeof (handler as ControllerV4).process === "function" &&
            (handler as ControllerV4).process.length === 2
        );
    }

    handle(req: Request, res: Response , handler : any): ModelView {
        const controller  = handler as ControllerV4;

        //컨트롤러가 req를 몰라도 되도록 Map에 req 정보를 담아서 넘김
        const paramMap : Map<string, string> = objectToMap(req.body);
        const model : Map<string, object> = new Map<string, object>();

        const viewName = controller.process(paramMap, model);

        const mv = new ModelView(viewName);
        mv.setModel(model); //set도 해줘야함!

        return mv;
    }

}
No newline at end of file

 

* interface 구분문제

  • 문제 : 아래의 조건만으로는 이게 V3의 구현체인지 ,V4의 구현체인지 알수없음. -> V4의 구현체인데 V3의 support에서 true가 되면서 bug가 생김.
supports(handler : any): boolean {;
    return (
        handler !== null &&
        "process" in handler &&
        typeof (handler as ControllerV4).process === "function" 
    );
}
export interface ControllerV4{
    /**
     * @param paramMap
     * @param model
     * @return viewName
     */
    process (paramMap : Map<string, string> , model : Map<string, object> ): string;
}

이것과

import {ModelView} from "../ModelView";

export interface ControllerV3{

    process (paramMap : Map<string, string>) : ModelView;
}

이것을 구분하는 방법?
instanceof?
  • 해결 : 매개변수의 갯수로 구별
    • 함수명.length => 매개변수의 갯수를 얻을수 있다.
supports(handler : any): boolean {;
    return (
        handler !== null &&
        "process" in handler &&
        typeof (handler as ControllerV4).process === "function" &&
        (handler as ControllerV4).process.length === 2
    );
}
supports(handler : any): boolean {;
    return (
         handler !== null &&
        "process" in handler &&
        typeof (handler as ControllerV3).process === "function" &&
         (handler as ControllerV3).process.length === 1
    );
}
  • 프론트 컨트롤러에서는 생성자에 새로운 컨트롤러추가 && 어댑터추가만 해주면된다!
export class FrontControllerServletV5 {

    private readonly urlPatterns:string;
    private handlerMappingMap : Map<String,any> = new Map<String, ControllerV4>; //모든타입(any)의 핸들러(컨트롤러)지원
    private handlerAdapters : MyHandlerAdapter[]= []; // 핸들러 어댑터들 저장. 핸들러 어댑터 목록

    constructor() {
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members/save", new MemberSaveControllerV3());
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members/new-form", new MemberFormControllerV3());
        this.handlerMappingMap.set("/front-controller/v5/v3/"+"members", new MemberListControllerV3());

        this.handlerMappingMap.set("/front-controller/v5/v4/"+"members/save", new MemberSaveControllerV4());
        this.handlerMappingMap.set("/front-controller/v5/v4/"+"members/new-form", new MemberFormControllerV4());
        this.handlerMappingMap.set("/front-controller/v5/v4/"+"members", new MemberListControllerV4());

        this.handlerAdapters.push(new ControllerV3HandleAdapter());
        this.handlerAdapters.push(new ControllerV4HandleAdapter());
    }

결과

 

* 프론트 컨트롤러 정리

  • 생성자 set 부분을 외부에서 의존성 주입하면 더 깔끔해진다.

  • 설계에서 모두 인터페이스 기반으로 잘 설계가 되었다.
  • 이제 Annotation 기반의 컨트롤러를 만들어서 어댑터만 잘 만들면 원하는 컨트롤러를 얼마든지 추가 할수있다.
  • 스프링이 동작하는 방식이 모두 interface로 만들고 구현체만 바꾸는 방식으로 매우 유연하다.
  • 스프링도 과거에 이방식을 사용하다가 @ 가 유행하니 @기반의 컨트롤러를 추가한것이다.

 

  • 이제 어느정도 프레임워크를 만들었다.
  • 요구사항을 구현해보자.

 

* / 접속시 index.html 로딩 구현

import {ControllerV4} from "../ControllerV4";

export class HomeController implements ControllerV4{

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        return "index";
    }
}

추가된 부분
server.ts에서 모든 응답에대해 frontController 실행하도록 수정

  • 홈 컨트롤러 추가
import {ControllerV4} from "../ControllerV4";

export class HomeController implements ControllerV4{

    process(paramMap: Map<string, string>, model: Map<string, object>): string {
        return "index";
    }
}
  • 구현이 매우쉬워졌다.

결과