[스프링 개발 입문] 6. 스프링 DB 접근 기술

HJ·2022년 7월 19일
0

김영한 님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard


18. h2 데이터베이스

18-1. 데이터 접근 기술

  • JDBC : 어플리케이션 서버와 DB를 연결할 때 필요한 기술

  • Spring JDBC Template : 어플리케이션에서 DB로 SQL을 편리하게 보낼 수 있음

  • JPA : JPA라는 기술이 SQL을 직접 만들어서 DB에 쿼리를 날려준다

  • SpringData JPA : 스프링에서 JPA를 편리하게 사용할 수 있게한 기술


18-2. 테이블 생성 및 데이터 삽입

create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);
  • id bigint generated by default as identity : DB가 자동으로 id 값을 채워줌

  • insert into MEMBER(NAME) values('spring') : 데이터 삽입




19. 순수 JDBC

19-1. 설정 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
  • build.gradle에 위의 코드 추가

  • Java는 DB와 연결하려면 JDBC 드라이버가 필수적으로 필요

  • DB가 제공하는 클라이언트가 h2database


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

  • spring이 DB와 연결하는 작업을 처리해주기 때문에 사용만 하면 된다


19-2. JdbcMemberRepository 객체 생성

19-2-1. 설명

  • 회원을 저장하는 역할은 MemberRepository가 수행

  • 이 때, 구현을 메모리에 할 것인지, DB와 연동하여 JDBC로 할 것인지를 결정

  • JdbcMemberRepositoryMemberRepository를 구현한 구현체


19-2-2. 코드


< JdbcMemberRepository >

public class JdbcMemberRepository implements MemberRepository {

    private final DataSource dataSource;

    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
  • 코드가 너무 길기 때문에 게시물 가장 아래쪽에 save()와 관련된 코드만 따로 작성

  • DB와 연결하려면 DataSource가 필요

    • javax.sql.DataSource

    • DataSource는 spring에게 주입 받아야 함


19-3. MemberService의 MemberRepository 객체 변경

19-3-1. 설명

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

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

    • JDBC 기반의 MemberRepository를 만들었지만 기존 코드에 변화를 주지 않았음

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

  • 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다


19-3-2. 코드

< SpringConfig >

private DataSource dataSource;

@Autowired
public SpringConfig(DataSource dataSource) {
    this.dataSource = dataSource;
}

@Bean
 public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
 }
  • 스프링 빈으로 등록되는 것을 MemoryMemberRepository에서 JdbcMemperRepository로 변경

  • 이렇게 되면 기존 MemberService에서는 MemberRepository 객체를 DI 받기 때문에 MemberService의 코드를 변경하지 않아도 JdbcMemperRepositoryMemberServiceMemberRepository에 주입된다

  • DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체

  • 스프링부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어두기 때문에 DI를 받을 수 있다


19-4. Jdbc를 이용한 save()


< JdbcMemberRepository >

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

            // RETURN_GENERATED_KEYS : DB에 insert 했을 때, key 값을 얻는다
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            
            // 1번은 sql의 (?)가 있는 부분을 의미하고, (?)에 member.getName()이 들어간다
            pstmt.setString(1, member.getName());
            
            // DB에 실제 쿼리가 날라가는 부분
            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);
        }
    }



20. 스프링 통합 테스트

  • Spring Container와 DB까지 연결한 통합 테스트 진행

20-1. MemberServieIntegrationTest 추가

  • 기존 MemberServiceTest 코드 복붙 후 코드를 수정하면서 진행

  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다는 의미


20-2. 객체 생성 -> 객체 주입


< 수정 전 >

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

< 수정 후 >

@Autowired
    MemberService memberService;
@Autowired
    MemberRepository memberRepository;
  • 직접 객체를 생성하는 것이 아니라 스프링 컨테이너에서 객체를 가져오도록 변경

  • 수정 전에는 MemoryMemberRepository로 객체를 생성했는데 수정 후에는 MemberRepository로 객체를 주입받는다


20-3. 데이터 초기화 방식 변경


< 수정 전 >

@AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

< 수정 후 >

@Transactional
class MemberServiceIntegrationTest {

}
  • DB에 데이터를 반영하려면 commit을 해야함 ( AutoCommit 제외 )

  • 테스트 종료 후, 롤백시키면 데이터가 반영되지 않는다

  • @Transactional : 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다 ( DB에 삽입했던 데이터가 지워진다 )

  • DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다

  • 참고 : @Commit이 붙으면 테스트 종료 후 commit




21. 스프링 JdbcTemplate

21-1. 설명

  • 순수 Jdbc와 동일한 환경설정을 하면 된다

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

  • 단> SQL은 직접 작성해야 한다


21-2. JdbcTemplateMemberRepository 객체 생성

public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
}

21-3. 회원 가입

    @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;
    }
  • SimpleJdbcInsert는 위에서부터 4줄의 코드를 작성하면 쿼리를 작성할 필요가 없다

  • 테이블명과 기본키를 지정해주면 INSERT 문을 만들어준다


