Day 79. Spring Framwork 3 : 게시판 CRUD 구현

ho_c·2022년 6월 12일
1

국비교육

목록 보기
62/71
post-thumbnail

오늘부턴 JSP, Servlet으로만 만들었던 게시판 CRUD를 Spring으로 구현할 것이다. 하지만 이보다 앞서 CRUD를 Spring으로 구현하는 기본 로직을 연습해보고, 게시판 기능을 구현할 것이다.


CRUD 연습

1. CREATE

가장 먼저 게시판에 글을 입력하는 로직을 만들어보자.

1) ojdbc, dbcp 라이브러리 추가 (Spring 전용 jdbc가 따로 있음)

일단 데이터를 저장하기 위해, DB 연결을 만들어줄 ojdbc라이브러리를 Maven으로 받아오자.

  • Maven repo에서 “ojdbc8”, “dbcp” 검색
  • pom.xml에 추가

라이브러리가 만들어졌다면, 입력 로직을 실행할 DAO를 만들어 준다. DAO 클래스까지 만들었다면, 이제 new를 이용해서 Controller에서 DAO 인스턴스를 사용해야 할 것 같지만, 스프링 프레임워크에서는 더 그러지 않는다. (의존성 때문에)

@RequestMapping("inputProc")
public String inputProc(MessagesDTO dto {
		
	MessagesDAO dao = new MessagesDAO();
		
	return "redirect:/";
	}

지난번에 배운 것처럼 root-context.xml이나, servlet-context.xml 같은 xml에 문서에 가서 DAO bean을 만들 수도 있지만, 이외에도 Spring에서 빈을 만드는 방식은 더 있기 때문에 이를 알아보자.


2) bean 생성방식

먼저 스프링에서 빈을 생성하는 방식은 총 3가지이다. 이중 XML과 Annotation만 알아보자.

(1) XML

XML로 인스턴스를 생성하는 방식은 지난번에 배운 <bean> 태그를 사용한다.

<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>

(2) Annotation

어노테이션으로 빈 생성의 핵심은 <context:component-scan>라는 xml태그이다. 이 태그를 넣어두면 @Component가 적용된 클래스를 찾는다. 그리고 해당 스크립트가 읽히게 될 때, 인스턴스로 메모리 상 Spring Container(pool)에 로딩해둔다.

결과적으로 <context:component-scan><bean>와 같은 역할을 한다. 다만 bean이 class 속성으로 해당 인스턴스의 경로를 끌어왔다면, component-scan은 base-package 속성으로 그 탐색 범위를 지정해줘야 한다.

그래서 Spring 부팅 순서에서 추가된 것은 클라이언트의 요청이 들어오면, Dispatcher가 만들어지면서, 핸들러 매핑-View Resolver 인스턴스-Component 인스턴스 생성 및 Spring Container에 로딩된다.

어노테이션으로 하는 DI : @Autowired

이 경우 한 가지 직면하는 문제가 있다. 어노테이션으로 bean으로 만들 경우, Component를 포함 상속받는 어노테이션 적용 대상들이 메모리에 로딩됐다. 그러면 둘 다 메모리에 올라와 있는 상태에서 DI를 적용하기 어려워진다.

즉, XML을 사용하면 문서를 읽어드리는 과정에서 스프링이 빈을 생성하고 DI한다. 이때 문제는 어노테이션으로 하면 이미 각각의 인스턴스들이 적재되어 있고, 이 둘의 의존관계를 만들기 어렵다는 것이다.

하지만 이 고민도 잠시, 어노테이션으로 빈을 만들면 DI도 어노테이션으로 해주면 된다.
그 방법이 @Autowired이다.

component-scan이 실행이 되면 스프링 풀에 인스턴스들이 적재된 상태가 되는 동시에, 내부에 @Autowired가 자료형을 바탕으로 적용된 인스턴스를 끌어와서 해당 변수에 넣어줘서 DI한다.

