Spring_Spring의 구성(Controller, Service, IoC, DI, Anotation, Redirect&Forward, Get&Post, 데이터의 흐름)
Controller와 Service
기본적으로 MVC패턴은 Model, View, Controller로 구현이 되어있다.
그런데 기본적으로 Spring Framwork의 MVC는 Model(DAO, DTO, Service), View, Controller로서 Model부분이 각각의 기능으로 구현된다는 부분에서 일반적인 MVC패턴과는 차이가 있다.
Spring Service
스프링은 기본적으로 Request를 Controller에서 받아서 Model로 넘겨주는데 이를 Model중에서도 Service에서 처리를 한다.
즉, 그 말은 컨트롤러에서 service를 가져올 객체를 생성한다는 것이다.
컨트롤러는 요청만 받아서 리턴을 해주는 역할이라 반드시 Service를 호출해야하며 이를 Controller의 Service 의존이라고 표현한다.
여기에서 Service는 서비스레이어(service Layer)단에서 세분화된 비지니스 로직을 처리하는 객체를 말한다.
Controller가 Request를 받으면 적절한 Service에 전달하고 전달받은 Service는 비지니스 로직을 처리한다.
그리고 각 데이터는 DTO로 데이터를 전달받고 DAO로 데이터 베이스를 접근하는 식으로 데이터가 흘러간다.
이때 Service는 하나만 만들고 여러개의 Controller가 하나의 Service를 호출할 수 도 있는데 가장 대표적인 예가 페이징처리기능이다.
Spring 객체생성
Member memberservice = new MemberService();
위는 일반적인 객체 생성 방법이다.
기존엔 개발자가 객체 선언과 할당을 모두 해주어야했다.
그러나 스프링을 사용하면 할당부분을 스프링에서 처리가 가능하다.
MemberService memberService;
그래서 기존과는 다르게 위처럼 선언만 해주면 되며,
이것이 스프링 프레임워크의 가장 대표적인 특징인 IOC(제어역전)이다.
IoC(Inversion of Control): 제어역전
JSP에선 선언과 할당의 제어를 본래 개발자가 진행하였다.
그러나 스프링에서는 제어부분을 스프링이 담당한다.
그렇게 제어하는 역할이 역전이 되었다 라고 하여 제어역전이라고 칭한다.
제어역전의 가장 큰 장점은 개발자가 프레임 워크의 기능을 호출하는 형태가 아니라 프레임워크가 개발자의 코드를 호출하기 때문에, 개발자는 전체를 직접 구현하지 않고 자신의 코드를 부분적으로 "끼워넣기"하는 형태로 구현할 수 있다. 이는 개발자로 하여금 구현하고자 하는 특정 분야의 기능에 집중할 수 있도록 한다.
프레임워크가 객체의 생성, 소멸과 같은 라이프 사이클을 관리하며 스프링으로부터 필요한 객체를 얻어올 수 있으며 객체 의존성을 역전시켜 객체간의 결합도를 줄이고 유연한 코드를 작성할 수 있게하여 가독성 및 코드중복, 유지보수를 편하게 할 수 있게 한다.
Spring Container
스프링 컨테이너는 자바 객체의 생명주기를 관리하며, 생성된 자바 객체들에게 추가적인 기능을 제공하는 역할을 한다.
개발자는 객체를 생성하고 소멸시킬 수 있는데, 스프링 컨테이너가 이 역할을 대신해준다. 즉, 제어의 흐름을 외부에서 관리하는 것이다.
또한 객체들간의 의존관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어준다.
스프링은 실행시 객체들을 담고있는 Container가 있고 그것이 스프링 컨테이너이다.
여기서 말하는 자바 객체를 스프링에서는 빈(Bean)이라고 부른다.
- MemberController - Bean1
- MemberService - Bean2
- MemberRepository - Bean3
이렇게 구분될 수 있으며, 이 Bean들은 어노테이션을 통해서 스프링으로 관리되며 생성자 주입방식을 통해서 서로 연결된다.
스프링은 스프링 컨테이너에 스프링Bean(객체)를 등록할 때 싱글톤으로 등록하는 것이 기본이다.
따라서 동일한 Spring Bean이면 모두 같은 인스턴스라고 생각한다.
Spring 어노테이션
스프링은 어노테이션을 통해서 필요한 의존객체의 "타입"에 해당하는 빈을 찾아서 주입한다.
어노테이션이 붙어있지않는 클래스들은 순수한 Java Class(POJO)이다.
어노테이션이 없다면 스프링은 해당 클래스가 어떤 역할을 하는지 알 수 없기에 각각의 객체들을 관리할 수 없다. 이에 정확한 어노테이션 작성이 중요시되며 의존성 주입을 할 대상을 찾지 못하면 애플리케이션 구동에 실패한다.
@Autowired
Autowired는 Controller를 생성할 때 작성해주는 어노테이션으로서 Service Class와 연결해준다.
단일 생성자에 한해서 Autowired는 생략 가능하다.
@Service
Controller에 Autowired라는 어노테이션이 붙듯이 Service 클래스 역시 Service라는 어노테이션이 붙는다.
@Repository
DB의 역할을 하는 클래스에 붙힌다.
POJO(Plain Old Java Object)란?
단순한 자바 오브젝트로서, 객체 지향적인 원리에 충실하면서 환경과 기술에 종속되지 않고 필요에 따라 재활용 될 수 있는 방식으로 설계된 오브젝트를 말한다.
그러한 POJO에 애플리케이션의 핵심 로직과 기능을 담아서 설계하고 개발하는 방법을 POJO 프로그래밍이라고 한다.
DI(Dependency Injection): 의존성 주입
객체간의 의존성이 존재할 경우 개발자가 직접 객체를 생성하거나 제어하는 것이 아니라, 제어반전에 의하여 특정 객체에 필요한 다른 객체를 프레임 워크가 자동으로 연결 시켜주는 것을 의미한다. 쉽게말하자면 각자의 클래스들을 이어주는 작성방법을 DI로 부른다.
그래서 개발자는 자신에게 필요한 객체를 직접 할당하지않고 interface를 통해 선언한 객체에 스프링 프레임 워크에 의해 주입받아 사용할 수 있기 때문에 비지니스 로직 개발에만 집중할 수 있다.
개발자는 객체를 선언만 할 뿐, 할당은 프레임워크에 의해서 자동으로 이루어진다.
DI의 3가지 방법
Field Injection(필드주입):권장x
@Autowired MemberService memberservice;
위와 같이 객체를 선언과 동시에 Autowired라는 어노테이션을 붙혀주면 필드로 바로 주입된다.
스프링 최초 버전에서는 주로 사용되었던 방법이지만 스프링은 순환참조 형식으로서 프로젝트가 진행되고 규모가 커질경우 혼란을 야기시킬 수 있다는 이유로 현재는 잘 사용하지는 않는다.
Setter Injection(수정자 주입):권장x
public void setMemberService(MemoryMemberRepository MemoryMemberRepository) {
this.memberRepository = memberRepository;
}
setter를 통해서 접근하는 방법이다.
이는 서비스구현체가 당장 주입되지 않아도 컨트롤러로 객체 생성이 가능하다.
메서드가 호출되는 시점에 주입이 되는것이기때문에 MemberService 객체가 없어도 당장은 에러가 나지 않는다. 그렇게 개발을 하다가 차후에 서버를 기동시키면 NullPointException 오류가 출력될 위험이 높기 때문에 해당 방법 역시 권장되지 않는 방법이다.
Constructor Injection(생성자 주입): 권장O
public MemberController(MemberService memberService){
this.memberService = memberService;
}
스프리은 여러가지 방법을 거치면서 DI하는 방법을 수정해왔고 지금 현재 안정적인 방법으로 선택된 것이 생성자 주입 방법이다.
또한 생성자 주입방법은 어플리캐이션 로딩시점에 스프링 컨테이너로 빈들이 생성이 되면서 각각의 연결고리가 맺어지게되는데 로딩시점에 이미 스프링 객체들간의 주입관계라던가 연결고리가 맺어진 상태로 실행이 되기 때문에 중간에 바꿀일이 없다고 판단하여 중간에 바뀔 가능성을 닫기위해 private final이라는 키워드로 객체를 잠궈버리며 fianl로 선언된 레퍼런스 타입 변수는 반드시 선언과 함께 초기화가 되어야한다. 단, setter주입시에는 의존관계 주입을 받을 필드에 final을 선언할 수 없다.
즉, 스프링에서 생성자 주입을 사용하는 가장 큰 이유는 객체가 불변하도록 할 수 있다는 점으로, 누군가가 Controller 내부에서 Service 객체를 바꿔치기 할 수 없다는 점이라고 할 수 있다.
예제를 통해서 알기
*MemberController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
package com.koreait.core.member.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import com.koreait.core.member.dto.Member;
import com.koreait.core.member.dto.MemberForm;
import com.koreait.core.member.service.MemberService;
@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";
}
// 회원 가입
@PostMapping(value = "/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
|
cs |
*Member.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package com.koreait.core.member.dto;
public class Member {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
|
cs |
*MemberForm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.koreait.core.member.dto;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
|
cs |
*MemberService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package com.koreait.core.member.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.koreait.core.member.dto.Member;
import com.koreait.core.member.repository.MemberRepository;
import com.koreait.core.member.repository.MemoryMemberRepository;
@Service
public class MemberService {
MemberRepository memberRepository;
// 생성자 주입
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 회원가입
public int join(Member member) {
memberRepository.save(member);
return member.getId();
}
// 전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
}
|
cs |
*MemberRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.koreait.core.member.repository;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.koreait.core.member.dto.Member;
public interface MemberRepository {
//회원 저장
Member save(Member member);
// 회원 전체 찾기
List<Member> findAll();
}
|
cs |
*MemoryMemberRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package com.koreait.core.member.repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;
import com.koreait.core.member.dto.Member;
@Repository
public class MemoryMemberRepository implements MemberRepository{
// 메모리 사용 - static
private static Map<Integer, Member> store = new HashMap<Integer, Member>(); // db처럼사용
private static int sequence = 0; // id
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public List<Member> findAll() {
return new ArrayList<Member>(store.values());
}
}
|
cs |
*home.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div>
<h1>Spring Boot</h1>
<p>회원관리</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</body>
</html>
|
cs |
*HomeController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package com.koreait.core.member.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
|
cs |
*createMemberForm.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div>
<form action="/members/new" method="post">
<div>
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을 입력하세요">
</div>
<button type="submit">등록</button>
</form>
</div>
</body>
</html>
|
cs |
*MemberList.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<div>
<table>
<thead>
<tr>
<th>no</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>
</body>
</html>
|
cs |
Point1. 각자의 역할
*MemberController.java: 컨트롤러
*Member.java: 아이디,이름 저장
*MemberForm.java: 이름저장
*MemberService.java: 서비스역할
*MemberRepository: dB역할
*MemoryMemberRepository.java: DB역할
*home.html: html
*createMember.html
Point2. 웰컴페이지 설정
templates안에 home.html과 index.html이 위치해있다고 가정해보자.
JSP경우엔 index.html이 웰컴페이지이기에 사용자는 html페이지로 무조건적으로 들어간다.
그런데 스프링에선 웰컴페이지를 본인이 설정할 수 있다.
HomeController에서 /를 Mapping시켜서 localhost:9090으로 들어오는 url은 모두 해당 컨트롤러를 지나가게 만들었고, return을 "home"으로 주어 사용자는 보이지않지만 서버 내부적으로는 HomeController를 지나서 Home.html로 데이터가 지나게된다.
정리하자면 Spring은 index.html이라는 웰컴페이지가 무조건적인 우선순위가 아니며 웰컴페이지를 메핑으로 개발자가 관리할 수 있음을 알 수 있다.
Point3. getmapping과 postmapping
지금 상황은 home.html은 기본값 Get방식을 사용해서 컨트롤러로 값을 이동시키고 createMember.html의 form에는 method="post"를 입력하여 post방식을 데이터를 넘겨주는 상황이다. 그리고 이 둘은 동일한 url을 가지고있다.
이 때 스프링은 동일한 url을 가지고있더라도 get과 post방식을 구분지어 데이터를 구분하며 @GetMapping과 @PostMapping이라는 어노테이션으로 구분지어 데이터를 전송하는 것이다.
즉, 서로 동일한 url을 가지고있어도 get과 post방식이 다르다면 스프링은 이를 구분하고 데이터를 정상적으로 전송하는 것이 가능하다.
Point4. Redirect와 Forward방식
스프링의 Redirect와 Forward로 데이터를 넘기는 방법이다.
스프링의 기본값은 Forward방식이기에 단순히 파란색박스처럼 return값만 입력하면된다.
Redirect로 변경하기위해서는 주황색 박스처럼 Redirect를 직접 기재해주어야하며 뒷부분에 url주소를 작성해준다.
현재 주황박스가 / 인 이유는 home화면으로 들어갈 때 homeController에서 / 이라는 경로를 입력해주었기 때문에 회원가입 후 다시 홈화면으로 돌아가라라는 명령의 의미를 가지고있다.
Point5. Spring의 장점: 객체지향방식
실무를 진행할 때 100% 갖추어져있는 상태에서 업무를 진행하는 경우는 운이 좋은 경우이다.
대부분의 경우에 어떤 한 부분의 개발을 진행하던 중간에 추가되고 보완되는 환경에서 작업을 하게되는데 스프링에서 추구하는 방식은 객체지향방식으로 이루어지기에 이러한 업무 방식에 적합하다고 할 수 있다.
각각의 기능들을 하나의 객체로 만들어서 끼워맞추듯 객체를 바꾸기만하면 작업을 할 수있기 때문이다.
그래서 Oracle을 사용할지 MySql을 사용할지 사용할 DB가 정해지지않았다고 가정하였고 MemberRepository.java와 MemoryMemberRepository.java를 생성하였다.
DB가 아직 연결준비가 되지 않았으나 어떠한 기능적 test를 원할 때는 우측의 java와 동일하게 코드를 작성하여 진행하면된다.
Point6. 데이터의 흐름
사용자가 9090host로 들어오면 HomeController에서 Mapping되어 home.html을 볼 수 있고 거기서 회원가입을 클릭하면 MemberController가서 get방식으로 Mapping되어서 creatememberForm을 호출한다.
거기서 사용자가 Form을 모두 입력 후 데이터를 보내면 post방식으로 요청이 가게되고 membercontroller에서 회원가입이 실행되고 아까와 동일한 방법으로 redirect로 오게된다.
회원목록도 위와 동일한 방법으로 데이터가 이동된다.