본문 바로가기
개발 관련 공부/스프링 김영한 로드맵

[스프링 입문] 섹션 6. 스프링 DB 접근 기술

by 슴새 2022. 9. 27.
반응형

H2 데이터베이스 설치

설치하고 실행하면 이런 옛스러운 윈도우 창이 뜬다.

파일로 직접 접근하는건 좋지 않으므로 url을 localhost: 어쩌구 test 로 바꿔주고 연결 누르면

mysql 워크벤치같은 화면이 뜬다.

여기서 sql문을 작성하고 실행할 수 있다. id와 name을 가지고 pk가 id인 테이블을 만들어주었다.(id는 자동 증가)

관리를 쉽게하기 위해 sql/ddl.sql을 만들어 위 내용을 기록한다.

스프링 JdbcTemplate

학교 그리고 싸피에서 배운 jdbc는 db에서 뭔가 하고싶을때마다 conn=.. pstmt=...를 일일히 치고 try-catch로 묶어야하는 번거로운 방식이었다. 하지만 이 jdbcTemplate은 순수 JDBC의 반복 코드를 대부분 제거해준다.

레포지토리 폴더에 관련 클래스를 추가

(어쩌구저쩌구 레포지토리.java들을 과거 만들어놓은 인터페이스를 통해 보다 쉽게 구현할 수 있다. 이런 것 때문에 인터페이스를 작성하는것이다.)

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;

    //생성자가 하나인 경우 Autowired를 생략할 수 있다.
    //@Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        //member table에 집어넣을 것
        //jdbcInsert를 쓰면 쿼리문을 짤 필요가 없다.
        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;
        };
    }
}

이제 localhost:8080 에서 회원가입하면 그 이름이 db에 들어가 목록에 뜬다.

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@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("이미 존재하는 회원입니다.");
    }
}

테스트를 돌려보면 모두 통과하는 걸 확인할 수 있다.

@Transactional

이렇게 하면 테스트 시 db에서 조작한 것들을 모두 없던 일로 만들 수 있다.

트랜잭션을 이런 식으로 쓰다니...신선하다.

JPA

jpa라는 기술을 사용하면 sql조차 작성하지 않아도 된다.

jpa가 내부적으로 jdbc를 사용하는데, 개발자 대신 적절한 sql문을 생성하고 데이터베이스를 조작해 객체를 자동 매핑해준다. (db의 테이블이랑 객체랑 연결)

개발 생산성을 크게 높일 수 있고, 객체 중심의 설계로 패러다임을 전환할 수 있다.

jpa를 사용하기 위해 라이브러리와 설정들을 추가해준다.

package hello.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
    //id는 pk나타냄. identity는 자동증가
    @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;
    }
}

맴버 클래스 수정

매핑을 위해 @Entity를 붙여주고, pk+자동생성&증가를 나타내기 위한 어노테이션 또한 나타내준다.

jpa 맴버 리포지토리 만들고

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {

    //jpa를 쓰려면 이 EntityManager를 주입받아야 된다.
    //스프링부트가 알아서 생성하고 db랑 연결해주는 듯
    private final EntityManager em;
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    public Member save(Member member) {
        em.persist(member); //persist 쓰면 알아서 insert를 만들어줌..
        return member;
    }
    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id); //간단해진 select문..
        return Optional.ofNullable(member); //null일수도 있으니까 ofNullable로..
    }
    public List<Member> findAll() {
        //객체를 대상으로 퀴리를 날림. *대신 객체 자체를 select.(일일히 받아서 new Member...안해도 된다는 뜻)
        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();
    }
}

코드 작성.

자세한건 주석 참고

@Transactional
public class MemberService {
    private final MemberRepository memberRepository;

jpa를 통한 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.

서비스 계층에 어노테이션 추가하기.

package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    private final EntityManager em;
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

config 가서 jpa 사용하도록 설정해준다.

이후 실행해보면 아까 작성한 테스트를 잘 통과하는 것을 볼 수 있다. 동작도 잘 됨.

스프링 데이터 JPA

스프링 데이터 jpa는 jpa를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트이다.

스프링 데이터 jpa를 사용하면 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.

그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
//인터페이스가 인터페이스 받을땐 extend
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,
        Long>, MemberRepository {
    Optional<Member> findByName(String name);
}

SpringDataJpaMemberRepository 라는 인터페이스를 만들자.

package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
    private final DataSource dataSource;
    private final EntityManager em;
    public SpringConfig(DataSource dataSource, EntityManager em) {
        this.dataSource = dataSource;
        this.em = em;
    }
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

바뀐 인터페이스에 맞게 SpringConfig 수정

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

이러고 돌리면 정상 동작한다. 

 

새 인터페이스엔 기존 인터페이스에 있었던 save 이런거 다 생략했는데 정상 동작하는 이유는..스프링 데이터 안에 만들어져 있기 때문이다. (스프링 데이터 안의 리포지토리 인터페이스들..) 심지어 페이징 기능도 자동 제공한다고 한다.

 

스프링 데이터 안에 없는 복잡한 쿼리의 경우

  • Querydsl 라이브러리
  • JPA가 제공하는 네이티브 쿼리
  • JdbcTemplate

중에서 한 방법을 쓰게 된다.

이 강의는 인프런 김영한님의 스프링 입문 강의를 듣고 정리한 것입니다.
강의 링크: https://url.kr/by9hmf

 

반응형

댓글