스프링 입문 - 섹션6 스프링 DB 접근 기술
CS/김영한 스프링 강의

스프링 입문 - 섹션6 스프링 DB 접근 기술

이 섹션 강의 목차는 이렇다.

물론 내가 볼거라 대충 쓸거임

 

- H2 데이터베이스 설치

- 순수 Jdbc

- 스프링 통합 테스트

- 스프링 JdbcTemplate

- JPA

- 스프링 데이터 JPA

 

H2 데이터베이스 설치는 별거 없고 resources의 application에 datasource로 연결해준다는 것만 알면 된다.

 

순수 Jdbc이전에, Jdbc는 자바랑 데이터베이스랑 connect만 해주는거라 안의 세부 내역은 직접 매핑을 해줘야 한다. sql문 짜서 결과 나오면 그걸 내 클래스 멤버의 어디에 대입해주고.. 하는거. 사용하기엔 너무 옛날 기술이라 이제 안쓰지만 이런게 세부적으로 작성된다는 것 정도는 알고 있어야 이해할 수 있다.

configure로 설정 변경해주고 다음에 구체적인 과정을 작성해줌.

 

여기에 볼만한 건 DataSource가 있는데, 위 application에서 작성한 내용이 들어가는 것. 스프링이 찾아서 컨테이너에 넣는다.

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    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);
    }
}

 

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

 

그리고 기본적으로 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 사용하며, 확장에는 열려있고, 수정, 변경에는 닫혀있다. DI를 사용해서 기존에 자신을 사용해주던 클래스는 아무것도 건들이지 않아도 되고(건드려도 이름 바꾸는 정도), 자신만 바꾸면 된다. 밑의 MemberService는 그냥 그대로 쓰면 되고 MemberRepository만 바꾸면 되는 방식.

 

통합테스트는 그냥 Test 클래스 위에 annotation으로 @SpringBootTest를 넣으면 스프링을 돌리면서 테스트를 진행 한다고. 사실 통합테스트보단 유닛테스트에 중요한게 많을 때가 많아서, 통합 테스트 없이 안되면 그건 코드 구조 자체가 잘못되었을 확률이 큼.

그리고 Test 클래스에 @Transactional 붙이면 각 메소드(테스트)가 끝날때마다 db를 rollback해서 비워준다. 테스트만.

 

 

JdbcTemplate는 query문은 작성하지만 위의 conn같은거 작성은 내부에서 알아서 해준다. Jpa를 사용해도 직접 query를 작성해야 하는 복잡한 상황일 땐 사용한다.

 

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    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);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());
        Number key = jdbcInsert.executeAndReturnKey(new
                MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }
    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id 
                = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }
    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member where 
                name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }
    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
        Member member = new Member();
        member.setId(rs.getLong("id"));
        member.setName(rs.getString("name"));
        return member;
    };
    }
}

 

 

JPA는 Jdbc까진 query문을 직접 작성했지만 이것도 알아서 작성해준다. query문 뿐만 아니라 모델도 앞에 @Entity 붙이면 알아서 추론해서 넣어줌. 그래서 진짜 자바만 생각하면 됨.

관련 라이브러리가 data-jpa. 여기 안에 jdbc도 포함되어 있음. hibernate가 클래스 만들면 테이블에 넣어주는 놈.

 

@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    private final EntityManager em;
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

중요한 건 EntityManager인데, data-jpa를 하면 스프링이 안에서 EntityManager라는걸 만들어준다. 세팅 정보랑 커넥션이랑 다 연결해줘서 DB 내부랑 통신하는걸 알아서 다 해줌. 그래서 얘를 주입받아서 사용해야 한다.

 

public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member);
        return member;
    }
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
            .getResultList();
    }
    public Optional<Member> findByName(String name) {
        List<Member> result = em.createQuery("select m from Member m where 
                m.name = :name", Member.class)
        .setParameter("name", name)
        .getResultList();
        return result.stream().findAny();
    }
}

또한 여기서부터 저장하는 DB 작동에는 @Transactional을 붙여야 한다. 즉, JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행돼야 함.

 

 

스프링 데이터 JPA는 여기서 더. interface만 만들어도 알아서 인스턴스 만들고 하는 것 까지.

@Configuration
public class SpringConfig {
    private final MemberRepository memberRepository;
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,
Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

이렇게 아예 query를 안 사용하지만, 제작자도 이건 도와주는 강력한 도구이지 모든걸 대응할 수 없기에, querydsl을 사용하거나 jdbc의 query문을 직접 사용할 수 있도록 제공하고 있다.