@Qualifier 어노테이션의 NoUniqueBeanDefinitionException 이슈 문제 해법

어노테이션을 이용하여 의존성을 주입하는 것 중에서 @Qualifier를 사용시 다음과 같은 에러가 발생하는 경우가 있다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony

에러 메시지에 나와 있는 것 처럼 의존성을 주입할 객체가 2개가 존재한다는 것이다(apple sony). 어떤 경우에서 이런 문제가 발생하는가?

public interface Speaker {
    public void volumeUp();
    public void volumeDown();
}

@Component("apple")
public class AppleSpeaker implements Speaker {
   public AppleSpeaker() {
        System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
    }
             ... 중 략 ...
}

@Component("sony")
public class SonySpeaker implements Speaker {
    public SonySpeaker() {
        System.out.println("▶▶▶▷  SonySpeaker 생성자 ----- 객체 생성");
    }
             ... 중 략 ...
}

위와 같을 경우 apple라는 id를 가진 AppleSpeaker 객체와 sony라는 id를 가진 SonySpeaker 객체를 Spring 컨테이너가 생성하게 된다.
이 두 객체는 둘 다 Speaker 타입의 인터페이스를 구현한 객체이다. 이를 경우 아래와 같이 Speaker 타입의 객체를 의존성 주입하고자 하면 Speaker 타입을 구현한 2개의 객체가 존재한다(AppleSpeaker, SonySpeaker). 따라서 아래의 speaker 변수에 어느 객체를 주입해야 할지 Spring 컨테이너가 결정할수가 없게된다. 따라서 위와 같은 에러를 발생하게 되는 것이다. 
그런데 이 문제를 해결하기 위해 @Qualifier라는 어노테이션이 존재하는데(혹은 @Resource) 이 어노테이션을 아래와 같이 적용을 해도 여전히 아래와 같은 동일한 에러가 발생한다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [polymorphism.Speaker] is defined: expected single matching bean but found 2: apple,sony


@Component
public class LgTV implements TV {

    @Autowired
    @Qualifier("apple")
    // @Resource(name="sony")
    private Speaker speaker;

    public LgTV() {
        System.out.println("▶▶▶▷  LG TV 생성자 ----- 객체 생성 ---");
    }
             ... 중 략 ...
}

이 이슈는 아마도 Spring 자체의 문제인듯하다(정확한건 잘 모르겠지만).
해결 하는 방법은 동일 타입의 객체가 여러개 있을 때 그 중 default로 주입할 클래스(혹은 최 우선 순위로 주입할 객체)를 선택할수 있게 하는 @Primary라는 어노테이션을 동일 타입의 클래스들 중 어느 하나에 지정해 주면 해결된다. 아래와 같이 

@Primary
@Component("apple")
public class AppleSpeaker implements Speaker {
    public AppleSpeaker() {
        System.out.println("▶▶▶▷  Apple Speaker 생성자~~~ 객체 생성 ~~~");
    }
             ... 중 략 ...
}

@Primary를 SonySpeaker 클래스나 AppleSpeaker 클래스느나 둘 중 어느 하나에 지정해 주면 문제가 깔끔하게 해결이 된다. 

전자정부프레임워크을 이용해서 웹 애플리케이션을 개발하면 통상적으로 MVC 모델 형태로 개발을 하게 된다.
이때 Controller 클래스(@Controller가 붙여지는 클래스)에 각종 사용자의 request 처리하는 코드가 모이게 된다.
따라서 request 관련을 살펴봐야 한다면 Controller 클래스만 보면 되므로 한 자리에서 파악할수 있는 잇점이 있어 편리하다.

그런데 Spring 프레임웤이 Controller 클래스를 인식하도록 할려면 환경을 어떻게 설정해야 하는가?
물론 전자정부프레임웤에서 

eGovFrame - Start - New Web Project - Project name과 Group Id 설정 후 - Next - Generate Example 창에서 

