본문 바로가기

programming/Gukbi

국비 교육 65일차 - annotation 사용 controller 제작

728x90
반응형

분명 원리는 같은데 훨씬 난이도가 있는 controller 제작이었다... 

이것만 잘 이해해도 앞으로의 spring 사용이 쉬워질거라고 강사님이 말씀해주셨으니, 다는 아니어도 꼭 구조의 흐름이 어떻게 흘러가는지만 이라도 이해하고 싶다...

 

 

일단 어노테이션을 사용하는 이유를 꼭 짚고 넘어가려고 한다. 

1. controller를 확정시키고 나서 코드를 바꾸지 않으려고 하기 때문에 -> 재활용성이 높아진다. 

2. 한 클래스 안에 있는 여러 메소드들을 불러오기 위해서 -> 이렇게 하면 한 메소드당 하나의 클래스를 만들지 않아도 되기 때문에, 응집력이 높은 프로그램을 만들 수 있다. 

 

 이건 어노테이션을 쓰기 이전의 web.xml 파일이다. 사용할 applicationContext.xml 의 경로만 따로 지정해 줬다. 

file_path도 지정해 줬다. src안에 있는 패키지 명들을 저장해주기 위해서 이다. 

이제 여기서, model들이 모아져 있는 패키지에서는 java 형식의 파일명들을 또 따로 가져와야하기 때문에 경로를 저장해줬다. 

<?xml version="1.0" encoding="UTF-8"?>
<beans>
 <component-scan base-package="com.sist.model"/>
</beans>

 이전의 코드에서는 applicationContext.xml 에서 호출할 class명들을 직접 저장을 해왔다면, 이번에는 그냥 패키지명만 저장을 해준다. 지금은 model 패키지 명만 저장을 했는데, 앞으로는 더 들어올 수도 있다는 것을 감안하고 프로그램을 짜야한다. 

 

우선 어노테이션의 적용범위를 확인해줄 필요가 있다. 

 

package com.sist.anno;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(RUNTIME) // Source, Class, Runtime (메모리 유지)
// 				  ===> ============= 컴파일 후에 자동 메모리 해제
@Target(METHOD) // 찾기(인덱스)
/*
 * 	@ TYPE
 * 	class A
 * 	{
 * 		@ FIELD
 * 		B b;
 * 		@CONSTRUCTOR
 * 		public A(@ PARAMETER)
 * 		{
 * 		}
 * 		@ METHOD
 * 		public void display()
 * 		{
 * 		}
 * 	}
 * 
 * 	@RequestMapping()
 */
public @interface RequestMapping {
	public String value();
}

 target(대상) 

 

method를 지정해줄때는 target(METHOD), target(TYPE), target(PARAMETER), target(FIELD)처럼 다르게 사용해준다. 

일단 우리가 가져올 값은 각각의 method이기 때문에, target을 메소드로 설정하고, String값을 value()로 받아오게 설정을 해준다. 

 

package com.sist.anno;

import java.lang.reflect.Method;
import java.util.Scanner;

class NoticeModel
{
	@RequestMapping("list.do")
	public void list()
	{
		System.out.println("목록 출력");
	}
	@RequestMapping("insert.do")
	public void insert()
	{
		System.out.println("데이터 추가");
	}
	@RequestMapping("update.do")
	public void update()
	{
		System.out.println("데이터 수정");
	}
	@RequestMapping("delete.do")
	public void delete()
	{
		System.out.println("데이터 삭제");
	}
	@RequestMapping("find.do")
	public void find()
	{
		System.out.println("데이터 찾기");
	}
}
public class MainClass2 {
	public static void main(String[] args) {
		try
		{
			Scanner scan=new Scanner(System.in);
			System.out.println("URI:");
			String uri=scan.next();
			Class clsName=Class.forName("com.sist.anno.NoticeModel");
			Object obj=clsName.getDeclaredConstructor().newInstance();
			Method[] method=clsName.getDeclaredMethods();
			for(Method m:method)
			{
				RequestMapping rm=m.getAnnotation(RequestMapping.class);
				if(rm.value().equals(uri))
				{
					m.invoke(obj, null);
					break;
				}
			}
		} catch (Exception ex) {
			
		}
	}
}

각각의 method들을 만들어 주고, 그 위에 @RequestMapping 이라는 이름의 어노테이션을 올려준다. 

찾아올때는 "메소드명.do"를 문자열값으로 지정해준다. 

 

 패키지명을 가져와서 그 안에 있는 클래스의 이름을 저장해준다. 

Class clsName=Class.forName("com.sist.anno.NoticeModel");

 그리고, 클래스 이름으로 새로운 메모리 공간을 obj에 생성한다. 

