백엔드/코딩 자율학습 스프링 부트 3 자바 백엔드 개발 입문 1~2장

2장. MVC 패턴 이해와 실습

dlng23 2024. 11. 30. 15:38

2.1 뷰 템플릿과 MVC 패턴

뷰 템플릿

화면을 담당하는 기술, 웹 페이지(View)를 하나의 틀(Template)로 만들고 여기에 변수를 삽입해 서로 다른 페이지로 보여줌

1장에서 스프링 부트 프로젝트를 만들 때 추가한 Mustache 도구가 뷰 템플릿을 만드는 도구임

 

MVC 패턴

화면을 담당하는 뷰 템플릿을 ''라고도 부름

컨트롤러(Controller)는 클라이언트의 요청에 따라 서버에서 처리하는 역할

모델(Model)은 데이터를 관리하는 역할

 

웹 페이지를 화면에 보여주고(View), 클라이언트의 요청을 받아 처리하고(Controller), 데이터를 관리하는(Model) 역할을 나누는 기법을 MVC 패턴(Model-View-Controller Pattern)이라고 함

 

 

2.2 MVC 패턴을 활용해 뷰 템플릿 페이지 만들기

뷰 템플릿 페이지 만들기

1. templates 디렉터리에서 New → File 을 눌러 greetings.mustache 파일 생성

확장자인 mustache는 뷰 템플릿을 만드는 도구, 뷰 템플릿 엔진

mustache의 기본 위치는 src > main > resources > templates

템플릿 엔진으로는 mustache 외에 Thymeleaf, JSP 등이 있음

 

2. mustache 파일을지원하는 플러그인을 발견했다고 뜨면 Install Handlebars/Mustache plugin 을 클릭하여 설치

더보기

메뉴에서 선택해 머스테치 플러그인 설치하는 방법

메뉴에서 File → Settings plugins [Marketplace] mustache 검색 Handlebars/Mustache 선택 [install] 

3. 제일 윗줄에 doc 입력한 후 Tab 키를 누르면 기본 HTML 코드가 자동으로 작성됨

본문에 <h1>홍팍님, 반갑습니다!</h1> 추가

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Document</title>
</head>
<body>
    <h1>홍팍님, 반갑습니다!</h1>
</body>
</html>

뷰 템플릿 작성 완료

이 페이지를 웹 브라우저에서 보기 위해 컨트롤러와 모델 이용

 

컨트롤러 만들고 실행하기

컨트롤러 만드는 위치:

src > main > java 디렉터리에 존재하는 기본 패키지 com.example.firstproject에 controller 패키지 생성

 

1. com.example.firstproject.controller 패키지 생성

 

2. controller 패키지에서 FirstController 클래스 생성

일반적으로 컨트롤러 이름은 ' ~ Controller ' 로 지음

 

3. 아래와 같이 작성

  • 이 클래스가 컨트롤러임을 선언하는 @Controller 어노테이션 작성
    → Controller 클래스 패키지(org.springframework.stereotype.Controller)가 자동 생성됨
  • 반환형이 문자열인 niceToMeetYou() 메서드 선언, 공백 문자열("")을 반환하도록 return ""; 추가
package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;

@Controller
public class FirstController {

    public String niceToMeetYou(){
        return "";
    }
}
더보기

클래스 패키지 자동으로 임포트하기

File → Settings 클릭 후 왼쪽 목록에서 Editor > General > Auto import 선택

Insert imports on paste 옵션을 Always로 선택, 아래 체크 박스 2개 모두 선택 후 [Apply], [OK] 클릭

  • insert imports on paste
    - Always : 코드 붙여 넣기 시 자동으로 import
    - Never : 코드 붙여 넣기 시 자동으로 import 하지 않음
    - Ask : 코드 붙여 넣기 시 확인한 후 선택해 import
  • Add unambiguous imports on the fly : 코드 변경 시 필요한 패키지 자동으로 삽입
  • Optimize imports on the fly : 코드 변경 시 불필요한 패키지 자동으로 삭제

 

4. niceToMeetYou() 메서드로 greetings.mustache 페이지를 반환하려면 파일 이름인 greetings만 반환값으로 적어주면 됨

return "greetings"; 로 작성

서버가 알아서 templates 디렉터리에서 greetings.mustache 파일을 찾아 웹 브라우저로 전송

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;

@Controller
public class FirstController {

    public String niceToMeetYou(){
        return "greetings";
    }
}

 

