해당 글은 DEPth IT 연합 프로젝트 동아리 활동의 일환인 서버 스터디에 관련되어 있습니다.🐧
H2 데이터베이스
교육용으로 적합하고 가볍고 편리한 H2 데이터베이스를 설치합니다. 해당 DB는 웹 화면을 제공하여서 교육에 더욱 적합합니다.
H2 DB 사이트에서 자신의 컴퓨터에 알맞은 버전을 다운로드 한 후에 압축을 풀어줍니다. 압축을 푼 폴더에는 bin이라는 하위 폴더가 들어있습니다. 여기에 h2.sh라는 파일을 실행하면 h2 DB를 실행할 수 있습니다.
(이때 mac 사용자는 h2.sh 파일 실행을 위해서 실행 권한을 주어야 합니다. 터미널에서 chmod 755 h2.sh라는 명령어를 먼저 실행하고 해당 파일을 실행할 수 있습니다.)
이때 웹 주소가 (자신의 ip 주소):8082로 되있는 경우 웹 화면이 표시되지 않는 경우가 있습니다. 이때에는 localhost:8082로 주소를 바꾸어 주면 웹 화면을 정상적으로 볼 수 있게 됩니다.

이러한 화면이 뜨는 것을 볼 수 있습니다. 이때 연결 버튼을 누르면 db 파일이 만들어지게 됩니다.
위와 같이 test db 파일이 만들어진 것을 확인 할 수 있습니다.
이때 JDBC URL에서와 같이 파일로 접근을 하게 되면 웹 콘솔과 어플리케이션이 동시에 접근하는 경우 충돌이 일어날 수 있습니다.
따라서 소켓을 통해서 접속하는 방식으로 바꾸게 된다면 여러 곳에서 접근이 가능하게 됩니다.
⚙️ tcp 소켓
직접 파일로 접근하는 경우에는 하나의 파일에는 동시에 여러 어플리케이션이 접근하여서 수정하지 못하도록 락이 걸립니다.
하지만 TCP를 통해서 DB 서버에 접근하게 된다면 DB 서버가 이러한 문제를 해결해 줍니다. 해당 부분은 DB의 특성에 연관이 있습니다.
🔥 저는 tcp 연결로 바꾸기 전에 만들어진 db 파일을 삭제하고 tcp 연결을 진행했습니다. 그러자 오류가 발생했습니다. 오류의 내용은 local에서 db를 찾을 수 없다는 내용이였습니다. tcp 연결로 바꾸는 경우에는 db파일이 만들어지지 않음으로 먼저 db 파일을 생성하고 tcp 연결로 바꾸어야지 해당 문제의 해결이 가능합니다.
해당 db에 위와 같이 테이블을 만들었습니다. sql문을 보면 id을 primary key를 사용함을 알 수 있습니다. 또한 generated by default as identity 라는 설정으로 null 값으로 id를 두었을시에 자동으로 id를 채워주는 설정 또한 지정했습니다.
위의 Table에 spring이라는 values를 넣은 결과 select 문을 통해서 바르게 들어간 것을 확인 할 수 있습니다.
순수 JDBC
연동을 위해서 build.gradle 파일에 필요한 라이브러리를 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
위와 같은 라이브러리를 추가합니다. spring-boot-starter-jdbc는 자바에서 DB와 연동되기 위해서 필요한 드라이버인 jdbc를 의미합니다. 밑에 추가한 라이브러리는 데이터베이스가 제공하는 클라이언트입니다.
이후 데이터베이스 사용을 위해서 접속 정보를 추가로 제공해줍니다. 스프링 부트에서 지원하는 기능입니다. 따라서 접속 경로만 제공해주면 됩니다. application.properties에 접속 경로를 작성합니다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
첫번째 줄은 JDBC URL을 넣어주면 됩니다.
두번째 줄은 h2를 사용할 것이기 때문에 h2 드라이버를 의미합니다. 위에서 추가한 라이브러리를 import해야 오류가 나지 않습니다.
마지막으로는 스프링부트 2.4부터 추가된 내용입니다. 해당 내용이 없으면 Wrong user name or password 오류가 발생합니다.
위 작업들을 통해서 jdbc api를 이용해서 데이터베이스를 사용할 수 있습니다.
JDBC Member Repository
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()); //1이 이때 물음표와 매칭이 되어서 name 저장된다.
pstmt.executeUpdate(); //실제 쿼리가 이때 날아간다.
rs = pstmt.getGeneratedKeys(); //key를 반횐해준다. RETURN_GENERATED_KEYS 관련되있다.
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(); //조회는 execute 사용
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() { //findall이기 때문에 list로 반환
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() { //트랜젝션을 위해서 connection 사용
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);
}
}
Member Repository를 implement 하여서 각 기능을 jdbc에 맞게 구현하였습니다. sql 쿼리문과 jdbc api를 이용해서 각 기능을 구현했습니다. 이전에는 memory에서 기능을 하는 것을 이제는 db에서 기능을 합니다.
이때 주의점이 있습니다. 데이터베이스를 사용하기 위해서 data source가 필요합니다. 해당 data source는 Config에서 스프링이 자동으로 빈을 생성해 관리해줍니다. 또한 트렌젝션을 위해서 connection을 사용해야 합니다. 마지막으로 사용한 후에는 클로즈를 진행하여야 합니다. 외부 네트워크와 연결되므로 자원 릴리즈가 필요합니다. 릴리즈 하지 않으면 오류가 발생합니다.
해당 JdbcMemberRepository는 Config에서 이전에는 Memory로 연결되어 있던 것을 Jdbc로 바꾸면 사용가능합니다. 이는 스프링의 특징으로서 객체지향적인 특성을 보여줍니다. 인터페이스에서 사용하는 것을 바꾸면 코드를 손대지 않고 바꿀 수 있습니다.
⚙️ S.O.L.I.D 원칙
SPR = 단일 책임 원칙: 클래스(객체)는 단 하나의 기능 담당 책임을 가져야한다. 즉, 클래스 별로 집중하는 기능을 달리한다.
OCP = 개방 폐쇄 원칙: 추상화 사용을 통해서 관계를 구축하는 것. 즉, 다형성과 확장이 가능하게 한다.
LSP = 리스코프 치환 원칙: 다형성을 이용하기 위한 원칙, 부모 메서드의 오버라이딩 조심스럽게 한다.
ISP = 인터페이스 분리 원칙: 인터페이스의 단일 책임 강조, 인터페이스를 각 사용에 맞게 잘 분리해야 한다.
DIP = 의존 역전 원칙: Class 참조시에 추상 클래스 및 인터페이스로 참조해야 한다.
문제 없이 정상 작동됨을 알 수 있습니다.
스프링 통합 테스트
이전과는 다르게 스프링 컨테이너와 DB까지 연결해서 테스트하는 것을 통합 테스트라고 합니다. 이를 위해서는 두가지 어노테이션이 필요합니다.
@SpringBootTest, @Transactional 입니다. 해당 어노테이션을 통해서 스프링 컨테이너와 테스트를 함께 실행하고 트랜잭션을 이용해서 rollback을 진행하여 다음 테스트에 영향을 주지 않습니다.