JAVA 웹개발 과정 국비 78일차 정리

78일차

※ 배우는 과정이므로 정확하지 않을 수 있습니다.
잘못된 점은 알려주시면 수정하도록 하겠습니다!


1. Spring Framework 이어서..

1) DBCP

OJDBC8은 지난 시간에 maven에 추가해주었다.
그러나 DBCP 라이브러리는 추가해주지 않았으므로 maven repository에 가서 코드 가져오기.
일시적으로 사용되는 것이 아니기 때문에 싱글톤으로 생성해주어야하는데, 스프링에서는 new를 해서 사용하지 않으므로 bean 태그를 이용해 코드를 추가한다.
root-context.xml 안에 아래 태그를 넣어준다.
JNDI를 왜 안쓰냐? 스프링이기때문에 쓸 필요가 없다!
xml에서는 set을 property로 대신한다.

참고로, 싱글톤을 쓰고싶지 않다면 bean 태그 안에 scope="prototype"을 넣어주면 된다.
원래는 scope="singleton" 속성이 생략되어 있다.

<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
	<property name="username" value="kh"/>
	<property name="password" value="kh"/>
	<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
	<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe"/>
	<property name="initialSize" value="30"/>
</bean>	

2) dataSource 생성 코드 위치

servlet-context.xml 에 만들지 않고 root-context.xml에 만드는 것일까? 아무데나 만들어도 상관없는걸까?
사실 어디에 만들어도 상관은 없지만, 컨벤션으로 굳어져있다.

  • root-context.xml : Request와 무관한 항목들을 명세
  • servlet-context.xml : Request와 관련된 항목들을 명세

즉, 데이터베이스 관련된 위 코드는 웹과 관련된 항목이 아니기 때문에 root-context.xml에 넣어준다.
servlet-context.xml안에 있는 viewResolverresources, component-scan은 모두 웹과 관련이 있다.

3) @Autowired 사용

실습에서는 ContactDAO 안에 메소드를 만들어 DB와 연동하므로 ContactDAO 안에 bds 변수를 생성해준다.
원래는 BasicDataSource의 인스턴스를 생성하여 getConnection을 해야하는데, 스프링에서는 마음대로 new할 수 없으므로 @Autowired 어노테이션을 사용한다.
@Autowired를 사용한다는 것은, 스프링 컨테이너가 만들어지면서 가지고 있던 BasicDataSource 인스턴스를 요청해서 가져오겠다는 뜻이다.
만약 위의 1번 과정이 없다면 이 과정에서 에러가 발생한다.

public class ContactDAO {

	@Autowired
	private BasicDataSource bds;
}

4) DI(Dependency Injection)

@Autowired 어노테이션을 사용하여 스프링 인스턴스 풀 안에 있는 인스턴스를 가져다가 집어넣는 이 과정을 바로 Dependency Injection이라고 부르고, 줄여서 DI라고 한다.

5) @Component와 @Repository

ContactDAO도 인스턴스 생성을 해줘야 하는데, 마찬가지로 스프링에서는 마음대로 new 할 수 없으므로 @Component를 붙여줘서 xml의 component-scan 코드가 돌 때 스프링이 인스턴스 생성을 할 수 있게 한다.
@Component를 그대로 붙여줘도 되지만, DAO일 경우에는 @Repository 어노테이션을 붙여서 구분을 해주는 것이 좋다.
Repository도 결국 Component를 상속받아서 만들어지는 class 이다. @Component와 동일하게 동작한다.
굳이 구분을 해주는 이유는 정확한 키워드를 이용해서 이 클래스의 역할을 쉽게 읽을 수 있게끔 하고, 향후 스프링 버전이 올라가면 키워드 별로 독단적인 별개의 기능들을 구현할 때도 유리하기 때문이다.

@Repository
public class ContactDAO {

	@AutoWired
	private BasicDataSource bds;
}

6) NullPointerException

