Post

Spring 스프링 입문 - 6. 스프링 DB 접근 기술

스프링 DB 접근 기술


H2 데이터베이스 설치

  • 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공

    순수 Jdbc

  • Jdbc 리포지토리 구현
  • 스프링 설정 변경
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @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);
       }
    }
    
  • private final DataSource dataSource;
  • DataSource는 데이터베이스 커넥션을 획득할때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링빈으로 만들어둔다. 그래서 DI를 받을 수 있다.

image

image

  • 개방-폐쇠 원칙(OCP, Open-Closed Principle)
    • 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI(Dependency Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.

스프링 통합 테스트

  • 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    @SpringBootTest
    @Transactional
    class MemberServiceIntegrationTest {
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;
    
    @Test
    public void 회원가입() throws Exception {
      //Given
      Member member = new Member();
      member.setName("hello");
      //When
      Long saveId = memberService.join(member);
      //Then
      Member findMember = memberRepository.findById(saveId).get();
      assertEquals(member.getName(), findMember.getName());
    }
    
    @Test
    public void 중복_회원_예외() throws Exception {
      //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));//예외가 발생해야 한다.
      assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
    }
    
  • @SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional: 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

스프링 JdbcTemplate

  • 순수 Jdbc와 동일한 환경설정을 하면 된다.
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    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

  • JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
  • JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
JPA 엔티티 매핑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
JPA 회원 리포지토리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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();
    }
}
  • JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.

    스프링 데이터 JPA

  • 스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어든다.
  • 여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.
  • 스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 개발이 정말 즐거워진다.
  • 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어든다.
  • 따라서 개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있다.
  • 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수이다.
  • 스프링 데이터 JPA 회원 리포지토리
    1
    2
    3
    
    public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    Optional<Member> findByName(String name);
    }
    
  • 스프링 데이터 JPA가 SpringDataJpaMemberRepository 를 스프링 빈으로 자동 등록해준다
    스프링 데이터 JPA 제공 클래스

    image

스프링 데이터 JPA 제공 기능
  • 인터페이스를 통한 기본적인 CRUD
  • findByName(),findByEmail()처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공

참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.


References: 김영한 - [스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술]
This post is licensed under CC BY 4.0 by the author.