반응형

애플리케이션에서 DB에 연동을 해서 저장하는 것을 해볼 것이다.
기존처럼 Memory에 저장하지 않고, 데이터베이스에 insert쿼리, slect쿼리를 날려서 넣고 빼는 것을 해보자.

이번시간에는, 정말 오래된 JDBC방식으로 해볼 것이다.
(아! 예전에는 이렇게 했구나.. 정도로 알면 될 것이다.)

 

먼저, build.gradle파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가해야 한다.

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리를 추가해 주었다.

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


코드를 보면, 'spring-boot-starter-jdbc' 를 볼 수 있는데,
자바는 기본적으로 db랑 붙으려면, jdbc드라이버가 꼭 있어야 한다. 이것을 가지고 서로 연동하는 것이다.
그리고 'com.h2database:h2'는 뭐냐면, db랑 붙을 때, 데이터베이스가 제공하는 클라이언트가 필요한데, 그게 h2database 클라이언트를 설정해준 것이다.

이렇게 두개의 라이브러리를 넣으면 되고,

 

이제 db에 붙으려면 접속정보같은 것들을 설정해 줘야 한다.

 

resources/application.properties 에다가 스프링 부트 데이터베이스 연결 설정을 추가해주자.

 

spring.datasource.url=jdbc:h2:tcp://localhost/~/test

url은  우리가 h2데이터베이스에 접근할때 사용하는 url을 적어주면 된다.

이 url과 동일하게 적어주면 된다.

spring.datasource.driver-class-name=org.h2.Driver

그리고 driver-class-name 이라는게 필요한데, 우리가 h2데이터베이스로 접근할 것이기 때문에 org.h2.Driver 를 넣어주면 된다.

h2.Driver에 빨간불이 뜨는데,  import가 안되었기 때문에 발생하는 것이다.

 

이것은 build.gradle에 가서코끼리 모앙을 눌러주면 된다.
아니면 Gradle메뉴에서 리로드 버튼을 눌러주면 된다.

spring.datasource.username=sa

그리고 스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다. spring.datasource.username=sa 공백 주의, 공백은 모두 제거해야 한다.

이렇게만 해두면, 데이터베이스에 접근할 준비는 끝났다. (원래는 데이터베이스 id,pw 등도 적어야 하는데 h2데이터베이스는 생략)
run시키면, 스프링이 DB와 연결하는 작업을 다 해준다. 그리고 이제 이것을 가져다 사용하면 된다.

 

이제, JDBC API를 가지고 개발을 해보자.
어디를 개발할 것이냐면, 기존에 MemeoryMemberRepository라고 MemberRepository인터페이스의 구현체를 만들어 놓았는데,
MemberRepository가 인터페이스이기때문에 구현체를 개발하면 된다.

JdbcMemberRepository라는 이름으로 클래스를 생성하자.

MemberRepository를 implements해주고, opt+enter 로 implement mothods를 해준다.

 

이제 하나씩 구현하면 된다.

그런데, 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게

고생하고 살았구나 생각하고, 참고만 하고 넘어가자.

 

먼저, Db에 붙으려면, DataSource라는것이 필요하다.
그리고 얘를 나중에 스프링한테 주입받아야 하는데,

우리가 이것을 세팅했으므로, 스프링부트가 DataSource라는 것을 접속정보를 이용해서 만들어 놓는다.

그러면, dataSouce.getConnection()을 해서 데이터베이스 커넥션을 얻을 수 있다.
그러면 진짜 데이터베이스와 연결된 열린 소켓을 얻을 수 있는 것이다.
여기다가 sql문을 날려서 db에 전달해 주는 것이다.

 

DataSourceUtils를 통해서 커넥션을 획득해야한다.
그래야 데이터베이스 트랜잭션같은게 걸리더라도 똑같은 데이터베이스 커넥션을 유지를 해야한다.
그것을 유지시켜누는 역할을 한다.

private Connection getConnection() {
    return DataSourceUtils.getConnection(dataSource);
}

그래서, 스프링 프레임웍을 사용할 때에는, 꼭 이렇게 커넥션을 가져와야 한다.

private void close(Connection conn) throws SQLException {
    DataSourceUtils.releaseConnection(conn, dataSource);

}


닫을 때도, DataSourceUtils를 통해서 release를 해야 한다.

이것을 주의해야하고, 나머지는 이러이러한게 있구나 정도로 참고하고 넘어가자.

최종적으로 작성된 JdbcMemberRepository의 코드이다.

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;

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

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

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

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

 

이제 서버를 돌리면 될까?

안된다.

왜냐하면, Configuration 해줘야 하기 때문이다.

우리가 지금까지 저장할 때는, 메모리에 저장했었다.


즉, MemoryMemberRepository를 사용하고 있었다.