"Generate Example"을 체크하게 되면 전자정부프레임웤에서 모든 환경설정을 해주게 되고 Controller 클래스가 정상적으로 인식이 된다.
그러나 만일 전자정부프레임웤의 기본 환경설정과 다르게 설정하기 원한다거나 그렇게 디폴트로 생성된 파일들 등을 사용하기 원치않는다면 이때 어떻게 설정해야
Controller 클래스를 인식하게 할수 있을까?
이에 대한 환경 설정에 대해서 정리해 보고자 한다.
본 포스트는 이해를 수이하기 위해서 DB 설정은 제외하고 순전히 Controller 클래스의 등록에 대한 부분만 다룬다.

eGovFrame - Start - New Web Project - Project name과 Group Id 설정 후 - Next - Generate Example 창에서 - "Generate Example"을 체크해제 - Finish

이렇게 프로젝트를 생성해서 아래와 같이 해당 프로젝트를 실행하면 404 에러를 출력하는 페이지만 보일 것이다(프로젝트명이 TestTest라고 하자).

http://localhost:8081/TestTest/

그렇다고 프로젝트가 잘못 생성된 것은 아니다. 확인하기 위해서 간단하게 index.jsp를 생성해서 실행해 보자.
해당 프로젝트의 src/main/webapp/ 아래에 index.jsp를 생성해야 한다. 중요한것은 webapp 폴더가 Root 폴더로 인식되기 때문이다.
Webapp 폴더에서 마우스 우측 클릭 - New - JSP File - File name을 index.jsp로 지정 후 Finish

<body> 태그 안에 
<h2>Hello world. I am index.jsp</h2>
를 추가후 해당 프로젝트 명에서 우측 클릭 후 Run As - Run on Server - Finish

정상적으로 index.jsp의 내용이 보일것이다.

이제부터 Controller 클래스가 인식되도록 환경 설정을 할 것이다. 우선 web.xml 파일을 아래의 내용과 같이 설정 내용을 작성한다.
최초 web.xml에는 아래의 내용만 되어 있을 것이다.

  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>


아래는 web.xml의 Controller 클래스가 인식되도로 하기 위한 기본 설정 내용이다.


*** web.xml의 기본 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    id="WebApp_ID" version="2.5">
  <display-name>MyTest</display-name>
  
        <!-- 인코딩 관련 -->
  	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>utf-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>*.do</url-pattern>
	</filter-mapping>
    
	<!-- 크로스 스크립팅이라는 해킹 기법을 사전에 막는 기능. -->
	<filter>
		<filter-name>HTMLTagFilter</filter-name>
		<filter-class>egovframework.rte.ptl.mvc.filter.HTMLTagFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>HTMLTagFilter</filter-name>
		<url-pattern>*.do</url-pattern>
	</filter-mapping>
    

	<!-- 어떤 객체들을 미리 만들어 놓을지가 작성된 설정 파일의 경로를 값으로 할당 -->
 	<context-param>
		<param-name>contextConfigLocation</param-name>
		<!-- Spring 환경설정 파일인 context-*.xml을 읽은다. 
		     src/main/resources/egov/spring/context-*.xml을 의미한다. -->
		<param-value>classpath*:egov/spring/context-*.xml</param-value>
	</context-param>

	<!--  -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
    
	<!-- MVC에서 Controller 역할을 하게 될 DispatcherServlet 객체 등록.
		*.do라는 요청이 들어오면 appServlet이름의 서블릿 클래스인
		org.springframework.web.servlet.DispatcherServlet를 실행한다.
		DispatcherServlet 클래스에 대한 설정 내용은 /WEB-INF/spring/appServlet/dispatcher-servlet.xml에
		설정되어 있다.
	 -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/dispatcher-servlet.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
    
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>
		
	
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
</web-app>


아래는 context-root.xml을 설정내용인데 현재 시점에서는 아래와 같이 파일만 생성해 둔다.

*** context-root.xml 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
</beans>


아래는 DispatcherServlet을 위한 환경 설정이다.


