스프링 DB 접근 기술
순수 JDBC → 스프링 JDBC로 발전함: 쿼리를 사용하여 DB에 접근, 데이터 저장/조회/수정 등을 수행함
JPA: 객체를 쿼리 없이 DB에 바로 저장할 수 있는 기술
JPA → 스프링 JPA: JPA를 더 편하게 쓸 수 있도록 스프링으로 한 번 감싼 것
H2
→ All platform → unzip → bin
h2.sh의 권한: 664 ⇒ chmod 755 h2.sh
alias 추가:
sudo vi ~/.zshrc
> alias h2=”h2.sh까지의 경로”
source ~/.zshrc
→터미널에서 “h2” 입력하여 실행
→ “연결” 클릭
“연결 끊기”버튼으로 나갈 수 있음
홈 디렉토리에 test.mv.db가 생성됨
이후부터는, JDBC URL “jdbc:h2:tcp://localhost/~/test”인 소켓 형식으로 접속 (파일 형태의 데이터베이스에 접속하면 각 애플리케이션들이 DB 접속할 때마다 충돌이 날 수 있기 때문)
쿼리
테이블 생성을 위해 아래 쿼리를 실행한다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
- generated by default as identity: 해당 컬럼이 DB에 의해 자동으로 값을 생성하고 관리되는 것을 의미한다. 구체적으로는, 값 없이 DB로 들어오면 DB가 알아서 값을 채워달라는 의미다. 따라서, 테이블에 새로운 레코드가 삽입될 때마다 자동으로 증가하는 일련번호(또는 ID)를 부여한다.
- generated by default: 컬럼 값이 개발자의 직접적인 값 입력 없이도 데이터베이스에 의해 자동으로 생성된다.
- as identity: 해당 컬럼은 IDENTITY 컬럼이다. IDENTITY 컬럼은 자동으로 증가하는 값을 가지며, 일반적으로 프라이머리 키(primary key)로 사용된다. 주로 테이블의 기본 키(primary key)로 사용되는 컬럼에 사용된다.
값을 넣는다.
insert into MEMBER(name) values('Spring1');
insert into MEMBER(name) values('Spring2');
SELECT * FROM MEMBER
id를 생략하고 insert를 해도 자동으로 들어가는 것을 확인할 수 있다.
쿼리는 보통 프로젝트에 파일로 만들어 관리한다.
순수 JDBC
Java Database Connectivity의 약자 자바 프로그램이 데이터베이스와 통신할 수 있도록 해주는 자바 API(응용 프로그래밍 인터페이스) 이 API로 데이터베이스에 접속하고 SQL 쿼리를 실행할 수 있다.
필요한 API 설치: build.gradle
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
→ src/main/resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
→ src/main/java/hello/hellospring/repository/JdbcMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository{
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
이제부턴 인메모리 데이터베이스가 아닌 h2 데이터베이스를 이용하는 방식의 새로운 구현체를 만들 것이다.
이제, DataSource를 주입받고, 주입받은 DataSource를 통해 DB connection 소켓을 얻어낸 다음, 쿼리를 날리면 된다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
- 연결할 때, 그리고 연결된 자원을 release할 땐 항상 DataSourceUtils을 통해야 한다.
- close(…) {…} : 자원의 해제는 자원의 할당과 역순이다.
- getConnection, preparedStatement 등은 메서드 호출 시 마다 해줘야 한다.
- 순수 JDBC 방식은 try-catch를 이용하여 예외처리를 잘 처리해줘야 한다.
MemoryMemberRepository를 Spring Bean으로 등록한 것과 같이, 순수 JDBC를 이용한 JdbcMemberRepository 역시 Spring Bean으로 등록해줘야 한다. (→ 자바 코드로 직접 스프링 빈 등록하는 방식)
// src/main/java/hello/hellospring/SpringConfig.java
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
// 1
@Autowired
DataSource dataSource1;
// 2
@Autowired
private DataSource dataSource2;
public SpringConfig(DataSource dataSource) {
this.dataSource2 = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource2);
}
}
application.properties에서 지정해준 덕에 스프링이 DataSource를 알고 있다. 따라서 이를 그대로 주입해줄 수 있다.
MemberRepository 인터페이스의 다형성을 활용하였다.
JdbcMemberRepository.java를 새로 생성하고, 이에 따라 SpringConfig.java 코드를 수정한 것 외에는 그 어떤 코드도 건들지 않았다. 특히, MemberService를 수정하지 않아도 된다.
이러한 설계 방식이 SOLID 원칙 중, OCP 원칙을 준수한다고 볼 수 있다. Open-Closed Principle(개방-폐쇄 원칙): 개방된 확장, 폐쇄된 수정(확장에는 열리고, 수정에는 닫힌다.) ⇒ 기능을 완전히 변경해도, 거의 수정하지 않는다.
'TIL' 카테고리의 다른 글
2023-11-01-TIL | Spring (1) | 2023.11.03 |
---|---|
2023-10-31-TIL (1) | 2023.11.01 |
2023-10-28-TIL (1) | 2023.10.31 |
2023-10-27-TIL (1) | 2023.10.27 |
2023-10-26-TIL (0) | 2023.10.27 |