티스토리 뷰

JDBC 프로그래밍의 단점을 보완하는 스프링.

 

https://dlagusgh1.tistory.com/274

 

MySQL, JDBC 연동

*자바, MYSQL 연동MySQL, JDBC 연동 1. JDBC 드라이버 다운로드 - https://mvnrepository.com/artifact/mysql/mysql-connector-java - 최신 버전 다운로드. - 다운로드 한 파일에서 .jar 파일만 따로 압출풀어서..

dlagusgh1.tistory.com

 

해당 블로그를 참고하시면 DB연동에 필요한 커넥션을 구하고 쿼리를 실행하기 위한 PreparedStatement를 생성하고 Exception 처리를 하는 등 데이터 처리와 상관 없는 코드도 구조적으로 반복된다.

 

이렇듯 구조적인 반복을 줄이기 위해 템플릿 메서드 패턴과 전략 패턴을 함께 사용한다. 스프링에선 JdbcTemplate 클래스를 제공하여 코드를 간단히 줄일 수 있다. 스프링이 제공하는 또 다른 장점은 트랜잭션 관리가 용이하다는 것이다.

JDBC API로 트랜잭션을 처리하려면 커밋과 롤백에 해당하는 메서드를 이용해야했는데 @Transactional 어노테이션을 통해 트랜잭션을 적용하고싶은 메서드에 붙이면 되기에 핵심코드에 집중 작성할 수 있게 된다.

 

 

 

디비 연동 프로그램 작성 시에 추가할 의존사항은 다음과 같다.

spring-jdbc  : JdbcTemplate 등 JdbcTemplate등 JDBC 연동에 필요한 기능을 제공한다.

tomcat-jdbc : DB 커넥션 풀 기능 제공한다.

mysql-connector-java : MySql 연결에 필요한 JDBC 드라이버를 제공한다.

 

커넥션 풀이란?)

 

실제 서비스 운영 환경에서는 서로 다른 장비를 이용해서 자바 프로그램과 DBMS를 실행한다. 자바 프로그램에서 DBMS로 커넥션을 생성하는 시간은 길기에 DB커넥션을 생성하는 시간은 전체 성능에 영향을 준다. 또한 동시에 접속하는 사용자 수가 많다면 사용자마다 커넥션을 생성해 부하를 주기에 충분하다.

 

최초 연결에 따른 응답 속도 저하와 동시 접속자가 많을 때 발생하는 부하를 줄이기 위해 사용하는 것이 커넥션 풀이다. 커넥션 풀은 일정 개수의 DB 커넥션을 미리 만들어두는 기법이다. DB 커넥션이 필요한 프로그램은 커넥션 풀에서 커넥션을 가져와 사용한 뒤 반납하는 식으로 운영된다. 생성시간을 아낄 뿐만 아니라 접속자 수가 많더라도 미리 생성되어 부하가 적다. 

 

이러한 이유로 실제 서비스 운영 환경에서는 매번 커넥션을 생성하지 않고 커넥션 풀을 사용하는데 제공 모듈로는 톰캣 JDBC, 히카리CP 등이 존재한다.

 

 

DataSource 설정

 

JDBC API는 DriverManager 외에 DataSource를 이용해서 DB 연결을 구하는 방법을 정의하는데,

 

Connection conn = dataSource.getConnection( );

 

이런식으로 커넥션을 구할 수 있다.

스프링이 제공하는 DB연동 기능은 DataSource를 사용해서 DB 커넥션을 구한다. 이러한 DataSource를 스프링 빈으로 등록해 객체를 주입받아 사용하는 것이다.

Tomcat JDBC 모듈은 javax.sql.DataSource를 구현한 DataSource 클래스를 제공한다.

 

@Configuration
public class DbConfig{
	
    @Bean
    public DataSource dataSource(){
    	DataSource ds = new DataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl(db address...);
        ds.setUsername("db_user_name");
        ds.setPassword("db_password");
        ...
        return ds;
    }
    
}

Tomcat JDBC 주요 프로퍼티의 경우 커넥션 을 몇 개 설정할건지 에 대한 메서드를 제공하는데 커넥션의 상태를 일단 알아두자.

 

커넥션 풀은 커넥션을 생성하고 유지하는데 커넥션 풀에 커넥션을 요청하면 해당 커넥션은 활성상태가 되고 커넥션을 다시 반환하면 유휴상태가 된다.

 

Connection conn = dataSource.getConnection( ); // 활성상태

conn.close( ); // 유휴상태

 

https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/tomcat/jdbc/pool/DataSourceProxy.html

 