DAO 인스턴스도 위와 같이 @Component를 이용하여 인스턴스를 잘 가져왔다.
그리고 DAO 안에 insert 메소드도 만들어서 클라이언트로부터 name값과 contact값을 잘 받아와서 DB에 넣어주려고 했다.
그러나 NullPointerException이 떴다.
에러 코드를 확인해보니 Connection con = bds.getConnection(); 부분이다.
즉, bds가 null 값이라는 것이다.
bds가 null값이라는 것은 위 3번의 @Autowired가 실패해서 인스턴스를 제대로 가져오지 못했다는 뜻이 된다.
그렇다면 왜 가져오지 못한걸까?
바로 아래 코드가 문제였다.

<context:component-scan base-package="kh.spring.controller"/>

이 코드는 component-scan이 돌 때 범위를 정해주는 코드인데, 위처럼 .controller까지 다 써주게 되면 kh.spring.controller 패키지만 스캔할 것이기 때문에 "kh.spring"까지만 써서 kh.spring.daokh.spring.dto 패키지를 모두 스캔할 수 있게 만들어준다.

7) UnsatisfiedDependencyException

스프링을 쓸 때 자주 볼수 있는 에러이다.
에러 메세지가 길게 나오는데, 그 중 expected single matching bean but found 2 이 부분을 보면 이해가 쉽다.
즉, 인스턴스를 찾아올 때 한 개의 인스턴스가 있어야 하는데 2개를 찾아서 어느 인스턴스를 가져와야 할지 모르겠다는 것이다.

인스턴스 생성 코드가 2개라면, 하나를 지워서 해결해준다.
코드 실습을 하는 와중 아래와 같이 xml에 bean코드와 @Component 어노테이션이 각각 존재했기 때문에, bean 코드를 지워주었다.

<bean class="kh.spring.dao.ContactDAO"/>
@Component
public class ContactDAO {
	...
}

8) redirect의 필요성과 방법

Contact 입력 프로그램을 만들어서 DB에 name과 contact 값을 저장시키는 프로그램을 만들었다.
그런데 inputForm.jsp에서 name과 contact값을 제출하고 home.jsp로 돌아올 때 브라우저의 주소창을 보니 home이 아닌 localhost/inputProc으로 나타난 것을 볼 수 있었다.

@RequestMapping("inputProc")
public String inputProc(ContactDTO dto) {
	try{
		int result = dao.insert(dto);
	}catch(Exception e) {
		e.printStackTrace();
		return "error";
	}
	return "home";
}

바로 forward를 타고 넘어갔기 때문이다.
이게 왜 문제가 될까? 제대로 home.jsp 페이지를 보여주는 것이니 상관 없지 않을까?
그런데 이 상태에서 F5를 누르게 되면 양식 다시 제출 확인 alert이 뜨면서 계속할꺼냐고 묻는 창이 나온다. 계속을 눌러도 사실 아무일도 발생하지 않는 것처럼 보인다.
그러나 이 사이에 DB에는 같은 값이 계속 저장이 되고 있다.

만약 게시판을 만든다고 했을 때, 글쓰기 페이지에서 내용을 작성하고 submit을 한 후, forward를 통해 게시판 목록으로 돌려보냈다면, 사용자가 게시판 목록을 보고있음에도 사실은 게시판 작성 주소에 머물러 있는 것과 동일하다. 따라서 사용자가 게시판 목록을 갱신해야지! 하고 F5를 누르게 되면 계속 글을 등록하게 되는 상황이 발생한다.

단순히 봤을 때는 redirect보다 forward가 좋은 기능처럼 보이지만 이런 상황이 발생할 수 있기 때문에 forward를 남발해서 쓰는 것은 좋지 않다.
즉, forward는 특정 내용을 담아서 보내야 할때 사용해야하고, redirect는 기존에 가지고 있는 내용을 모두 버리고 이동해야 하는 경우 사용한다.

