상세 컨텐츠

본문 제목

[스프링2] 8장. DB 연동 (1)

22-23/22-23 Spring 2

by YUZ 유즈 2022. 11. 17. 10:00

본문

728x90

 

 

8장의 키워드

# JDBC API

# DataSource

# JDBC Template

# 커넥션 풀

 


  • 웹 애플리케이션은 데이터 보관을 위해 DBMS를 사용
  • JAVA: JDBC API, JPA, MyBatis 등의 기술 사용

 

JDBC 프로그래밍이란?

자바에서 제공하는 표준 SQL 인터페이스 API로써, 여러 개의 관계형 데이터베이스에 접근하여 SQL 문을 수행하여 처리

출처: https://deeplearning20min2.tistory.com/9

 

JDBC API를 사용한 DB 연동

코드가 구조적으로 반복되는 문제

  • DB 연동에 필요한 Connection 구함
  • 쿼리를 실행하기 위한 PreparedStatement 생성
  • finally 블록에서 ResultSet, PreparedStatement, Connection 닫음

템플릿 메서드 패턴, 전략 패턴 사용하여 구조적 반복 줄이기

템플릿 메서드 패턴
전략 패턴

템플릿 메서드 패턴특정 작업을 처리하는 일부분을 서브 클래스로 캡슐화하여 전체적인 구조는 바꾸지 않으면서 특정 단계에서 수행하는 내용을 바꾸는 패턴

전략 패턴같은 문제를 해결하는 여러 알고리즘이 클래스별로 캡슐화되어 있고 이들이 필요할 때 교체할 수 있도록 함으로써 동일한 문제를 다른 알고리즘으로 해결

 

⇒ 템플릿 메서드 패턴과 전략 패턴을 엮은 JdbcTemplete 클래스 제공

 

@Transactional 애노테이션

스프링이 제공하는 장점: 트랜잭션 관리가 쉽다!

트랜잭션을 적용하고 싶은 메서드에 @Transactional 애노테이션을 붙인다.

트랜잭션: DB의 상태를 변화하는 작업을 수행하는 최소 단위

@Transactional
public void insert(Member member){
  ...
}

 

프로젝트 생성

  • pom.xml 의존 추가
  • 3장 예제 코드 복사
  • DB 테이블 생성

 

pom.xml 의존 추가

  • Spring-jdbc: JdbcTemplete 등 JDBC 연동에 필요한 기능 제공
  • tomcat-jdbc: DB 커넥션풀 기능 제공
  • mysql-connector-java: MySQL 연결에 필요한 JDBC 드라이버 제공

# 트랜잭션 기능을 사용하기 위해 Spring-tx 모듈 필요, Spring-jdbc가 포함하고 있어 추가하지 않았다.

 

✏️ 커넥션 풀(Connection Pool)?

1. 자바 프로그램에서 DBMS로 커넥션을 생성하는 시간이 매우 길다.

  • DB 커넥션을 생성하는 시간이 전체 성능에 영향을 줄 수 있다.
  • 동시 접속자가 많을수록 생성하는 DB 커넥션 증가, DBMS에 부하

2. 응답 시간, 부하를 줄이기 위해 커넥션 풀을 사용

  • DB 커넥션을 미리 만들어 둠
  • 커넥션이 필요한 프로그램은 커넥션을 사용하고 반납

 

DataSource 설정

JDBC API는 Datasource를 이용하여 DB 연결을 구하는 방법을 정의한다.

  • 커넥션 활성(active) 상태: 커넥션 풀에 커넥션을 가져왔을 때
  • 커넥션 유휴(idle) 상태: 커넥션 풀 반환
@Configuration
public class DbConfig {

	@Bean(destroyMethod = "close") // 스프링 빈 등록
	public DataSource dataSource() {
		DataSource ds = new DataSource(); // DataSource 객체 생성
		ds.setDriverClassName("com.mysql.jdbc.Driver"); // JDBC 드라이버 클래스 지정
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8"); // JDBC URL 지정
		ds.setUsername("spring5");
		ds.setPassword("spring5");
		ds.setInitialSize(2); // 생성한 초기 커넥션 개수
		ds.setMaxActive(10); // 커넥션 풀에서 가져올 수 있는 최대 커넥션 개수
        	ds.setTestWhileIdle(true); // 유휴 커넥션 검사
		ds.setMinEvictableIdleTimeMillis(60000 * 3); // 최소 유휴 시간 = 3분
		return ds;
	}

// DbQuery.java
public class DbQuery {
	private DataSource dataSource;

