Day 77. Spring Framwork 2 : DI, 스프링MVC 기본

ho_c·2022년 6월 9일
0

국비교육

목록 보기
61/71
post-thumbnail

지난 시간에 이어, 스프링 프레임워크의 핵심들을 알아보자.

1. DI : 의존성 주입

이제 두 객체 간의 의존관계도 직접 DL을 하거나, new로 생성을 하지 않는다. 결과적으로 세팅은 개발하지만 전반적인 과정은 스프링이 진행한다.

일단 DI로 객체 간의 의존관계를 만드는 방식은 2가지이다.

① 생성자를 사용
② Setter를 사용

이제 이를 직접 코드상에 명시하는 것이 아닌, xml 설정 파일을 통해서 스프링 컨테이너 생성 시 자동으로 만들어지게 한다.

먼저 다음의 두 클래스가 있다.

[ SamsungTV.class ]

public class SamsungTV implements TV{
	
	private Speaker speaker ;

	public SamsungTV() {
	}
	
	
	public SamsungTV(Speaker speaker) {
		System.out.println("삼성TV가 생성되었음");
		this.speaker = speaker;
	}

	public void powerOn() {}

	public void powerOff() {}



	public Speaker getSpeaker() {
		return speaker;
	}

	public void setSpeaker(Speaker speaker) {
		this.speaker = speaker;
	}
	
}

[ AppleSpeaker.class ]

public class AppleSpeaker implements Speaker{
	
	public AppleSpeaker() {
		System.out.println("Apple Speaker Setting Complete");
	}

	public void volumUp() {
	}

	public void volumDown() {
	}

}

일단 두 클래스 모두 각자의 인터페이스를 가지고 있고, 삼성TV 클래스는 Apple 스피커 클래스를 내부에서 사용할 것이다. 즉, 삼성TV가 애플 스피커를 의존한다.

따라서 이 의존관계를 앞선 2가지, Setter와 생성자를 통해서 만들어줄 수 있다. 물론 DL을 사용해도 되지만, 더 깔끔한 방법인 DI를 권장하며, new 생성자는 결합도을 직접 꽂아버리는 것이기 때문에 사용하지 않는다.

이제 결합을 해보자. 결합 자체는 코드 상이 아닌, XML파일을 통해서 진행한다. 나의 경우 스프링 컨테이너가 분석할 파일의 이름을 context.xml로 설정하였다.

[ context.xml ] : 빈 생성

<bean id="tv" class="kh.spring.tv.SamsungTV"></bean>
<!-- TV tv = new SamsungTV(); -->
	
<bean id="speaker" class="kh.spring.speaker.AppleSpeaker"></bean>
<!-- Speaker speaker = new AppleSpeaker(); -->

1) 생성자 사용

앞선 DL에서 배웠듯이, <bean> 태그는 스프링 컨테이너가 메모리에 생성되면서 생성할 클래스를 명시한다. 그래서 아래 코드는 2개의 인스턴스를 생성한다.

이 과정에서 삼성TV 인스턴스가 생성되면서, 그 때 Speaker speaker라는 참조변수를 매개변수로 전달해야 한다. 이를 위해서 사용하는 태그가 바로 <constructor-arg>이다.

[ context.xml ]

<bean id="tv" class="kh.spring.tv.SamsungTV">
	<constructor-arg ref="speaker"></constructor-arg>
	<!-- new SamsungTV(speaker) -->
</bean>
<!-- TV tv = new SamsungTV(); -->
	
<bean id="speaker" class="kh.spring.speaker.AppleSpeaker"></bean>
<!-- Speaker speaker = new AppleSpeaker(); -->

결과적으로 위와 같이 작성할 수 있다.

추가로 <constructor-arg><bean> 태그 안에 사용할 수 있는 내부 태그로 2가지 속성이 있다.

  • value : 매개변수의 자료형이 기본형 int, String, long, boolen 같은 경우 사용
  • ref : 참조변수일 경우

그리고 그 값으로 전달할 bean의 id 속성 값을 넣어주면 된다.

번외로 삼성TV에 인스턴스 변수와 생성자를 추가하면 다음도 가능하다.

private int price;

public SamsungTV(Speaker speaker, int price) {
	System.out.println("삼성TV가 생성되었음");
	this.speaker = speaker;
	this.price = price;
}
<bean id="tv" class="kh.spring.tv.SamsungTV">
	<constructor-arg ref="speaker"/>
	<constructor-arg value="1000000"/>
	<!-- new SamsungTV(speaker, price) -->