@Qulifier(ID)

이 외에 같은 자료형의 인스턴스를 넣고 싶다면 @Component를 넣어줄 때, @Component(ID)로 id를 설정해서, @Autowired 밑에 @Qulifier(ID)를 설정해서 불러줄 수 있다.


3) DB 연결 만들기 (DAO)

연결을 만들어내기 위해서 DataSource 클래스를 이용할 것인데, 여기서도 new를 사용하지 않는다. 근데 문제는 DataSource는 우리 소유, 즉 우리가 가진 소스코드가 아닌 외부에서 만든 클래스라 XML로 빈을 만들어줘야 한다.

  • root-context로 이동
    해당 XML은 외부 라이브러리 세팅과 관련된 설정을 하는 문서이다. 그래서 우리는 톰캣 서버가 구동되면서 Spring 컨테이너 안에다가 DBCP를 로딩해놓을 것이다.

방식은 앞서 말한 것처럼 어노테이션을 사용할 수 없으니 XML로 해주고, 빈 생성은 기존 DBCP처럼 Setter를 활용한다.

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

이 과정 어딘가 익숙하지 않나?

바로 톰캣 환경에서 사용했던 JNDI 방식이다. 우리가 톰캣에서 JNDI로 했던 것을 이제 스프링에서는 이렇게 대체가 된다. 그리고 저 코드는 톰캣이 실행되면서 DBCP 인스턴스를 스프링 컨테이너에 적재해놨기 때문에, MessagesDAO에서 @Autowired를 통해서 DI를 해줄 수 있다.

public class MessagesDAO {
	@Autowired
	private DataSource bds;
}

4) DAO insert() 생성 및 Controller에서 적용

[ MessagesDAO ]

public int insert(MessagesDTO dto) throws Exception{
		
	String sql = "insert into messages values(messages_seq.nextval, ?, ?, sysdate)";
		
	try(
		Connection con = bds.getConnection();
		PreparedStatement pstat = con.prepareStatement(sql);
		){
	pstat.setString(1, dto.getWriter());
	pstat.setString(2, dto.getContents());
	return pstat.executeUpdate();
	}
}

[ Controller ]

try {
	dao.insert(dto);
} catch (Exception e) {
	return "error";
}

2. READ

기본 과정은 데이터 입력과 똑같다. 출력 메서드를 쓰다 보면 데이터를 담을 List 컬렉션과 DTO를 만드려면 new를 사용한다.

그래서 의존성 문제에 대해서 고민이 생길 수 있는데, 어차피 new을 아예 사용을 안하는 건 불가능하다. 그리고 다음 코드처럼 단발성으로 지역변수 형태로 사용하는 객체나, 데이터를 담는 DTO을 bean으로 사용하면, Spring 컨테이너가 불필요한 메모리 자원을 너무 많이 만들게 된다.

1) 데이터 불러오기

public List<MessagesDTO> selectAll() throws Exception{
		
	String sql = "select * from messages";
		
	try(
			Connection con = bds.getConnection();
			PreparedStatement pstat = con.prepareStatement(sql);
			ResultSet rs = pstat.executeQuery();
			){
		// 단발성 데이터 : 지역변수	
		List<MessagesDTO> list = new ArrayList<MessagesDTO>();
			
		while(rs.next()) {
			MessagesDTO dto = new MessagesDTO();
			dto.setSeq(rs.getInt("seq"));
			dto.setWriter(rs.getString("writer"));
			dto.setContents(rs.getString("contents"));
			dto.setWrite_date(rs.getTimestamp("write_date"));				
				
			list.add(dto);
				
		}
			
		return list;
	}

2) 데이터 넘기기

데이터를 넘기는 방법은 2가지인 것도 마찬가지이다. request에 담거나, session에 담거나. 그러나 굳이 session에 영구적으로 담았다가 지울 필요가 없다. 그래서 request방식을 택할 껀데, 이때 스프링에서는 아래 코드처럼 request 객체를 사용할 필요가 없다.

