코딩항해기

[Spring] @Controller와 @RequestMapping 본문

Spring

[Spring] @Controller와 @RequestMapping

miniBcake 2024. 10. 8. 23:01

 

 

 

[Spring] springframework.DispatcherServlet 구조

org.springframework.web.servlet.DispatcherServlet DispatcherServlet은 이름에도 쓰여있듯이 Servlet 파일이다. Spring에서는 페이지 이동 구분을 ViewResolver가 해주기 때문에 ViewResolver에 대한 의존성을 가지고 있으

minibcake.tistory.com

 

기존에 DispatcherServlet을 사용하기 위해 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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--HandlerMapping과 ViewResolver에 대한 의존성을 가지고 있으므로 추가-->
    
    <!--ds의 HandlerMapping은 한 개라 id 생략가능-->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <!--SI 주입-->
        <property name="mappings">
            <props>
                <!--<prop key="키 값 ">밸류 값</prop>-->
                <prop key="/login.do">login</prop>
                <prop key="/boardWrite.do">boardWrite</prop>
            </props>
        </property>
    </bean>

	<!--Bean 등록-->
    <bean class="com.koreait.app.view.member.LoginController" id="login"/>
    <bean class="com.koreait.app.view.board.BoardWriteController" id="boardWrite"/>

    <!--어노테이션 스캔-->
    <!--컨트롤러 Autowired 스캔 이후 Repository도 필요하므로 같이 스캔-->
    <context:component-scan base-package="com.koreait.app.view"/>
    <context:component-scan base-package="com.koreait.app.biz"/>

    <!--ViewResolver는 여러 개 될 수 있으므로 id를 반드시 기입-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <!--SI 주입-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

 

척보기에도 xml에 설정이 굉장히 많은데, xml의 설정은 어노테이션으로 줄일 수 있으므로 어노테이션을 통해 HandlerMapping xml 설정을 간략하게 줄여보자.

 

@Controller

Controller 어노테이션은 Component 어노테이션의 기능을 수행한다. 현재 xml에서 Component 기능이 필요한 부분은 어디일까, 바로 bean이다.

	<!--Bean 등록-->
    <bean class="com.koreait.app.view.member.LoginController" id="login"/>
    <bean class="com.koreait.app.view.board.BoardWriteController" id="boardWrite"/>

 

 

 

[Spring] @어노테이션 의존성 주입 (Component, Autowired, Qualifier)

[Spring] xml Bean을 통한 의존성 주입 (CI, SI)의존성 주입 DI (Dependency Injection)Spring 컨테이너에서 객체 Bean을 먼저 생성해두고 생성한 객체를 지정한 객체에 주입하는 방식을 의존성 주입이라고 한다.

minibcake.tistory.com

 

Component처럼 대상이 되는 객체 위에 @Controller를 달아 bean을 등록할 수 있다. 이 때 id설정이 필요하므로 @Controller("id")를 통해 id값을 지정할 수 있다.

 

 

[Spring] springframework.Controller interface 구조

org.springframework.web.servlet.mvc.Controller 구조분석 JSP 기반 프로젝트에서 oooAction으로 사용자의 요청을 처리했던 것처럼 Spring에서도 oooController가 사용자의 요청을 처리한다. oooController는 스프링 프레

minibcake.tistory.com

 

그런데 지금 Controller에 어노테이션을 달면 오류가 발생한다.

 

바로 구현하는 Controller interface와 어노테이션 Controller는 다른 객체이기 때문이다.

다시 말해 어노테이션만 사용하든가 인터페이스만 구현하든가 둘 중 한 가지만 해야한다. 이번에는 어노테이션을 사용하는 방식에 대해 정리할 예정이므로 인터페이스 구현을 지우고 어노테이션을 다시 import 하도록 하겠다.

import org.springframework.stereotype.Controller;

 