</bean>

2) Setter 사용

Setter의 경우에는 <property>라는 내부 태그를 사용하고, 생성자의 경우와 마찬가지로 속성이 존재한다.

  • name : 세팅할 변수명 → setSpeaker, setPrice 가 호출된다.
  • ref : 참조변수일 경우 사용
  • value : 기본형일 경우 사용

따라서 xml에서는 다음과 같이 사용한다.

<bean id="tv" class="kh.spring.tv.SamsungTV">
	<!-- Setter -->
	<property name="speaker" ref="apple"/>
	<property name="price" value="1000000"/>
</bean>

2. Spring MVC

이제 웹 프로젝트로 넘어가보자. 그 전에 Spring을 이용해서 웹 프로젝트를 하려면 다음 2가지를 알아야 한다.

  • Spring MVC
  • Spring 부팅 순서

Spring MVC를 보기 전 간단한 요약으로 MVC1부터 쭉 살펴보자.

MVC1

[ 구조 ]
Client(JSP) → Tomcat → JSP( Control) → DAO (Model) → DB → DAO → JSP ( Control ) → JSP (View)

MVC1은 프론트와 백엔드가 한 JSP에 묶여있어, 애초에 코드 분류가 안되기 때문에 최소한의 유지보수가 불가능했다. 그래서 나온 게, 지난 세미 내내 사용했던 MVC2이다.


MVC2

[ 구조 ]
Client → Tomcat → Servlet(Controller) → DAO(Model) → DB → DAO → Servlet → JSP (View)

MVC2의 경우, 프론트와 백엔드가 JSP와 서블릿으로 구분된다. 그래서 사용자의 요청에 대해 톰캣이 맵핑으로 요청을 전달하고 서블릿 컨트롤러를 거쳐 DAO로 넘어가 DB 작업을 처리한다.

DB작업 결과는 컨트롤러가 JSP에 보내서 EL/JSTL을 통해서 View를 채운 뒤, 클라이언트에게 반환한다. 결과적으로 MVC2의 포인트는 프론트, 백엔드, DB의 구분이 확실하게 이뤄져서 유지 보수가 가능하다는 것이다.

하지만 MVC2의 경우도 의존성 문제가 존재하고, 코드 중복도(맵핑 시, 조건문 반복)와 유지 보수의 한계가 있었다. 이를 해결할 수 있는게 바로 ‘Spring MVC’이다.


Spring MVC

[ 구조 ]
Client(URL) → Tomcat → DS (Front Controller) → HM → DS → HandlerAdapter → Controller (service) → DAO → DB → DAO → (Service) → HandlerAdapter → DS → View Resolver(객체) → JSP

  • DS : Dispatcher Servlet
  • HM : HandlerMapping
  • HA : HandlerAdapter

(1) Tomcat

스프링이 추구하는 POJO는 상속을 지양하여 서블릿을 거의 사용하지 않는다. 다만 사용자가 URL을 통해 요청하게 되면, WAS인 Tomcat으로 넘어가게 된다.

(2) Dispatcher Servlet

이후 톰캣은 요청을 서블릿으로 보내는데, 이 서블릿은 우리 아닌 스프링이 만드는 서블릿이며, Dispatcher Servlet이라고 한다.

  • Spring MVC에서 진정한 의미의 프론트 컨트롤러
  • ‘흐름 제어’가 중심 기능이다.
  • 요청이 들어오는 순간에 생성된다.
  • 사용자 요청의 URL을 처리하기 위해, Handler Mapper 인스턴스를 통해 URL과 매칭되는 컨트롤러를 탐색한다.

(3) HandlerMapping

Handler Mapper는 각 URL에 연결된 Controller의 정보를 담고 있다. 그래서 URL을 확인한 뒤, 적합한 Controller를 다시 Dispatcher한테 전달한다.

  • URL과 Controller의 정보를 담고 있다.
  • @Controller을 대상으로 탐색한다.

(4) HandlerAdapter - Controller

그러면 Dispatcher는 HandlerAdapter를 통해 처리를 위임하고, 선택된 컨트롤러가 로직을 실행한 뒤 결과를 리턴하면 다시, HA가 ModelAndView 객체에 담아서 DS에 리턴한다.

(5) ViewResolver