	public DbQuery(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public int count() {
		Connection conn = null;
		try {
			conn = dataSource.getConnection(); // 커넥션 풀에서 커넥션 가져오기
			try (Statement stmt = conn.createStatement();
					ResultSet rs = stmt.executeQuery("select count(*) from MEMBER")) {
				rs.next();
				return rs.getInt(1);
			}
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			if (conn != null)
				try {
					conn.close(); // 커넥션 종료
				} catch (SQLException e) {
				}
		}
	}

}

커넥션 풀에서 커넥션을 가져오면 conn은 활성 상태, conn.close()에서 커넥션을 반환하면 유휴 상태

MaxActive가 10으로 설정, 활성 상태의 커넥션 = 10개라고 가정하면, 다른 커넥션이 반환될 때까지 대기한다.

대기 시간 내에 풀에 반환된 커넥션이 있으면 해당 커넥션을 사용, 없으면 익셉션 발생

 

커넥션 풀을 사용하는 이유: 성능

커넥션 풀에 생성된 커넥션은 지속적으로 재사용, 영구적인 것은 아님

일정 시간 내에 쿼리를 실행하지 않으면 커넥션을 끊는다.

 

예) 커넥션 특정 커넥션이 5분 이상 유휴 상태로 존재하면 DBMS는 해당 커넥션 연결을 끊는다. 하지만 커넥션은 여전히 커넥션 풀 속에 남아있다. DBMS와의 연결이 끊긴 커넥션을 사용하면 익셉션이 발생한다.

⇒ 커넥션 풀의 커넥션이 유효한지 주기적인 검사가 필요

 

Jdbc Templete

1. Jdbc Templete 생성

// MemberDao.java 
public class MemberDao {

	private JdbcTemplate jdbcTemplate;

	public MemberDao(DataSource dataSource) { // dataSource 주입 받음
		this.jdbcTemplate = new JdbcTemplate(dataSource); // jdbcTemplate 생성
	}
// AppCtx.java
@Configuration
public class AppCtx {

	@Bean(destroyMethod = "close")
	public DataSource dataSource() {
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		...
	}

	@Bean
	public MemberDao memberDao() { // 빈 객체 등록
		return new MemberDao(dataSource());
	}

2. Jdbc Templete을 이용한 조회 쿼리 실행

Jdbc Templete 클래스는 SELECT 쿼리 실행을 위한 query() 메서드 제공

sql 파라미터로 전달받은 쿼리 실행, RowMapper을 이용하여 ResultSet의 결과를 자바 객체로 변환

// p192 MemberDao.java 
 
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"),
                            rs.getTimestamp("REGDATE").toLocalDateTime());
                    member.setId(rs.getLong("ID"));
                    return member;
                }
            }, email);

    return results.isEmpty() ? null : results.get(0);
}
  • RowMapper은 ResultSet에서 데이터를 읽어와 Member 객체로 변환해주는 기능 제공
  • RowMapper의 타입 파라미터로 Member 사용 = RowMapper<Member>
  • MapRow() 메서드는 파라미터로 전달받은 Resultset에서 데이터를 읽어와 Member 객체를 생성하여 리턴
  • results.isEmpty(): 결과가 존재하는지 확인, 존재하지 않으면 길이가 0인 List를 반환  

 

3. 결과가 1행인 경우 사용할 수 있는 queryForObject() 메서드

Member 테이블의 전체 행 개수를 구하는 코드

public int count() {
	List<Member> results = jdbcTemplate.query(
    	"select * from MEMBER",
        new RowMapper<Member>() {
        	@Override
            public Member mapRow(ResultSet rs, int rowNum)
            	throws SQLException {
                    return rs.getInt(1);
                });
        }
   return results.get(0);
 )}
// Memberdao.java
public int count() {
    Integer count = jdbcTemplate.queryForObject(
            "select count(*) from MEMBER", Integer.class); 
    return count;
}

두 번째 파라미터는 칼럼을 읽어올 때 사용할 타입 지정

 

4. JdbcTemplete을 이용한 변경 쿼리 실행

INSERT, DELETE, UPDATE ⇒ update() 메서드 사용

쿼리 실행 결과로 변경된 행의 개수 리턴

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

 

5. PreparedStatememtCreator를 이용한 쿼리 실행

지금까지는 위 4번의 방법으로 쿼리에서 사용할 값을 인자로 전달

PreparedStatement에서 파라미터의 값을 직접 설정할 때가 있다. 

public interface PreparedStatementCreator {
	PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
// 200p
jdbcTemplate.update(new PreparedStatementCreator() {
@Override
    public PreparedStatement createPreparedStatement(Connection con) // Connection 파라미터
    
    	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;
        }
});