DataSourceProxy (Apache Tomcat 7.0.109 API Documentation)

void setPropagateInterruptState(boolean propagateInterruptState) Configure the pool to propagate interrupt state for interrupted threads waiting for a connection A thread waiting for a connection, can have its wait interrupted, and by default will clear t

tomcat.apache.org

DataSource에 대한 api method 참고.

 

커넥션 풀에 생성된 커넥션은 지속적으로 재사용 되는데 한 커넥션이 영원히 유지되는 것은 아니다. DBMS 설정에 따라 일정 시간 내에 쿼리를 실행하지 않으면 연결을 끊기도 한다. 이러한 상황에 커넥션을 풀에서 가져와 사용하면 끊어진 커넥션이니 입셉션이 나기에 커넥션이 유효한지 주기적으로 검사가 필요하다.

 

 

스프링을 사용하면 DataSource나 Connection, Statement, ResultSet을 직접사용하지 않고 JdbcTemplate을 사용해 편리하게 쿼리를 사용가능하다.

 

@Component
public MemberDAO{

    prirvate JdbcTemplate jdbcTemplate;
    
    MemberDAO(DataSource ds){
    	this.jdbcTemplate = new JdbcTemplate(ds);
    }
    
}

 

그리고 나서 컴포넌트를 설정하거나 MemberDAO를 빈 설정을 하면 기본적인 설정은 끝이다.

 

JdbcTemplate 클래스는 SELECT 쿼리 실행을 위해 query( )메서드를 제공한다.

자주 사용되는 메서드는 다음과 같다.

 

List<T> query(String sql, RowMapper<T> rowMapper)

List<T> query(String sql, Objects[] args, RowMapper<T> rowMapper)

List<T> query(String sql, RowMapper<T> rowMapper, Objects.. args)

 

query 메서드는 sql 파라미터로 전달받은 쿼리를 실행하고 RowMapper를 이용해 ResultSet의 결과를 자바 객체로 변환한다. sql 파라미터가 "select * from where email = ?"이라고 가정을 한다면 args 파라미터를 이용해 각 ?에 대한 인덱스 파미터들이 매칭된다.

 

쿼리 실행 결과를 자바 객체로 변환 시 사용하는 RowMapper 인터페이슨 다음과 같다.

public interface RowMapper<T>{
	T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

 

mapRow 메서드는 SQL 실행 결과로 구한 ResultSet에서 한 행의 데이터를 읽어와 자바 객체로 변환하는 매퍼 기능을 구현한다.

 

예시로 구현한 모습을 보자. { 람다로도 가능! }

 

public class MemberDAO{
	
    private JdbcTemplate jdbcTemplate;
    
    ... // 기존과 같은 생성자 코드
    
    public Member selectByEmail(String email){
    	List<Member> results = jdbcTemplate.query(
        	"select * from MEMBER where email = ?",
            new RowMapper<Member>(){
            	@Override
                public Member mapRow(ResultSet rs, int rowNum) throws SQLException{
                	
                    Member member = new Member(
                                      rs.getString("EMAIL"),
                                      rs.getString("PASSWORD"),
                                      rs.getString("NAME")
                                    );
                    
                    member.setId(rs.getLong("ID"));
                    
                    return member;
                }
            },
            email
       );
       
       return results.isEmpty() ? null : results.get(0);
    }
    
}

 

List<T> query(String sql, RowMapper rowMapper, Objects.. args)의 메서드를 사용했고 ?가 많을 땐 계속 인자를 추가하면 된다.

 

만약 결과가 한 행일때 List로 반환하면 비효율적이다.

그래서 queryForObject 메서드, 쿼리 실행 결과 행이 한 개인 경우에 이용할 수 있다.

2번째 인자타입은 칼럼을 읽어올 때 불러올 타입이다.

// 그냥 1개 행만 반환, 파라미터를 넣는 경우 //

public int count(){
	return JdbcTemplate.queryForObject("select count(*) from MEMBER", Integer.class);
}



public double avgHeight(){
	return JdbcTemplate.queryForObject(
    	"select avg(height) from FURNITURE where TYPE = ? AND STATUS = ?", 
         Double.class,
    	 100, "S"	
    );
}

 

실행 결과 칼럼이 두개라면 앞서 말한 queryForObject 메서드와 RowMapper 인터페이스를 구현해 적절히 사용하면 된다. /

+-26

ex) select * from MEMBER where ID = ? {Member 칼럼이 여러가지 있다고 가정 }

 

 

 

 

 

JdbcTemplate을 이용해 변경 쿼리도 실행 가능하다.