이점은 SpringConfig 에서도 확인할 수 있는데,

MemoryMemberRepository를 스프링빈으로 등록해서 사용했었다.

이것을 방금 만든, JdbcMemberRsporitrory로 바꾼다.

그리고, 작성해둔 JdbcMemberRepository를 살펴보면,

DataSource를 넣어줘야하는데,

 

이것은 스프링이 제공을 해주는데, 어떻게 제공을 해주냐면,

이렇게 작성을 하면 된다.
@Configuration 한 것도 스프링빈으로 관리가 되기 때문에,
이렇게 작성을 해두면, 스프링이 자체적으로 스프링빈을 생성해준다.

즉, 스프링이 dataSource를 스프링빈으로 등록해준 것이다.

 

오직 JdbcMemberRepository 를 만들어서 MemberRepository인터페이스를 구현했다.
그리고, SpringConfig에서 스프링이 제공하는 @Configuration만 수정했다.
스프링빈으로 등록하는 부분을 수정했을 뿐이다.

이외 기존의 다른 어떤 코드도 변경하지 않았다.

 

이제 실행해보자.
실행하기전에 h2 DB를 실행해두는 것을 잊으면 안된다.

 

h2 DB를 실행하고,

서버를 실행하고, localhost:8080에 접속해보자.

기존에 h2 DB에 쿼리문으로 'spring' 과 'spring2'를 작성했었다.
회원 목록을 확인해보자.

DB에 저장해둔 회원이름을 확인할 수 있다.

이번에는 회원가입을 해보자.

jpa라는 이름으로 등록했다.

회원목록을 확인해보면, jpa가 저장되어있다.

 

이번에는 DB Console 에서 확인해보자.

jpa가 잘 등록되어 있다.

 

애플리케이션에서 데이터베이스에 접근한 것이 굉장히 잘 동작하고 있는 것을 확인할 수 있다.

 


스프링을 사용하는 이유

 

스프링을 쓰는 이유가 바로 이런 것이다.

객체 지향 설계가 좋다좋다 하지만, "왜 좋은가?"라고 한다면,
이렇게 인터페이스를 두고 구현체를 바꿔기기하는 것을 "다형성을 활용한다" 라고 이야기 할 수 있는데,

스프링은 이것을 굉장히 편리하게 할 수 있도록 스프링 컨테이너가 이것을 지원해 주는 것이다.

그리고 소위 말하는 "DI" (의존성 주입) 덕분에 이러한 것을 굉장히 편리하게 하는 것이다.

기존의 코드는 손대지 않고,
오직 어플리케이션을 설정하는 어셈블리코드만 수정하고,
나머지 실제 어플리케이션과 관련된 코드는 하나도 손대지 않고 구현클래스를 바꿀 수 있다.

이것을 편리하게 해주는 것이 스프링의 장점이다.

 

MemverService는 사실 MemberRepository를 의존하고 있다.

MemberRepository 인터페이스는 구현체로
- MemoryMemberRepository
- JdbcMemberRepository
가 있다

그런데 스프링 컨테이너에서 기존에는 MemoryMemberRepository를 스프링 빈으로 등록했었다면,

이제는, MemoryMemberRepositoryRepository를 빼고,
Jdbc버전의 JdbcMemberRepository를 스프링빈으로 등록했다.

그리고나면 나머지는 하나도 손댈 것이 없다.

구현체가 JdbcMemberRepository로 바뀌어서 서버가 돌아간다.

 

 

 

SOLID (객체 지향 설계) - 위키백과, 우리 모두의 백과사전

 

ko.wikipedia.org

SOLID 라는 객체지향 프로그래밍 및 설계의 다섯 가지 기본 원칙이 있는데,

두문자 약어 개념
S SRP 단일 책임 원칙 (Single responsibility principle) 클래스는 하나의 책임만 가져야 한다.
O OCP 개방-폐쇄 원칙 (Open/closed principle)“소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.”
L LSP 리스코프 치환 원칙 (Liskov substitution principle)“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.” 계약에 의한 설계를 참고하라.
I ISP 인터페이스 분리 원칙 (Interface segregation principle)“특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.”[4]
D DIP 의존관계 역전 원칙 (Dependency inversion principle)프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.”[4] 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

이중에서 OCP(개방-폐쇄 원칙)
객체 지향에서 말하는 다형성 이라는 개념을 잘 활용하면, 잘 지킬 수 있다.
우리가 했던 것처럼 기능을 완전히 변경을 해도, 애플리케이션전체를 수정할 필요가 없는 것이다.

조립하는 코드는 어쩔 수 없이 수정해야하지만,
실제 어플리케이션이 동작하는데에 필요한 코드들은 하나도 변경하지 않을 수 있다.

이것이 개방 폐쇄 원칙이 지켜진 것이라고 할 수 있다.

 

 

반응형