이제 선언부를 강제하는 인터페이스가 없기 때문에 handleRequest 메서드를 사용할 필요가 없다. 메서드의 자유도가 올라가기 때문에 성능을 위해 가벼운 구성을 마음대로 할 수 있다.

 

ModelAndView로 반환 타입을 가질 필요 없이 이동 경로만 알려주면 되기 때문에 반환타입을 String으로 변경하고 메서드 명을 mainPage()로 어떤 기능을 하는지 명확하게 알려줄 수 있는 이름으로 변경했다. 인자 또한 둘 다 사용하지 않기 때문에 지웠다. POJO라고 보기 어려운 request와 response가 사라져 POJO에 더욱 가까워진 형태로 바뀌었다.

 

    public String handleRequest() throws Exception {
        return "redirect:main.jsp"; //ViewResolver를 통해 이동하지 않고 리다이렉트로 페이지이동
        //WEB-INF 하위에 있는 페이지가 아니라는 말과 같음
    }

 

 

이처럼 아무것도 사용하지 않을 때는 매개변수를 비우면 되지만 다른 객체에 의존할 때는 다시 new를 사용해야하는 문제가 있다. 예를 들어 DTO가 대표적이다. 그 외에도 session(request를 통해 getSession해야한다) 등 필요한 부분이 생길 수 있다.

 

그런 상황에는 필요한 객체를 전부 스프링에게 요청하면 된다. 매개변수로 작성하게 되면 해당 메서드에 필요한 매개변수를 스프링에서 전부 생성해 실행할 수 있도록 지원하기 때문에 new를 사용하지 않고(직접 객체 관리를 하지 않고) 객체를 사용할 수 있다.

 

이때 DTO를 받아오는 것을 커맨드 객체라고 하는데 DTO의 경우 필드명과 View(페이지)의 데이터 name이 동일하다면 자동 SI 주입이 이뤄져 별도의 setter 과정도 거칠 필요가 없다.

 

그렇게 객체 관리하는 모든 코드가 사라져 아주 간결한 형태로 남게 된다.

 

    public String login(HttpSession session, MemberDTO memberDTO) throws Exception {
        //1. 사용자(클라이언트, 브라우저)가 보낸 파라미터에서 값 추출
        //String mid = request.getParameter("mid");
        //String password = request.getParameter("password");

        //2.DB연동
        //MemberDAO memberDAO = new MemberDAO();
        //MemberDTO memberDTO = new MemberDTO();
        //memberDTO.setMid(mid);
        //memberDTO.setPassword(password);
        memberDTO = memberDAO.selectOne(memberDTO);
        
        //로그인 결과에 따른 페이지 이동 구분
        if(memberDTO != null){
            //request.getSession().setAttribute("mid", memberDTO.getMid());
            session.setAttribute("mid", memberDTO.getMid());
            return "redirect:main.jsp";
        }
        return "redirect:login.jsp";
    }
    public String boardList(BoardDAO boardDAO, BoardDTO boardDTO, Model model) throws Exception {
        System.out.println("boardList.do");
        model.addAttribute("datas", boardDAO.selectAll(boardDTO)); //DB로부터 받은 데이터를 요청에 저장
        return "boardList"; //ViewResolver를 통해 path 완성
    }

 

 

이 때 사용하는 Model객체는 ModelAndView의 Model로 View기능이 필요없기 때문에 Model만 불러와 사용했다.

session의 경우에도 request를 부를 필요없이 HttpSession을 통해 바로 불러와 사용할 수 있다.

 

뿐만 아니라 하나의 객체에 하나의 메서드만 있어야할 필요도 없으므로 비슷한 기능을 하는 메서드들을 모아 응집도를 높여 유지보수를 용이하게 할 수 있다.

 

@Controller("login")
public class LoginController {

