인프런 김영한 스프링 입문
섹션 6. 스프링 DB 접근 기술
- 순수 JDBC
이번 시간에는 애플리케이션에서 DB에 연동하여 메모리가 아닌 DB에 쿼리를 날리는 것을 해볼 것임.
그 중에서도 가장 오래 된, 20년 전 방식.
(편하게 듣고 필요할 때 찾아보면 됨, 어떤 방식으로 발전 되어왔는지 알아보기 위한 것)
1. build.gradle > dependencies에 아래 코드 추가 후 우측 상단의 코끼리 아이콘 클릭
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
spring-boot-starter-jdbc : 자바는 기본적으로 DB와 붙으려면 jdbc 드라이브가 꼭 필요함.
runtimeOnly 'com.h2database:h2' : DB와 붙을 때 데이터베이스가 제공하는 클라이언트를 필요로 함, h2 클라이언트.
2. 'src>main>resources' 경로의 'application.properties' 파일에 아래 코드 입력.
(DB에 붙기 위한 접속 정보)
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
파일에서 코드가 빨간색으로 안뜨면 완료.
만약 h2.Driver가 빨간색으로 뜰 시 gradle sync 하기.
다른 DB라면 위 파일에 id, password 입력해야하나 h2 는 생략가능
본격적으로 jdbc api를 이용해 개발 시작.
3. 'repository' 아래에 'JdbcMemberRepository'파일 생성 후 아래와 같이 코드 수정,
빨간줄에 'option+Enter' -> 'Implement methods' -> ok 클릭
public class JdbcMemberRepository implements MemberRepository
기존에 MemoryMemberRepository에서 구현 한 것 처럼, MemberRepository Interface를 만들어 뒀기 때문에 구현체를 만들면 됨.
4. 메인 함수 내부에 아래 코드 입력,
dataSource import 후 'cmd+n' -> constructor > '엔터' 클릭하면 아래 생성자 메소드가 생김
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
//dataSource.getConnection();
}
DB에 붙으려면 javax.sql.DataSource의 'DataSource'라는 것이 필요함.
이는 나중에 spring에게 주입받아야함.
위에서 셋팅 한 뒤, 스프링 부트가 dataSource를 만들어 둠, 나중에 스프링을 통해 주입받을 수 있음.
'getConnection()' -> 얘를 통해 database connection을 얻을 수 있음.
여기에 sql문을 날려 DB에 전달해주면 됨.
5. 파일 전체 코드 복붙
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(?)"; //save하는 sql
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null; //결과를 받음
try {
conn = getConnection(); // 커넥션 받아옴
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
//두번째 인자는 insert를 해봐야 id값을 얻을 수 있었는데, 이를 위해 사용됨.
pstmt.setString(1, member.getName());
//첫번째 인자, parameterIndex: 1에서 이 '1'이 위 sql문의 물음표와 매칭됨, member.getName()으로 값을 입력
pstmt.executeUpdate();
//위 코드에서 DB에 실제 쿼리가 날라감.
rs = pstmt.getGeneratedKeys();
// prepareStatement()의 두번째 인자와 매칭됨, 방금 생성한 키를 꺼내옴.
if (rs.next()) { //값이 있으면 꺼내옴
member.setId(rs.getLong(1)); //getLong으로 값을 꺼내고 member.setId()로 셋팅해줌
} else {
throw new SQLException("id 조회 실패"); //실패시 출
}
return member;
} catch (Exception e) { // 위 코드들이 exception을 엄청 많이 던짐, try catch문 잘 사용해줘야함.
throw new IllegalStateException(e);
} finally {
//사용한 자원들 release 해줘야 함. DB connection은 외부 네트워크가 연결 된 것이기 때문에 끝나면 바로 자원을 끊어야함,
//resource를 반환해야 함.
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);
// 조회는 executeUpdate()가 아닌 executeQuery()
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(); //없을 시 empty
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
//Spring과 연결할 때 'DataSourceUtils'를 통해서 connection을 획득해야함!
//DB transaction이 걸릴 수 있는데 이때 connection을 똑같은 것을 유지시켜줌
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();
}
}
//역시 Datasource'Utils'를 통해서 release 해줘야함.
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
6. 'SpringConfig'파일을 아래와 같이 수정
지금까지 다 메모리에서 해왔음, SpringConfig에 'MemoryMemberRepository()'를 spring bean으로 등록하고 있었는데, 이를 수정해주는 과정임.
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 {
//스프링부트가 설정파일을 보고 자체적으로 DB와 연결할 수 있는 dataSource를 만들어줌.
private DataSource dataSource;
//스프링이 만들어준 dataSource를 주입
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
지금까지 JdbcMemberRepository라는 구현체를 만들어 interface를 확장하고, 그 외 어떤 코드도 수정하지 않음,
스프링이 제공하는 configuration만 수정함.
7. 스프링 실행 (이때 terminal 에서 h2를 실행시키고 있어야만 함)
**주의
스프링부트 2.4부터는 'application.properties' 파일에 아래 코드를 꼭 추가해주어야 한다.
아니면 '회원목록' 페이지에서 위 사진과 같이 에러가 뜬다.
spring.datasource.username=sa
위 코드만 추가해주면 정상적으로 DB에 있는 회원 목록이 뜬다!
8. 웹에서 회원가입 후 확인해보기
'회원가입' 페이지에서 이름 추가 후 '회원목록'을 확인해보면 잘 들어가있다.
DB에서 확인해보아도 잘 들어가 있다.
스프링을 왜 사용하냐?
-> 위 수업과 같은 경우 때문. 객체지향적 설계가 좋은 이유는, 소위 '다형성을 활용한다'고 하는데, 인터페이스를 두고 구현체 바꿔끼기를 할 수 있음. 스프링은 이를 매우 편리하게 구현할 수 있도록 컨테이너가 이를 지원해줌. 'Dependency Injection' 덕분에 매우 편리함.
예를들어 과거엔 MemoryMemberRepository에서 JdbcMemoryRepository를 의존하는 것으로 수정하기 위해 'MemberService'파일 자체의 코드를 수정했어야 했음.
그러나 여기선 애플리케이션을 설정하는 코드(어셈블리, 즉 애플리케이션을 조립하는 코드, SpringConfig)만 수정하면 나머지 실제 어플리케이션과 관련된 코드는 바꾸지 않아도 됨. 이것이 스프링의 장점.
MemberService는 MemberRepository를 의존하고 있음.
MemberRepository는 구현체로 MemoryMemberRepository와 JdbcMemberRepository가 있음.
기존에는 memory버전을 스프링빈으로 등록했다면, 얘를 빼고 jdbc 버전의 repository를 등록, 이 외엔 손댈게 없음.
그러면 구현체가 jdbc로 바뀌어 DB로 돌아감.
이를 'SOLID 단일 원칙'이라고 부름.
이중 가장 중요한 것은 '개방, 폐쇄 원칙 OCP(Open-Closed Principle)'이라고 함.
-> 확장에는 열려있고, 수정에는 닫혀있다.
객체지향에서 말하는 다형성이라는 개념을 잘 활용하면 기능을 완전히 변경해도 애플리케이션 전체를 수정할 필요 없음.
(물론 조립하는 코드는 수정해야하긴 하나 어쩔 수 없음)
-> 이것이 개방, 폐쇠 원칙이 지켜진 것임.
스프링의 DI를 사용하면 '기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
-> 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
'Backend' 카테고리의 다른 글
김영한 스프링 입문 6. 스프링 DB 접근 기술 - 스프링 통합 테스트 (1) | 2024.06.06 |
---|---|
김영한 스프링 입문 6. 스프링 DB 접근 기술 - H2 데이터베이스 설치 (0) | 2024.05.16 |
[인프런] 스프링 입문 - 섹션 1. 프로젝트 환경설정 (2) | 2021.11.27 |