딸기말차

[Spring] 3. Spring JDBC, Mybatis 본문

Bootcamp/Spring

[Spring] 3. Spring JDBC, Mybatis

딸기말차 2023. 8. 14. 19:39

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io


1. Spring JDBC

JDBC(Java Database Connectivity)는 자바 데이터 접근 기술의 근간이라 할 수 있는 데이터 액세스 기술이다.

그러나 시간이 지남에 따라 SQL문이 지나치게 복잡해지면서 개발이나 유지관리에 어려움이 생기기 시작했고, 특히 Connection 객체 같은 공유 리소스를 제대로 처리해 주지 않으면 버그를 발생시키는 원인이 되었다.

 

스프링에서 제공하는 JDBC는 이러한 기존 JDBC의 장점과 단순함을 유지하면서 단점을 보완하여, 간결한 API 뿐만 아니라 확장된 JDBC의 기능도 제공한다.

실제 개발을 진행할 때는 스프링 JDBC 기능보다는 마이바티스나 하이버네이트 같은 데이터베이스 연동 관련 프레임워크를 사용하지만, 스프링 JDBC의 기본적인 기능을 알아두면 도움이 된다.


2.  JDBC 실습

1. web.xml

톰캣 실행 시, web.xml에서 스프링의 ContextLoaderListener를 이용해 빈 설정 XML 파일들을 읽어 들인다.

즉, action-service.xml과 action-datasource.xml을 읽어 WebApplicationContext를 생성한다.

 

1)  RootApplicationContext

최상위 설정파일로, 스프링 프레임워크 내에서 Component-Scan, Security, DB설정 등이 포함된다.

2) WebApplicationContext

RootApplicationContext의 자식으로, 대부분 DispatcherServlet을 위한 설정이 들어간다.

3) ContextLoaderListener

Context가 Loading 되는 것을 감지하기 위한 Listener

4) ContextConfigLocation

ContextLoaderListener가 동작하기 전 설정파일의 위치를 알려준다.

/* Context가 Loading 되는 것을 감지하기 위한 Listener */
<listener>
    <listener-class>
    	org.springframework.web.context.ContextLoaderListener
    </listener-class>
</listener>

/* ContextLoaderListener가 동작하기 전 설정파일의 위치를 알려주는 contextConfigLocation  */
<context-param>
	<param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/config/action-service.xml
      /WEB-INF/config/action-dataSource.xml
    </param-value>
</context-param>

2. action-dataSource.xml

 

1) PropertyPlaceholderConfigurer

외부 properties 파일을 가져와, 스프링 설정파일에서 사용 가능하게 해준다.

2) SimpleDriverDataSource

JDBC 드라이버를 구성해, 사용자의 DB 정보를 등록한다.

/* 외부 설정파일을 가져오기 위한 PropertyPlaceholderConfigurer */
<bean id="propertyConfigurer"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <list>
            /* DB 설정파일을 가져온다. */
            <value>/WEB-INF/config/jdbc.properties</value> 
        </list>
    </property>
</bean>

/* 사용할 DB 정보를 가져오기위한 SimpleDriverDataSource */
<bean id="dataSource"
    class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
    <property name="driverClass" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>

/* DB에 직접 연결할 DAO Interface의 구현체 등록 */
<bean id="memberDAO" class="com.lhs.spring.member.dao.MemberDAOImpl">
    <property name="dataSource" ref="dataSource" /> 
</bean>

3. jdbc.properties

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@localhost:1521:XE
jdbc.username=scott
jdbc.password=tiger

4. MemberController

해당 컨트롤러의 실행 순서는 다음과 같다.

1. listMembers.do 를 통해 메인 페이지 접근

2. memberForm.do 요청이 들어올 시, 회원가입 페이지로 이동

3. MemberForm.jsp에서 addForm.do 요청 시, DB INSERT

public class MemberControllerImpl extends MultiActionController implements MemberController {
	private MemberService memberService;

	public MemberControllerImpl() {
		System.out.println("public MemberControllerImpl()");
	}

	/*
	 * 1. action-servlet.xml로 부터 service를 주입받는다.
	 * 2. service의 구현체는 action-service.xml에서 주입받는다.
	 */
	public void setMemberService(MemberService memberService) {
		this.memberService = memberService;
	}

	/*
	 * 1. /member/listMembers.do 요청
	 * 2. Service -> DAO를 통해 멤버들 조회
	 * 3. ModelAndView에 조회한 멤버들, viewName(listMembers) 저장
	 */
	@Override
	public ModelAndView listMembers(HttpServletRequest request, HttpServletResponse response) throws Exception {
		List<MemberVO> membersList = memberService.listMembers();

		ModelAndView mv = new ModelAndView(getViewName(request));
		mv.addObject("membersList", membersList);
		return mv;
	}
	
