(인프런) 김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술을 공부하고 리뷰한 글입니다.
스프링 DB 접근 기술 4가지 방식대로 차례로 진행할 예정이다.
1. 순수 JDBC
1. 환경 설정
1. build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
2. application.properties 파일에 스프링 부트 데이터베이스 연결 설정
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
주의) 스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다. spring.datasource.username=sa 공백 주의, 공백은 모두 제거해야 한다.
2. Jdbc 리포지토리 구현
JDBC API로 직접 코딩하는 것은 20년 전 이야기로 참고정도만 하자.
1. Jdbc 회원 리포지토리
1) repository 패키지에 JdbcMemberRepository 클래스 생성
2) JdbcMemberRepository 클래스 코드 작성
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);
}
}
2. 스프링 설정 변경
1) SpringConfig 클래스 수정
기존에는 주석처리된 부분처럼 실제 DB가 아닌 메모리 구현체를 만들어 DB처럼 사용했기 때문에 MemoryMemberRepository를 스프링 빈으로 등록해서 사용했다. 하지만 이제 h2 데이터베이스를 사용하기 때문에 저 부분을 아래와 같이 JdbcMemberRepository를 스프링 빈으로 등록하고 DataSource와 의존 관계를 설정해주었다.
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
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);
}
}
- DataSource: 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 등록해 둔다. 그러므로 DI를 받을 수 있다.
3. 구현 클래스 추가 이미지
MemberService는 MemberRepository와 의존 관계에 있기 때문에 구현체를 MemoryMemberRepository → JdbcMemberRepository 로 바꾸어도 설정 파일 외 다른 코드는 바뀌지 않는다.
4. 스프링 설정 이미지
2번에서 스프링 설정 파일(SpringConfig)를 고치면 아래 그림과 같이 바뀌게 된다.
5. 정리
스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경 할 수 있다!
개방-폐쇄 원칙(OCP): 확장에는 열려있고 수정, 변경에는 닫혀있다.
다형성 개념을 잘 활용하면 기능을 변경해도 애플리케이션 전체를 수정할 필요가 없다. 조립하는 코드(Spring Config)만 수정하면 된다는 것이 객체지향에서 다형성의 매력이다.
'Spring > 스프링 입문' 카테고리의 다른 글
[스프링 입문] 06. 스프링 DB 접근 기술 - 스프링 JdbcTemplate (0) | 2022.04.29 |
---|---|
[스프링 입문] 06. 스프링 DB 접근 기술 - 스프링 통합 테스트 (0) | 2022.04.29 |
[스프링 입문] 06. 스프링 DB 접근 기술 - H2 데이터베이스 설치 (0) | 2022.04.28 |
[스프링 입문] 05. 회원 관리 예제(웹 MVC 개발) - 회원 웹 기능3(조회) (0) | 2022.04.28 |
[스프링 입문] 05. 회원 관리 예제(웹 MVC 개발) - 회원 웹 기능2(등록) (0) | 2022.04.28 |