*** dispatcher-servlet.xml 환경 설정 내용 ***

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
		xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
                
    <!-- xmlns:mvc="http://www.springframework.org/schema/mvc" -->
    
    	<!-- annotation을 사용하도록 설정  -->
    	<mvc:annotation-driven /> <!-- 근데 이것 없이도 정상적으로 동작함  -->
    	
   <!--
    base-package는 src/main/java/ 하위에 있는 패키지 명 중에서 ""안에 지정한 패키지 이하를 스캔하라는 뜻임.
    즉 base-package="egovframework"라고 하면 
    egovframework.example.cmmn
    egovframework.example.cmmn.web
    egovframework.example.sample.service
    egovframework.example.sample.service.impl
    egovframework.example.sample.web
    모두가 다 해당된다.
    
    base-package="egovframework.example"과 같이 해도 된다.
    
    다음과 같은 식으로도 가능
    <context:component-scan base-package="com.yk.yboard, com.yk.common"/>
    
    아래는 버스앱의 경우에 대한 것이다.
    <context:component-scan base-package="com.hubizict.bus" />
    
    <context:include-filter type="" expression=""/>는 자동 스캔 대상에 포함시킬 클래스를 의미
    <context:exclude-filter type="" expression=""/>는 자동 스캔 대상에 포함시키지 않을 클래스를 지정하는 의미 
     -->
    <context:component-scan base-package="com.joe">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    </context:component-scan>
    	
</beans>


이상의 작업을 통해서 eGov 프레임워크가 Controller 클래스를 인식하도록 설정이 되었다.
이제 Controller 클래스를 간단하게 만들어 보자. 해당 프로젝트의 src/main/java/ 아래에 패키지를 만든다.
src/main/java/ 위에서 마우스 우측 클릭 - New - Package
패키지 이름을 com.joe.egov.test를 만들어 보자.
생성된 패키지명 위에서 마우스 우측 클릭 - New - Class - MyController로 컨트롤러 클래스를 만들자.
이제 MyController 클래스를 다음과 같이 작성해 보자.


package com.joe.egov.test;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class MyController {
	@RequestMapping(value="/mytest.do", method=RequestMethod.GET)
	public String test(){
		System.out.println("####### Hello this is MyController 클래스 ");
		return "test.jsp";
	}
}


이제 webapp 폴더 아래에 test.jsp파일을 생성해서 원하는 내용으로 작성해두면 MyController 실행후 test.jsp가 실행되어 확인이 편리할 것이다.
아래와 같이 실행해서 컨트롤러 클래스가 정상적으로 작동하는지 확인해 보자.

http://localhost:8081/TestTest/mytest.do


Spring(혹은 전자정부프레임워크)에서 maven build가 정상적으로 수행된 후에 『Run As - Java Application』으로 해당 클래스를 실행했을 때 다음과 같은 에러가 발생했다면 무엇이 문제라는 것일까?


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'messageBean' defined in class path resource [context-hello.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.joe.MessageBean]: Specified class is an interface

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1105)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1050)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:510)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)

at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)

at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)

at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)

at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)

at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)

at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)

at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)

at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)

at com.joe.HelloApp.main(HelloApp.java:10)

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.joe.MessageBean]: Specified class is an interface

at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:68)

at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1098)

... 13 more


위의 에러메시지에서 몇 가지 단서들을 확인할수 있는데 

 -. context-hello.xml에 있는 messageBean이라는 이름의 bean 객체를 생성하지 못했다는 것이고

    Error creating bean with name 'messageBean' defined in class path resource [context-hello.xml]

 -. bean을 생성하기 위해 지정된 클래스가 interface이기 때문에 bean(객체)를 생성할수 없다

    Failed to instantiate [com.joe.MessageBean]: Specified class is an interface


참고로 context-hello.xml 파일의 내용은 다음과 같다. 


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">


<bean name="messageBean" class="com.joe.MessageBean" />


</beans>


그러면 bean을 생성할 대상이 되는 클래스인 com.joe.MessageBean를 보면 다음과 같다.