Object obj=clsName.getDeclaredConstructor().newInstance();

 그러면 이제 그 안에 있는 여러 메소드들을 메소드 배열안에 담아준다. 

Method[] method=clsName.getDeclaredMethods();

 

그리고 나서 for문을 돌려서 getAnnotation()을 통해 저장한 어노테이션을 구해와서 rm 변수에 담아준다. 

이때 담기는 값들은 

@RequestMapping("find.do")
                ========= 여기

find.do에 해당한다. 즉, 만들어줄 view의 파일명이라고 할 수 있다. 위 예제에서는 사용자가 요청한 uri에서 값을 가져오는 것이 아니라, console에서 직접 값을 입력해서 비교해줬다. 

 

RequestMapping rm=m.getAnnotation(RequestMapping.class);
				if(rm.value().equals(uri))
				{
					m.invoke(obj, null);
					break;
				}

만약 rm.value()가 uri와 같다면, 메소드를 실행하라는 명령을 내려준다. 

실행이 되었으면 break를 걸어서 멈춰준다. (그래서 한번만 실행된다)

 

여기까지가 기본적으로 작동하는 원리이고, 앞으로 구현할 부분은 기본 원리는 같은데, uri를 받아오는걸 진짜로 코딩을 해줘야 한다. 

 

 

 


 

앞으로 만들 프로젝트들은 하나의 클래스(기능, model)안에 여러가지 메소드를 가지고 있게 된다. 

그렇기 때문에, 클래스 하나하나의 다양한 메소드들을 가져올 수 있는 코드를 짜야 한다. 그것을 위해서 따로 만들어준 클래스가 FileConfig이다. 

 

FileConfig 클래스는 한 패키지 안에 여러가지 파일들이 있을 경우를 생각해서, 

.java로만 끝나는 파일들을 가져오게 하기 위해서 만들어준 클래스이다. 

 

package com.sist.controller;
import java.util.*;
import java.io.*;
public class FileConfig {
	
	// 자바 파일만 가져오게 하기
	public List<String> componentScan(String path, String pack)
	{
		List<String> list=new ArrayList<String>();
		//String path="C:\\webDev\\webStudy\\JSPMVCLastProject\\src\\";
		path+=pack.replace(".", "\\");
		try 
		{
			File dir=new File(path);
			File[] files=dir.listFiles();
			for(File f:files)
			{
				//System.out.println(f.getName());
				String s=f.getName();
				String ext=s.substring(s.lastIndexOf(".")+1);
				if(ext.equals("java"))
				{
					String clsName=pack+"."+s.substring(0,s.lastIndexOf("."));
					//System.out.println(clsName);
					list.add(clsName);
				}
			}
		} 
		catch (Exception ex) {}
		return list;
	}
	/*
	 * public static void main(String[] args) { FileConfig fc=new FileConfig();
	 * fc.componentScan("com.sist.model"); }
	 */
}

 매개변수로는 src폴더의 path와 패키지명을 받아온다. 

 

List<String> list=new ArrayList<String>();
		//String path="C:\\webDev\\webStudy\\JSPMVCLastProject\\src\\";
		path+=pack.replace(".", "\\");

자바 파일명들을 저장해야하기 때문에 List형으로 메소드를 만든다. 패키지 명은 [com.sist.model]과 같이 .으로 구분해주기 때문에, 이를 경로명에 맞게 넣어주기 위해서는 .을 전부 \\(역슬러쉬)로 변경하여 path에 추가해줘야 한다. 

 

File dir=new File(path);
File[] files=dir.listFiles();

파일명들을 저장해주기 위해 dir 변수를 잡아준다. 파일 배열에 파일의 리스트들을 전부 담아준 다음, for문을 돌려 파일 이름을 하나씩 저장해주면 된다. 

 

 

String s=f.getName();
String ext=s.substring(s.lastIndexOf(".")+1);

 

 

 s변수에는 BoardModel.java, FoodModel.java 등등의 파일명들이 저장되어 있다. 이게 확장자가 java로 되어 있는 java파일인지 확인해주기 위해서 변수 ext에 .뒤에 있는 확장자명(java, xml 등등)을 잘라서 저장해준다. 

 

 

if(ext.equals("java"))
{
 String clsName=pack+"."+s.substring(0,s.lastIndexOf("."));
 list.add(clsName);
}

 만약 잘라준 확장자명이 java가 맞다면, clsName에 패키지명+확장자명 java를 잘라낸 파일명(ex BoardModel)을 더해서 저장을 해준다. 

 

출력을 해주면 

com.sist.Model.BoardModel

이렇게 나오게 된다. 

여튼 이렇게 만들어준 파일명들을 전부 list에 담고, componentScan 메소드에서는 list를 return값으로 만들어 준다.

 