@RequestMapping("toOutput")
public String toOutput(HttpServletRequest request) {
		
	try {
		List<MessagesDTO> list = dao.selectAll();
		request.setAttribute("list", list);
		request.getRequestDispatcher(null);
	
	} catch (Exception e) {
		e.printStackTrace();
		return "error";
	}
		
	return "output"; // 기본 방식이 forward이다.
}

스프링 프레임워크는 DB에서 꺼내서 넘기는 데이터를 ‘Model data’라고 부르고, 이를 관리해주는 클래스로서 Model을 제공한다. 그래서 쉽고 편하게 Model 인스턴스에다가 넣어두면 View에서 사용할 수 있다.

@RequestMapping("toOutput")
public String toOutput(Model model) {
		
	try {
		List<MessagesDTO> list = dao.selectAll();
		model.addAttribute("list", list);	
			
	} catch (Exception e) {
		e.printStackTrace();
		return "error";
	}
		
	return "output"; // 기본 방식이 forward이다.
}

3. DELETE

데이터 삭제하기는 앞의 두 개보다 훨씬 쉽고 편하다. 삭제할 시퀀스 값을 전달받아서 DB에서 삭제한 뒤, redirect:toOutput으로 uri를 비우면서 재출력 시키면 된다.

@RequestMapping("deleteProc")
public String deleteProc(int seq) {
		
	try {
			
		dao.delete(seq);
			
	}catch (Exception e) {
		e.printStackTrace();
		return "error";
	}
		
	return "redirect:toOutput";
		
}

4. Update

드디어 마지막 수정이다. 수정 역시도 별반 다를게 없기에 DAO, Controller 코드만 작성하겠다.

[ Controller ]

@RequestMapping("modifyProc")
public String modifyProc(MessagesDTO dto) throws Exception{
		
		dao.update(dto);
			
	return "redirect:toOutput";
}

[ DAO ]

public int update(MessagesDTO dto) throws Exception{
		
	String sql = "update Messages set writer=?, content=? where seq=?";
		
	try(
			Connection con = bds.getConnection();
			PreparedStatement pstat = con.prepareStatement(sql);
			){
		pstat.setString(1, dto.getWriter());
		pstat.setString(2, dto.getContents());
		pstat.setInt(3, dto.getSeq());
			
		return pstat.executeUpdate();
	}
}

5. @ExceptionHandler

앞선 메서드에선 try-catch을 통해 예외를 처리해줬다. 이와 달리 스프링에선 어노테이션을 통해서 처리한다. 먼저 기본에 있던 try-catch는 모두 없애자.

@RequestMapping("deleteProc")
public String deleteProc(int seq) {
	
		dao.delete(seq);	
		
	return "redirect:toOutput";
}

그리고 하단에 ExceptionHandler 어노테이션을 정의해주자.

@ExceptionHandler
public String execeptionHandler(Exception e) {
	e.printStackTrace();
	return "error";
		
}

이렇게 정의를 해주면, 메서드들이 실행되고 리턴으로 넘어가기 전에 ExceptionHandler를 거치게 된다. 이 과정에서 예외가 발생하면 정의된 것처럼 error.jsp로 넘어가게 된다.

그래서 모든 예외를 인식할 수 있도록 Exception e를 인자값으로 넣어준다. 더 나아가 디테일한 예외처리도 어노테이션을 통해서 정의할 수 있다.

@ExceptionHandler(NumberFormatException)
public String execeptionHandler(Exception e) {
	e.printStackTrace();
	return "error";
		
}

이렇게 정의해주면, 숫자형식에서 발생하는 예외들만 해당 메서드에서 인지해서 처리하게 된다.


게시판 만들기

스프링 프레임워크를 이용한 기본 CRUD 구현법을 이해했다면, 이제 이를 토대로 게시판을 한번 더 만들자.