package com.joe;


public interface MessageBean {

public void sayHello(String name);

}


따라서 결국은 문제가 된 것은 context-hello.xml에서 bean 생성 할 대상으로 지정한 클래스가 interface여서 발생한 에러였다. Java는 근본적으로 interface를 막바로 객체(bean) 생성할수가 없다. 만일 객체를 생성할수 있다고 한들 그 객체가 행할수 있는 기능(메소드) 자체가 정의되어 있지 않기 때문에, 위에서 보듯이 sayHello()라는 메소드가 무엇을 행할지 내용이 없다. 이것이 interface이다. 


따라서 interface는 객체를 막바로 생성할수가 없고 생성할수 있다고 해도 그 객체(bean)이 아무런 동작도 할것이 없는것이다.

따라서 interface를 구현한(implements) 클래스를 객체로(bean)으로 생성하도록 context-hello.xml의 내용을 변경해 주어야 한다.


여기서 MessageBean 인터페이스를 구현한 하위 클래스는 다음과 같다.


package com.joe;


public class MessageBeanEng implements MessageBean {

@Override
public void sayHello(String name){

System.out.println("Hello, "+name);

}

}


따라서 context-hello.xml의 내용을 바꾸어 주어야하는데 interface인 MessageBean을 그 구현 클래스인 MessageBeanEng로 변경해 주어야 하는 것이다.


<bean name="messageBean" class="com.joe.MessageBean" />


를 아래와 같이 바꾸어 주어야 한다.


<bean name="messageBean" class="com.joe.MessageBeanEng" />



Spring(혹은 전자정부프레임워크)에서 자주 사용되는 개념인 DI(Dependency Injection)에 대해서 간단한 예제를 통해 살펴보고자 한다.


DI란 '의존성 주입'이라고 번역이 되는데 표현 자체가 거창해서 그렇지 사실은 그동안 프로그래밍에서 사용되어 오던 것이다.

예를들어 업로드 파일을 서버에 저장하는 SaveFile이라는 클래스가 있을 때 저장되는 파일 이름을 모두 대문자로 변환해서 저장하는 ToUpper라는 클래스를 SaveFile에서 사용한다고 하면 SaveFile은 ToUpper 클래스에 의존되어 있다고 표현한다.


class SaveFile

{

private File file;

private ToUpper upper;


private void saveFile() {

//여기서 upper객체를 사용해서 파일이름을 대문자로 변환후 저장하는 코드

}

.......

}


이럴 경우 ToUpper 클래스를 SaveFile 클래스 내부에서 new로 생성해서 사용할수도 있지만 ToUpper 클래스를 외부에서 주입해서(이걸 DI라고 한다) 사용할수 있을 것이다.

가장 보편적인 방법은 SaveFile의 생성자에서 매개인자(파라미터)로 ToUpper 클래스의 객체를 받아서 사용하는 형태가 있을 것이다.


class SaveFile

{

private File file;

private ToUpper upper;


public SaveFile(ToUpper upper){ //생성자를 통해 의존성 주입(DI)하는 방식

this.upper = upper;

}


private void saveFile() {

//여기서 upper객체를 사용해서 파일이름을 대문자로 변환후 저장하는 코드

}

.......

}


그런데 이 방식외에 Spring 컨테이너의 도움을 받아서 XML 설정 파일의 property 태그를 이용하는 방법도 있다.

이때 사용되는 것은 setter 메소드이다.



class SaveFile

