research

Web Presentation Patterns — 실전 예시와 템플릿

Web Presentation Patterns — 실전 예시와 템플릿

이론: 08-web-presentation-patterns

예시: Academic Life System의 MVC 구현

Page Controller 방식 (Drogon)

각 Controller가 특정 URL 그룹을 담당하고, 해당 도메인 객체와 대화한다:

// Page Controller 예시 (Drogon 스타일)
class RecommendationController : public HttpController<RecommendationController>:
    
    // GET /recommendations/{studentId}
    getRecommendations(req, resp):
        studentId = req.getParameter("studentId")
        semester = req.getParameter("semester")
        
        // Domain에 위임
        student = Student.find(studentId)
        recommendations = student.getRecommendations(semester)
        
        // View에 데이터 전달
        data = {
            "student": student,
            "recommendations": recommendations,
            "semester": semester
        }
        resp.render("recommendation_list.csp", data)

MVC 전체 흐름

Front Controller가 필요해지는 순간

Page Controller로 시작했는데 이런 상황이 생기면:

// 모든 Controller에 반복되는 코드
class StudentController:
    getProfile(req, resp):
        if not authenticate(req): return 401    // 반복!
        if not authorize(req, "student"): return 403  // 반복!
        log(req)                                 // 반복!
        ...

class RecommendationController:
    getRecommendations(req, resp):
        if not authenticate(req): return 401    // 같은 코드!
        if not authorize(req, "student"): return 403
        log(req)
        ...

→ Front Controller로 공통 처리를 분리:

Drogon에서는 이 역할을 Filter/Middleware가 담당한다.

Application Controller 예시

복잡한 화면 흐름이 있을 때 — 예: "수강 신청 마법사" (3단계 프로세스):

class EnrollmentWizardController:
    // 화면 흐름 관리
    getNextPage(currentStep, input) -> PageInfo:
        switch currentStep:
            case "select_courses":
                if input.courses.isEmpty():
                    return PageInfo("select_courses", error="과목을 선택하세요")
                return PageInfo("confirm", data=input)
            
            case "confirm":
                if input.confirmed:
                    return PageInfo("result", data=processEnrollment(input))
                return PageInfo("select_courses", data=input)
            
            case "result":
                return PageInfo("done")

템플릿

Page Controller 템플릿

// ============================================
// [기능영역]Controller (Page Controller)
// URL 패턴: /[base-path]/*
// ============================================

class [기능영역]Controller:

    // GET /[base-path]/
    index(req, resp):
        [목록데이터] = [도메인객체].findAll()  // 또는 findBy조건()
        resp.render("[목록뷰].csp", {"items": [목록데이터]})

    // GET /[base-path]/{id}
    show(req, resp):
        [id] = req.getParameter("id")
        [객체] = [도메인객체].find([id])
        if [객체] == null:
            resp.renderError(404)
            return
        resp.render("[상세뷰].csp", {"item": [객체]})

    // POST /[base-path]/
    create(req, resp):
        // 1. 입력 파싱
        [데이터] = parseRequest(req)
        
        // 2. 검증
        errors = validate([데이터])
        if errors:
            resp.render("[입력폼].csp", {"errors": errors, "data": [데이터]})
            return
        
        // 3. 도메인 로직 실행
        [결과] = [도메인서비스].[동작]([데이터])
        
        // 4. 리다이렉트 또는 결과 표시
        resp.redirect("/[base-path]/" + [결과].id)

    // PUT /[base-path]/{id}
    update(req, resp):
        [id] = req.getParameter("id")
        [객체] = [도메인객체].find([id])
        [변경사항] = parseRequest(req)
        [객체].update([변경사항])
        [객체].save()
        resp.redirect("/[base-path]/" + [id])

    // DELETE /[base-path]/{id}
    destroy(req, resp):
        [id] = req.getParameter("id")
        [객체] = [도메인객체].find([id])
        [객체].delete()
        resp.redirect("/[base-path]/")

MVC 시퀀스 다이어그램 템플릿

URL → Controller 매핑 테이블

## [프로젝트명] URL 라우팅

| HTTP Method | URL Pattern | Controller.Method | 설명 |
|-------------|------------|-------------------|------|
| GET | /[리소스]/ | [리소스]Ctrl.index | 목록 |
| GET | /[리소스]/{id} | [리소스]Ctrl.show | 상세 |
| GET | /[리소스]/new | [리소스]Ctrl.newForm | 생성 폼 |
| POST | /[리소스]/ | [리소스]Ctrl.create | 생성 처리 |
| GET | /[리소스]/{id}/edit | [리소스]Ctrl.editForm | 수정 폼 |
| PUT | /[리소스]/{id} | [리소스]Ctrl.update | 수정 처리 |
| DELETE | /[리소스]/{id} | [리소스]Ctrl.destroy | 삭제 |

View 선택 가이드

## [프로젝트명] View 전략

### Template View 사용 시 (대부분의 경우)
- 프레임워크: __________ (CSP, Jinja, Razor 등)
- 공통 레이아웃: __________ (base template)
- 뷰 파일 위치: __________

### 로직 침투 방지 규칙
- [ ] View에 3줄 이상의 연속된 로직이 없는가?
- [ ] View에서 DB를 직접 호출하지 않는가?
- [ ] 조건부 표시는 도메인 객체의 boolean 메서드를 호출하는가?
  - ✅ `if student.isEligible()`
  - ❌ `if student.gpa >= 3.0 and student.credits >= 130`

Page Controller vs Front Controller 비교

| 기준 | Page Controller | Front Controller | |------|----------------|-----------------| | 복잡도 | 단순한 사이트 | 복잡한 네비게이션 | | 공통 처리 | 각 Controller에서 반복 또는 상속 | 한 곳에서 일괄 처리 | | URL 구조 | /recommendations, /enroll | /app?action=recommend | | 프레임워크 | 대부분 기본 제공 | 직접 구현 또는 프레임워크 지원 | | 추천 | 대부분의 경우 이걸로 시작 | 인증/인가/로깅이 복잡할 때 |

Two Step View — 다국어/멀티 테마 시

Application Controller — 화면 흐름 State Diagram

템플릿: View에서 필요한 데이터 명세

## [프로젝트명] View 데이터 명세

| View | 필요한 데이터 | 타입 | 출처 |
|------|-------------|------|------|
| RecommendationView | student | Student | StudentMapper |
| RecommendationView | courses | List<Course> | RecommendationService |
| TranscriptView | enrollments | List<Enrollment> | Student.getEnrollments() |

### 공통 처리 (Cross-cutting)
- [ ] 인증: (방식 기술)
- [ ] 로깅: (방식 기술)
- [ ] 에러 처리: (공통 에러 페이지 또는 JSON 에러 응답)
- [ ] CORS: (필요 여부)