그러면 이제 진짜 컨트롤러인 DispatcherServlet에서 받아온 list명을 어떻게 처리해주는지 잘 생각하면서 또 흐름을 파악해 주면 된다. 

 

package com.sist.controller;

import java.io.*;
import java.lang.reflect.Method;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;

public class DispatcherServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private List<String> clsList=new ArrayList<String>();
   
	public void init(ServletConfig config) throws ServletException {
		String xmlPath=config.getInitParameter("ContextConfigLocatation");
		String filePath=config.getInitParameter("file_path");
		try
		{
			// xml파싱
			DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
			DocumentBuilder db=dbf.newDocumentBuilder();
			Document doc=db.parse(new File(xmlPath));
			
			Element beans=doc.getDocumentElement();
			NodeList list=beans.getElementsByTagName("component-scan");
			FileConfig fc=new FileConfig();
			for(int i=0; i<list.getLength(); i++)
			{
				Element component=(Element)list.item(i);
				String pack=component.getAttribute("base-package");
				List<String> fList=fc.componentScan(filePath, pack);
				for(String s:fList)
				{
					clsList.add(s);
				}
			}
			
			
		} 
		catch (Exception ex) {
			
		}
		
		for(String ss:clsList)
		{
			System.out.println(ss);
		}
	}

	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String cmd=request.getRequestURI();
		// http://localhost:8096/JSPMVCLastProject/main/main.do
		cmd=cmd.substring(request.getContextPath().length()+1);
		System.out.println(cmd);
		try
		{
			for(String cls:clsList)
			{
				Class clsName=Class.forName(cls);
				if(clsName.isAnnotationPresent(Controller.class)==false) // 클래스 중에 어노테이션을 가진게 없다면
				{
					continue;
				}
				Object obj=clsName.getDeclaredConstructor().newInstance(); // 메모리 할당 완료
				// 메소드 찾기
				Method[] methods=clsName.getDeclaredMethods();
				for(Method m:methods)
				{
					RequestMapping rm=m.getAnnotation(RequestMapping.class);
					if(rm.value().equals(cmd))
					{
						// public String boardList(HttpServletRequest request,response)
						
						String jsp=(String)m.invoke(obj, request,response);
						if(jsp.startsWith("redirect")) // sendRedirect
						{
							
						}
						else // forward
						{
							
						}
						return;
					}
				}
			}
		}catch(Exception ex) {}
	}

}

 진짜 컨트롤러의 역할을 해주는 DispatcherServlet

 

일단 이전 예제에서 한것과 마찬가지로 class의 이름들을 가져와서 메모리 할당을하고 메소드를 찾아서 실행을 시켜주는게 주요 목적이기 때문에, 먼저 클래스의 이름을 전부 받아오는 코드를 짜준다. 

 

private List<String> clsList=new ArrayList<String>();

 클래스의 리스트를 받아올 리스트인 clsList 변수를 만들어 준다. 

 

String xmlPath=config.getInitParameter("ContextConfigLocatation");
String filePath=config.getInitParameter("file_path");

 applicationContext.xml의 경로값을 받아올 xmlPath 변수하나, src폴더의 경로값을 받아오는 filePath변수를 하나 가져온다. 

 

try
		{
			// xml파싱
			DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
			DocumentBuilder db=dbf.newDocumentBuilder();
			Document doc=db.parse(new File(xmlPath));
			
			Element beans=doc.getDocumentElement();
			NodeList list=beans.getElementsByTagName("component-scan");
			FileConfig fc=new FileConfig();
			for(int i=0; i<list.getLength(); i++)
			{
				Element component=(Element)list.item(i);
				String pack=component.getAttribute("base-package");
				List<String> fList=fc.componentScan(filePath, pack);
				for(String s:fList)
				{
					clsList.add(s);
				}
			}
		} 
		catch (Exception ex) {}

 

DocumentBuilderFactory dbf=DocumentBuilderFactory.newInstance();
			DocumentBuilder db=dbf.newDocumentBuilder();
			Document doc=db.parse(new File(xmlPath));

일단 try~catch절 안에서는 xml의 경로를 이용해서 그 안의 내용을 파싱을 해온다.

 

Element beans=doc.getDocumentElement();
NodeList list=beans.getElementsByTagName("component-scan");

Element에는 beans안에 있는 내용을 읽어오고, 또 태그이름으로 그 안에 있는 패키지명을 읽어와서 list에 저장을 해준다. 여기서는 패키지명을 하나만 저장해줬지만, 앞으로 할 코딩에서는 여러개 패키지가 들어올 수 있다. 

 

위에서는 com.sist.model을 읽어온다. 

 

