* 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에서는 이를 이용해서 회원가입등 비즈니스 로직을 처리한다.
- 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";
}
}
- 홈 컨트롤러 추가
import {ControllerV4} from "../ControllerV4";
export class HomeController implements ControllerV4{
process(paramMap: Map<string, string>, model: Map<string, object>): string {
return "index";
}
}
- 구현이 매우쉬워졌다.
'JS > boostCamp' 카테고리의 다른 글
24.10.5~7. 개발일지 // 회원가입구현, redirect, cookie, session, 인증 (0) | 2024.10.07 |
---|---|
24. 10. 4. 개발일지 // 정적서빙버그 fix (2) | 2024.10.05 |
24. 10. 2. 개발일지 // 프론트컨트롤러 v3, 동적렌더링, mapToObj (2) | 2024.10.02 |
24. 10. 1. 개발일지 // 미들웨어, 프론트 컨트롤러, static serving, 동적렌더링 (0) | 2024.10.01 |
24.9.30. 개발일지 // ts설정, favicon, view to dist, 조건부 요청 (0) | 2024.09.30 |