5. 서버 실행 후, localhost:8080/greetings.mustache 로 접속 → 404 Not Found 에러

 

6. FirstController에 페이지(greetings.mustache)를 반환해 달라는 URL 요청을 접수하는 부분 추가

  • niceToMeetYou() 메서드 앞에 @GetMapping() 추가 
    → 자동으로 org.springframework.web.bind.annotation.GetMapping 패키지 임포드 됨
  • @GetMapping 괄호 안에 URL 주소인 "/hi" 넣어줌
    localhost:8080/hi 로 접속하면 geetings.mustache 파일을 찾아 반환하라는 의미
package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class FirstController {

    @GetMapping("/hi")
    public String niceToMeetYou(){
        return "greetings";
    }
}

 

7. 서버 재시작 후, localhost:8080/hi  접속

더보기

한글 깨짐 현상(???, ????!)이 발생하는 경우

src > main > resources > application.properties 파일을 열어 
server.servlet.encoding.force=true 코드 추가 후 서버 재시작

 

모델 추가하기

1. "홍팍님, 반갑습니다!" 에서 다른 이름으로 바꾸기 위해 머스테치 문법을 사용하여 뷰 템플릿 페이지에 변수 삽입

{{변수명}}

greetings.mustache에서 홍팍님을 {{username}}님 이라고 수정

 

2. 서버 재시작 후, localhost:8080/hi 에 접속 → username 변수를 찾을 수 없어 에러 발생

에러를 해결하기 위해 모델 사용

 

3. 모델은 컨트롤러의 메서드에서 매개변수로 받아옴 

FirstController로 가서 niceToMeetYou() 메서드에 Model 타입의 model 매개 변수 추가

Model 클래스 패키지 임포트

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class FirstController {

    @GetMapping("/hi")
    public String niceToMeetYou(Model model){
        return "greetings";
    }
}

 

4. 모델을 통해 변수 등록 → addAttribute() 메서드 사용

model.addAttribute("변수명", 변숫값) //변숫값을 "변수명"이라는 이름으로 추가

niceToMeetYou() 메서드 내부에 model.addAttribute("username", "홍팍"); 코드 추가

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class FirstController {

    @GetMapping("/hi")
    public String niceToMeetYou(Model model){
        model.addAttribute("username", "홍팍");
        return "greetings";
    }
}

 

5. 서버 재시작 후 localhost:8080/hi에 접속

 

변숫값을 바꿔서도 출력해 봄

 

 

2.3 MVC의 역할과 실행 흐름 이해하기

/hi 페이지의 실행 흐름

FirstController.java 코드를 보며 컨트롤러 동작 이해해보기

 

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller // 1
public class FirstController {

    @GetMapping("/hi") // 2
    public String niceToMeetYou(Model model){ // 3, 4
        model.addAttribute("username", "dlng23"); // 5
        return "greetings"; // 6
    }
}

1. 이 파일이 컨트롤러 임을 선언

2. 클라이언트로부터 "/hi" 라는 요청을 받아 접수

3. "/hi" 라는 요청을 받음과 동시에 niceToMeetYou() 메서드 수행

4. 뷰 템플릿 페이지에서 사용할 변수를 등록하기 위해 모델 객체를 매개변수로 가져옴

5. 모델에서 사용할 변수 등록

6. 메서드 수행 겨과로 greetings.mustache 파일 반환 

 → 서버가 알아서 templates 디렉터리에 있는 해당 뷰 템플릿 페이지 찾아 웹 브라우저로 전송

 

컨트롤러가 @GetMapping("/hi")로 클라이언트의 요청을 받으면 niceToMeetYou()메서드 수행

뷰 템플릿 페이지에서 사용할 변수는 모델을 통해 등록하고 메서드의 반환값으로 greetings.mustache 파일 반환

 

/bye 페이지의 실행 흐름

localhost:8080/bye 로 요청 받았을 때 "xx님, 다음에 또 만나요!" 출력

 

FirstController 그대로 사용

 

FirstController

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class FirstController {

    @GetMapping("/hi")
    public String niceToMeetYou(Model model){
        model.addAttribute("username", "dlng23");
        return "greetings";
    }

    @GetMapping("/bye") // 1
    public String seeYouNext(Model model){ // 2, 3
        model.addAttribute("nickname", "dlng23"); // 4
        return "goodbye"; // 5
    }
}