FileConfig fc=new FileConfig();
			for(int i=0; i<list.getLength(); i++)
			{
				Element component=(Element)list.item(i);
				String pack=component.getAttribute("base-package");
				List<String> fList=fc.componentScan(filePath, pack);
				for(String s:fList)
				{
					clsList.add(s);
				}
			}

 list를 만들어서 패키지명들을 저장했으면, FileConfig 클래스의 메모리를 할당해서 java파일으로만 이루어진 클래스 리스트를 받아올 준비를 한다. 

 

Element component=(Element)list.item(i);
String pack=component.getAttribute("base-package");

 패키지명이 저장된 만큼 그 안에 있는 패키지 명들을 component라는 변수 안에 저장을 해준다. 

component의 base-package라는 태그안에 패키지명이 저장이 되어 있기 때문에, getAttribute를 통해 패키지명들을 가져와서 pack이라는 변수에 메모리 할당을 한다. 

 

그러면 FileConfig에서 만들어뒀던 메소드를 이용해서 자바파일들의 패키지명+클래스명 리스트를 받아올 수 있는 준비가 완료된다. 

 

List<String> fList=fc.componentScan(filePath, pack);
				for(String s:fList)
				{
					clsList.add(s);
				}

 매개변수를 path(경로명), pack(패키지명)으로 잡았기 때문에 fList에 자바파일들의 패키지명+클래스명 리스트를 받아올 수 있다. 그리고 이것을 clsList에 add하고 마무리 하면 클래스명으로 메모리를 할당받을 준비가 끝난다. 

 

// com.sist.Model.BoardModel - 위의 코드를 실행해서 clsList에 넣어준 결과값

이걸 기억하고 시작을 해야한다. clsList에는 위와 같은 패키지명+클래스명이 저장되어 있다. 

String cmd=request.getRequestURI();
		// http://localhost:8096/JSPMVCLastProject/main/main.do
		cmd=cmd.substring(request.getContextPath().length()+1);

일단 저번 코드와 똑같이 사용자가 보내준 uri를 받아온다. 

그리고 getContextPath()를 사용해서 폴더명+파일명까지만 잘라와 cmd에 저장해준다. 

 

폴더명까지 저장해주는 이유는 다른 폴더에 같은 이름의 파일명이 존재할 수 있기 때문에, 구분을 해주기 위해서이다. 

 

try
		{
			for(String cls:clsList)
			{
				Class clsName=Class.forName(cls);
				if(clsName.isAnnotationPresent(Controller.class)==false) // 클래스 중에 어노테이션을 가진게 없다면
				{
					continue;
				}
				Object obj=clsName.getDeclaredConstructor().newInstance(); // 메모리 할당 완료
				// 메소드 찾기
				Method[] methods=clsName.getDeclaredMethods();
				for(Method m:methods)
				{
					RequestMapping rm=m.getAnnotation(RequestMapping.class);
					if(rm.value().equals(cmd))
					{
						// public String boardList(HttpServletRequest request,response)
						
						String jsp=(String)m.invoke(obj, request,response);
						if(jsp.startsWith("redirect")) // sendRedirect
						{
							
						}
						else // forward
						{
							
						}
						return;
					}
				}
			}
		}catch(Exception ex) {}

 여기는 아직 완성된 코드는 아니지만 내용을 살펴보면, 

 

클래스 안에는 2개의 어노테이션이 들어가게 된다. 

@Controller - 클래스 위에 올라갈 어노테이션 @ TYPE
@RequestMapping - 메소드 위에 올라갈 어노테이션 @ METHOD

 

for(String cls:clsList)
			{
				Class clsName=Class.forName(cls);
				if(clsName.isAnnotationPresent(Controller.class)==false) // 클래스 중에 어노테이션을 가진게 없다면
				{
					continue;
				}

만약 컨트롤러가 없으면 그 클래스는 넘어가서 다음 클래스를 확인해준다. 

Controller 어노테이션이 있으면, 클래스 이름으로 obj에 메모리 할당을 하고 메소드들을 가져와서 method배열에 담아준다. 

 

if(rm.value().equals(cmd))
 {
   // public String boardList(HttpServletRequest request,response)
						
     String jsp=(String)m.invoke(obj, request,response);

 

그리고 메소드안에 RequestMapping이라는 어노테이션이 있으면, 그게 웹과 관련된 어노테이션이기 때문에, value값을 cmd에 저장한 값과 비교해서 같을때 메소드를 실행해준다. 

jsp에는 클래스의 메소드들을 실행한 return값이 저장된다. 

 

만약 그 return값이 redirect로 시작하면... 다시 돌려보내주고, 

아니면... forward 하고 메소드를 종료하면 된다. 

728x90
반응형