BackEnd/Spring

[Spring] 스프링 DB 접근 기술

샤아이인 2022. 1. 14.

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

섹션 6. 스프링 DB 접근 기술

1, 2 단원의 JDBC 노가다 방식은 생략하였습니다. 저도 예전에 JDBC 써써 손수 다 해본적 있으니.. 정리는 생략하는걸로..

 

1. 스프링 통합 테스트

이번시간에는 전체적인 Spring 코드를 테스트 하는 시간을 갖었다. 우선 코드를 살펴보자.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
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.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void join() {
        //given
        Member member = new Member();
        member.setName("hello");

        //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("이미 존재하는 회원입니다.");
    }
}
 

@Transactional

가장 중요하게 배운 내용은 @Transactional 이다. 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

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

 

여기서 한가지 신기한점! 위의 코드는 MemberRepository 를 주석처리 하여도 정상 실행된다. 다음과 같이 말이다.

    @Autowired MemberService memberService;
//  @Autowired MemberRepository memberRepository;
 

MemberService를 Autowired할 수 있다는 건 MemberService 빈이 이미 생성되어 있다는 거고

MemberService 빈이 생성되려면 MemberService 생성자의 매개변수로 받을 MemberRepository가 필요하기 때문에 MemberRepository 빈도 이미 생성되어 있다는 거니

결국 MemberRepository 빈을 전달하면서 MemberService 빈을 생성했기 때문에 이후부터는 필드 주입이든 생성자 주입이든 MemberService를 Autowired하기 위해 MemberRepository가 함께 쓰이지 않아도 되는 것 입니다!

 

2. 스프링 Jdbc template

생성자가 딱 1개이면 @Autowired 는 생략할 수 있다.

 

기존의 jdbc를 사용할때는 항상 동일하게 작성하는 중복되는 부분이 너무 많다. 가령, connection 얻어오고, 거기에 stmt 얻고, 쿼리 날리고 등등... 이와같이 중복된 작업을 제거하기위해 나온것이 Jdbc template이다.

 

주로 템플릿 메소드 페턴을 사용하여 작성된 라이브러리라 하셨다. 디자인 패턴을 공부해둔 덕에 대충 어떨것 같다는 느낌은 온다?

// import 생략

public class JdbcTemplateMemberRepository implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;

    @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");
        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;
        };
    }
}
 
 

3. JPA

이번시간에는 JPA를 사용해보는 시간이였다. JPA는 ORM기술로 객체와 관계형데이터베이스를 mapping시키는 기술이다.

 

우선 mapping된 domain을 확인해 보자. @Entity 로 맵핑하였다.

IDENTITY는 정보가 추가될때마다 자동으로 1씩 증가하는 pk열을 지정할때 사용한다.

@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 레포지토리를 생성해 주는데, EntityManager를 새로 만들게 된다.

EntityManager 는 스프링이 생성해주며, 데이터베이스 커낵션 정보랑 여러 정보를 자동으로 모아서 만들어진다. 내부적으로 datasource 를 다 갖고있다.

 

JPA는 인터페이스 이며, Hibernate가 구현체 이다.

 

다음은 JPA로 구현한 레포지토리 이다. 위에 Transacntional이 있는데 JPA를 통항 모든 데이터의 변경은 트렌젝션 안에서 이루어져야 한다.

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

위의 코드에서 findAll()의 구현부가 조금 다르다.

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
 

"select m from Member m" 은 JPQL 이라는 쿼리문인데 객체를 대상으로 쿼리를 날린다. 그럼 이게 SQL로 번역이 된다.

from 부분에 Member m 은 Member as m 으로 별명을 붙인것 이다.

select 부분에서 m.id 나 m.name 과 같이 테이블의 열 속성값이 아닌, 객체 자체 m을 넣어준다는 특징이 있다.

 

4. 스프링 데이터 JPA

이번에는 class가 아닌 interface를 구현하였다. 다음 코드를 살펴보자.

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

스프링 data jpa 가 JpaRepository를 받고 있으면 구현체를 알아서 만들어 빈에 자동으로 등록시켜준다.

 

이후 config에 등록하여 그냥 사용하면 끝이다. injection 받아 사용하면 된다.

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

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

스프링 컨테이너에서 MemberRepository 를 찾는다. 하지만 구현해논 MemberRepository 가 없는 상황이다.

위에서 말했듯 SpringDataJpaMemberRepository 의 구현체는 알아서 구현되었기 때문이다.

 

다음 그림을 살펴보자.

JpaRepository를 보면 이미 기본적으로 사용할수 있는 기능들의 선언부가 다 있다.

따라서 기본적인 기능의 interface를 전부 상속받는 것 이다. 이에 따른 구현체는 알아서 만들어 bean에 등록시켜 준다!

 

이외 조금은 특별한, 공통적으로 인터페이스 에 구현하지 못하는 기능들 예를 들어 findBySchool과 같이 학교를 기준으로 찾는다 거나 하는 기능은 추가적으로 적어주면 끝이다.

 

위에서는 Optional<Member> findByName(String name); 을 추가해주었다.

댓글