1. @GetMapping("/bye") 어노테이션 추가

2. /bye 요청을 처리할 seeYouNext() 메서드 생성

3. 모델 객체를 매개변수로 가져옴

4. 모델 변수 등록

5. 반환값에 보여 줄 뷰 템플릿 페이지 적음

 

goodbye.mustache

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Document</title>
</head>
<body>
    <h1>{{nickname}}님, 다음에 또 만나요!</h1>
</body>
</html>

templates 디렉터리에서 goodbye.mustache 파일 생성 후 코드 작성

 

서버 재시작 후 localhost:8080/bye 접속

 

컨트롤러는 클라이언트의 요청을 @GetMapping("/bye") 로 받음

return 값으로 goodbye.mustache 반환함

"nickname" 이라는 변수 등록, "dlng23" 이라는 값을 연결해 goodbye.mustache 파일에서 사용할 수 있게 함

 

 

2.4 뷰 템플릿 페이지에 레이아웃 적용하기

레이아웃(layout) : 화면에 요소를 배치하는 일

웹 페이지는 같은 요소로도 레이아웃에 따라 다른 느낌을 줄 수 있음

 

헤더 - 푸터 레이아웃(header-footer layout)

가장 기본이 되는 레이아웃, 샌드위치 구조

 

상단의 헤더(header) 영역에는 사이트 안내를 위한 내비게이션을 넣음

헤더와 푸터 영역 사이에는 사용자가 볼 핵심 내용인 content를 배치

하단의 푸터(footer) 영역에는 사이트 정보를 넣음

 

/hi 페이지에 헤더-푸터 레이아웃 적용하기

부트 스트랩(Bootstrap) : 웹 페이지를 쉽게 만들 수 있도록 작성해 놓은 코드 모음 

코드를 가져와 사용하기만 하면 됨

 