	/*
	 * 1. /member/memberForm.do 요청
	 * 2. ModelAndView에 viewName(memberForm) 저장
	 * 3. memberForm.jsp로 이동
	 */
	@Override
	public ModelAndView memberForm(HttpServletRequest request, HttpServletResponse response) throws Exception {
		ModelAndView mv = new ModelAndView(getViewName(request));
		return mv;
	}
	
	/*
	 * 1. /member/addMember.do 요청
	 * 2. INSERT Query를 위한 MemberVO
	 * 3. Service -> DAO를 통해 INSERT 실행
	 * 4. listMembers.do로 redirect
	 */
	@Override
	public void addMember(HttpServletRequest request, HttpServletResponse response) throws Exception {
		MemberVO memberVO = new MemberVO();
		memberVO.setId(request.getParameter("id"));
		memberVO.setPwd(request.getParameter("pwd"));
		memberVO.setName(request.getParameter("name"));
		memberVO.setEmail(request.getParameter("email"));
		
		memberService.addMember(memberVO);
		response.sendRedirect(request.getContextPath() + "/member/listMembers.do");
	}

	private String getViewName(HttpServletRequest request) throws Exception {
    }
}

3.  Mybatis 

어플리케이션의 규모가 작을 때는 JDBC를 이용해 충분히 개발할 수 있었다. 그러나 인터넷 사용자가 폭발적으로 증가하고, 어플리케이션의 기능이 복잡해짐에 따라 기존의 JDBC로 개발하는 데는 한계가 드러나게 되었다.

 

기존 JDBC로 개발할 경우 반복적으로 구현해야 할 SQL문도 많을 뿐만 아니라,  SQL문이 Java 코드에 섞여 코드를 복잡하게 만든다. 때문에 유지보수가 힘들어지게 되었고, 자연스럽게 마이바티스(MyBatis)나 하이버네이트 같은 데이터베이스 연동 관련 프레임워크를 사용하게 되었다.

* 기존의 JDBC 연동 과정
1. Connection 객체 생성
2. Statement 객체 생성
3. SQL 전송
4. 결과 반환 (ResultSet)
5. 객체 close

1. Mybatis의 동작 구조

* Mybatis 동작 구조
1. SqlMapConfig.xml에 각 기능별로 실행할 SQL문들(~.xml)을 <mappers> 태그에 등록한다.
2. DB와 연동하는 데 필요한 데이터를 <typeAliases> 를 통해 각각의 매개변수에 저장한다.
3. 애플리케이션에서 요청한 SQL문을 <mapper namespace="실행할 SQL ID">를 통해 선택한다.
4. 파라미터와 SQL문을 결합한다. 
이때 resultMap과 parameterType에 일반 변수나 객체를 쓸 수도 있지만, <typeAliases> 에 등록한 매개변수를 사용할 수도 있다.
5. 파라미터와 결합된 SQL문을 DBMS에서 실행한다.
6. DBMS에서 반환된 결과를 DAO로 반환한다.

2. Mybatis의 특징

* Mybatis 특징
1. SQL 실행 결과를 자바 Beans 또는 Map 객체에 mapping 해 주는 Persisitence 솔루션으로 관리한다.
즉, SQL을 소스 코드가 아닌 XML로 분리해 사용한다.
2. SQL문과 프로그래밍 코드를 분리해서 구현이 가능하다.
3. DataSource 기능과 트랜잭션 처리 기능을 제공한다.

4.  Mybatis 실습_1) xml 

1. SqlMapConfig.xml

Mybatis의 설정 정보에 관한 xml로, 각 태그의 의미는 다음과 같다.

 

1) <typeAliases>

resultMap이나 parameterType에 개발자가 선언한 객체를 사용해야하는 경우, 불필요한 패키지 경로 등을 줄여서 사용하기 위해 별명(사용할 객체의 임시명)을 붙일 수 있다.

2) <environments default="이름"> 

DB에 연결할 설정에 대한 정보를 선언하는 부분으로, 여러 종류의 environment 중 기본으로 연결할 정보를 설정한다.
즉, 여러 종류의 <environment>를 집어 넣으면 여러 DB에 연결할 수 있게 되고, <environment id="이름"> 으로 구분하여 사용이 가능하다.
3) <transactionManager type="JDBC">

트랜잭션의 제어를 누가 할 것인가에 관한 설정이다.

1. JDBC : JDBC가 커밋 / 롤백을 직접 처리하기 위해 사용한다. (수동 commit)
2. MANAGED : 트랜잭션에 대해 직접적인 영향을 행사하지 않는 것 의미한다. (자동 commit)

4) <dataSource type="POOLED">

실제 DB 접속에 관한 정보를 넣는 태그이다.