{

private File file;

private ToUpper upper;


//setter만드는 규약에 맞게 setter 메소드를 만들어 두면 Spring 컨테이너가 

//XML 설정 파일의 property 태그를 이용해서 ToUpper 클래스의 객체를

//아래 메소드의 파라미터인 mUpper에 주입(DI)해 준다.

//따라서 ToUpper 클래스의 객체를 new로 생성하지 않아도 정상적으로 사용할수 있는것이다.

//setter를 만드는 규칙은 XML 설정 파일의 property name가 upper로 되어 있는 것을 근거로

//upper의 첫 글자를 대문자로(U) 바꾼 후 앞에 set을 추가한 형태인 setUpper로 만들면

//Spring 컨테이너가 알아서 이 setter를 찾아서 해당 property의 ref가 지정한 bean을

//mUpper에 주입해 준다. 이것이 프라퍼티를 이용한 DI 방식이다.

public void setUpper(ToUpper mUpper) {

this.upper = mUpper;

}


private void saveFile() {

//여기서 upper객체를 사용해서 파일이름을 대문자로 변환후 저장하는 코드

}

.......

}


※ ToUpper 클래스의 내용은 생략(해당 클래스가 잘 만들어져 있다고 가정...)


아래는 XML 설정파일의 정보이다. 파일 이름이 MyBean.xml이라고 가정한다.


<?xml version = "1.0" encoding = "UTF-8"?>

<beans xmlns = "http://www.springframework.org/schema/beans"

   xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"

   xsi:schemaLocation = "http://www.springframework.org/schema/beans

   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">


   <!-- SaveFile 클래스를 임의 클래스에서 객체로 사용하도록 bean 정보 설정 -->

   <bean id = "mySaveFile" class = "com.joe.SaveFile">

      <!-- SaveFile 클래스의 setUpper()에게로 ref가 지정하는 bean(객체)를 Spring 컨테이너가 주입해준다

즉 SaveFile 클래스에서 setUpper()의 매개인자로 ref가 지정하는 bean을 주입해 준다.

즉 setUpper(myUpper)식이 되는 것이다.

 -->

      <property name = "upper" ref = "myUpper"/>

   </bean>


   <!-- SaveFile 클래스가 사용하게 될 ToUpper 클래스에 대한 bean -->

   <bean id = "myUpper" class = "com.joe.ToUpper"></bean>

</beans>


이제 사용하는 방법은 다음과 같다.


package com.joe;


import org.springframework.context.ApplicationContext;

import org.springframework.context.support.ClassPathXmlApplicationContext;


public class MainApp {

   public static void main(String[] args) {

      ApplicationContext context = new ClassPathXmlApplicationContext("MyBean.xml");


      SaveFile sf = (SaveFile) context.getBean("mySaveFile");

      sf.saveFile();

   }

}

Spring MVC의 @ModelAttribute 어노테이션에 대한 개념 정리

 

Spring MVC에서 @ModelAttribute을 메소드의 파라미터로 사용할 경우 프로그램이 어떤 식으로 돌아가는지를 정리하고자 한다.

다른 어노테이션에 비해 @ModelAttribute는 내부적으로 돌아가는 부분이 많은 것 같다.

즉 Spring framework이 내부에서 알아서 처리해 주는 부분이 다른 어노테이션에 비해 더  많은 것 같다. 따라서 개발자의 손을 떠나 보이지 않는 가운데서 처리되는 부분에 대한 개념이 없다면 어둠 속에서 더듬이가 될수 밖에 없는 것이다.

 

여기 다음과 같은 빈 클래스가 있다고 할때

 

public class MemberInfo 

{

     private int seq;

     private String name;

     private int age;

 

     //이하 getter, setter는 생략

}

 

http://localhost:8080/member/info?name=Gildong&age=25seq=327

 

와 같이 접속되어 들어올 때 Controller 클래스의 아래 메소드가 실행될 것이다.

 

@Controller

@RequestMapping("/member/*")

public class MemberController

{

  ... 중 략 ...

@RequestMapping(value = "/info", method=RequestMethod.GET)

public void show(@RequestParam("seq") int seq, @ModelAttribute("myMEM") MemberInfo info, Model model)

{

     logger.info("####### info.getName() "+info.getName());

     logger.info("####### info.getAge() "+info.getAge());

 

     try {

          //service.read(seq)가 MemberVO 객체를 반환한다고 할 경우

          model.addAttribute(service.read(seq));

     }catch(Exception e) {

         e.printStackTrace();

     }

}