1. 부트스트랩 홈페이지 ( https://getbootstrap.com ) 에 접속 (v5.0.2 사용)

 

2. 스크롤을 내려 스타터 템플릿에서 [copy] 버튼을 눌러 코드 복사 

 

3. greetings.mustache 파일로 와 파일 내용을 모두 지운 후 복사한 내용 붙여 넣음

 

4. greetings.mustahe 페이지의 본문 영역을 <!-- --> 주석 표시를 이용하여 3개의 레이아웃으로 나눔

content 부분을 {{username}}님, 반갑습니다! 로 수정

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>Hello, world!</title>
</head>
<body>
    <!-- navigation -->

    <!-- content -->
    <h1>{{username}}님, 반갑습니다!</h1>

    <!-- site info -->

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

</body>
</html>

 

5. localhost:8080/hi 로 접속하여 잘 출력되는지 확인

 

6. 부트 스트랩 검색창에서 'navbar' 를 입력한 후 목록이 뜨면 Navbar 선택 

 

7. 코드 복사 후, greetings.mustache 파일로 와서 <!-- navigation --> 주석 아래에 붙여 넣기

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>Hello, world!</title>
</head>
<body>
    <!-- navigation -->
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="#">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                            Dropdown
                        </a>
                        <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                            <li><a class="dropdown-item" href="#">Action</a></li>
                            <li><a class="dropdown-item" href="#">Another action</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="#">Something else here</a></li>
                        </ul>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
                    </li>
                </ul>
                <form class="d-flex">
                    <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>
    
    <!-- content -->
    <h1>{{username}}님, 반갑습니다!</h1>

    <!-- site info -->

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

</body>
</html>

 

8. 서버 재시작 후 localhost:8080/hi 에 접속하여 확인

 

9. 푸터 영역에 사이트 정보 추가

    <!-- site info -->
    <div class="mb-5 container-fluid">
        <hr>
        <p>ⓒ Cloudstudying | <a href="#">Privacy</a> | <a href="#">Terms</a></p>
    </div>

 

10. ctrol + F9 (망치 아이콘)으로 서버를 재시작하지 않고 프로젝트를 빌드해 수정된 HTML 코드 빠르게 반영

 

11. localhost:8080/hi 새로고침

 

12. 콘텐트 영역 간격을 넓히기 위해 <div></div> 태그로 콘텐트 부분을 감싸고 부트스트랩에서 제공하는 class 속성 추가

배경 색상 어둡게 (bg-dark), 텍스트 색상 하얗게(text-white), 상하좌우 여백 5배만큼(p-5)

    <!-- content -->
    <div class="bg-dark text-white p-5">
        <h1>{{username}}님, 반갑습니다!</h1>
    </div>

 

/bye 페이지에 헤더-푸터 레이아웃 적용하기

/hi페이지를 템플릿화(코드를 하나의 틀로 만들어 변수화)하여 사용

 

템플릿 파일 만들고 적용하기

1. templates 디렉터리에서 새로운 디렉터리 layouts 생성

layouts 디렉터리에 header.mustache 파일, footer.mustache 파일 생성

 

2. greetings.mustache 코드에서 상단 내비게이션 바 부분 발췌하여 header.mustache 파일로 만듦

 

header.mustache

<!doctype html>
<html lang="en">
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

    <title>Hello, world!</title>
</head>
<body>
    <!-- navigation -->
    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
            <a class="navbar-brand" href="#">Navbar</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="#">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                            Dropdown
                        </a>
                        <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
                            <li><a class="dropdown-item" href="#">Action</a></li>
                            <li><a class="dropdown-item" href="#">Another action</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="#">Something else here</a></li>
                        </ul>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
                    </li>
                </ul>
                <form class="d-flex">
                    <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-success" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>

 

 

3. greetings.mustache 파일에서 잘라낸 부분을 템플릿으로 대체

{{>파일명}} 으로 작성

 

layouts 디렉터리 안에 header.mustache 파일을 만들었기때문에 {{>layouts/header}} 로 작성

 

greetings.mustache

{{>layouts/header}}

    <!-- content -->
    <div class="bg-dark text-white p-5">
        <h1>{{username}}님, 반갑습니다!</h1>
    </div>

    <!-- site info -->
    <div class="mb-5 container-fluid">
        <hr>
        <p>ⓒ Cloudstudying | <a href="#">Privacy</a> | <a href="#">Terms</a></p>
    </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

</body>
</html>

 

4. 같은 방식으로 푸터 영역부터 잘라내어 footer.mustache 파일에 붙여넣고 greetings.mustache에 {{>layouts/footer}} 작성

 

footer.mustache

<!-- site info -->
<div class="mb-5 container-fluid">
    <hr>
    <p>ⓒ Cloudstudying | <a href="#">Privacy</a> | <a href="#">Terms</a></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>

</body>
</html>

 

greetings.mustache

{{>layouts/header}}

    <!-- content -->
    <div class="bg-dark text-white p-5">
        <h1>{{username}}님, 반갑습니다!</h1>
    </div>

{{>layouts/footer}}

 

핵심 부분만 보이기 때문에 어떤 페이지인지 명확하게 인식하기 쉬움

 

5. 망치 아이콘 누르고 localhost:8080/hi 새로고침

문제 없이 잘 나옴

 

템플릿 적용하기(goodbye.mustache)

1. goodbye.mustache 파일

  • 콘텐트 영역을 제외하고 기존 코드를 지운 후 헤더 템플릿과 푸터 템플릿 삽입
  • 콘텐트 영역을 <div>로 감싸고 부트스트랩의 class 속성을 적용

2. 망치 아이콘 누르고 localhost:8080/bye 접속

greetings.mustache와 구분하기 위해 Controller에서 변수값을 dlng23→홍길동으로 수정하였음

 

 

셀프 체크

quote.mustache

{{>layouts/header}}
    <div class="bg-dark text-white p-5">
        <h1>{{randomQuote}}</h1>
    </div>
{{>layouts/footer}}

 

SecondController.java

package com.example.firstproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class SecondController {

    @GetMapping("random-quote")
    public String randomQuote(Model model){
        String[] quotes = {
                "행복은 습관이다. 그것을 몸에 지니라. " +
                        "-하버드-",
                "고개 숙이지 마십시오. 세상을 똑바로 정면으로 " +
                        "바라보십시오. -헬렌켈러-",
                "고난의 시기에 동요하지 않는 것, 이것은 진정 " +
                        "칭찬받을 만한 뜅난 인물의 증거다. -베토벤-",
                "당신이 할 수 있다고 믿든 할 수 없다고 믿든 " +
                        "믿는 대로 될 것이다. -헨리포드-",
                "작은 기회로부터 종종 위대한 업적이 시작된다. " +
                        "-데모스테네스-"
        };
        int randInt = (int) (Math.random() * quotes.length);
        model.addAttribute("randomQuote", quotes[randInt]);
        return "quote";
    }
}

 

localhost:8080/random-quote