파라미터로 전달받는 Connection을 이용하여 prepareStatement 객체를 생성하고 인덱스 파라미터를 알맞게 설정한 뒤 prepareStatement 객체를 리턴한다.

 

6. Insert 쿼리 실행 시 KeyHolder를 이용해서 자동 생성 키값 구하기

update() 메서드는 PreparedStatement를 실행한 후 자동으로 생성된 키 값을 keyHolder에 보관한다.

getKey() 메서드를 이용하여 keyHolder에 보관된 Key 값을 구한다.

public void insert(Member member) {
    KeyHolder keyHolder = new GeneratedKeyHolder(); // 자동 생성된 키값 구해줌
    jdbcTemplate.update(new PreparedStatementCreator() { // 파라미터: PreparedStatementCreator, keyHolder 
        @Override
        public PreparedStatement createPreparedStatement(Connection con)
                throws SQLException {
            // 파라미터로 전달받은 Connection을 이용해서 PreparedStatement 생성
            PreparedStatement pstmt = con.prepareStatement(
                    "insert into MEMBER (EMAIL, PASSWORD, NAME, REGDATE) " +
                    "values (?, ?, ?, ?)",
                    new String[] { "ID" });
            // 인덱스 파라미터 값 설정
            pstmt.setString(1, member.getEmail());
            pstmt.setString(2, member.getPassword());
            pstmt.setString(3, member.getName());
            pstmt.setTimestamp(4,
                    Timestamp.valueOf(member.getRegisterDateTime()));
            // 생성한 PreparedStatement 객체 리턴
            return pstmt;
        }
    }, keyHolder);
    Number keyValue = keyHolder.getKey();
    member.setId(keyValue.longValue());
}

 

🚨DB 연동 과정에서 발생할 수 있는 익셉션

  • DB에 연결할 권한이 없는 경우, 암호를 잘못 지정했을 때
  • DB를 실행하지 않았거나 방화벽에 막혀 있는 경우
  • 잘못된 쿼리 사용

퀴즈

1. 빈칸, 선택 문제

1. 웹 애플리케이션은 데이터 보관을 위해 DBMS를 사용한다. DB 연동을 위해서 JAVA에서는 (JDBC API), (JPA), (MyBatis) 기술을 지원한다.

2. JDBC API를 사용하여 발생하는 구조적인 문제를 해결하기 위해 템플릿 메서드 패턴, 전략 패턴을 엮은 (JdbcTemplete 클래스)을/를 사용한다.

3. JDBC API로 트랜잭션을 처리할 때 트랜잭션을 적용하고 싶은 메서드에 (@Transactional) 애노테이션 을/를 붙여준다.

4. JAVA에서 DBMS로 커넥션을 생성하는데 시간이 걸리기 때문에  커넥션을 (커넥션 풀)에 미리 생성하여 저장한다.

5. 커넥션 풀에서 커넥션을 가져오면 해당 커넥션은 (활성 상태), 커넥션을 커넥션 풀이 반환하면 해당 커넥션은 (유휴 상태)가 된다.

6. JDBC API는 (DataSource) 클래스를 사용하여 DB와의 연동을 정의한다. 

7. Jdbc Templete에서 SELECT 쿼리를 수행하기 위해 (query()) 메서드 사용, INSERT, UPDATE, DELETE 쿼리를 수행하기 위해 (update()) 메서드를 사용한다.

 

2. 코드 문제

1. QUERY 구문을 작성한다.

  • DB 연동에 필요한 Connection을 구한다.
  • 쿼리를 실행하기 위한 Statement 를 생성한다.
  • Statement에서 지원하는 executeQuery() 메서드를 사용하여 MEMBER의 count를 구해보자.# MEMBER의 count를 구하는 sql문 = select count(*) from MEMBER
  • 힌트: 책에 있음
public int count() {
		Connection conn = null;
		try {
			conn = /* 채우기 */
			try (Statement stmt = /* 채우기 */ 
					ResultSet rs = /* 채우기 */) {
				rs.next();
				return rs.getInt(1);
			}
		} catch (SQLException e) {
			throw new RuntimeException(e);
		} finally {
			if (conn != null)
				try {
					conn.close();
				} catch (SQLException e) {
				}
		}
	}

2. 결과가 1행일 때 사용할 수 있는 메서드를 이용하여 다음 코드를 작성하자.

MEMBER의 급여(sal)의 평균을 구한다고 가정, 반환 타입은 Double

힌트: select AVG(sal) from MEMBER

	public int average() {
		Integer average = jdbcTemplate.?어떤 메서드일까요?(/* 채우기 */);
		return average;
	}

 

 

 


Corner Spring #2

Editor : Otcr

728x90

관련글 더보기