꼬물꼬물

스프링 DB 접근 기술 본문

스터디/스프링 입문

스프링 DB 접근 기술

멩주 2022. 8. 29. 02:04

H2 데이터베이스 설치

JDBC URL: 내 홈의 파일 경로 -> jdbc:h2:tcp://localhost/~/test 로 변경

 

cd ~ // ll 을 하면 test.mv.db가 있어야 한다!

Table 생성

drop table if exists member CASCADE;
create table member
(
-- LONG으로 선언, generated by default as identity는 값을 세팅하지 않고 insert 시 DB가 자동으로 값을 채워준다.--
 id bigint generated by default as identity, 
 name varchar(255),
 primary key (id)
);

src와 같은 level에서 sql 문 관리

순수 JDBC

build.gradle 추가

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-jdbc' // java와 db를 연동하려면 jdbc가 꼭 있어야 한다.
   runtimeOnly 'com.h2database:h2' // db가 제공하는 client가 필요하다!
}

application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

repository/JdbcRepository.class

public class JdbcMemberRepository implements MemberRepository {
    // db와 연동하려면 datasource가 필요하다.
    private final DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource) {
        // spring을 통해 주입
        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); // RETURN_GENERATED_KEYS: 값 반환?

            pstmt.setString(1, member.getName());

            pstmt.executeUpdate(); // 실제 쿼리 날림
            rs = pstmt.getGeneratedKeys(); // RETURN_GENERATED_KEYS와 매칭되는 것. 저장한 member의 id 값을 반환한다.

            if (rs.next()) { // 값이 있으면
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;

        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            // 자원 반환해야 한다. release
            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(); // 조회는 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() {
        // 같은 connection 유지를 위해
        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); }
}

 

SpringConfig.class

// 하단의 Spring 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();
        return new JdbcMemberRepository(dataSource);
    }

}

스프링을 쓰는 이유? 객체지향. 다형성(MemberRepository를 인터페이스로 두고 Memory에서 Jdbc로 바꿔 쓸 수 있다.) 

스프링 컨테이너가 이런 다형성을 지원해 준다.

기존의 코드 수정없이 config 코드만 수정되면 다른 곳의 수정이 필요 없다. 이를 스프링이 편리하게 해준다.

개방-폐쇠 원칙(OCP, Open-Closed Principle)

  - 확장에는 열려있고, 수정/변경에는 닫혀있다.

스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

 

@Transactional: db는 트랜잭션 단위로 실행된다. commit, rollback. 이를 Test에 붙이면 test시작시 Transaction이 시작되고 test가 끝나면 Rollback 해준다.

 

test/MemberServiceIntegrationTest.class

@SpringBootTest
@Transactional // transactional이 없다면 db에 저장된다.
class MemberServiceIntegrationTest {

    // 스프링 컨테이너에게 service와 repository 제공하도록
    @Autowired MemberService memberService;
    @Autowired
    MemberRepository memberRepository;

    @Test
    void 회원가입() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get(); // 레포지토리에 있는 것을 확인해야한다.
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        // when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// 람다 로직 실행시 IllegalStateException이 터져야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }


}

@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행

@Transactional: 테스트 케이스에 이 어노테이션이 있다면, 테스트 시작 전에 트랜잭션을 시작, 완료 후에 롤백한다. DB에 데이터가 남지 않아 다음 테스트에 영향을 주지 않는다. (Test case에서만! Service에 붙었을 경우 제대로 실행된다.)

 

스프링 컨테이너 포함: 통합테스트

스프링 컨테이너가 포함되지 않은 단위테스트에 익숙해져야 한다!! 좋은 테스트~!!

 

스프링 JDBCTemplate

- 순수 Jdbc와 동일한 환경설정

- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API의 반복 코드를 대부분 제거해준다.

- 디자인 패턴 중 templateMethod Pattern을 따른다.

 

repository/JdbcTemplateMemberRepository.class

public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

//    @Autowired 생성자가 하나만 있을 때 스프링 빈으로 등록되면 @Autowired를 생략할 수 있다.
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.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 Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("SELECT * FROM MEMBER WHERE name = ?", memberRowMapper(), name);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("SELECT * FROM MEMBER", memberRowMapper());
    }

    private RowMapper<Member> memberRowMapper(){
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

 

JPA

- JPA를 사용하면 기존의 반복 코드, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.

- SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.

- 개발 생산성을 크게 높일 수 있다.

 

build.gradle

dependencies {
// implementation 'org.springframework.boot:spring-boot-starter-jdbc' // java와 db를 연동하려면 jdbc가 꼭 있어야 한다.
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

application.properties

spring.jpa.show-sql=true // jap가 날린 sql 보여주기
spring.jpa.hibernate.ddl-auto=none // create로 하면 ddl 실행

External Library에 hibernate가 들어와야 한다.

- JPA는 인터페이스, 구현체로 hibernate가 있다.

 

repository/JpaMemberRepository.class

public class JpaMemberRepository implements MemberRepository {

    // implementation하면 스프링 부트가 자동으로 EntityManager을 생성해준다. Injection만 해주면 됨.
    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member member) {
        em.persist(member);
        // jpa가 insert 쿼리를 만들고 id까지 SetId를 해준다,
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        // jpql 작성
        List<Member> result = em.createQuery("SELECT m FROM Member m WHERE m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        // 객체를 대상으로 query를 날리면 sql로 자동 번역된다.
        return em.createQuery("SELECT m FROM Member m", Member.class)
                .getResultList();
    }
}

MemberService.class에는 @Transactional 추가. JPA에서는 모든 데이터 변경이 transaction 안에서 이루어져야 한다.

 

 

스프링 데이터 JPA

- 반복적인 CRUD를 제공한다.

- 핵심 비즈니스 로직을 개발하는데 집중할 수 있다.

 

SpringConfig.class

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

    // spring data jpa가 만들어놓은 구현체에 injection 된다.
    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        // JpaRepository를 상속받은 interface의 구현체를 스프링빈에 등록해 둠.
        this.memberRepository = memberRepository;
    }

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

}

 

repository/SpringDataJpaRepository.interface

// JpaRepository를 상속받으면 스프링이 스프링 빈에 자동 등록 해준다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // JPQL: SELECT m FROM Member m WHERE m.name = ?으로 번역
    // interface 이름만으로 해결된다.
    @Override
    Optional<Member> findByName(String name);
}

- 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해준다.

- 인터페이스를 통한 기본적인 CRUD 제공

- findByName() 처럼 메서드 이름 만으로 조회 기능 제공

- 페이징 기능 자동 제공

'스터디 > 스프링 입문' 카테고리의 다른 글

AOP  (0) 2022.08.30
회원 관리 예제 - 웹 MVC 개발  (0) 2022.08.29
스프링 빈과 의존관계  (0) 2022.08.28
회원 관리 예제  (0) 2022.08.28
스프링 웹 개발 기초  (0) 2022.08.27