스프링 입문-02_DB연결

junkyu lee·2022년 5월 25일
0

Spring

목록 보기
3/6

스프링 데이터 엑세스 방식

  • H2 데이터베이스 설치

  • 순수.Jdbc : 옛날 방법

  • 스프링 통합 테스트

  • 스프링 JdbcTemplate

  • JPA : 쿼리없이 객체를 불러와 저장하는 방법

  • 스프링 데이터 JPA


1. H2 데이터베이스 설치

https://www.h2database.com/html/download-archive.html

  • 위의 링크에서 1.4.200 버전을 설치 및 압축해제

    # mac에서는 추가적으로 권한 설정이 필요
    chmod 755 h2.sh
    
    # 실행
    ./h2.sh
    # http://localhost:8082/login.jsp?jsessionid=<session_key>
  • h2 초기화면.

  • jdbc:h2:~/test: 파일경로.
  • 파일이 없는 상태라 연결 에러가 뜸

  • jdbc:h2파일 생성

    $ vi test.mv.db

  • 파일로 접근하면 동시에 접근할 때 충돌이 날수도 있음 (애플리케이션, 콘솔)

    • jdbc:h2:tcp://localhost/~/test 로 접근 : 소캣을 통한 접근 설정

테이블 생성

  • h2까지 들어왔다면 쿼리를 통해 테이블 생성

create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
  • 데이터가 잘 적재 되는지 확인

    insert into member(name) values('spring1');
    
    select * from member;

  • ddl문을 프로젝트 내에 저장하여 보관하는 방법을 지향

2. 고대의 DB 접속방법(순수 JDBC)

연결 준비

  • 라이브러리 추가

    dependencies {
       implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
       implementation 'org.springframework.boot:spring-boot-starter-web'
       testImplementation 'org.springframework.boot:spring-boot-starter-test'
       runtimeOnly('org.springframework.boot:spring-boot-devtools')
       implementation 'org.springframework.boot:spring-boot-starter-jdbc'
       runtimeOnly 'com.h2database:h2'
    }
  • JdbcMemberRepository.java

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

properties

  • datasource.url 추가

  • h2.driver 추가

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

    • 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

  • SpringConf

    package hello.hellospring;
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.JdbcTemplateMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import javax.sql.DataSource;
    
    @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);
         }
    }

3. 스프링 통합 테스트

  • 테스트인 경우 편하게 @Autowired 을 통해 스프링 구현체에 연결하면 된다.
  • 테스트는 반복 가능해야한다.
  • 테스트를 위한 DB에서 테스트를 진행해야한다.

@Transactional

  • 테스트 마다 디비가 commit 되기 전에 rollback시켜 테스트 과정이 디비에 반영되지 않도록 한다.

단위테스트 : 코드를 단위로 쪼개서 테스트하는 방법

통합테스트 : db 연동 및 전체 과정을 테스트하는 방법

=> 단위테스트가 더 좋은 테스트 (강사 피셜)

4. 스프링 JDBC Template

  • 순수 Jdbc와 동일한 환경설정을 하면 된다.
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.
  • 하지만 SQL은 직접 작성해야 한다

findById()

@Override
public Optional<Member> findById(Long id) {
    List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
    return result.stream().findAny();
}
  • jdbcTemplate : 쿼리 결과를 memberRowMapper()의 반환 값으로 만들어 준다.
  • result.stream().findAny(); : Optional 반환
private RowMapper<Member> memberRowMapper() {
    return (rs, rowNum) -> {
        Member member = new Member();
        member.setId(rs.getLong("id"));
        member.setName(rs.getString("name"));
        return member;
    };
}
  • findByName() : 이름으로 조회, findAll() : 전체 조회

save()

@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;
}
  • 각 key값에 맞게 insert를 수행하는 코드

모든 기능을 구현했다면 spring conf를 변경해주면 된다.

@Bean
public MemberRepository memberRepository() {
    // 메모리 저장방식
    // return new MemoryMemberRepository();

    // 고대 jdbc
    // return new JdbcMemberRepository(dataSource);

    // JDBC Template
    return new JdbcTemplateMemberRepository(dataSource);
}

테스트 진행

5. JPA

자동으로 sql 쿼리를 작성해주는 방식
SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다

기존 jdbc 는 제거하고 spring-boot-starter-data-jpa 추가

  • build.gradle
//  implementation 'org.springframework.boot:spring-boot-starter-jdbc'
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  • properties
spring.jpa.show-sql=true //자동으로 생성된 sql을 보여주는 기능
spring.jpa.hibernate.ddl-auto=none // 객체로 테이블을 생성해주는 기능 default=True

ORM : 객체 관계 매핑

db가 알아서 생성해 주는것 IDENTITY

private final EntityManager em : 모든 동작을 관리. 스프링 부트가 자동으로 생성한 매니저를 코드상에서 injection 하면됨

  • select
@Override
public Optional<Member> findById(Long id) {
    Member member = em.find(Member.class, id);
    return Optional.ofNullable(member);
}
    @Override
    public List<Member> findAll() {
        List<Member> result = em.createQuery("select m from Member m", Member.class)
                .getResultList();
        return result;
    }
    
   

-> 변수 인라인화 후

@Override
public List<Member> findAll() {
    return em.createQuery("select m from Member m", Member.class)
            .getResultList();
}
  • "select m from Member m" : JPQL

객체를 대상으로 쿼리를 날린다. 엔티티

맴버 엔티티를 조회, 객체 자체를 select

@Transactional : 데이터를 변경할때는 있어야 하는 어노테이션

SpringConfig.java

package hello.hellospring;

import hello.hellospring.repository.*;
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.persistence.EntityManager;

@Configuration
public class SpringConfig {

    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em){
        this.em = em;
    }

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

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

6. 스프링 데이터 JPA

인터페이스 만으로 기능을 구현

스프링 데이터 JPA가 extends된 구현체를 자동으로 생성

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

생성된 구현체는 SpringConfig에서 injection 할 수 있다.

@Configuration
public class SpringConfig {
		private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
  • JpaRepository에 제공하는 공통 기능들

  • 인터페이스를 통한 기본적인 CRUD
  • findByName() , findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공
profile
가끔 기록하는 velog

0개의 댓글