스프링은 기본적으로 forward로 동작한다. 그렇다면 어떻게 redirect를 사용해야할까?
아래와 같이 return 값에 키워드 redirect:를 써주면 된다.
이 키워드를 사용하면 dispatcher는 viewResolver를 거치지 않고 뒤에 문자열만 그대로 클라이언트에게 보낸다.
주의할 점은 redirect:home으로 사용하면 localhost/home의 주소로 클라이언트에게 보내버리는데, 스프링에서는 WEB-INF 때문에 외부에서 다이렉트로 접근이 불가능하다. 따라서 redirect:/ 로 바꿔서 컨트롤러를 거치게 만들어줘야 한다.

@RequestMapping("inputProc")
public String inputProc(ContactDTO dto) {
	try{
		int result = dao.insert(dto);
	}catch(Exception e) {
		e.printStackTrace();
		return "error";
	}
	return "redirect:/";
}

2. 그 외..

  • gradle : maven 업그레이드 버전이라고 생각하면 된다.

3. MVC2 와 Spring MVC의 차이

1) 서버에서 jsp로 값 전달

MVC2 에서는 request.setAttribute를 사용하여 request에 담고 getRequestDispatcher를 사용하여 url을 전달해주고, forward로 페이지를 이동시켰다.
스프링에서는 두가지 방법으로 서버에서 jsp로 값을 전달할 수 있다.

① ModelAndView 객체 사용

잘 쓰이지 않는 방법이다.
ModelAndView 객체에 값을 담아주고, setViewName 메소드에 보낼 페이지 주소를 적어주는 것이다.

@RequestMapping("outputProc")
public ModelAndView outputProc() {
	
	ModelAndView mav = new ModelAndView();
	
	try {
		List<ContactDTO> list = dao.selectAll();
		mav.addObject("list",list);
		mav.setViewName("output");
	}catch(Exception e) {
		e.printStackTrace();
		mav.setViewName("error");
	}
	return mav;
}

② model 객체 사용

매개변수에 필요로 하는 값을 담으면 forward할 때 자동으로 가져간다.
리턴 값을 String으로 많이 쓰는 이유는 String이 편해서도 있지만, 인자값을 jsp로 보낼 때 model 객체를 많이써서 그렇기도 하다.

@RequestMapping("toOutput")
public String outputProc(Model model) {
	
	try {
		List<ContactDTO> list = dao.selectAll();
		model.addAttribute("list", list);
	}catch(Exception e) {
		e.printStackTrace();
		return "error";
	}
	return "output";
}

2) @ExceptionHandler

MVC2에서는 전체 if문을 감싸는 try-catch문을 써줬었는데, 스프링에서는 매번 컨트롤러에서 기능이 붙을 때 마다 try-catch를 써줘야 하는 불편함이 있다.
각각의 메소드에 throws Exception을 해주면 Dispatcher에게 예외가 전가되어버리는데, 클라이언트에게 에러 메세지를 띄워버릴 수 있기 때문에 좋은 방법이 아니다.

스프링은 더욱 편하게 예외처리를 할 수 있는 방법이 있다.
@ExceptionHandler를 사용하면 전가된 예외가 모두 이곳으로 들어간다.
따라서 적절하게 ExceptionHandler를 사용해주자.
예외 종류를 나눠서 적절한 조치를 취해줄 수도 있다.

@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception e) {
	e.printStackTrace();
	System.out.println("예외 코드가 실행되었습니다.");
	return "redirect:/";
}
@ExceptionHandler(NumberFormatException.class)
public String exceptionHandler1() {
	System.out.println("예외 코드가 실행되었습니다.");
	return "redirect:/";
}
@ExceptionHandler(ArrayIndexOutOfBoundsException.class)
public String exceptionHandler2() {
	System.out.println("예외 코드가 실행되었습니다.");
	return "redirect:/";
}

댓글남기기