  ... 후 략 ...

}

 

이 메소드가 실행되면 info.jsp가 자동으로 실행되게 된다. 따라서 info.jsp가 만들어져 있어야 되고 없으면 404 Not found 에러가 발생할 것이다.

그리고 info.jsp의 아래 코드에서 결과가 나오게 될 것이다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

<!DOCTYPE html>

<html>

<head>

<meta charset="EUC-KR">

<title>Insert title here</title>

</head>

<body>

     <h3>회원 이름(info.name) : ${info.name }</h3> <%-- 여기서는 아무것도 안 나옴 --%>

     <hr/>

     <h3>회원 이름(myMEM.getName()) : ${myMEM.getName() }</h3>   <%-- 회원 이름이 정상적으로 출력된다. --%>

     <h3>현재 이름(myMEM.name) : ${myMEM.name }</h3>             <%-- 회원 이름이 정상적으로 출력된다 --%>

     <hr/>

     <h3>회원 번호 : ${memberVO.seq }</h3>    <%-- 회원 번호가 정상적으로 출력된다 --%>

</body>

</html>

 

JSP의 코드는 위의 내용이 전부이다. 무엇이 어떻게 돌아가기에 http url로 들어온 회원 이름과 회원 번호가 Controller에 저장이되고 Controller에서 JSP로 특별하게 보내는 코드도 없어 보이는데 JSP 코드에서 저렇게 값이 정상적으로 출력이 된단 말인가?

이것이 @ModelAttribute의 위력이고 편리함이면서 동시에 개발자를 더듬이로 만드는 측면이기도 하다.

단순한 @ModelAttribute가 무슨 역할을 했단 말인가?

 

@ModelAttribute 선언 후 자동으로 진행되는 작업들은 다음과 같다.

     ① @ModelAttribute 어노테이션이 붙은 객체를 자동으로 생성한다. 

         위의 코드에서는 MemberInfo 클래스의 객체 info를 자동으로 생성한다. 

         이때 @ModelAttribute가 지정되는 클래스는 빈 클래스라야 한다. 

         즉 MemberInfo 클래스는 beans 클래스라야 한다.

         그리고 getter와 setter가 명명 규칙에 맞게 만들어져 있어야 한다.

 

   ② 생성된 오브젝트에(info) HTTP로 넘어 온 값들을 자동으로 바인딩한다. 

       위의 코드의 경우는 name=Gildong&age=25seq=327 이렇게 들어오는 

       name, age, seq의 값이 MemberInfo의 해당 변수의 setter를 통해서 

       해당 멤버 변수에로 binding된다.

 

   ③ @ModelAttribute 어노테이션이 붙은 객체가(여기서는 MemberInfo 객체) 
       자동으로 Model 객체에 추가되고 따라서 MemberInfo 객체가 .jsp 뷰단까지 전달이 된다.

 

이상의 작업이 개발자를 대신해서 Spring framework가 알아서 다 처리해 준다. 편리하긴하다. 

이때 @ModelAttribute() 괄호 안에 지정한 문자열(위의 경우에는 myMEM)의 의미를 알아야 한다. 이 문자열의 이름으로(이것이 객체이다) Model 객체에 자동으로 추가가 되고 따라서 JSP 뷰단으로 안전하게 넘어가게 된다. 즉 MemberInfo 객체가 Model 객체에 추가될 때 @ModelAttribute()의 괄호 안에 지정한 문자열의 이름으로 추가된다는 점이다. 그리고 이 문자열 이름은 MemberInfo의 객체인 것이다. 

만일 @ModelAttribute()의 괄호 안에 아무런 문자열도 지정하지 않으면 JSP 페이지에서 MemberInfo 객체에 저장되어 있는 값을 사용할수가 없게된다.

 

보이지 않는 가운데서 내부적으로 Spring에 의해 처리되는 이상의 작업들로 인해 info.jsp에서 다음 코드가 유효하게 동작하는 것이다.

 