	//로그인 기능
    public String login(HttpSession session, MemberDTO memberDTO, MemberDAO memberDAO) throws Exception {
        memberDTO = memberDAO.selectOne(memberDTO);
        if(memberDTO != null){//로그인 성공
        	//로그인 성공 시 세션에 정보 저장 == 로그인
            session.setAttribute("mid", memberDTO.getMid());
            return "redirect:main.jsp";
        }
        return "redirect:login.jsp";
    }

    //로그아웃기능
    public String logout(HttpSession session) throws Exception {
        session.removeAttribute("mid");//세션에서 mid를 삭제해 로그아웃
        return "redirect:login.jsp";
    }
}

 

신나게 코드를 정리하다보니 한 가지 의문이 발생한다. 그럼 이 메서드를 HandlerMapping이 어떻게 알아보는 것일까?

이 때 @RequestMapping 어노테이션이 사용된다.

 

@RequestMapping

메서드 명도 다르고, 메서드도 여러 개이므로 어떤 메서드가 어떤 기능을 하는지, 어떤 때에 해당 메서드를 실행해야하는지 알 수 없다. 이를 설정하는 것이 RequestMapping 어노테이션이다. 다시 말해 HandlerMapping 의존 주입을 해주는 것이다.

    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <!--SI 주입-->
        <property name="mappings">
            <props>
                <!--<prop key="키 값 ">밸류 값</prop>-->
                <prop key="/login.do">login</prop>
                <prop key="/boardWrite.do">boardWrite</prop>
            </props>
        </property>
    </bean>

 

바로 이 부분의 설정을 대신해주는 것이다.

 

정리한 코드에 어노테이션을 달면 이러한 형태가 된다.

@Controller("login")
public class LoginController {

	//로그인 기능
    @RequestMapping("/login.do")
    public String login(HttpSession session, MemberDTO memberDTO, MemberDAO memberDAO) throws Exception {
        memberDTO = memberDAO.selectOne(memberDTO);
        if(memberDTO != null){//로그인 성공
        	//로그인 성공 시 세션에 정보 저장 == 로그인
            session.setAttribute("mid", memberDTO.getMid());
            return "redirect:main.jsp";
        }
        return "redirect:login.jsp";
    }

    //로그아웃기능
    @RequestMapping("logout.do")
    public String logout(HttpSession session) throws Exception {
        session.removeAttribute("mid");//세션에서 mid를 삭제해 로그아웃
        return "redirect:login.jsp";
    }
}

 

어노테이션을 통해 HandlerMapping을 생성했으므로 ds(DispatcherServlet) xml에는 ViewResolver와 어노테이션의 스캔 범위를 지정하는 context:component-scan 태그만이 남게 된다.

 

<?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.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--어노테이션 스캔-->
    <!--컨트롤러 Autowired 스캔 이후 Repository도 필요하므로 같이 스캔-->
    <context:component-scan base-package="com.koreait.app.view"/>
    <context:component-scan base-package="com.koreait.app.biz"/>

    <!--ViewResolver는 어러 개가 될 수 있으므로 id를 반드시 기입-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
        <!--SI 주입-->
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

 

 

실행 흐름

서버가 web.xml설정을 참고해 DispatcherServlet을 생성하고 DispatcherServlet은 해당 xml의 설정을 참고하게 된다. 이때 지정한 범위에 대한 어노테이션 스캔이 이뤄지고 어노테이션에 따라 설정을 완료하며 ViewResolver를 생성해 HandlerMapping과 ViewResolver에 대해 의존성을 가진 DispatcherServlet이 생성된다.

 

이후 사용자가 .do로 끝나는 요청을 하게 되면 해당 url패턴을 캐치하는 DispatcherServlet으로 가게되고, 어노테이션 설정에 따라 해당하는 요청을 수행해 사용자에게 응답하게 된다. 응답 시 forward 방식이라면 ViewResolver를 통해 path를 완성해 응답하게 된다. (페이지 로딩 시 데이터가 필요한 경우라면 사용자의 접근을 막기 위해 WEB-INF하위에 있기 때문에 ViewResolver를 통해 path를 완성한다.)