들어가며
🗄️ H2 데이터베이스: 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면을 제공합니다.
🔌 순수 JDBC: 고대 개발자의 삶을 돌아보고, 객체지향적인 설계가 좋은 이유에 대해서 알아봅시다.
1. H2 데이터베이스
1) 다운로드 및 설치
- https://www.h2database.com/html/main.html
- 위 주소에서 원하는 버전의 H2 데이터베이스를 다운로드 합니다. (버전은 스프링 부트 버전에 맞춥니다.)
- 아래와 같이 실행파일을 눌러 실행합니다.
2) 데이터베이스 생성
- 데이터베이스 파일 생성 방법은 다음과 같습니다.
- 최초 한번: jdbc:h2:~/test
- 내장 모드로 데이터베이스에 접속합니다.
- 데이터베이스가 애플리케이션 프로세스 내에서 실행되기에, 애플리케이션이 실행되는 동안에만 데이터베이스에 접근할수 있습니다.
- 이후: jdbc:h2:tcp://localhost/~/test
- 서버 모드로 데이터베이스에 접속합니다.
- 데이터베이스 서버가 독립적으로 실행되고, 애플리케이션은 TCP 소켓을 통해 서버에 접속합니다. 즉, 네트워크를 통해 데이터베이스에 접근하는 방식입니다.
- 이 방식은 여러 애플리케이션 또는 사용자가 동시에 데이터베이스에 접속해야 하는 상황에서 사용됩니다.
- 최초 한번: jdbc:h2:~/test
- create table member로 멤버 테이블을 생성하고, insert into member values(name)으로 원하는 값을 넣습니다.
- 테이블 관리를 위해 프로젝트 루트에 sql/ddl/sql 파일을 생성합니다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
2. 순수 JDBC
⚠️이렇게 JDBC API로 직접 코딩하는 것은 약 20년 전 이야기입니다.
따라서 고대 개발자들이 이렇게 고생하고 살았구나를 생각하고, 정신건강을 위해 참고만 하고 넘어가면 됩니다.
1) 환경설정 및 JDBC Repository 구현
- build.gradle: jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
- resources/application.properties: 스프링부트 데이터베이스 연결 설정 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
- JDBC Respository 구현
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
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(); // 조회는 update가 아니라 query
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);
}
}
- SpringConfig - 스프링 설정 변경
package hello.hello_spring;
import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import hello.hello_spring.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 {
private 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);
}
}
2) 객체지향적인 설계가 좋은 이유
💡 다형성을 활용할 수 있기 때문입니다. 즉, Interface를 두고 구현체를 바꾸어 낄 수 있습니다.
Spring은 이를 편리하게 할 수 있도록 IoC컨테이너 + DI가 이를 지원합니다. (OCP 원칙 준수)
- Spring을 쓰는 이유: 아래 코드가 가능하기 때문입니다.
- DataSource: 데이터베이스 커넥션을 획득할 때 사용하는 객체입니다.
Spring Boot는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 Spring Bean으로 만들어둡니다. 따라서 DI(Dependency Injection)를 받을 수 있습니다.
- DataSource: 데이터베이스 커넥션을 획득할 때 사용하는 객체입니다.
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
...
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
- 객체지향적인 설계가 왜 좋을까요?
- 다형성을 활용할 수 있기 때문입니다. 즉, Interface를 두고 구현체를 바꾸어 낄 수 있습니다.
- Spring은 이를 편리하게 할 수 있도록 IoC컨테이너 + DI가 이를 지원해 줍니다.
- 애플리케이션 설정을 조립하는 코드만 수정하면, 실제 어플리케이션 관련 코드는 수정하지 않아도 문제 없이 돌아갑니다. 이를 편리하게 해 주는 것이 Spring의 장점입니다.
- 구현 클래스 추가 이미지를 살펴보자.
- MemberService → interface: MemberRepository를 의존하고 있습니다.
- interface: MemberRepository는 두 개의 구현체를 가지고 있습니다.(MemoryMemberRepository, JdbcMemberRepository)
- 스프링 컨테이너에서 설정을 어떻게 바꾸는가?
- 기존: memory버전의 memberRepository를 등록합니다.
- 변경: jdbc버전의 memberRepository를 등록합니다.
- 즉, 구현체만 바꾼 뒤 서비스가 돌아가게 해 주는 것입니다.
- 이는 Solid 원칙 중 OCP를 잘 따르는 것이라고 볼 수 있습니다.
(OCP: 개방-폐쇄 원칙, 확장에는 열려있고, 수정 및 변경에는 닫혀있다.)
- 이는 Solid 원칙 중 OCP를 잘 따르는 것이라고 볼 수 있습니다.
- => 이처럼 인터페이스 기반의 다형성, 객체지향의 다형성 개념을 잘 활용하면 기능을 변경해도 애플리케이션 전체를 수정할 필요가 없어집니다.
- 스프링의 DI(Dependencies Injection)
- DI를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있습니다.
- 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8
[지금 무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 | 김영한 - 인프
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., 스프링 학습 첫 길잡이! 개발 공부의 길을 잃지 않도록 도와드립니다. 📣 확
www.inflearn.com
반응형