호출의 결과를 사용자에게 보여주기 위해선, View를 선택해야 한다. 컨트롤러로부터 반환받는 값에 미리 세팅해둔 접두사와 접미사를 연결시켜서 ‘경로’를 만들어낸다.

  • DispatcherServlet 인스턴스가 만들어지면서, 함께 만들어진다. (요청이 있을 때)
  • return 으로 경로 코드를 쉽게 처리할 수 있다.
  • 경로 설정에 대해 유지 보수가 용이하다.
  • 숙련되면 여러 개를 설정해서 분산처리가 가능하다.
  • 사용 언어와 환경이 바뀔 때, 다른 맵칭 정보를 연결해줄 수 있다.

스프링 부팅 과정

이번에는 웹 프로젝트로서 스프링이 실행될 때, 부팅 순서를 알아보자.

(1) Tomcat 실행

웹 프로젝트니 당연히 서버가 먼저 실행되어야 한다. 따라서 Tomcat이 제일 먼저 실행된다. 그런데, 톰캣이 실행되면서 src/main/webapp/web.xml을 분석한다.

이때, web.xml파일을 보면 다음과 같은 코드가 있다.

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

이 코드는 new를 통해서 ContextLoaderListener 인스턴스를 생성한다.

(2) ContextLoaderListener 생성

해당 인스턴스 내부에는 다음과 유사한 코드가 숨겨져 있다.

AbstractApplicationContext ctx = new GenericXmlApplicationContext("context.xml");

지난 시간에 배운 것처럼 위 코드는 스프링 컨테이너 인스턴스를 메모리에 생성하는 것이다. 즉, 톰캣이 켜지면서 ContextLoaderListner가 만들어지고, 이후 스프링 컨테이너가 call-back으로 만들어진다.

(3) Spring Container 생성

다만 스프링 컨테이너는 그 상단에 있는 경로를 참조한다.

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>

root-context.xml은 웹과 관련이 없는 jdbc, json에 대한 설정 문서이다. 결과적으로 서버가 동작하면서 스프링 컨테이너는 자신의 영역 안에 개발자가 설정해둔 bean을 보관하면서 만들어진다. 그리고 DI를 통해서 각각을 조립해둔다.

이렇게 부팅이 일차적으로 끝나고, 일시적인 대기 상태에 돌입한다.


앞전 세팅이 끝나고 대기 상태인 서버에 Client가 요쳥(Request)하는 순간, 다음 단계가 실행된다.

(1) DispatcherServlet(DS)객체 생성

Tomcat이 요청을 받는 순간, 메모리에는 DispathcerServlet 객체가 적재된다.
앞 부분읜 web.xml의 하단을 보면 요청 시 다음의 프로세스가 실행된다고 적혀 있다.

<!-- Processes application requests -->
<servlet>
	<servlet-name>appServlet</servlet-name>
DispatcherServlet 객체 생성	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
생성 시, 참조하는 경로		<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

(2) servlet-context.xml 참조

스프링 컨테이너 생성 시, root-context.xml를 참조한 것처럼 DS는 servlet-context.xml을 참조한다.

해당 문서는 웹 프로젝트과 관련된 설정이 담겨져 있는데 여기서 두 가지 동작이 일어난다.

<annotation-driven/>

위 문서를 참조하면 먼저 @Controller 처리된 컨트롤러들을 모두 분석하여 내부 URL이 어떤 것이 있는지 확인하고 그 정보를 담은 HandlerMapping 인스턴스를 만들어 둔다. 이 정보는 해당 컨트롤러에 어떤 url이 입력되어 있는지에 대한 것이다.

ViewResolver 객체 생성

그 다음으로는 ViewResolver라는 빈을 생성한다.

<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<beans:property name="prefix" value="/WEB-INF/views/" />
	<beans:property name="suffix" value=".jsp" />
</beans:bean>

이 부팅 순서의 요점은 servlet-context의 빈들은 요청이 없으면 만들어지지 않는다는 점이다. 즉, 생성 시점 자체가 첫 요청 이후이기 때문에 그전에는 사용할 수 없다.


3. 스프링 기본 사용법

1) HandlerMapping

스프링에서 @RequestMapping(“/”)의 역할은 MVC2에서 if(uri.equals(“/”)){} 와 같은 조건식과 똑같다.

2) ViewResolver

ViewResolver는 말 그대로 결과를 반영해서 보내줄 View를 찾는 기능이다. 사실 컨트롤러의 return 안에 직접 그 경로를 입력해줘도 된다.

하지만 그 경우 유지보수가 어려워지기 때문에 ViewResolver을 통해서 경로 String을 최대한 효율적으로 작성하고, 변경 시에도 ViewResolver만 수정하면 된다.

