스프링 DB 접근 기술 - 순수 JDBC

Chooooo·2022년 8월 9일
1
post-thumbnail

순수 JDBC 세팅하기

이전에는 회원 정보를 메모리에 저장해서 사용했다. (데이터가 다 삭제 돼...)
지금부터는 애플리케이션에서 DB를 접근, 데이터를 넣고 빼는 것을 순수 JDBC 버전, 즉 옛날 방식??으로 구현할꺼야!
이번에 공부한 내용은 현재는 많이 쓰이지 않는 방식임을 미리 알고 진행하자

우선 build.gradle에 밑에 라이브러리 추가해줘

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

위 라이브러리는 각각 JDBC, H2 데이터베이스와 관련된 라이브러리이다. 참고로 위는 자바가 DB를 사용하기 위해 필요한 JDBC드라이버를 위한 라이브러리.

밑에 작성할 코드는 H2가 제공하는 DB클라이언트를 위한 라이브러리이다.
다음은 스프링부트가 데이터베이스를 연결할 수 있도록 연결 설정을 추가해야해.
src-resoucres-applicaition.properties에 코드 작성.

spring.datasource.url = jdbc:h2:tcp://localhost/~/test   //나는 test2이기에 뒤에 test2를 붙여서 해줌.
spring.datasource.driver-class-name = org.h2.Driver
spring.datasource.username=sa

이때 H2와 드라이버에 에러가 생긴다면 build.gradle의 sync를 맞춰주자
데이터베이스의 경로를 지정해 주었고 드라이버를 설정, 데이터베이스 회원 정보를 등록하였다! 위와 같이 코드 작성을 완료하였다면 데이터베이스에 접근할 준비가 끝난거야!


JDBC API를 이용하여 DB연결하기

이제는 JDBC API를 가지고 JDBC리포지토리를 구현할꺼야
지금까지는 리포지토리 인터페이스를 MemoryMemberRepository로 구현하였다면, 이제는 실제 데이터베이스로 리포지토리를 구현!

repository 폴더에 JdbcMemberRepository 클래스를 생성하고 코드 작성!
DB에 연결해서 사용하려면 data source라는 것이 필요하다.
그러므로 DataSource Type의 변수를 final로 선언하고, 생성자를 통해 주입받는다.
spring boot가 Data Source를 만들어놓기 때문에, spring을 통해 주입받을 수 있어!!

public class JdbcMemberRepository implements MemberRepository{

    private final DataSource dataSource;


    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
        //dataSource.getConnection();
    }
    ...
    ...
    ...

이후 dataSource.getConnection()을 통해 connection을 받을 수 있고, 여기에 sql문을 전달하여 DB에 정보를 넣고 뺄 수 있다!


메소드 구현

아래에 save, findById, findByName, findAll 구현하기! 생각할 것은... 이렇게 JDBC API로 직접 코딩하는 것은 20년 전에 사용하던 방법이기에 참고만 하자

save()

@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);
    }
}

findById()

@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);
    }
}

findByName()

@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);
    }
}

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);
    }
}

getConnection() , close()

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);
}

구현할 클래스에 대해 다시 설명하면
해당 리포지토리는 dataSource라는 DataSource객체를 갖는다. 이 DataSource는 JdbcRepository가 생성될 때 스프링에 주입 돼!
DB에서 데이터를 가지고 오기 위해 연결을 하기 위해서는 Connection이 필요한데, 이는 DataUtils에 의해 dataSoirce를 넘겨 가져올 수 있다. (마찬가지로 DataSourceUtils를 통해 Connection을 release한다)

구현한 함수에 대해서!
구현한 save()함수는 회원을 DB에 등록하는 함수다.
sql을 정의하고 Connection, PreparedStatement, ResulSet객체를 정의해 놓고 진행
conn을 받고, sql과 RETURN_GENERATED_KEYS을 설정하고 pstmt에 받는다. 이때 RETURN_GENERATED_KEYS은 DB에 insert 할 때 insert를 한 후에야 할 수 있는 key값인 1,2,3... 과 같은 값들을 가져올 수 있도록 설정

setString메서드는 1로 sql의 '?'와 매칭을 하고 그곳에 getName()으로 회원의 이름을 넣는다.

executeUpdate() DB에 쿼리를 전송하고, 다음 줄에서 생성된 key를 받아 rs에 받는다. (이때 위에서 설정한 RETURN_GENERATED_KEYS과 연관이 있어!)
결과 값(생성된 key값)이 있으면 이를 꺼내 Long으로 바꾼 후 회원의 id로 설정해 준 후 member 객체를 반환.

마지막으로 예외처리를 전부 한 후에는 받아온 자원들을 close로 해제해 주어야 한다. 해제해 주지 않으면 DB connection이 계속 쌓이고 이후 문제의 원인이 될 수 있어.

다음에 구현된 findById() 함수는 id로 특정 회원을 검색하는 함수