1. type : Connection Pool을 사용할건지에 대한 여부를 결정한다.
2. UNPOOLED : Connection 객체를 별도로 저장하지 않고, 객체 호출 시 매번 생성하여 사용한다.
3. POOLED : 최초 Connection 객체를 생성할 때 그 정보를 pool 영역에 저장하여 재사용이 가능하다.
<configuration>
	/* resultMap이나 parameterType 작성에 불필요한 패키지 경로 등을 줄이기 위해 */
	<typeAliases>
		<typeAlias type="com.lhs.spring.vo.MemberVO" alias="memberVO"></typeAlias>
	</typeAliases>
	
        /* DB에 연결할 설정에 대한 정보 선언 */
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC"></transactionManager>
			<dataSource type="POOLED">
				<property name="driver" value="oracle.jdbc.driver.OracleDriver"></property>
				<property name="url" value="JDBC:oracle:thin:@localhost:1521:XE"></property>
				<property name="username" value="scott"></property>
				<property name="password" value="tiger"></property>
			</dataSource>
		</environment>
	</environments>

	/* 실행할 Query가 작성 된 xml 파일들을 <mapper>를 통해 등록 */
	<mappers>
		<mapper resource="mybatis/mappers/member.xml"></mapper>
	</mappers>
</configuration>

2. member.xml

실제 동작할 쿼리를 작성한 xml로, SqlMapConfig.xml의 <mapper> 에 등록해 사용 가능하다.

 

1) <mapper namespace="mapper.member">

DAO에서 해당 xml에 접근하기 위한 태그로, 내부 쿼리메서드를 실행하고 싶을 시, session.selectList("mapper.member.selectAllMemberList"); 이런식으로 접근이 가능해진다.

2) <resultMap>

개발자가 만든 객체인 memberVO와, DB 레코드 내용이 상이할 경우 Query 실행 시 오류가 발생할 수 있다.

이를 대비해 둘 사이에 공통으로 들어가는 변수를 사용해, resultMap의 type을 직접 만들어 사용 가능하다.

3) <select>, <insert>

Query를 DBMS에서 사용하는 것 처럼 작성해 사용할 수 있다.

/* namespace 옵션을 통해, DAO에서 해당 파일로 접근 가능하게 해준다. */
<mapper namespace="mapper.member">

	/* 개발자가 만든 memberVO와, DB 레코드가 다를 경우를 대비해 공통으로 사용할 수 있는 객체를 만든다. */
	<resultMap id="memResult" type="memberVO">
		<result property="id" column="id"></result>
		<result property="pwd" column="pwd"></result>
		<result property="name" column="name"></result>
		<result property="email" column="email"></result>
		<result property="joinDate" column="joinDate"></result>		
	</resultMap>

	/* DB SELECT Query */
	<select id="selectAllMemberList" resultMap="memResult">
		<![CDATA[
			select * from t_member order by joinDate desc
		]]>
	</select>
	
    	/* DB INSERT Query */
    	<insert id="addMember" parameterType="memberVO">
		<![CDATA[
		 	insert into t_member(id, pwd, name, email) values(#{id}, #{pwd}, #{name}, #{email})
		]]>      
	</insert>
	
</mapper>

5. Mybatis 실습_2) Controller, DAO

1. MemberServlet

해당 프로젝트는 Spring을 사용하지 않고 진행했기 때문에, 이전 처럼 Servlet을 Controller 처럼 사용하였다.

 

1) request.getPathInfo()를 통해 어떤 요청이 전달됐는지 확인한다.

2) 전달 된 요청이 GET 방식인지 POST 방식인지에 따라 doGet(), doPost()로 구별해 요청에 맞는 메서드를 실행한다.

@WebServlet("/member/*")
public class MemberServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private final MemberDAO memberDAO = new MemberDAO();
	
	/* GET 방식으로 들어온 요청 실행 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String path = request.getPathInfo();	
		if (path.equals("/mem.do"))
			members(request, response);
		else if (path.equals("/memberForm.do"))
			memberForm(request, response);	
	}

	/* POST 방식으로 들어온 요청 실행 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {		
		String path = request.getPathInfo();	
		if (path.equals("/addMember.do"))
			addMember(request, response);
	}
	
	/* GET 방식인 mem.do로 요청이 들어올 경우, DB에 접근해 SELECT Query 실행  */
	private void members(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		List<MemberVO> members = memberDAO.selectAllMembers();
	
		request.setAttribute("members", members);
		RequestDispatcher requestDispatcher = request.getRequestDispatcher("/test/listMembers.jsp");
		requestDispatcher.forward(request, response);
	}
	
	/* GET 방식인 memberForm.do로 요청이 들어올 경우, memberForm.jsp로 forward */
	private void memberForm(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		RequestDispatcher requestDispatcher = request.getRequestDispatcher("/test/memberForm.jsp");
		requestDispatcher.forward(request, response);
	}
	
	/* POST 방식인 addMember.do로 요청이 들어올 경우, INSERT Query 실행 후 mem.do로 Redirect */
	private void addMember(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		MemberVO memberVO = new MemberVO();
		memberVO.setId(request.getParameter("id"));
		memberVO.setPwd(request.getParameter("pwd"));
		memberVO.setName(request.getParameter("name"));
		memberVO.setEmail(request.getParameter("email"));
		memberDAO.addMembers(memberVO);
		
		response.sendRedirect(request.getContextPath() + "/member/mem.do");
	}
}

