웹 MVC 개발
섹션 5. 회원 관리 예제 - 웹 MVC 개발
간단한 예제를 통해 mvc 패턴을 이해한다.
HomeController
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
내장 톰캣 서버가 우선은
- 스프링 컨테이너에 관련 Controller가 있는지 우선 찾고,
- 없으면 static 파일을 찾는다.
등록된 Controller에서 request로 들어온 url과 맵핑된 처리를 한다.
위 예시에서는 로컬 환경에서 8080 포트로 스프링 서버를 실행하였기 때문에,
http://localhost:8080 만 입력하게 되면 resource의 home.html을 가져와 응답으로 전달한다.
home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1>
<p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</div> <!-- /container -->
</body>
</html>
"/members/new" - 회원 가입 요청
"/members" - 회원 목록 조회 요청
두 요청을 연결하기 위한 html 파일을 만들었다.
MemberController
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping(value = "/members/new")
public String createForm() {
return "members/createMemberForm";
}
}
home으로 들어와서 ‘회원 가입’을 누르게 되면 http://localhost:8080/members/new로 요청이 전달된다. 맵핑에 따라서 members/createMemberForm 의 경로에 따라 createMemberForm.html 파일을 응답으로 전달한다.
createMemberForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<form action="/members/new" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을 입력하세요">
</div>
<button type="submit">등록</button>
</form>
</div> <!-- /container -->
</body>
</html>
클라이언트는 방금 받은 createMemberForm에서 name을 입력하여 서버로 전달할 것이다. 따라서 이에 대한 새로운 Controller, MemberForm을 만들어서 서버단에서 회원가입 처리를 하도록 한다.
MemberForm
package hello.hellospring.controller;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
웹 등록 화면에서 데이터 name을 전달 받을 class를 만들고, 캡술화를 위해서 private으로 선언한 뒤, getter, setter를 만든다.
실제로 클라이언트가 값을 입력하여 데이터를 전달 받고, 서버에 저장하는 로직은 다음과 같다.
HomeController
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/members/new")
public String createForm(){
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(MemberForm form){
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
}
먼저 home에서 회원가입 버튼을 누르면 home.html에서 확인할 수 있듯이
- “members/new”로 HTTP의 “GET” 메소드로 요청이 전달되고,
- 해당 요청이 Controller로 전달되면 @GetMapping 어노테이션에 의해서 resource의 createMemberForm.html이 응답으로 전달된다.
- 회원 가입 창에서 클라이언트가 name을 입력하면, “POST”메소드로 요청이 전달되고,
- 해당 요청이 Controller로 전달되면 @PostMapping 어노테이션으로 맵핑된 create 메소드가 실행된다.
- 위에서 만든 MemberForm클래스인 form을 만들고, MemberForm의 setter로 방금 받은 name을 form의 name으로 저장한다.
- 그 다음 MemberService의 join메소드를 호출하여 회원가입 로직을 실행한다.
- 회원가입 절차가 끝나면, 다시 home 화면으로 리다이렉트한다.
주목할 점은, Controller에서는 요청에 대해서 서버가 어떤 동작을 해야할 지에 대한 처리를 관리하고, 실제 해당 동작에 대한 로직은 Service에서 관리하는 것을 확인할 수 있다.
이어서 회원 목록 조회를 위해 Controller에 내용을 추가한다.
MemberController
...
@GetMapping("/members")
public String list(Model model){
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
...
- 홈화면에서 회원 목록을 클릭하면, “/members”로 Get 요청이 전달된다.
- Controller에서 맵핑된 list가 실행되고,
- MemberService의 findMember 메소드로 memberRepository에 등록된 회원을 가져온뒤,
- model에 추가하였고,
- “members/memnerList”로 리다이렉트 시켰다.
memberList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
- 참고
MemberService
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* 회원가입
* */
public Long join(Member member){
//같은 이름이 있는 중복 회원X
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
MVC 패턴 (Model-View-Controller)
3개로 역할을 나누어 개발하는 방법론을 의미한다.
- Model : 어플리케이션이 “무엇”을 할 것인지를 정의한다.
- ex) 처리되는 알고리즘, DB 와 상호작용(CRUD Create Read Update Delete), 데이터 등등..
- Controller : Model이 어떻게 처리할 지를 알려주는 역할을 한다.Service layerservice는 데이터를 받아서 실제 비즈니스 로직을 처리하는 것이며, DB의 활용이 필요한 경우 해당 처리를 하는 DAO를 호출하는 역할을 수행한다.Service단을 두지 않으면, 비슷하거나 같은 로직이더라도 Controller로 모든 것을 처리하면 재사용이 어렵고 변화에 대응하기 어려우며 확장하기 어려워진다.결국 정리하면, Controller는 view와 model의 중개인이다.
- 스프링은 아니지만, https://github.com/runner-be/RunnerBe-Server - 이러한 패턴을 활용해서 내가 만들었던 프로젝트를 소개한다. 참고하면 이해하는데 도움이 될 것이다.
- 이렇게 나눈 이유는, 비즈니스 로직의 재사용성을 높이기 위해 만들었다.
- controller는 처리해야 할 데이터를 프론트단에서 전달 받고, 이를 처리할 service를 선택하여 호출하는 역할을 수행한다.
- 사용자의 요청을 받아서 처리되는 부분을 구현한다.
- View는 화면에 보여주기 위한 역할을 한다
이러한 MVC패턴은 프로젝트의 크기가 커질수록 Controller에 다수의 Model과 View가 복잡하게 연결되면서 유지 보수가 점점 어려워진다.
MVP, MVVM, Viper, Clean Architecture, Flux, Redux, RxMVVM….등 이를 해결하기 위해 새로운 패턴들이 파생되었다.
더 알아보면 좋은 것들
- REST API
- HTTP Method
- Query String, Pathvariable