"인프런의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 듣고 작성한 글 입니다."
어플리케이션에서 데이터베이스에 접근하여 데이터를 삽입, 조회 해보자.
우선 최근에 사용하는 JPA 나 MyBatis 와 같은 기술이나 스프링 JdbcTemplate 같은 것이 나오기 전에 개발자들이 했었던 순수 JDBC 만으로 데이터베이스에 접근해보자.
이번 강의는 정신건강을 위해 편안하게 들어보라고는 하지만, 그래도 이게 왜 정신건강에 안 좋은건지 느껴보기 위해 제대로 강의를 한번 들어봐야 겠다.
먼저 bulid.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 아래와 같이 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
자바는 기본적으로 데이터베이스와 연결해주려면 jdbc 드라이버가 반드시 있어야 한다. 이를 가지고 데이터베이스와 어플리케이션간에 서로 연동을 하기 때문이다.
두번째 문자열은 데이터베이스와 연결할 때 h2 데이터베이스의 서버에 접속하여 사용해야 하기 때문에 h2 데이터베이스 라이브러리를 의존성 주입 해준 것이다.
이후 resources/application.properties 파일에 다음과 같이 스프링 부트 데이터베이스 연결 설정을 추가해준다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test // 데이터베이스 서버 접속 경로
spring.datasource.driver-class-name=org.h2.Driver // 데이터베이스 드라이버
spring.datasource.username=sa // 데이터베이스 유저명
이런식으로 경로와 username, 비밀번호가 있으면 비밀번호 까지 작성해주면 그 이후부터는 스프링 부트가 알아서 설정을 통해 데이터베이스 서버를 찾아서 접속하는 방식으로 연결해준다.
이제 JDBC API 를 가지고 본격적으로 개발을 시작해보자.
기존에는 MemoryMemberRepository 에서 메모리 상에 데이터를 저장하고 검색했었다.
여기서 MemoryMemberRepository 는 인터페이스 구현체에 해당하며, 실제로 우리가 활용해야 하는 메소드들은 MemberRepository 인터페이스에 선언되어 있다.
그렇기 때문에 MemoryMemberRepository 라는 구현체를 만들어서 메모리 상에서 데이터 관리를 했던 것과 같이 JDBC API 를 활용하는 JdbcMemberRepository 구현체를 repository 패키지에 하나 만들어준다.
- JdbcMemberRepository.java
public class JdbcMemberRepository implements MemberRepository{
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
데이터베이스와 어플리케이션을 연결해주려면 우선 DataSource 객체가 필요하다.
applicaton.properties 파일에서 데이터베이스에 대한 접속 설정을 작성해 두었는데, 이를 통해 스프링 부트가 DataSource 객체를 만들어서 보관해둔다.
그리고 이 객체를 생성자를 통해 의존성으로서 스프링에게 주입 받아야 한다.
객체를 주입받고 나면 getConnection() 메소드를 통해 데이터베이스에 대한 커넥션을 얻을 수 있다.(진짜 데이터베이스와 연결된 네트워크 소켓을 얻는다는 뜻)
JdbcMemberRepository 에 위의 설명대로 아래와 같은 코드를 추가해 줄 수 있다.
- JdbcMemberRepository.java
public class JdbcMemberRepository implements MemberRepository{
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
// 이하 코드 동일
}
언뜻 보면 간단해 보이지만 SQL 쿼리를 데이터베이스에 전달해주는 코드를 작성하는 순간부터 어마어마 해지기 시작한다.
하나하나 작성하고 있기엔 시간이 너무 오래 걸리니 아예 pdf 파일에 있는 코드를 복붙 해버리자.
- JdbcMemberRepository.java
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(); // 데이터베이스 커넥션을 받아옴
// 첫번째 파라미터에서 sql 을 넣음
// 두번째 파라미터 에서는 데이터를 삽입해야 데이터베이스에서
// id 값을 자동으로 지정해주는 방식으로 만들었기 때문에,
// 데이터베이스 에서 자동으로 지정해준 키 값을 반환받게끔 해주는 옵션이다.
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
// 파라미터 인덱스 : 번호 별로 sql 에서 values 에 작성해준 ? 에 매칭된다.
// 매칭된 ? 에 member.getName() 반환값을 넣어준다.
pstmt.executeUpdate();
// DB 에 쿼리 전달(데이터가 삽입되기 때문에 update 메소드 수행)
rs = pstmt.getGeneratedKeys(); // DB 에서 삽입한 값에 생성해준 키 값을 반환받는다.
if (rs.next()) { // rs 에 결과값이 존재할 경우 실행하는 조건문
member.setId(rs.getLong(1));
// rs 에 반환받은 id 값을 member 객체에 세팅해준다.
} else {
throw new SQLException("id 조회 실패");
// rs 에 결과값이 존재하지 않을 경우 익셉션 발생
}
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();
// 단순 데이터 조회이기 때문에 일반적인 query 메소드 수행
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
// 반환값이 있을 경우 member 객체에 값 세팅 후 반환
} else {
return Optional.empty();
// 값이 없을 경우 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() {
// 스프링을 통해서 데이터베이스 연결을 수행할 때는
// DataSourceUtils 를 통해서 커넥션을 획득해야 한다.
// (사실 이런 코드를 쓸 일이 없기는 한데 그냥 참고 차원에서 알고만 있자.)
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
{
// 역순으로 close() 메소드를 수행해준다.
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 를 활용한다.
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
위와 같이 JdbcMemberRepository 가 완성되고 나면 이제 설정을 잡아줌으로서 JdbcMemberRepository 를 스프링 빈으로서 등록해야 한다.
- SpringConfig.java
@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);
}
}
SpringConfig 객체 생성자에서 DataSource 객체 값을 파라미터로 하여 SpringConfig 클래스에 의존성을 주입시켜 준다.
이렇게 해놓으면 스프링이 동작할 때 @Configuration 어노테이션과 그 내부에 있는 @Component 어노테이션을 확인하고 SpringConfig 클래스 객체를 만들어 스프링 빈으로 등록할 때,
해당 생성자를 활용하면서 DataSource 클래스 객체를 SpringConfig 객체에 의존성으로 주입시켜 줄 수 있게 된다.
DataSource 클래스 객체를 의존성 주입 시켜주는데 성공하고 나면 드디어 JdbcMemberRepository 객체를 스프링 빈으로 등록시켜 줄 수 있게 된다.
memberRepository() 메소드에서 이전에 MemoryMemberRepository 객체를 생성하여 반환해주던 코드를 주석처리 하고 JdbcMemberRepository 객체를 생성하여 반환해주는 코드로 바꾼 다음 파라미터 값으로 DataSource 객체를 세팅해준다.
이와 같이 설정파일을 통해 직접 자바 코드로 객체를 스프링 빈에 등록하는 방식을 사용하면 중간에 Repository 를 다른것으로 변경해줘야 한다고 해도, 스프링 빈을 등록해주는 메소드에서 반환해주는 객체값만 변경해주면 쉽게 해결할 수 있게 된다.(이것이 바로 우리가 스프링을 사용하는 이유이다.)
객체 지향적인 설계가 좋다고 하는데 왜 좋은가 하면, 위와 같은 경우를 소위 다형성을 활용한다고 한다.(객체의 다형성)
인터페이스 구현체를 필요에 따라 바꿔 끼우는 것을 해보았는데, 스프링은 이를 편리하게 할 수 있도록 스프링 컨테이너가 지원을 해준다.
소위 말하는 의존성 주입(DI) 덕분에 구현체를 필요에 따라 바꾸는 등의 작업을 굉장히 편리하게 할 수 있게 되는 것이다.
과거의 경우 Repository 를 다른 것으로 바꾸는 순간 MemberService 에서 참조하고 있는 Repository 또한 같이 바꿔줘야 했다.(어쨌든 코드의 수정이 일어나야 한다.)
그런데 여기서 Service 를 담당하는 클래스가 여러개일 경우 그 클래스들 마저도 전부 참조하고 있는 Repository 에 대한 코드를 수정해줘야 한다.
하지만 이렇게 자바 코드로 직접 스프링 빈을 등록해 줄 경우, 다른 코드들은 건드릴 필요 없이 오직 자바 코드를 통해 설정해주는 클래스(SpringConfig) 내부에서 Repository 를 스프링 빈에 등록해주는 메소드의 코드만 바꿔주는 것만으로 Repository 변경을 완료할 수 있다. - 실제 어플리케이션에 관련된 코드는 하나도 손 댈 필요 없다.
(설정 파일에 적혀있는 코드들을 일명 어셈블리 코드라고 한다. - 스프링이 동작하며 어플리케이션 환경을 조립해주는 코드)
스프링 컨테이너의 내부 상황은 다음과 같아진다.(Member 도메인 기준)
지금까지 실습해본 방식의 설계를 개방 - 폐쇄 원칙(OCP : Open - Close Principle : 확장에는 열려있고, 수정, 변경에는 닫혀있다.) 이라고 한다.(SOLID 원칙 중 하나)
* 객체 지향에서 다형성이라는 개념을 잘 활용하면 기능을 완전히 변경하는 일이 생겨도 어플리케이션 전체를 수정할 일이 없게 된다.
* 스프링의 DI 를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
이제 h2 데이터베이스를 실행한 상태로 어플리케이션을 실행해본 다음, 화면에서 회원 목록을 확인해보면 SQL 로 회원 테이블에 저장해 뒀던 spring, spring2 데이터들이 정상적으로 잘 출력되는 것을 확인해 볼 수 있다.
회원 가입 기능 또한 정상적으로 잘 수행되며, h2 console 에서 member 테이블을 확인해보면 데이터 삽입이 정상적으로 잘 되어 있는 것을 확인해 볼 수 있다.
'Spring basic' 카테고리의 다른 글
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 스프링 JdbcTemplate (0) | 2021.11.11 |
---|---|
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 스프링 통합 테스트 (0) | 2021.11.10 |
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - H2 데이터베이스 설치 (0) | 2021.11.09 |
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 회원 웹 기능 : 등록 및 조회 (0) | 2021.11.08 |
스프링 입문 : 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 회원 웹 기능 : 홈 화면 추가 (0) | 2021.11.08 |