본문으로 바로가기

목차

    0. 환경

    • m1 macbook
    • IntelliJ IDEA(m1) - 202102
    • java 11(AdoptOpenJDK-11.0.11)

    1. build.gradle

    • jdbc, h2 데이터베이스 관련 라이브러리 추가합니다.
    dependencies {
      //자바가 db에 연결을 하기 위해선 jdbc 필요합니다.
      implementation 'org.springframework.boot:spring-boot-starter-jdbc'
      //h2db 연결을 위한 클라이언트
      runtimeOnly 'com.h2database:h2'
    }
    • 추가 후 import(Load Gradle changes)를 해줍니다.

    load gradle changes

    2. H2DB 연결 정보 설정(application.properties)

    • resources/application.properties
    • application.properties 파일에 다음과 같이 스프링 부트 데이터베이스 연결 설정 추가해 줍니다.
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa

    [주의!]

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

    3. Jdbc 리포지토리 구현

    • hello/hellospring/repository/JdbcMemberRepository.java
    • 메모리 기반의 저장소에서 H2DB로 변경을 위해 작성합니다. 따라서 MemberRepository 인터페이스를 두고 구현체로 작성합니다.
    • JDBC API로 직접 코딩하는 것은 매우 과거의 이야기 입니다. 참고만 또는 복사해서 실행 정도만 해보고 넘어가는 게 좋습니다.
    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);
            }
        }
    
        /*
            스프링에서 DataSourceUtils를 통해서 커넥션을 획득을 해야 합니다.
            이유는 트랜잭션에 걸리지 않게 데이터 베이스 커넥션을 같게 유지를 시켜 줍니다.
            닫을 때도 마찬가지로 DataSourceUtils를 사용해야 합니다.
         */
        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);
        }
    }

    4. 스프링 설정 변경

    인터페이스를 사용한 구현 클래스
    다형성

    • 메모리 기반의 저장소에서 H2DB로 변경을 위해 구현체를 바꿔줍니다.
    • 객체지향 설계의 장점인 다형성을 활용할 수 있습니다.
    • 인터페이스를 두고 쉽게 구현체를 변경함으로써 특별하게 코드를 수정할 필요가 사라집니다.
    • 스프링은 이것을 편하게 구현 및 개발 할 수 있도록 스프링 컨테이너가 DI(의존성 주입)을 제공합니다.
    • 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있습니다.
    • 개팡-폐쇄 원칙(OCP)이 지켜졌습니다. (확장에는 열려있고, 수정에는 닫혀있다.)  

     

    [기존 설정 파일]

    package hello.hellospring;
    
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    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 {
    
        @Bean
        public MemberService memberService() {
            return new MemberService(memberRepository());
        }
    
        @Bean
        public MemberRepository memberRepository() {
            return new MemoryMemberRepository();//구현체
        }
    }

     

    [설정 파일 변경]

    package hello.hellospring;
    
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    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 final 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);//h2 jdbc 구현체
        }
    }
    • DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체입니다.
    • 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둡니다. 따라서 따로 빈 등록 없이 DI를 받을 수 있습니다.
    • 스프링이 설정 파일을 보고 알아서 빈을 생성해줍니다.

    5. 테스트

    1. 회원 가입을 해봅니다.

    회원 가입
    회원 가입 등록

    2. 회원이 잘 등록되었는지 회원 목록을 클릭해봅니다.

    회원 목록
    회원 목록 결과

    3. H2 Web Console을 이용해 테이블에 잘 들어갔는지 확인해봅니다.

    H2 Web Console

    4. 스프링 부트 재시작 후에도 잘 저장이 되어있는지 위의 과정처럼 확인해봅니다.

    6. SOLID (객체 지향 설계)

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