후라이
[Spring Boot] - 스프링 입문 (6) 본문
앞에서 h2 database 연결을 무사히 마친 뒤,
이제 springboot와 Database를 연결해보자.
1. h2 console에서 다음과 같이 member table을 만들기
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
insert into member(name) values('spring');
insert into member(name) values('spring2');
select * from member;
를 수행하면
다음과 같이 테이블에 값이 잘 들어감을 확인할 수 있다.
2. springboot와 database 연결
build.gradle 의존성이 잘 추가되어있는지 확인
스프링 부트 데이터베이스 연결 설정
이 부분이 매우 중요하다.
강의 설명에는
application.properties에
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
이것만 추가하면 된다고 했지만,
코드상 문제가 없음에도 웹에서는 White label 500 오류가 발생했다.
org.h2.jdbc.JdbcSQLInvalidAuthorizationSpecException: Wrong user name or password [28000-214]
구글링으로 뒤지고 뒤져서,,
password까지 입력을 해주고 나니 오류가 발생하지 않았다.
참고로
h2 version 1.4.200
을 사용하였다.
username과 password들 모두 명시해서 application.properties에 작성해야
White label 오류없이 회원가입/회원목록 조회가 원활하게 진행되었다.
3. JdbcMemberRepository 작성
repository에 JdbcMemberRepository
java class를 추가한다.
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;
//rs -> 결과를 받음.
try {
conn = getConnection(); //connection으로 받아옴
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);
}
}
위 코드는 MemberRepository 인터페이스를 구현한다.
(1) save(Member member)
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
SQL 쿼리를 생성한다.
간단히 member table에 'name'만 삽입하는 쿼리를 작성하도록 하자.
'Connection' : DataBase와 연결을 설정
'PreparedStatement' : 연결 초기화 (Statement.RETURN.GENERATED_KEYS를 사용하면 자동으로 생성된 키 반환)
(PreparedStatement에 대해서 이 다음에 정리하겟음.)
'ResultSet' : 실행 결과 저장
try {
conn = getConnection(); //connection으로 받아옴
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);
}
}
예외처리와 관련된 블록
try 블록 안에서 DataBase와의 연결을 설정, 쿼리 실행
catch 블록에서 예외 처리
finally 블록에서 데이터베이스와의 연결을 닫음
pstmt = conn.prepareStatment(sql, Statement.RETURN_GENERATED_KEYS);
: 데이터베이스 SQL문을 실행하기 위한 PreparedStatement 객체 생성
pstmt.setString(1, member.getName());
: PreparedStatement 객체에 파라미터 설정
첫번째 "?" 자리에 member name 설정
pstmt.executeUpdata();
: SQL 쿼리를 실행하여 데이터베이스에 변경 적용
rs = pstmt.getGeneratedKeys();
: 자동으로 생성된 키를 ResultSet 객체로 가져옴
if (rs.next()) {...} else {...}
: ResultSet에서 데이터 가져오기가 성공한 경우, Member객체의 Id를 설정
(2) findById(Long id)
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);
}
}
ID를 매개변수로 받고, 해당하는 Member 객체를 Optional로 감싸서 반환한다.
(Optional은 값이 있을 수도 있고 없을 수도 있는 컨테이너 객체)
String sql = "select * from member where id = ?";
: SQL 쿼리를 정의, member 테이블에서 주어진 ID와 일치하는 행을 선택
Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null;
: save()에서 처럼 데이터베이스 연결과 쿼리 실행 결과를 저장
pstmt = conn.prepareStatement(sql);
: 데이터베이스에서 SQL 문을 실행하기 위한 PreparedStatement 객체 생성
pstmt.setLong(1, id);
: PreparedStatement 객체에 파라미터 설정, 첫 번째 "?" 자리에 주어진 ID를 설정
return Optional,of(member);
return Optional.empty();
: Optional을 사용하여 Member 객체를 반환, 주어진 ID에 해당하는 멤버가 있으면 해당 멤버를 Optional로 감싸서 반환,
그렇지 않으면 Optional.empty() 반환
(3) findAll()
@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);
}
}
데이터베이스에서 모든 멤버를 조회하는 메소드 구현
String sql = "select * from member";
: member 테이블의 모든 열을 조회하도록 하는 SQL 쿼리문
List<Member> members = new ArrayList<>();
: member 객체들을 담을 ArrayList 생성
while(rs.next()) {}
: rs에서 데이터를 가져오는데 성공한 경우, 새로운 Member 객체를 만들고 해당하는 ID와 이름을 설정한 후 리스트에 추가
(4) findByName(String name)
@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);
}
}
(5) getConnection()
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
데이터베이스 연결을 얻기 위한 getConnection()
return DataSourceUtils.getConnection(dataSource);
: DataSourceUtils 클래스의 getConnection() 메소드를 호출하여 데이터베이스 연결을 가져 옴
dataSource
이 객체는 데이터베이스 연결 정보를 담고 있다.
보통 프로그램이 시작할 때 Spring 또는 JavaEE와 같은 프레임워크에서 이를 설정하고 제공
DataSourceUtils
이 클래스는 데이터베이스 연결과 관련된 유틸리티 메소드를 제공하는 클래스로,
데이터베이스와의 연결을 획득하고 관리한다.
Datasource는 DB에 접근하고 DB Connection을 관리하는 인터페이스라고 생각하면 된다.
보통 getConnection()으로 해당 DB Connection을 가져옴.
(6) close()
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);
}
}
데이터베이스 관련 자원들을 닫는 메소드이다.
rs.close(); : ResultSet 닫음
pstmt.close(); : PreparedStatement 닫음
close(conn); : Connection 닫음
private void close(Connection conn) throws SQLException
: DataSourceUtils 클래스의 releaseConnection()으로 데이터베이스 연결 해제.
이 과정에서SQLException이 발생할 수 있으므로 따로 예외처리.
4. SpringConfig 수정
package hello.hellospring.service;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
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);
}
}
의존성 주입(DI)을 사용하여 Bean을 구성하는 클래스
SpringConfig는 스프링 어플리케이션을 담당한다.
@Configuration 어노테이션으로 명시.
DataSource를 통해 JdbcMemberRepository가 생성되고,
MemberService는 생성자를 통해 MemberRepository를 주입받아 생성된다.
다른 코드는 건들지 않고
JdbcMemberRepository(datasource)를 반환하도록 하면 됨.
(구현체가 바로 바뀜, 매우 간단함)
개방-폐쇄 원칙(OCP, Open-Closed Principle)
: 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다.
확장에 대해서는 개방적, 수정에 대해서는 폐쇄적
기능을 완전히 변경해도 어플리케이션 전체를 수정할 필요가 없음.
-> 어플리케이션의 동작을 변경하는 중요한 요소이며, 스프링의 의존성 주입 기능을 통해 이를 손쉽게 처리
(참고)
위처럼 데이터베이스 인터페이스인 JDBC는 이러한 OCP의 법칙을 잘 따르는 예시 중 하나이다.
만약 자바 어플리케이션으로 MySQL -> Oracle로 변경하고자 할 때, connection만 변경하면 된다.
'Spring' 카테고리의 다른 글
[Spring MVC] 웹 시스템 | 서블릿 | HTML | HTTP API (0) | 2024.12.15 |
---|---|
객체지향에서의 다형성, 그리고 OCP (2) | 2024.12.07 |
[Spring Boot] - H2 데이터베이스 연결 / Database not found (0) | 2024.02.12 |
[Spring Boot] - 스프링 입문 (5) (1) | 2024.01.21 |
[Spring Boot] - 스프링 입문 (4) (0) | 2024.01.21 |