본문 바로가기

springboot

[스프링 입문] 5. 스프링 DB 접근 기술

  • 순수 JDBC
  • 스프링 통합 테스트
  • JDBC Template
  • JPA
  • 스프링 데이터 JPA

 

h2 데이터베이스의 환경설정을 마쳤다. 

 

순수 JDBC

build.gradle에 관련 라이브러리와 application-properties에 연결 설정을 추가했다.

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

JdbcMemberRepository를 새로 만들어 아주 긴(...) 코드를 작성하고 SpringConfig의 MemberRepository 빈을 수정한다.

private final DataSource dataSource;

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

@Bean
public MemberRepository memberRepository() {
    //return new MemoryMemberRepository();
    return new JdbcMemberRepository();
}

 

 

실행하면 등록과 조회가 잘 된다.

 


 

스프링 통합 테스트

스프링 컨테이너와 DB까지 연결한 테스트를 통합 테스트라고 한다. 

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    
}

스프링 컨테이너와 함께 테스트를 실행하기 때문에 @SpringBootTest 어노테이션을 추가한다.

원본 DB 데이터와 다음 테스트에 영향을 주지 않게 하기 위해 @Transactional 어노테이션을 추가한다.

 

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    @Autowired MemberRepository memberRepository;
    @Autowired MemberService memberService;

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

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

        // then
        Member findMember = memberService.findOne(saveId).get();
        Assertions.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));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");


    }
}

 

 

테스트를 돌리면 성공한다.

 


 

JDBC Template

JDBC Template를 사용하면 반복 코드를 대부분 제거할 수 있다. 하지만 sql 쿼리는 직접 작성해야 한다. 

JdbcTemplateMemberRepository를 생성한다.

 

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"); // member table에서 id key로 insert 가능

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

SpringConfig의 MemberRepository 구현체도 교체한다.

@Bean
public MemberRepository memberRepository() {
    //return new MemoryMemberRepository();
    //return new JdbcMemberRepository(dataSource);
    return new JdbcTemplateMemberRepository(dataSource);
}

 

테스트도 모두 성공한다.

 


 

JPA

JPA는 반복 코드는 물론이고 기본적인 sql도 직접 만들어서 실행해준다. 

 

build.gradle에 관련 라이브러리를 추가하고, application-properties에도 JPA 설정을 추가한다.

//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
spring.jpa.show-sql=true // JPA가 날리는 sql을 볼 수 있다. 
spring.jpa.hibernate.ddl-auto=none // create로 하면 create table까지

 

JPA를 사용하기 위해서는 Entity가 필요하다. 객체와 relational database를 mapping하기 위해서 @Entity 어노테이션을 Member에 붙인다.

따라서 Member는 JPA가 관리하는 Entity가 된다. 

 

@Entity
public class Member {

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

    private String name;

 

JPA를 활용해 데이터를 관리할 JpaMemberRepository도 생성한다.

JPA는 EntityManager로 모든 게 동작한다. EntityManager는 스프링부트가 자동으로 생성해준다.

EntityManager가 쿼리를 모두 만들어서 실행해준다. 

public class JpaMemberRepository implements MemberRepository{
    private final EntityManager em; // spring-boot가 자동으로 DB와 연결해서 EntityManager를 생성하기 때문에 injection 받으면 됨

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

    @Override
    public Member save(Member member) {
        em.persist(member);
        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) {
        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() {
        return em.createQuery("select m from Member m", Member.class) // Member Entity를 조회하되, 객체 자체를 select
                .getResultList();
    }
}

더 간단해졌다.

특별한 쿼리는 jpql 쿼리를 사용한다. JPA가 자동으로 sql 쿼리로 번역해준다.

 

또한 JPA는 모든 data변경이 일어날 때 Transaction이 반드시 있어야한다. MemberService에 @Transactional 어노테이션을 추가해준다. 

(이 프로그램에서는 join에만 추가해줘도 된다.)

 

SpringConfig도 수정해준다.

Jpa는 EntityManager가 반드시 필요하기 때문에 생성자를 통해 받는다.

@Configuration
public class SpringConfig {

    private final EntityManager em;
    
    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }
    
    ...
    
    @Bean
    public MemberRepository memberRepository() {
        //return new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        //return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

 

 

테스트 모두 통과한다.

 

콘솔을 보면 JPA에 의해 Hibernate 쿼리가 날라가는 걸 볼 수 있다. 

Hibernate query

 


 

스프링 데이터 JPA

스프링 데이터 JPA는 JPA를 편하게 사용하도록 도와주는 기술이다.

물론 스프링 데이터 JPA는 JPA를 공부한 후에 공부해야한다. 

 

SpringDataJpaMemberRepository를 만든다. 이 때, 이 레포지토리는 interface로 생성한다. 

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

JpaRepository<Data, PK> 로 지정한다.

JpaRepository를 extends하고 있다면, 자동으로 SpringDataJpaMemberRepository의 구현체를 만들어준다. 

 

 

끝이다. SpringConfig를 수정한다.

@Configuration
public class SpringConfig {


    private final MemberRepository memberRepository; // 이렇게 해놓으면 스프링 데이터 jpa가 알아서 구현체를 만들어서 스프링 빈에 등록해줌

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

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

MemberRepository는 알아서 구현되기 때문에 Injection으로 넣어주면 된다. 

테스트도 모두 통과한다.

 

스프링 데이터 JPA는 기본적인 CRUD, findByName(), findByEmail처럼 method의 이름만으로 조회 기능과, 페이징 기능도 자동으로 제공한다. 

 

스프링 데이터 JPA 제공 클래스

 

 

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이나 네이티브 쿼리를 통해 작성한다고 한다.