Connection conn = null;
try {
DriverManager.registerDriver(new org.h2.Driver());
//이거 안적어줘도 등록되어 있는 드라이버를 인식 자동으로 인식하기는 함
conn = DriverManager.getConnection("jdbc:h2:~/JDBC", "sa", "");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
DriverManager
는 라이브러리에 등록된 드라이버들을 관리하고 커넥션을 가져오는 기능을 제공한다.드라이버가 JDBC 인터페이스를 구현하고 DB를 작동시켜 SQL을 던지고 결과를 받음
드라이버를 통해 DB에 접근할 수 있는 커넥션을 얻을 수 있음
매니저로 커넥션 객체를 가져옴(url, 유저네임 , 패스워드) - 데이터베이스와 연결
Connection
: 특정 데이터베이스와 연결 객체로 DB와 이루어지는 기능들은 이 객체를 사용함
Statement
: SQL문을 실행해 작성한 결과를 담는 곳으로 먼저connection을 사용해서 생성해야 함
public void insert(Person person) {
String query = "INSERT INTO PERSON(id, name) "
+ "VALUES(" + person.getId() + ", '" + person.getName() + "')";
Statement statement = connection.createStatement();
statement.executeUpdate(query);
}
PreparedStatement
: sql문을 파라미터 없이 미리 컴파일해서 성능개선하고 이후에 파라미터 세팅
public void insert(Person person) {
String query = "INSERT INTO PERSON(id, name) VALUES(?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(query);
preparedStatement.setInt(1, person.getId());
preparedStatement.setString(2, person.getName());
preparedStatement.executeUpdate();
}
ResultSet
- SQL문에 대한 결과를 저장하는 곳, 내부에 있는 자료를 가르키는 커서를 통해 next()
함수로 데이터를 가지고 온다.
public Member findById(String memberId){
String sql = "select * from member where member_id = ?";
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet rs = null;
try {
connection = getConnection();
connection.prepareStatement(sql);
preparedStatement.setString(1, memberId);
rs = preparedStatement.executeQuery();
if (rs.next()){ // 선택한 멤버의 값을 새 멤버에 세팅
Member member = new Member();
member.setMemberId(rs.getString("memberId"));
member.setMoney(rs.getInt("money"));
return member;
}else {
throw new NoSuchElementException
("member not found memberId=" + memberId);
}
} catch (SQLException e) {
}finally {
close(connection, preparedStatement, rs);
}
}
executeQuery()
: select문을 실행해서 select된 데이터가 담근 Resultset을 반환
executeupdate
: DML(insert,update,delete)를 실행하고 업데이트 된 row의 개수를 반환
JDBC 드라이버 인스턴스 생성
DriverManager.*registerDriver*("드라이버 객체");`
JDBC 드라이버 인스턴스를 통해 DBMS에 대한 연결 생성
Connection conn = DriverManager.getConnection("URL", "user", "password");
Statement
생성 또는 PreparedStatement
생성
conn.createStatement(); // Statement는 완성된 쿼리문을 담음
// PreparedStatement
PreparedStatement stmt = conn.prepareStatement("완성되지 않은 SQL");`
`stmt.setString(1, 인자값 1);`
`stmt.setString(2, 인자값 2);`
`stmt.setInt(3, 인자값 3);
ResultSet / int
결과 받음ResultSet rs = stmt.executeQuery(”완성된 SELECT SQL”);
int rs = stmt.executeUpdate(”완성된 INSERT/UPDATE/DELETE SQL”);
ResultSet
Statement
Connection
를 close rs.close(); , stmt.close();`\ connet.close();
데이터를 가지고 올 때
는 DB에 접근해서 SQL을 실행하고 실행 결과를 자바 객체에 맵핑하며 데이터를 저장할 떄
는 SQL로 DB에 Record단위로 저장한다애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.
DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다.
DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달한다.
DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.
DB는 커넥션 생성이 완료되었다는 응답을 보낸다.
DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.
이러면 매번 커넥션을 생성해야 하기 때문에 시간이 오래걸린다. 그래서 나온 것이 커넥션 풀인데 아래에서 설명한다.
DriverManagerDataSource
라는 DataSource를 구현한 클래스를 제공한다DriverManagerDataSource driverManagerDataSource =
new DriverManagerDataSource(URL, USERNAME, PASSWORD);
Connection connection = driverManagerDataSource.getConnection();
설정과 사용
을 분리함으로써 이후 변경에 더 유연하게 대처할 수 있게 된다. 그러나 DriverManagerDataSource
방법은 설정과 사용을 분리하는 것일뿐 커넥션 풀을 사용하는 방법은 아니다.데이터베이스 연결을 미리 생성하여 관리하는 기술로 애플리케이션 시작 시 미리 정의된 개수의 데이터베이스 연결을 생성한다. 커넥션이 필요할 때는 DB에서 커넥션을 받는 것이 아니라 이미 만들어진 커넥션을 커넥션 풀에서 가져다가 쓰고 종료하지 않은 상태로 풀에 반환한다. 커넥션 풀
의 모든 커넥션은 DB와 TCP/IP로 연결되어 있어 별다른 처리가 필요하지 않다.
이러한 커넥션 풀은 스프링부트에서 HikariCP
로 기본설정되어 제공한다.
널리 사용되는 커넥션 풀 라이브러리 중 하나로 대량의 동시 연결 요청에 대해 최적화되어 있다.
사용하는 이유
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/스키마이름");
hikariConfig.setUsername("");
hikariConfig.setPassword("");
hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
Connection connection = dataSource.getConnection();
// 최초로 커넥션을 얻어오는 시점에 커넥션 풀이 채워진다
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="webuser"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
데이터베이스의 URL, 아이디, 비밀번호를 설정하고 커넥션을 가지고 올 때, 커넥션 풀에 최대 사이즈만큼 다른 쓰레드를 이용해서 커넥션이 채워지게 된다.
자동 커밋
: sql문장 하나를 실행할 때마다 자동으로 commit 실행, DB의 기본 설정임수동 커밋
: 수동 커밋모드로 설정해야 트랜잭션 기능을 제대로 사용할 수 있다. 마지막에 commit; 를 해야 커밋이 실행된다. 수동으로 커밋을 하기 전까지는 다른 세션에서는 변경된 데이터를 볼 수 없다.set auto commit false
set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
update member set money=10000 + 2000 where member_id = 'memberB';
commit; // 마지막에 커밋을 해주면 A에서 2000원이 감소하고 B에서 2000원이 감소
한다. 이렇게 작업단위가 하나로 묶이는 것이다.
set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
//성공해서 A의 돈 2000원 감소
update member set money=10000 + 2000 where member_iddd = 'memberB';
//쿼리 예외발생으로 실행되지 않음, B의 돈은 10000원 그대로
commit;
개발자가 sql문을 잘못 작성해서 A의 돈만 줄어드는 상황이 발생했고 commit를 했다면 A의 돈만 감소하고 B의 돈은 그대로인 치명적인 문제가 발생하게된다. commit를 하기 전에 이러한 문제가 발생했음을 인지했다면 rollback을 해야한다.
원자성
: 여러개의 작업을 하나로 묶었을 때, 하나의 작업처럼 성공하거나 실패 즉, 한 번에 커밋하거나 한 번에 롤백한다.일관성
: 모든 트랜잭션은 일관성있는 DB 상태를 유지해야한다격리성
: 동시에 실행되는 트랜잭션들이 서로에게 영향을 주지 않는다.지속성
: 트랜잭션이 성공적으로 종료되면 결과가 기록되어야 한다.트랜잭션은 비즈니스 로직이 있는 서비스계층에서 사용한다.
원자성에 따라 한 번에 커밋 또는 롤백해야 하기 때문이다.
트랜잭션을 시작하려면 커넥션이 필요하다. 따라서 서비스 계층에서 커넥션을 만들어야하고 트랜잭션이 커밋 또는 롤백으로 종료된 후에 커넥션을 종료해야 한다.
커넥션 생성 -> 트랜잭션 시작 -> 커밋,롤백 -> 트랜잭션 종료 -> 커넥션 종료
같은 세션을 사용하기 위해 트랜잭션을 사용하는동안 같은 커넥션을 유지해야한다. 따라서 커넥션을 끊는 것은 물론이고 새로 getConnection()으로 새로운 커넥션을 가지고 오는 것도 안된다.
JDBC
Connection con = dataSource.getConnection();
con.setAutoCommit(false);//트랜잭션 시작
// 비즈니스 로직 수행
memberRepository.update(con, fromId, fromMember.getMoney() - money);
// 파라미터로 같은 커넥션을 넘겨준다
con.commit(); , con.rollback //성공시 커밋 , 실패시 롤백
con.setAutoCommit(true); // 기본값으로 세팅하고 닫기
con.close()
트랜잭션 추상화
: 스프링에서는 다양한 기술들의 트랜잭션 시작,종료방법을 이미 인터페이스로 구현해놓았다. 트랜잭션 매니저
라고 부른다.
트랜잭션 동기화
: 앞에서 같은 세션을 사용하기 위해 트랜잭션을 사용하는동안 같은 커넥션을 유지해야한다고 말했다. 이를 위해 파라미터로 커넥션을 넘겼는데 스프링에서 같은 커넥션을 유지하기 위한 기능을 제공한다. 이것을 트랜잭션 동기화 매니저
라고 부른다.
트랜잭션 매니저, 동기화 매니저를 사용하면서 커넥션을 파라미터로 넘기지 않는 매서드와 넘기는 매서드 둘 중에 넘기는 매서드가 필요없어진다.
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야한다.
트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환하고, 없으면
커넥션을 새로 만들어서 반환한다.
Connection con = DataSourceUtils.getConnection(dataSource);
repository는 동기화 매니저에 저장된 커넥션을 가져가 사용한다.
커밋 또는 롤백으로 트랜잭션을 종료하면 트랜잭션 매니저가 동기화 매니저가 보관한 커넥션을 가져와 커넥션을 닫는다.
트랜잭션 동기화 매니저가 관리하는 커넥션인 경우 커넥션을 닫지 않고 그 외에는 닫는다.
DataSourceUtils.releaseConnection(con, dataSource);
PlatformTransactionManager
를 사용한다private final PlatformTransactionManager transactionManager;
TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());
transactionManager.commit(status);
transactionManager.rollback(status);
=> 커넥션을 파라미터로 전달할 필요가 없다.
클라이언트가 서비스를 요청해서 비즈니스 로직를 실행
서비스 계층에서 트랜잭션 매니저를 통해 트랜잭션 시작을 요청
트랜잭션을 시작하기 위해서 DB 커넥션이 필요한데 매니저는 datasource를 사용해서 커넥션을 생성
커넥션을 수동 커밋모드로 변경
생성된 커넥션은 동기화 매니저에 보관
서비스 계층에서 비즈니스 로직을 실행하면서 repository의 메서드를 호출
repository의 메서드들은 트랜잭션이 시작된 커넥션을 동기화 매니저에서 가지고 옴
획득한 커넥션을 사용해서 repository에서 SQL을 DB에 전달하고 데이터를 가져옴
비즈니스 로직이 완료되어 커넥션을 종료하려면 동기화 매니저에 맡겨놓은 커넥션을 가지고 와야한다. 그걸 가져와서 트랜잭션을 커밋 또는 롤백하고 커넥션을 닫는다
데이터소스 - 트랜잭션 매니저 - repository - service
private final TransactionTemplate txTemplate;
// TransactionTemplate 을 사용하려면 transactionManager 가 필요하다
public MemberService(PlatformTransactionManager transactionManager,
MemberRepository memberRepository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.memberRepository = memberRepository;
}
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
트랜잭션 템플릿을 사용할 때는 람다식으로..
txTemplate.executeWithoutResult((status) -> { 비즈니스 로직 }
TransactionTemplate을 포함한 지금까지의 과정 모두 큰 문제점이 하나 있는데 비즈니스 로직에 트랜잭션 과정이 들어가 있어 서비스 계층이 순수하지 못하다는 것이다.
@Transactional
메서드 또는 클래스에 사용하면 자동으로 Transaction Proxy 생성해준다. 그리고 @Transactional이 적용되려면 스프링 빈이어야 한다.@Bean
DataSource dataSource() {
return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
(SET LOCK_TIMEOUT)
select for update
select * from member where member_id='memberA' for update
시스템 예외
인지, 비즈니스 예외
인지 생각해 보아야 한다.체크 예외 : 컴파일러가 체크하는 예외 , 반드시 throws로 던지거나 try-catch로 처리해야한다. 체크예외의 경우 모든 예외를 던지거나 처리해야하므로 귀찮아질 수 있고 해당 계층에서 처리할 수 없는 예외까지 다루어야한다는 단점이 있다.
언체크 예외 : 컴파일러가 체크하지 않는 예외로 예외를 잡지않아도 throws를 생략할 수 있다. 예외를 잡지않으면 자동으로 throws 한다
비즈니스 로직상
발생한 예외나 매우 중요한 문제는 체크 예외를 사용한다.의존성
이 생기게된다. 따라서 나중에 기술을 변경하면 그 기술에 의존성이 있는 모든 코드를 수정해야한다.
static class RunRepository extends RuntimeException{
public RunRepository(String message) {
super(message);
}
}
static class Repository extends RuntimeException{
public void call(){
try {
sql();
} catch (SQLException e) {
throw new RunRepository("message");
}
}
public void sql() throws SQLException {
throw new SQLException();
}
}
다른 예외로 변환할 때
는 꼭 기존예외를 포함해야한다. 그렇게 하지 않으면 예외가 발생한 지점을 알 수 없다.public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
//기존 예외인 SQLException의 e를 가지고 있어야한다
}
e.printStackTrace()
는 System.out을 사용하는 것으로 실무에서는 사용하지 않는다.try {
controller.request();
} catch (Exception e) {
//e.printStackTrace();
log.info("ex", e);
}
public class MyDbException extends RuntimeException{
// 런타임 예외를 상속받으면서 런타입오류 클래스가 됨
public MyDbException() {
}
public MyDbException(String message) {
super(message);
}
public MyDbException(String message, Throwable cause) {
super(message, cause);
}
public MyDbException(Throwable cause) {
super(cause);
}
}
catch (SQLException e) {
throw new MyDbException(e);
오류코드
를 Exception과 함께 반환한다. 서비스 계층에서는 여기에 담긴 오류코드를 확인하려면 또 특정기술에 의존성을 가지게 되므로 다른 예외클래스를 생성한다.public class MyDuplicationEx extends MyDbException{
public MyDuplicationEx() {
}
public MyDuplicationEx(String message) {
super(message);
}
public MyDuplicationEx(String message, Throwable cause) {
super(message, cause);
}
public MyDuplicationEx(Throwable cause) {
super(cause);
}
}
이전에 생성했던 RuntimeException을 상속받은, MyDbException을 상속받으면 의미있는 예외클래스를 만들 수 있다.
catch (SQLException e) {
//h2 db
if (e.getErrorCode() == 23505) {
throw new MyDuplicateKeyException(e);
}
이렇게 직접 만든 예외로 처리를 하면 특정 기술에 의존하지 않고 서비스 계층에서 예외를 처리할 수 있지만 직접 예외코드를 찾아보고 예외를 만드는 것은 굉장히 힘들다
그런데 스프링에서는 데이터베이스에서 발생한 오류코드를 스프링에서 정의한 예외로 자동변환해주는 기능을 제공한다. 각 예외들은 기술에 종속적이지 않게 만들어졌다. 따라서 우리가 직접 예외를 새로 만들 필요가 없다.
private final SQLErrorCodeSQLExceptionTranslator exTranslator;
public MemberRepositoryV4_2(DataSource dataSource) {
this.dataSource = dataSource;
this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
catch (SQLException e) {
throw exceptionTranslator.translate("save", sql, e);
변수로 예외변환기를 선언하고 생성자로 datasource를 주입해서 사용한다.
에러
예외