백/spring

김영한T 스프링 입문강의 - [섹션6] 스프링 DB 접근 기술 (JDBC, Jdbc Template)

복지희 2023. 11. 19. 01:00

실제 db와 연동을 해보기 위해, h2 db를 설치해주었다.

https://www.h2database.com/html/download-archive.html

 

Archive Downloads

 

www.h2database.com

최근에 나온 버전은 일부기능이 정상작동하지 않는다고 하여, 1.4.200 버전을 다운받아주었다.

 

설치 후, h2 콘솔에 들어간 후에

연결 버튼을 눌렀을 때 에러없이 정상적으로 들어가지고, 본인의 홈디렉토리에 test.mv 파일이 생겼다면 성공이다.

그 이후부터는 JDBC URL 자리에 jdbc:h2:tcp://localhost/~/test 이렇게 접속해준다.

 

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

 

들어가서 table을 생성하는 sql문을 작성한다.

Member 테이블이 생긴것을 확인할 수 있다.

 

 

 


<순수 JDBC>

스프링과 db를 연결해주기 위해서 적어주어야하는 코드들이다.

build.gradle 안의 dependencies에 추가해준다.

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

 

다음은 application.properties에 추가해준다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

원래는 비밀번호 등을 적어주는데, h2 db는 생략한다.

 

예전에 JDBC API로 직접 코딩하였었다. 순수 JDBC는 지금은 사용하지 않는 방법이지만, 이해를 위해 정리하고 넘어간다.

MemberRepository를 상속받는 JdbcMemberRepository 클래스를 만든 후에(생략),

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

이렇게 기존에 적었던 Configuration 코드를

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository(){
        return new JdbcMemberRepository();
    }
}

이렇게 저장소를 JdbcMemberRepository를 반환하게만 바꿔주면 설정이 끝난다.

이게 자바의 매우 큰 장점!!

 

작성을 마친 후 스프링 테스트를 진행할 때, 테스트를 진행할 때마다 db에 정보가 쌓이게 된다.

그니까, 이미 db에 "spring" 이름의 회원이 저장되어있다면, 다음 테스트시 "spring"이름으로 저장하지 못하게 된다.

그래서 @AfterEach를 사용해서 deleteAll()을 만들고...하는 작업을 해도...괜찮지만

스프링이 제공해주는 기능이 있다!

@Transactional 애노테이션을 테스트케이스에 달면, 테스트를 끝내면 db에 넣었던 데이터들이 깔끔하게 지워준다.

이렇게 하면 동일한 테스트를 여러번 실행할 수 있다!

@SpringBootTest
@Transactional
public 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();
        Assertions.assertThat(member.getName()).isEqualTo(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 = org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,
                ()->memberService.join(member2));
        //예외 발생해야함

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

통합 테스트코드다.

 

 

 


<Jdbc Template>

JdbcTemplate 는 Jdbc Api의 반복코드를 대부분 제거해주지만, SQL은 직접 작성해야한다.

순수 jdbc와 동일한 환경설정을 해준다.

저장소 클래스를 하나 만들어주고,

public class JdbcTemplateMemberRepository implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    ...

JdbcTemplate은 DataSource를 필요로 한다. DataSource는 스프링 빈으로 등록되어 있어야 한다. 

@Configuration
public class SpringConfig {
    private final DataSource dataSource;

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

그래서 이렇게 스프링빈에 넣어주고,

 

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());
        //결과를 memberRowMapper로 묶어서 반환
        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;
            }
        };
    }
}
    @Bean
    public MemberRepository memberRepository(){
//        return new MemoryMemberRepository();
        return new JdbcTemplateMemberRepository(dataSource);
    }

Configuration에서 다시 조립해준다.