이러한 ViewResolver가 View를 찾고 DS에게 돌려주면, DS는 기본 forward 방식을 따라서 페이지 이동한다. (이때, JSP는 외부접근을 막아 보안을 하기 위해서 WEB-INF 폴더에 저장한다.

3) Redirect 하기

일반 MVC2에서 응답을 하는 방법은 forwardredirect를 response 객체를 통해서 개발자가 자유롭게 설정하였다.

하지만 스프링에서는 기본값은 forward이다. 때문에 생기는 문제 중 하나는 URL이 비워지지 않기 때문에 같은 요청을 반복해서 보낼 수 있다.

이를 해결하기 위해선 url을 비우는 리다이렉트를 해야하는데, 스프링의 경우 전제 조건이 있다.

  • return 값을 “redirect: view”로 보낸다.
  • 리다이렉트 하는 경로는 컨트롤러에 RequestMapping이 되어 있는 곳에만 가능하다.

이렇게 redirect가 붙어 있으면 그 결과를 ViewResolver한테 DS가 보내지 않는다.

4) 프론트로부터 값 전달 받기

스프링의 편의기능 중 하나가, 바로 넘겨 받는 값을 자동으로 DS가 채워준다는 것이다.

먼저 MVC2에서 값을 넘겨받기 위해서는 다음과 같이 프론트의 name값을 getParameter();로 끌어왔다.

String writer = request.getParameter(“writer”);
String contents = request.getParameter(“contents”);

스프링에서도 이와 같이 받아올 수 있지만, 중요한 것은 컨트롤러 메서드에 기본 Servlet처럼 매개변수가 존재하지 않는다. 어떻게 해야될지 고민이 되겠지만 스프링에서는 개발자가 매개변수를 정하면 자동으로 그 값을 넣어준다.

매개변수는 3가지 경우로 나뉜다.

(1) request, response

이 경우는 MVC2와 동일하다. 다만 우리가 매개변수를 채워줄 뿐이다.

@RequestMapping("inputProc")
public String inputProc(HttpServlet request) { 
	String writer = request.getParameter("writer");
	String contents = request.getParameter("contents");

	System.out.println(writer + " : " + contents);
	
	return "redirect:/"; 
	}

(2) String name명

동일하게 프론트에 표기된 name 속성의 값과 매개변수가 같다는 전제하에서는 단순 매개변수로도 쉽게 받아줄 수 있다.

@RequestMapping("inputProc")
public String inputProc(String writer, String contents) { 
	System.out.println(writer + " : " + contents);

	return "redirect:/"; 
	}

(3) DTO

확장적으로 여러 개의 매개변수를 사용하지 않고, DTO 생성해두면 DS가 자동으로 값을 세팅해준다.

[ MessagesDTO ]

public class MessagesDTO {
	
	private int seq;
	private String writer;
	private String contents;
	private Date write_date;
	
	public MessagesDTO() {
		super();
	}

	public MessagesDTO(int seq, String writer, String contents, Date write_date) {
		super();
		this.seq = seq;
		this.writer = writer;
		this.contents = contents;
		this.write_date = write_date;
	}

	public int getSeq() {
		return seq;
	}

	public void setSeq(int seq) {
		this.seq = seq;
	}

	public String getWriter() {
		return writer;
	}

	public void setWriter(String writer) {
		this.writer = writer;
	}

	public String getContents() {
		return contents;
	}

	public void setContents(String contents) {
		this.contents = contents;
	}

	public Date getWrite_date() {
		return write_date;
	}

	public void setWrite_date(Date write_date) {
		this.write_date = write_date;
	}
}
@RequestMapping("inputProc")
public String inputProc(MessagesDTO dto) { 
	System.out.println(dto.getWriter() + " : " + dto.getContents());

	return "redirect:/"; 
	}

그 원리는 DTO를 작성하고, 그 안의 멤버필드의 이름과 name속성 값이 일치할 때, 이를 매개변수로 활용하면 DS가 Setter를 통해서 값을 세팅한다.

쉽게 말하면 다음 과정을 스프링이 실행해주는 것이다.

MessagesDTO dto = new MessagesDTO(); // 기본 생성자
dto.setWriter(writer);
dto.setContents(contents);

다만 한 가지 주의점은 무조건 기본 생성자가 존재해야된다는 것이다. 우리가 흔히 사용할 때 기본 생성자를 덮어씌운 생성자만 활용하는데, 이 경우 스프링이 생성자를 만들어도 빈값이 존재하기 때문에 setter를 사용할 수 없어 400 bad request가 발생한다.

profile
기록을 쌓아갑니다.

0개의 댓글