public void update(Member member){
	jdbcTemplate.update(
    	"update MEMBER set NAME = ?, PASSWORD = ?",
        member.getName(), membergetPassword());
}

 

위 코드는 인덱스 파라미터의 값으로 사용하며 대부분 이와 같은 방법을 사용한다.

그러나. PreparedStatement의 set 메서드를 사용해 직접 인덱스 파라미터의 값을 설정해야 할 경우도 존재한다.

이 경우 PreparedStatementCreator를 인자로 받는 메서드를 이용해 PreparedStatement를 직접 생성하고 설정해야한다.

 

public interface PreparedStatementCreator{
	PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}

 

메서드로 전달 받은 Connection 객체를 이용해 PreparedStatement 객체를 생성하고 인덱스 파라미터를 알맞게 설정한 뒤에 리턴하면 된다.

 

EX)

jdbcTemplate.update(new PreparedStatementCreator(){
	@Override
    public PreparedStatement createPreparedStatement(Connection con) throws SQLException{
    	PreparedStatement pstmt = con.prepareStatement(
        	"insert into MEMBER(EMAIL, PASSWORD, NAME, REGDATE) values(?, ?, ?, ?)"
           );
        )
        pstmt.setString(1, member.getEmail());
        pstmt.setString(2, member.getPassword());
        pstmt.setString(3, member.getName());
        pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
    	
        return pstmt;
    }

});

 

 

 

insert 쿼리 실행 시 KeyHolder를 이용해 자동 생성 키 값을 구할 수 있다.

설명을 위해,테이블에 auto_increment라는 키워드가 붙은 ID가 있다고 가정해보겠다.

 

 

public void insert(final Member member){
	KeyHolder keyHolder = new GeneratedKeyHolder();
   jdbcTemplate.update(new PreparedStatementCreator(){
	@Override
    public PreparedStatement createPreparedStatement(Connection con) throws SQLException{
    	PreparedStatement pstmt = con.prepareStatement(
        	"insert into MEMBER(EMAIL, PASSWORD, NAME, REGDATE) values(?, ?, ?, ?)"
           );
        )
        pstmt.setString(1, member.getEmail());
        pstmt.setString(2, member.getPassword());
        pstmt.setString(3, member.getName());
        pstmt.setTimestamp(4, Timestamp.valueOf(member.getRegisterDateTime()));
    	
        return pstmt;
    }, keyHolder);
}

// after using
Number keyValue = keyHodler.getKey();
member.setId(keyValue.longValue())

 

 

트랜잭션 처리)

 

스프링이 제공하는 @Transactional 어노테이션을 이용해 트랜잭션 범위를 매우 쉽게 지정가능 하다.

제대로 작동하려면 일단 두가지 내용이 필요하다.

1. 플랫폼 트랜잭션 매니저 빈 설정

2. @Transactional 어노테이션 활성화 설정

 

 

@Configuration
@EnableTransactionManagement
public class AppCtx{
	
    @Bean
    public DataSource dataSource(){
    	DataSource ds = new DataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl(db address...);
        ds.setUsername("db_user_name");
        ds.setPassword("db_password");
        ...
        return ds;
    }
    
    @Bean
    public PlatformTransactionManager platformTransactionManager(){
    	DataSourceTransactionManager tm = new DataSourceTractionManager();
        tm.setDataSource(dataSource());
        return tm;
    }
}

위 코드에서 트랜잭션을 적용할 dataSource를 넣어주는 것이 포인트.

그다음은 트랜잭션을 적용할 스프링 빈 객체의 메서드에 @Transactional 어노테이션을 추가하면 끝이다.

 

그러면 트랜잭션을 시작하고 커밋하고 롤백하는 것은 누가 어떻게 처리하는가?

여기서 프록시라는 개념이 다시 재등장한다.

프록시 객체는 커밋, 롤백의 과정을 전부 처리한다.(AOP개념과 비슷)

RuntimeException을 상속받은 입셉션이 등장-> 롤백

JdbcTemplate은 DB 연동과정에 문제가 있다면 DataAccessException 발생시키는 데 이것은 RuntimeException을 상속받으니 롤백.

SQLException은 RuntimeExcption을 상속받지 않는데 트랜잭션이 롤백이 되지않는다.

만약 SQLException 발생 시 롤백을 하고 싶은경우. 

@Transactional(rollbackFor = SQLException.class) : 여러 입셉션 지정하고 싶은 경우 { ... , ... } 같은 형식으로 지정하면 된다.

noRollbackFor 속성은 입셉션이 발생해도 롤백시키지 않고 커밋할 입셉션 타입을 지정할 때 사용한다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함