1. 회원가입 페이지 매핑

Point. 매핑 시, 검색 효율 높이기

첫 번째는 회원가입 페이지로 이동하는 단순 API를 만들 것이다. 여기서 고려할 것은 세미 땐 프론트에서 바로 JSP 경로 입력으로 끝났지만, Spring에선 DS를 거쳐야 해서 매핑을 해줘야 한다.

이때, 매핑은 @Controller @RequestMapping 어노테이션으로 만들어 요청되는 엔드포인트를 분석한다. 하지만 프로젝트가 확장되어 매핑 요소들이 늘어나면 그만큼 매핑 목록도 늘어나 검색 비효율을 일으킨다. 이 때문에 다음처럼 @Controller 자체를 분류시킨다.

[ JSP ]

$("#join").on("click", function(){
	location.href="/member/toJoin";				
});

[ Controller ]

@Controller
@RequestMapping("/member/") // member파트는 모두 여기 있다.
public class MemberController {
	
	@RequestMapping("toJoin")
	public String toJoin() throws Exception{
    
		return "/member/join";
	}
}

2. 아이디 중복체크 기능

Point. ajax반환에는 return을 사용하지 않는다. 그리고 ajax세팅이 필요하다.

이번에는 회원가입 시, 아이디 중복체크 기능을 만들어보면서 Spring에서 Ajax를 사용하는 방법을 알아보자.

일단 프론트에 작성 Ajax는 다음과 같고, 반환되어 오는 String은 따로 사용하지 않고 콘솔 출력되도록 설정했다.

$("#id").on("blur", function(){
	$.ajax({
		url:"/member/idDuplCheck",
		data: {"id":$("#id").val()}
	}).done(function(resp){
		console.log(resp);
	});
});

위 코드에 의해서 id 컴포넌트의 초점이 상실되면 ajax가 실행되어, 아래 나오는 /member/ 컨트롤러 휘하의 idDuplCheck가 실행된다. 그러면 해당 메서드는 DB에서 DAO를 통해서 조회를 실행한 뒤, 그 값을 반환한다.

@ResponseBody
@RequestMapping(value="idDuplCheck", produces = "text/html; charset=utf8") // Ajax세팅
public String idDuplCheck(String id) throws Exception {
		
	if(dao.idCheck(id)) {
		return "사용 불가능";
	} else {
		return "사용 가능";
	}
}

하지만 위 코드를 보면 return이 존재하지 않는다. 배운 거랑 조금 문제가 있다. 그럼 일단 생각해보자.

컨트롤러에서 DS에 보내는 응답은 2가지 설정을 통해서 ViewResolver에서 사용해서 jsp로 View만들어 반환하는 것이었다. 이 말은 곧 페이지 간의 이동이 발생한다는 것이다.

그러나 Ajax에 대한 응답은 페이지 이동이 불필요하다. 애초에 이 기술 자체가 페이지 이동 없는 데이터 교환이 목적인데 말이다. 따라서 forward도 아니고, redirect도 아니다. 그냥 단순한 String만 클라이언트에 보내면 된다.

이를 위해서, 해당 메서드가 ajax를 사용하기 위한 메서드임을 알려주는 어노테이션이 RespomseBody이다. 이걸 위에 붙여주게 되면 DS가 우리한테 받은 값 그대로 클라이언트한테 String으로 보내준다.

다만 한글인 경우 브라우저의 ajax가 이를 분석해줄 수 없다. 따라서 매핑 안에 인자값을 줘서 ajax한테 해당 데이터가 어떻게 설정되어 있는지를 알려준다. 추가로 RequestMapping 은 기본으로 uri만 인자값으로 받았지만, 위와 같은 형태로 추가적인 설정이 가능하다. (전달방식, 세팅 등등)

profile
기록을 쌓아갑니다.

0개의 댓글