2. MemberDAO

DB에 접근하기 위한 DAO로, 기존 JDBC 방식과 유사하지만 SqlSessionFactory를 사용하는 점과, mapper를 통해 Query를 실행한다는 차이가 존재한다.

public class MemberDAO {

	/* Mybatis를 사용하기 위해 SingleTon 방식으로 SqlSessionFactory 선언 */
	public static SqlSessionFactory sqlMapper = null;
	
	public MemberDAO() {
	}

	/* Mybatis의 설정 정보를 불러와 SqlSessionFactory에 등록 */
	private static SqlSessionFactory getInstance() {
		if (sqlMapper == null) {
			try {
				/* 설정파일의 경로 저장 */
				String resource = "mybatis/SqlMapConfig.xml";
				/* resource 패키지를 읽기위한 Resources 클래스를 이용해 설정파일을 읽어옴 */
				Reader reader = Resources.getResourceAsReader(resource);
				/* 설정파일을 읽어와서 build */
				sqlMapper = new SqlSessionFactoryBuilder().build(reader);
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return sqlMapper;
	}

	public List<MemberVO> selectAllMembers() {
		sqlMapper = getInstance();
        	/* SqlSessionFactory에서 가져온 SqlSession 객체 */
		SqlSession session = sqlMapper.openSession();
		
		/* mapper를 namespace와 id를 사용해 호출, Query 실행 */
		return session.selectList("mapper.member.selectAllMemberList");
	}
	
	public void addMembers(MemberVO memberVO) {
		sqlMapper = getInstance();
        	/* SqlSessionFactory에서 가져온 SqlSession 객체 */
		SqlSession session = sqlMapper.openSession();
		
		/* mapper를 namespace와 id를 사용해 호출, Query 실행 */
		session.insert("mapper.member.addMember", memberVO);
		/* Mybatis는 기본적으로 트랜잭션을 지원하기 때문에, DB Table 내용 변경 시 commit을 해야한다. */
		session.commit();
	}

}

3. EncodeFilter

MemberServlet을 보면, request.setCharacterEncoding 이나 response.setContentType을 통해 인코딩을 변환하는 부분이 존재하지 않는다. 

해당 부분은 doGet()이나 doPost()에 들어가는 공통 코드이고 모든 페이지에 적용되야하는 부분이기 때문에, Filter를 구현하여 이를 해결하였다.

@WebFilter("/*")
public class EncodeFilter implements Filter {
	ServletContext context;

    public EncodeFilter() {
    }

    /* 필터를 실행하는 init 메서드 */
    public void init(FilterConfig fConfig) throws ServletException {
    	context = fConfig.getServletContext();
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;
		httpRequest.setCharacterEncoding("utf-8");
		httpResponse.setContentType("text/html; charset=utf-8");
		
		chain.doFilter(request, response);
	}
}

6. 29일차 후기

JDBC는 이제는 워낙 오래 된 기술이기 때문에, 실제로 거의 사용하지 않는다. 하지만 Mybatis의 경우, 전자정부 프레임워크를 사용하는 기업에선 무조건 사용해야 한다.

때문에 Mybatis의 사용법은 고정 된 설정파일을 제외하고는 익혀둘 필요가 있다. mapper를 통해 Query xml을 등록하고, DAO에서 mapper의 namespace와 id를 사용해 Query를 호출하는 방법 등은 몇 번 정도 직접 사용해보면 좋다고 생각한다.

 

평소의 수업에는 자주 사용하지 않기 때문에 금방 생각나진 않지만, Filter를 사용하면 인코딩이나 허가 되지 않은 사용자를 접근할 수 없게하는 등 공통 기능을 편리하게 구현할 수 있다. 추후 Spring의 인터셉터를 다루기 위해서도, Filter를 사용하는 연습을 해두면 많은 도움이 될 것이라 생각한다.

'Bootcamp > Spring' 카테고리의 다른 글

[Spring] 6. STS  (0) 2023.08.19
[Spring] 5. Transaction, Annotation  (0) 2023.08.17
[Spring] 4. Spring Mybatis  (0) 2023.08.16
[Spring] 2. MVC  (0) 2023.08.12
[Spring] 1. DI, IOC, AOP  (0) 2023.08.11