21-4. 회원 조회

@Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memeberRowMapper(), id);
        return result.stream().findAny();
    }
  • 작성한 쿼리가 실행되고, 결과는 memberRowMapper()를 통해 객체로 mapping하여 넘겨줌

  • memberRowMapper()은 따로 작성해주어야 함


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



22. JPA

22-1. 설명

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다

  • JPA를 사용하면 넣으면 JPA가 중간에서 DB에 SQL을 날리거나 데이터를 가져오는 것을 전부 처리해줌

  • JPA는 자바의 표준 인터페이스이고 Hibernate 라는 오픈소스 구현체가 사용됨

  • JPA는 ORM 기술

    • ORM : 객체와 관계형 데이터베이스 테이블을 매핑
    • 이 때 사용하는 어노테이션이 @Entity

22-2. 설정 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  • spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함하기 때문에 이전에 작성했던 jdbc는 제거해도 된다

  • JPA는 EntityManager로 동작

  • build.gradle에 data-jpa가 추가되면 스프링이 자동으로 EntityManager를 생성


spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
  • show-sql : JPA가 생성하는 SQL을 출력한다

  • ddl-auto : 객체를 보고 테이블을 자동으로 생성해주는 기능

    • none 를 사용하면 해당 기능을 끈다

    • create 를 사용하면 Entity 정보를 바탕으로 테이블을 직접 생성


22-3. Entity Mapping


< Member >

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

}
  • JPA를 사용하기 위해서는 @Entity 어노테이션을 이용해서 Entity를 Mapping 해야함

  • @Entity

    • javax.persistence.Entity

    • 이 어노테이션이 붙으면 JPA가 관리하는 Entity가 된다

  • PK를 mapping 해줘야 함

    • @Id : 객체의 PK를 의미

    • @GeneratedValue : PK 값을 위한 자동 생성 전략을 의미

    • GenerationType.IDENTITY : DB가 Id를 자동으로 생성해줌

  • String name에 @Column(name = "username")을 붙이면 name이 DB 테이블의 username이라는 이름의 column이 된다


22-4. JpaMemberRepository 객체 생성


< JpaMemberRepository >

public class JpaMemberRepository implements MemberRepository {

    private final EntityManager em;

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

    • spring이 현재 DB와 연결까지 해서 자동으로 생성

    • properties에서 설정한 정보들과 DB 연결 정보를 내부에 가지고 있음

    • 또 내부적으로 DataSource를 가지고 있어서 DB와 통신하는 것을 EntityManager 내부에서 다 처리해줌

    • 만들어진 EntityManager를 Injection 받아서 사용하면 된다


22-5. JPA를 이용한 저장, 조회


< JpaMemberRepository >

    @Override
    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
    	// 조회할 type과 식별자를 parameter로 넣어준다
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
    	// JPQL : 객체 지향 쿼리, 객체를 대상으로 쿼리를 날린다
        // 그러면 날린 쿼리가 SQL로 번역이 됨
        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() {
    	// 이것도 JPQL
        // Entity를 대상으로 쿼리를 날림
        // 객체 자체를 select
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

22-6. EntityManager & MemberRepository DI 설정


< SpringConfig >

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

	// 원래는 @PersistenceContext가 필요
    // 없어도 Spring에서 DI를 해줌
    private EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }
    
    @Bean
    public MemberRepository memberRepository() {
        return new JpaMemberRepository(em);
    }
    
}    

22-7. 주의사항

  • JPA를 사용해 데이터를 저장하거나 변경하려면 항상 @Transactional이 있어야 함

  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야함

  • MemberService에 @Transactional 추가

    • 메서드가 정상 종료되면 트랜잭션을 commit한다

    • 만약 런타임 예외가 발생하면 rollback한다




23. SpringData JPA

23-1. 설명

  • SpringData JPA는 JPA를 편리하게 사용하도록 도와주는 기술

  • SpringData JPA를 사용하면, repository에 구현 클래스 없이 인터페이스 만으로 개발할 수 있음

  • SpringData JPA가 repository 인터페이스를 스프링 빈으로 자동 등록해준다

  • 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다

  • 설정은 이전 JPA 설정과 동일하게 사용


23-2. SpringDataJPA 인터페이스 설계


< SpringDataJpaMemberRepository >

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    Optional<Member> findByName(String name);

}
  • JpaRepository를 extends하는 인터페이스가 있으면 SpringData JPA가 자동으로 인터페이스에 대한 구현체를 만들어주고 스프링 빈까지 등록해준다

  • 이렇게 등록된 스프링 빈이 Injection 된다


23-3. DI


< SpringConfig >

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

    @Bean
    public MemberService memberService() {

        return new MemberService(memberRepository);
    }
}
  • Spring Container에서 memberRepository를 찾는 과정에서 JpaRepository를 extends하는 인터페이스가 있으면 등록한 객체가 없어도 Injection 받을 수 있다

23-4. SpringData JPA 제공 클래스

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

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

  • 페이징 기능 자동 제공

0개의 댓글