본문 바로가기
Spring

[Spring] DB 연동 - JDBC

by Amy IT 2022. 8. 25.

 

목차

     

    Spring 프로젝트에서 JDBC 기술을 이용해 DB를 연동하는 방법에 대해 알아보도록 하겠습니다. 순수 JDBC를 사용하는 방법과 JdbcTemplate을 사용하는 방법을 알아볼 텐데요, 아래는 프로젝트의 구조입니다. 

     

    실습을 위해 오라클 DBMS의 scott 계정에 test라는 테이블을 만들어 멤버를 조회하고 추가하는 등의 작업을 해 보도록 하겠습니다.

    create table test
    ( num number(4) primary key,
      username varchar2(10),
      address varchar2(10) );
    
    insert into test values ( 1, '홍길동' , '서울');
    insert into test values ( 2, '이순신' , '강원');
    insert into test values ( 3, '유관순' , '제주');
    insert into test values ( 4, '강감찬' , '서울');
    insert into test values ( 5, 'aaa' , 'aaa');
    commit;

     

    순수 JDBC

    DB 연동을 위한 환경설정

    dependency 추가

    DB 연동을 위해 필요한 라이브러리를 pom.xml에 추가해 줍니다. 저는 오라클 DBMS 연동을 위한 오라클 라이브러리와, DataSource 사용을 위한 DBCP2 라이브러리를 추가해 주었습니다. 추가 후 Maven Update를 실행합니다.

    <!-- https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 -->
    <dependency>
    	<groupId>com.jslsolucoes</groupId>
    	<artifactId>ojdbc6</artifactId>
    	<version>11.2.0.1.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
    <dependency>
    	<groupId>org.apache.commons</groupId>
    	<artifactId>commons-dbcp2</artifactId>
    	<version>2.1</version>
    </dependency>

     

    jdbc.properties

    DB 연동을 위해 필요한 정보를 properties 파일로 저장해 줍니다.

    jdbc.driver=oracle.jdbc.driver.OracleDriver
    jdbc.url=jdbc:oracle:thin:@localhost:1521:xe
    jdbc.userid=scott     
    jdbc.passwd=tiger

     

    applicationContext.xml

    저는 XML 파일에서 Configuration 설정을 하겠습니다. 컴포넌트를 스캔할 패키지 경로와, properties 파일 경로를 지정합니다. BasicDataSource 빈을 등록하면서 properties 파일의 정보를 가져와 설정해 줍니다.

    <?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 http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
    <!-- component-scan -->
    <context:component-scan base-package="com.*"></context:component-scan>
    <!-- properties파일 읽어오기 -->
    <context:property-placeholder location="com/config/jdbc.properties"/>
    <!-- dataSource 빈 등록 - properties파일의 정보 가져와서 설정 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    	<property name="driverClassName" value="${jdbc.driver}"></property>
    	<property name="url" value="${jdbc.url}"></property>
    	<property name="username" value="${jdbc.userid}"></property>
    	<property name="password" value="${jdbc.passwd}"></property>
    </bean>
    
    </beans>

     

    DataSource

    스프링은 DataSource를 통해 DB와의 Connection을 얻습니다. DataSource는 JDBC 사양의 일부이며 일반화된 Connection Factory입니다. DataSource는 다음과 같은 역할을 수행합니다.

     

    • DB Server와의 기본적인 연결
    • DB Connection Pooling 기능
    • Transaction 처리

    기존에 DataSource를 사용하지 않고 DriverManager 객체를 사용해 DB와 연결하는 방법에서는 Connection을 얻고자 할 때마다 url, username, password 같은 파라미터를 전달해야 했습니다. 하지만 DataSource를 사용하면 처음 DataSource 객체를 생성할 때만 DB 연동에 필요한 정보를 파라미터로 전달해 생성하고 이후에는 dataSource.getConnection() 호출만으로 Connection을 얻을 수 있습니다.

    또한, 매 요청마다 새롭게 Connection이 맺고 끊어지는 작업은 시간이 많이 소요됩니다. 만약 일정량의 Connection을 미리 생성해서 저장소에 저장해 두었다가 프로그램에서 요청이 있을 때 저장소에서 Connection을 꺼내 제공하고, 또 사용한 후 다시 회수하여 저장해 둔다면 시간을 절약할 수 있을 것입니다. 이러한 프로그래밍 기법을 Connection Pooling이라고 합니다.

     

    DataSource를 사용해 DB와 연결하고 select, insert 명령을 실행해 보도록 하겠습니다.

     

    MemberDTO.java

    public class MemberDTO {
    	int num;
    	String username;
    	String address;
    	
    	@Override
    	public String toString() {
    		return "MemberDTO [num=" + num + ", username=" + username + ", address=" + address + "]";
    	}
    	
        //Constructor, Getters and Setters
    }

     

    MemberDAO.java

    XML 파일에서 등록한 DataSource 빈이 자동 주입되도록 합니다. DriverManager.getConnection 대신 dataSource.getConnection으로 미리 생성되어 있는 Connection을 얻고 있습니다. 기존에 DB 연동을 위한 정보들을 직접 DriverManager.getConnection 메소드의 파라미터로 전달하며 Connection을 얻었던 코드가 사라졌습니다. 

    @Repository
    public class MemberDAO {
    	@Autowired
    	private DataSource dataSource;
    	
    	//전체 멤버 조회
    	public List<MemberDTO> select() {
    		List<MemberDTO> list = new ArrayList<MemberDTO>();
    		Connection con = null;
    		PreparedStatement pstmt = null;
    		ResultSet rs = null;
    		try {
    			con = dataSource.getConnection();
    			
    			String sql = "select * from test";
    			pstmt = con.prepareStatement(sql);
    			rs = pstmt.executeQuery();
    			
    			while (rs.next()) {
    				int num = rs.getInt(1);
    				String username = rs.getString(2);
    				String address = rs.getString(3);
    				list.add(new MemberDTO(num, username, address));
    			}
    		} catch (SQLException e) {
    			e.printStackTrace();
    		} finally {
    			try {
    				if(rs!=null) rs.close();
    				if(pstmt!=null) pstmt.close();
    				if(con!=null) con.close();
    			} catch (SQLException e) {
    				e.printStackTrace();
    			}
    		}
    		return list;
    	}
    	//멤버 추가
    	public void insert(int num, String username, String address) {
    		Connection con = null;
    		PreparedStatement pstmt = null;
    		try {
    			con = dataSource.getConnection();
    			
    			String sql = "insert into test (num, username, address) "
    					+ " values (?, ?, ?)";
    			pstmt = con.prepareStatement(sql);
    			pstmt.setInt(1, num);
    			pstmt.setString(2, username);
    			pstmt.setString(3, address);
    			int n = pstmt.executeUpdate();
    			System.out.println(n);
    			
    		} catch (SQLException e) {
    			e.printStackTrace();
    		} finally {
    			try {
    				if(pstmt!=null) pstmt.close();
    				if(con!=null) con.close();
    			} catch (SQLException e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }

     

    MemberService.java

    @Service
    public class MemberService {
    	@Autowired
    	private MemberDAO dao;
    	
    	//전체 멤버 조회
    	public List<MemberDTO> select() {
    		return dao.select();
    	}
    	//멤버 추가
    	public void insert(int num, String username, String address) {
    		dao.insert(num, username, address);
    	}
    }

     

    TestMain.java

    Main에서는 Service의 빈을 얻어서 Service의 select와 insert 메소드를 호출합니다. 모든 멤버의 리스트가 출력됩니다. 

    public class TestMain {
    	public static void main(String[] args) {
    		ApplicationContext ctx = new GenericXmlApplicationContext("com/config/applicationContext.xml");
    		MemberService service = ctx.getBean("memberService", MemberService.class);
    		
    		//멤버 추가
    		service.insert(10, "Amy", "Seoul");
    		//전체 멤버 조회
    		List<MemberDTO> list = service.select();
    		for (MemberDTO dto : list) {
    			System.out.println(dto);
    		}
    	}
    }

     

     

    JdbcTemplate

    그런데 순수 JDBC 기술을 사용하면 다음과 같은 문제점이 있습니다.

     

    • 쿼리를 실행하기 위한 많은 코드가 반복됩니다. ex) Connection 얻기, 쿼리 실행 결과를 ResultSet에 담기, 사용한 자원 해제 위한 close() 코드 등
    • 데이터베이스 로직에서 예외 처리 코드를 수행해야 합니다.
    • 트랜잭션을 처리해야 합니다.

     

    이러한 문제점들을 해결하기 위해 Spring에서는 JdbcTemplate이라는 클래스를 제공합니다. JdbcTemplate은 Spring JDBC 접근 방법 중 하나로, 내부적으로 Plain JDBC API를 사용하지만 위와 같은 문제점들을 제거한 형태의 클래스입니다. Spring JDBC가 하는 역할은 다음과 같습니다. 

     

    • Connection 열기와 닫기
    • Statement 준비와 닫기
    • Statement 실행
    • ResultSet Loop 처리
    • Exception 처리와 반환
    • Transaction 처리

    Spring Framework가 low-level의 디테일들을 모두 처리해주기 때문에 개발자는 핵심적인 로직에만 집중할 수 있게 됩니다.

     

    DB 연동을 위한 환경설정

    dependency 추가

    JdbcTemplate을 사용하기 위해 Spring Framework 라이브러리를 추가해 줍니다.

    <!-- https://mvnrepository.com/artifact/com.jslsolucoes/ojdbc6 -->
    <dependency>
    	<groupId>com.jslsolucoes</groupId>
    	<artifactId>ojdbc6</artifactId>
    	<version>11.2.0.1.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
    <dependency>
    	<groupId>org.apache.commons</groupId>
    	<artifactId>commons-dbcp2</artifactId>
    	<version>2.1</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-jdbc</artifactId>
    	<version>4.3.22.RELEASE</version>
    </dependency>

     

    jdbc.properties

    properties 파일은 위의 예제와 동일합니다.

    jdbc.driver=oracle.jdbc.driver.OracleDriver
    jdbc.url=jdbc:oracle:thin:@localhost:1521:xe
    jdbc.userid=scott     
    jdbc.passwd=tiger

     

    applicationContext.xml

    XML 파일에서는 JdbcTemplate 빈을 등록하면서 DataSource 빈을 주입받도록 합니다. DAO 클래스에서 JdbcTemplate을 사용하게 되는데, DAO 클래스의 생성자나 setter를 이용해 DataSource 빈을 주입한 후 주입받은 DataSource를 이용해 JdbcTemplate을 생성(new JdbcTemplate(dataSource))하여 사용하는 방법도 가능합니다.

    <?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 http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
    <!-- component-scan -->
    <context:component-scan base-package="com.*"></context:component-scan>
    <!-- properties파일 읽어오기 -->
    <context:property-placeholder location="com/config/jdbc.properties"/>
    <!-- dataSource 빈 등록 - properties파일의 정보 가져와서 설정 -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
    	<property name="driverClassName" value="${jdbc.driver}"></property>
    	<property name="url" value="${jdbc.url}"></property>
    	<property name="username" value="${jdbc.userid}"></property>
    	<property name="password" value="${jdbc.passwd}"></property>
    </bean>
    <!-- jdbcTemplate 빈 등록 - dataSource 빈 주입 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    	<property name="dataSource" ref="dataSource"></property>
    </bean>
    
    </beans>

     

    Querying (SELECT)

    JdbcTemplate은 SELECT 쿼리 실행을 위한 query() 메소드를 제공합니다. 자주 사용되는 메소드는 다음과 같습니다. query() 메소드 매개변수로 mapRow() 메소드를 오버라이딩한 RowMapper 인터페이스를 넘겨주면, ResultSet의 결과를 RowMapper를 통해 지정한 타입의 자바 객체로 변환하여 자동으로 List에 담아서 반환해 줍니다. PreparedStatement에서 ? 자리에 필요한 매개변수는 순서대로 적어줍니다.

     

    • List<T> query(String sql, RowMapper<T> rowMapper)
    • List<T> query(String sql, Object[] args, RowMapper<T> rowMapper)
    • List<T> query(String sql, RowMapper<T> rowMapper, Object... args)

     

    조회 결과가 1행인 경우 queryForObject() 메소드를 사용할 수도 있습니다. select count(*) 등의 쿼리문을 수행할 경우 RowMapper 대신 반환 타입 클래스를 지정함으로서 조회 결과 데이터 타입을 지정할 수 있습니다.

     

    • T queryForObject(String sql, RowMapper<T> rowMapper, Object... args)
    • T queryForObject(String sql, Class<T> requiredType, Object... args)

     

    MemberDAO.java

    JdbcTemplate을 사용하니 DAO 클래스의 코드가 훨씬 간결해진 것을 확인할 수 있습니다.

    @Repository
    public class MemberDAO {
    	@Autowired
    	private JdbcTemplate jdbcTemplate;
    	//멤버 전체 목록 
    	public List<MemberDTO> select() {
    		List<MemberDTO> list = jdbcTemplate.query(
    				"select * from test", 
    				new RowMapper<MemberDTO>() {
    					@Override
    					public MemberDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
    						MemberDTO dto = new MemberDTO();
    						dto.setNum(rs.getInt(1));
    						dto.setUsername(rs.getString(2));
    						dto.setAddress(rs.getString(3));
    						return dto;
    					}
    		});
    		return list;
    	}
    	//멤버 num을 이용한 조회
    	public MemberDTO selectOne(int num) {
    		MemberDTO dto = jdbcTemplate.queryForObject(
    				"select * from test where num=?", 
    				new RowMapper<MemberDTO>() {
    					@Override
    					public MemberDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
    						MemberDTO d = new MemberDTO();
    						d.setNum(rs.getInt(1));
    						d.setUsername(rs.getString(2));
    						d.setAddress(rs.getString(3));
    						return d;
    					}
    		}, num);
    		return dto;
    	}
    	//멤버 address를 이용한 인원수 조회
    	public int countByAddress(String address) {
    		int n = jdbcTemplate.queryForObject(
    				"select count(*) from test where address=?", 
    				Integer.class, address);
    		return n;
    	}
    }

     

    MemberService.java

    @Service
    public class MemberService {
    	@Autowired
    	private MemberDAO dao;
    	//멤버 전체 목록
    	public List<MemberDTO> select() {
    		return dao.select();
    	}
    	//멤버 num을 이용한 조회
    	public MemberDTO selectOne(int num) {
    		return dao.selectOne(num);
    	}
    	//멤버 address를 이용한 인원수 조회
    	public int countByAddress(String address) {
    		return dao.countByAddress(address);
    	}
    }

     

    TestMain.java

    public class TestMain {
    	public static void main(String[] args) {
    		ApplicationContext ctx = new GenericXmlApplicationContext("com/config/applicationContext.xml");
    		MemberService service = ctx.getBean("memberService", MemberService.class);
    		//멤버 전체 목록
    		List<MemberDTO> list = service.select();
    		for (MemberDTO dto : list) {
    			System.out.println(dto);
    		}
    		System.out.println("=============");
    		//멤버 num을 이용한 조회
    		MemberDTO member = service.selectOne(10);
    		System.out.println(member);
    		System.out.println("=============");
    		//멤버 address를 이용한 인원수 조회
    		int n = service.countByAddress("서울");
    		System.out.println(n);
    	}
    }

     

    Updating (INSERT, UPDATE, and DELETE)

    JdbcTemplate을 이용해 INSERT, UPDATE, DELETE 명령을 실행할 때는 update() 메소드를 사용합니다. commit은 자동으로 이루어집니다.

     

    • int update(String sql) 
    • int update(String sql, Object... args)

     

    MemberDAO.java

    @Repository
    public class MemberDAO {
    	@Autowired
    	private JdbcTemplate jdbcTemplate;
    
    	public int update(int num, String name, String address) {
    		return jdbcTemplate.update(
    				"update test set username=?, address=? where num=?", 
    				name, address, num);
    	}
    }

     

    MemberService.java

    @Service
    public class MemberService {
    	@Autowired
    	private MemberDAO dao;
    
    	public int update(int num, String name, String address) {
    		return dao.update(num, name, address);
    	}
    }

     

    TestMain.java

    5번 멤버의 데이터가 변경된 것을 확인할 수 있습니다.

    public class TestMain {
    	public static void main(String[] args) {
    		ApplicationContext ctx = new GenericXmlApplicationContext("com/config/applicationContext.xml");
    		MemberService service = ctx.getBean("memberService", MemberService.class);
    		
    		int n = service.update(5, "bbb", "bbb");
    		System.out.println(n);
    		
    		List<MemberDTO> list = service.select();
    		for (MemberDTO dto : list) {
    			System.out.println(dto);
    		}
    	}
    }

     

     

     

    참고

     

    댓글