<h3>회원 이름(myMEM.getName()) : ${myMEM.getName() }</h3>   <%-- 회원 이름이 정상적으로 출력된다. --%>

<h3>현재 페이지(myMEM.name) : ${myMEM.name }</h3>             <%-- 회원 이름이 정상적으로 출력된다 --%>

 

여기서 ${myMEM.getName()}과 ${myMEM.name}의 차이가 무엇인고 하면 전자의 경우는 MemberInfo의 메소드를 직접 호출해서 사용한 경우이고 후자의 경우는 MemberInfo의 멤버 변수 name을 JSTL에서 사용하면 자동으로 name의 getter인 getName()이 호출되게 되는 것이다.

※ 본 내용은 구멍가게 코딩단의 "코드로 배우는 스프링 웹 프로젝트"라는 책의 내용을 공부하다가 정리하게 된 내용이다.


Spring MVC의 동작 구조, 동작 흐름을 파악하는 것이 그렇게 간단하지가 않다.

게시판 관련 작업을 한다고 할때 다음과 같은 클래스들이 있다고 가정해 보자.


BoardDAO : interface로 MyBatis XML Mapper를 활용하여 DB 연동 작업

BoardDAOImpl : BoardDAO interface를 구현한 클래스

BoardController : @RequestMapping 어노테이션을 활용하여 HTTP URL과 jsp 뷰단을 mapping(연결)하는 역할


이상과 같을 때 게시물에 대한 페이징 처리를 위해 Criteria라는 클래스가 있다고 하자.


public class Criteria 

{

private int myPage;          //MySQL limit 구문에서 시작 페이지로 지정할 변수

private int perPageNum;   //한 페이지당 보여질 게시물 갯수


public int getMyPage() {  //myPage에 대한 getter

... 생 략 ...

}

public void setMyPage(int myPage) {  //myPage에 대한 setter

... 생 략 ...

}


... 이하 생략 ...

}


이 클래스는 전체 게시물을 하나의 페이지당 몇 개씩 보여줄 것인지의 정보와 전체 페이지들 중에서 몇 번째 페이지의 게시물을 가져올 것인지에 대한 정보를 관리하는 역할하는 클래스이다.

이를 때에 BoardController에 아래와 같은 페이징 처리하는 메소드가 있다고 가정하자.


@Controller

@RequestMapping("/board/*")

public class BoardController 

{

private static final Logger logger = LoggerFactory.getLogger(BoardController.class);


... 중략 ...


@RequestMapping(value = "/listCri", method=RequestMethod.GET)

public void listAll(Criteria cri, Model model) throws Exception

{

model.addAttribute("list", service.listCriteria(cri));

}


... 이하 생략 ...

}


이상의 상황에서 다음과 같은 url 접속이 있을 때 위의 각각의 클래스들이 어떻게 연결지어가면서 동작하는가 하는 것이다.

(아래에서 myPage와 perPageNum은 Criteria의 멤버 변수명과 일치해야 한다)


http://localhost:8080/board/listCri?myPage=3&perPageNum=15


와 같이 접속해 오면 @RequestMapping 어노테이션에 의해 BoardController의 다음 메소드가 실행이 될 것이다.


@RequestMapping(value = "/listCri", method=RequestMethod.GET)

public void listAll(Criteria cri, Model model) throws Exception

{

model.addAttribute("list", service.listCriteria(cri));

}


그리고 여기서 개발자의 손을 떠난 보이지 않는 영역가운데서 Spring에 의해서 모종의 동작들이 내부적으로 진행이 되는 것이다. 위의 listAll() 메소드에 개발자가 작성한 코드는 딸랑 한 줄 뿐이다. 이런 면이 편리하기도 하지만 내막을 모르면 개발자는 눈감고 더듬는 더듬이가 되는 것이다. 

위의 메소드에서 내부적으로 Spring 프레임웤이 자동으로 Criteria 클래스의 객체를 생성하면서 http url에 있는 myPage, perPageNum의 값을 각각 Criteria의 두 멤버 변수의 getter, setter를 이용해서 값을 할당하게 된다.

