스프링

스프링 DB 접근 기술

수풀속의고라니 2022. 5. 20. 20:41
728x90

H2 데이터베이스 설치


 

 

drop table if exists member CASCADE;


create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

 

위와 같은 테이블 생성

 

자바에서의 Long 타입은 sql에서는 bigint

 

generated by default as identity는 null값이 들어왔을 때 DB에서 알아서 값을 채워주도록 해주는 쿼리문

 

순수 JDBC


이 부분은 학원에서 하던 방식이어서 내용은 추가하지 않았음

 

@Configuration
public class SpringConfig {


    private DataSource dataSource;

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


    //스프링 빈을 등록한다는 의미
    //아래의 로직을 스프링 빈에 등록 - 멤버 서비스가 스프링 빈에 등록
    @Bean
    public MemberService memberService(){
        //멤버서비스는 생성자라서 멤버리퍼지토리 인스턴스가 들어와야 하며,
        //MemoryMemberRepository()를 받은 memberRepository()를 넣어줌
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        //DB랑 연결
//        return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}

애플리케이션 설정 파일 부분만 참고

 

 

개방-폐쇄 원칙(OCP, Open-Closed Principle)
- 확장에는 열려있고, 수정, 변경에는 닫혀있다.


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

객체 지향 개발이 중요한 이유가 이러한 인터페이스 등을 활용한 다형성에 있다. 

 

스프링 통합 테스트


스프링 컨테이너와 DB까지 연결한 통합 테스트 진행

 

@SpringBootTest
//테스트 케이스에 이 어노테이션을 달면 테스트를 실행할 때 트랙잭션을 먼저 실행
//DB에 데이터를 가지고 한 모든 작업을 테스트가 끝난 이후 롤백을 해줌
//따라서 테스트를 반복실행이 가능해짐
@Transactional
class MemberServiceIntegrationTest {
    
    //테스트에서는 굳이 생성자를 만들어서 할 필요 없이 가장 편한 방법으로
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

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

        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }
}

 

@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.


@Transactional

테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 

이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

통합테스트도 중요하지만 스프링 컨테이너를 올리지 않고도 실행 가능한 단위 테스트로 잘게 쪼개서 테스트 하는 것이 좋은 테스트 케이스인 경우가 많고 중요하다.

 

스프링 JDBC Temlplate


public class JdbcTemplateMemberRepository implements MemberRepository{

    private JdbcTemplate jdbcTemplate;

    @Autowired //생성자가 1개일 때 이 어노테이션 생략 가능 - 지금 생략 가능
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        //이 친구는
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        //테이블명, pk를 넣어주고,
        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) {
        //1. jdbcTemplate에서 쿼리를 날리면 그 결과를 memberRowMapper()를 통해서 매핑을 함
        //2. 이 결과를 List로 받아서 Optional로 바꿔서 반환
        //query(sql, rowMapper, Object) --> List 타입으로 반환
        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 new RowMapper<Member>() {
//            @Override
//            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
//                
//                Member member = new Member();
//                member.setId(rs.getLong("id"));
//                member.setName(rs.getString("name"));
//                return member;
        
        //위의 형식을 아래처럼 람다 형식으로 바꿀 수 있음
        //ResultSet 결과를 member로 매핑을 해서 돌려주면 값들이 member에 들어가게 됨
        return (rs, rowNum) -> {
            
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

 

JDBC 탬플릿을 사용해서 리포지토리를 만들었음

 

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

SpringConfig에서도 다시 설정해줌

 

JPA


이 기술을 사용하면 sql 쿼리도 자동으로 처리를 해주며, sql과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임의 전환이 가능함

 

<application.properties>

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

스프링 부트에 JPA를 추가해줌

 

show-sql : JPA가 생성하는 SQL을 출력한다.
ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다.
create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자

 

JPA 회원 리포지토리

public class JpaMemberRepository implements MemberRepository{
    
    //jpa는 이거로 모든게 작동됨
    //그래서 JPA를 사용하기 위해서는 이걸 주입받아야 함
    private final EntityManager em;
    
    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }
    
    @Override
    public Member save(Member member) {
        //이렇게 하면 JPA가 인서트 쿼리를 만들어서 디비에 자동으로 집어넣고
        //pk값인 id 까지 자동으로 세팅해줌
        em.persist(member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        //pk의 경우 아래처럼 조회하는 것이 가능
        //조회하는 것은 em.find(조회할 타입, 식별자 pk)를 사용하면 됨
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }
    
    //pk 기반이 아닌 것들은 jpql을 작성해 주어야 함
    
    @Override
    public Optional<Member> findByName(String name) {
        //name은 id와 다르게 pk가 아니기 때문에 jpql이라는 객체지향 쿼리언어 사용해야 함
        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이라는 것
        //보통 테이블을 대상으로 쿼리를 날리는데 이 방식은 객체를 대상으로 쿼리를 날리게 됨
        //select가 조회하는 대상이 Member의 객체인 m
        List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
        return result;
    }
}

 

JPA를 사용하기 위해서는 꼭 트랜잭션이 있어야 하기 때문에 서비스 계층에 아래와 같이 트랜잭션을 추가해줌

 

@Transactional
public class MemberService {

 

 

<SpringConfig>

 

@Configuration
public class SpringConfig {

    private EntityManager em;

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

    //스프링 빈을 등록한다는 의미
    //아래의 로직을 스프링 빈에 등록 - 멤버 서비스가 스프링 빈에 등록
    @Bean
    public MemberService memberService(){
        //멤버서비스는 생성자라서 멤버리퍼지토리 인스턴스가 들어와야 하며,
        //MemoryMemberRepository()를 받은 memberRepository()를 넣어줌
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        //DB랑 연결
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

 

JPA를 사용하도록 위와 같이 스프링 설정도 변경해준다.

DataSource 대신에 EntityManager 사용

 

스프링 데이터 JPA


스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어든다.


여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다.


스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이기 때문에 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 함

 

스프링 부트 설정은 앞의 JPA 설정을 그대로 사용하면 됨

 

//<엔티티 타입의 클래스, pk의 타입>
public interface SpringDataJpaRepository  extends JpaRepository<Member, Long>, MemberRepository {
    //이렇게 인터페이스만 있으면 스프링 데이터 JPA가 리포지토리를 갖고 있으면 구현체를 자동으로 만들고, 스프링 빈에 자동 등록
    //우리는 이렇게 만들어진 구현체를 그냥 가져다 쓰면 됨

    @Override
    Optional<Member> findByName(String name);
}

스프링 데이터 JPA 회원 리포지토리를 위와 같이 인터페이스 형태로 생성해줌

위의 내용이 현재 기능에서 리포지토리에 필요한 모든 내용의 끝

 

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

 

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

 

 

스프링 데이터 JPA 제공 기능

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

 

위의 그림을 보면 대략적으로 어떤 기능들이 제공되고 있는지 확인할 수 있음

 

하지만 공통 기능은 개별적인 개발 환경에서 똑같이 제공되지 못하기 때문에 이런 경우에 사용하는 것이 바로 아래와 같은 것

 

@Override
Optional<Member> findByName(String name);

인터페이스 아래에 이렇게 하면 By다음에 있는 것을 읽어서

 

select m from member m where m.name=?

이러한 jpql 형태로 자동으로 변환해서 조회하게 됨.

이름을 설정하는 방법도 여러가지가 있으며 따라서 단순한 쿼리의 경우에는 인터페이스 이름을 설정하는 것 만으로도 쿼리 설정이 끝나게 됨

 

 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. 

 Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적쿼리도 편리하게 작성할 수 있다. 

 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.

728x90