위 save() 함수와 다른 점은 executeUpdata()를 사용하지 않고 executeQuery()를 사용한다는 점. DB에 저장(갱신)할 때는 executeUpdata()를 사용하고 DB를 조회할 때는 executeQuery()를 사용한다.

결과값을 rs로 받고 값이 있다면(검색되는 회원 객체가 있다면) 회원 객체를 생성, 반환된 회원 객체의 id와 name을 가져와 세팅하고 해당 객체를 반환한다!

findAll() 함수는 등록된 모든 회원을 리스트 형태로 검색하는 함수

등록된 모든 회원을 검색하니 sql은 비교적 간단하다. 쿼리를 전송해 받은 결과값 rs가 있다면 ArrayList를 하나 생성, rs를 돌며 회원 객체를 생성, id와 name을 세팅하고 ArrayList에 담아 리스트를 반환한다!

findByName() 함수는 name변수로 특정 회원을 검색하는 함수

(일단 이런 함수들을 이렇게 짰구나... 정도로 이해하고 넘어가)


MemoryMemberRepository에서 JdbcMemberRepository로 변경하기

이렇게 각 메서드를 jdbc를 통해서 구현했으니, MemoryMemberRepository에서 JdbcMemberRepository로 바꿔야 한다. (이유는...)

이전에는 SpringConfig 클래스에서 @Bean을 통해 스프링 컨테이너에 MemoryMemberRepository를 올려주었다. (아래 코드와 같이..)

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

여기서 중요한게 memberRepository()에서 return new MemoryMemberRepository()return new JdbcmemberRepository()로 바꾸면 된다는 것!

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
    	// Jdbc Repository로 변경
        return new JdbcMemberRepository();
    }
}

jdbc에 필요한 DataSource는 spring에서 제공하기 때문에 injection(주입)을 통해 받으면 된다. @Configuation도 spring bean으로 관리가 되기 때문에 스프링 부트가 dataSource를 bean으로 생성하고 관리한다.

@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();
    }
}

이후 H2 데이터베이스를 실행 후 테스트 돌리면 모두 통과해!

위 코드들을 보면 String 변수를 통해 SQL문을 직접 작성하고, connection, PreparedStatement, ResulSet 변수를 또 선언하며 많은 코드들을 작성했어
(괜히 20년 전에 쓰던게 아니다..)

순수 jdbc에서 어떤 식으로 코드의 가독성과 유지보수성을 올릴 수 있을지 공부해야겠다.


스프링을 사용하는 이유?

공부하면서 스프링을 사용하면 좋은 이유들을 알 수 있었다.

MemoryMemberRepository에서 JdbcMemberRepository로 바꾸는 과정에서 SpringConfig에서만 코드를 수정했어. MemberService나 MemberController에서는 수정된 코드 없이 SpringConfig에서만 코드를 수정하여 리포지토리를 바꿀 수 있었어

스프링은 다형성을 사용할 수 있도록 spring container에서 dependency injection(DI)와 같은 것을 지원한다.
기존에는 서비스의 코드가 리포지토리를 의존하는 코드라면 리포지토리가 변경될 때 서비스 코드까지 변경해야 하는 경우가 생겼다면... 이제는 스프링의 DI를 사용하여 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있다.
따라서 객체지향적인 설계가 가능하고 다형성을 활용할 수 있다!!!
(이번 상황 처럼 DB의 구현체를 Memory에서 Jdbc로 변경할 때에 코드를 많이 수정하지 않고 SpringConfig에서만 수정하고 구현체를 변경했잖아)


MemberService는 MemberRepository 인터페이스에 의존하고, 이 인터페이스는 MemoryMemberRepository와 JdbcMemberRepository 구현체로 각각 구현.


기존에는 MemoryMemberRepository를 spring bean으로 등록하고 사용하고 있었는데...
이 이미지는 기존에 MemoryMemberRepository를 사용하던 멤버 서비스가 Config 변경을 통해 JdbcMemberRepository를 사용하는 것을 보여준다.
(다른 코드는 변경하지 않았다. 하지만 다형성의 성질을 이용하여 JdbcMemberRepository를 생성할 수 있어 이를 사용하게 된다.)
이러한 것을 개방-폐쇄 원칙(OCP : Open-Closed Principle)이라고 한다.
확장에는 열려있고, 수정과 변경에는 닫혀있는 개발 방식을 말한다. 객체지향의 다형성 개념을 활용하여 기능을 완전히 변경하더라도 애플리케이션 전체 코드를 수정하지 않고 조립(Configuration)만을 수정, 변경하는 것을 의미!!
--> spring DI를 활용하면 기존 코드를 변경하지 않고 설정만으로 구현 클래스를 변경할 수 있다!

이 글은 강의 : 김영한 - "스프링 입문-코드로 배우는 스프링 부트, 웹 MVC, DB접근기술"을 듣고 정리한 내용입니다.

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글