그런 후에 model을 이용해서 jsp 단에 해당 값을 넘기고 @RequestMapping의 value에 지정한 값인 listCri라는 이름의 jsp인 listCri.jsp를 실행한다(이것 또한 명시적으로 return "listCri"라고 하지 않았지만 또 역시 내부적으로 Spring에 의해처 처리가 되는 영역이다).


그런데 여기서 Criteria의 멤버 변수 perPageNum을 pgNum으로 변경할 경우 다음과 같이 접속을 하면 어떻게 될까?


http://localhost:8080/board/listCri?myPage=3&pgNum=15


만일 멤버 변수만 perPageNum을 pgNum으로 변경했다면 위의 접속은 15페이지 값이 적용되지 않는다.

원리상 되야 될것 같은데 정상동작하지 않는다.

해법은 perPageNum에 맞게 명명된 getter와 setter를 pgNum에 맞게 변경해 주어야 한다. 이것까지 변경해 주어야 위의 url은 비로소 정상 동작을 하는 것이다.

pgNum에 대한 getter, setter 명명 규칙대로 변경하면 getter는 getPgNum()이 될 것이고 setter는 setPgNum()이 될 것이다.

이렇게 getter, setter 명명 규칙에 맞게 메소드 명을 변경하지 않으면(대소문자도 정확히) 아래 URL은 정상 동작을 기대하지 말아야 한다.


http://localhost:8080/board/listCri?myPage=3&pgNum=15


순수 Java/JSP로 개발할 때는 개발자가 직접 코딩해야 하던 것들이 Spring을 이용하면 개발자의 손 가락을 쉬게해 주는 여러 편리한 면도 있지만 이 편리함이라는 게 개발자의 손을 떠난 영역에서 동작하는 것들이 점점 많아 지게 된다는 이야기가 되고 개발자는 더듬이가 되어가야 한다는 이야기로 귀결하는 것 같다.

그래도 아무튼 뭐 어쩌겠는가?




Spring에서의 @RequestMapping 어노테이션에 대한 간단 정리


-. @RequestMapping 어노테이션은 Spring 웹 애플리케이션에서 가장 자주 사용되는 annotation이다.

-. @RequestMapping은 http request로 들어오는 url을 특정 controller 클래스나 메소드로 연결시키는 역할을 한다.

-. @RequestMapping은 controller에 있어서 class에 적용할수도 있고 특정 method에 적용할수도 있다.


아래의 예를 통해서 확인해 보자.


@Controller

@RequestMapping("/home")

public class TestController 

{

     //아래 @RequestMapping은 

     //hostname:port/home/에 대한 http request url에 대응하는 역할

    @RequestMapping("/")

    String getName(){

        return "Hello from getName() method";

    }

 

     //아래 @RequestMapping은 

     //hostname:port/home/info/에 대한 http request url에 대응하는 역할

    @RequestMapping("/info")

    String showInfo(){

        return "Hello from showInfo() method";

    }

}


-. http://localhost:8080/home/은 getName()을 호출하게 된다.

-. http://localhost:8080/home/info은 showInfo()을 호출하게 된다.


아래와 같이 @RequestMapping을 이용해서 특정한 method에 Multiple URI를 적용할수도 있다. 


@Controller

@RequestMapping("/home")

public class TestController 

{

    @RequestMapping(value={"", "/info", "info*","view/*,**/msg"})

    String myMultiMapping(){

        return "Hello from myMultiMapping()";

    }

}


위와 같이 @RequestMapping은 와일드 카드(wildcards)도 사용할수 있다. 

위의 코드에서 아래의 모든 URL들은 myMultiMapping() 메소드로 연결될 것이다.


localhost:8080/home

localhost:8080/home/

localhost:8080/home/info

localhost:8080/home/infosub

localhost:8080/home/view/

localhost:8080/home/view/view



+ Recent posts