목차
- H2 데이터베이스 설치
- 데이터 베이스 접근 기술 (차례대로 4가지)
- 순수 Jdbc : (20년 전 쯤..)
- 스프링 JdbcTemplate : 스프링에서 Jdbc API에서의 반복 코드를 대부분 제거해줌(sql은 직접 작성필요)
- JPA : 개발자가 직접 쿼리를 짜지 않아도 데이터베이스를 조작할 수 있게됨 (10년전쯤..)
- 스프링 데이터 JPA : 스프링에서 JPA를 편리하게 쓸 수 있도록 개발된 기술
이제 드디어 임시로 뒀던 메모리 레포지토리를 진짜 데이터베이스로 갈아끼울 차례다
4가지 방법으로 알려주시는데, 그냥..거의 역사 발전한 수준..
실제 sql쿼리를 날려 데이터베이스에 접근해 데이터를 넣고, 삭제하고, 조회하고 등등..
순수 Jdbc
순수 jdbc는 정리에서는 생략.. 코드는 설명하지 않고 핵심만 정리하기로 함
일단 MemoryMemberRepository와 하는 일은 같지만, 이제 Jdbc를 이용하여, 이를 우리가 생성한 데이터베이스로 저장하고, 불러오는
JdbcMemberRepository 클래스를 생성하였다. (Memory..)와 하는일은 같다고 생각하면됨
중요한 것은 이 부분인데, 다른 자바코드(MemberService등)를 하나도 건들이지 않고, MemberRepository 빈을 설정해돈
SpringConfig에서 MemeoryMemeberRepository를 JdbcMemberRepository로 변경하기만하면,
우리가 원하는대로 기능하면서 메모리가 아닌 데이터베이스로의 삽입,조회가 가능해진다는 사실이다.
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
//return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
*조립부분만 변경함
이는 자바(객체지향)의 다형성이라는 개념과 연관된 개념으로
인터페이스를 두고 구현체는 바꿔끼는 방식으로 설계했기 때문에 이런 내용이 가능해진다.
MemberRepository라는 데이터저장소에 접근해서 특정 일을 하는 것을 인터페이스로 구현해놓고,
실제 구현체는 여러가지 방식으로 구현해두면, 실제 조립부분만 변경하면 실제 애플리케이션 코드부분에서는 하나도 수정하지 않아도 된다는 큰 강점을 가진다.
스프링 컨테이너에서 기존에는 memory와 연결해두었지만, 이를 jdbc로 변경해주면서 구현체만 변경되어 이로 작동하는 것이다.
이를 개방-패쇄 원칙이라고 한다.
스프링 JdbcTemplate
스프링 JdbcTemplate과 MyBatis같은 라이브러리는 JDBC API에서 반복적으로 사용되는 코드의 대부분을 제거해준다.
하지만 여전히 SQL문은 직접 작성해야한다.
여기도 안하려고 했으나, 실무에서 많이 사용된다고 한다. 환경설정은 순수 jdbc와 동일
환경설정
build.gradle에 아래 2줄을 추가해준다.
java는 db와 붙으려면 jdbc드라이버가 꼭필요하고,
dependencies { //다운 받은 라이브러리 들
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
}
새로고침을 꼭 해주어야 한다. (build gradle 상에 코끼리 모양같이 생긴 새로고침있음)
src>main>resources>application.properties 에 db접속정보를 입력해준다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
mysql등을 해본 사람은 알 것.. ID,비번은 h2에서는 생략해도 된다고한다.
JdbcTemplateMemberRepository생성
이제 MemoryMemberRepository를 대체하여 JdbcTemplate을 이용한 맴버 레포지토리를 생성해준다.
Repository하위에 다음과 같이 생성해준다.
JdbcTemplateMemberRepository
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import javax.xml.crypto.Data;
import java.util.List;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;//jdbcTemplate사용을 위해 변수선언
//생성자, (사실 생성자가 1개일 경우 Autowired생략이 가능하다!)
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {//spring이 datasource를 자동으로 주입해줌
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
당연히 MemberRepository 인터페이스를 상속받아 함수들을 Override해야하며,
jdbc는 datasource가 요구되므로 이를 생성자에서 넣어주어야 한다.
조회(findById) 구현
@Override
public Optional<Member> findById(Long id) {
//(실행할 쿼리문, 결과를 저장할 wrapper )
List<Member> result = jdbcTemplate.query("select * from member where id = ?",memberRowMapper());
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;
};
}
}
jdbcTemplate.query를 이용하여, 연결된 db로 작성한 쿼리문을 넘기고 결과를 받아온다.
.query("sql문", 결과를 저장할 mapper, sql문에 들어갈 변수) 3개의 파라미터로 구성되어 있다.
구현한 Mapper가 Member List형식을 리턴하기 때문에 결과를 MemberList에 저장해준다.
List<Member> result = jdbcTemplate.query("select * from member where id = ?",memberRowMapper(),id);
위의 코드는 다음 코드를 람다식으로 전환한 것이다.
쿼리의 결과를 ResultSet이라는 자료형으로 받아온다.
멤버객체를 하나 생성한 후 해당 멤버객체에 결과값에서 넘어온 rs에 접근하여 id와 name을 저장한 후 리턴한다.
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;
}
}
findAll과 findByName도 로직은 유사하다.
Name은 id대신 쿼리문에 name을 넣어서 검색하면되고, All은 별도의 변수 없이 그냥 전부를 검색하기 때문에 다음과 같이 작성해서
동일하게 만들어둔 mapper를 사용하면 된다.
@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());
}
회원 가입 구현
@Override
public Member save(Member member) { //document참고
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
//insert 문을 자동으로 만들어줌
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
save(회원 가입)기능은 JdbcTemplate의 documentation을 보고 활용한 것이기 때문에 자료를 살펴보는 것이 좋다.
통합테스트
통합테스트는 DB, 스프링 컨테이너 까지 사용하여 테스트해보는 것으로 속도가 단위테스트에 비해서는 느리다.
단위테스트는 우리가 이전에 작성했던 MemberSerivceTest로 순수자바코드로 짜여진 테스트를 말한다.
통합테스트보다는 단위테스트를 잘 짜는 것이 중요하다고 한다.
@Transactianal 커밋하지않고 테스트끝나면 롤백해서 디비에 반영되지 않게하면서 테스트를끝까지 돌릴수 있또록함 (데베에 들어가면 다음ㅌㅔ스트에 이미인쓴 데이터라에러나니깐)
test>java>service하위에
MemberSerivceIntegrationTest 를 만들어준다.
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 {
//test에서는 단순 테스트기 때문에 편하게 autowired사용해도 문제 없음
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository; //MemberRepository 인터페이스로 변경해두면, config해둔 구현체로 가져옴
@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("이미 존재하는 회원입니다.");
}
}
기본적인 내용은 MemberSerivceTest와 동일하다.
달라진 점은
1. @SpringBootTest annotation을 달아주어야만 한다는 사실과
2. 메모리를 비우는 코드가 사라졌다는 사실이다.
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
이는 사라졌다기 보다는 @Transactional로 그 기능이 대체 될 수 있다.
Transactioanl을 이용하면, 데이터베이스에 sql을 commit하지 않고 rollback하여, 실제 db에는 데이터가 들어가거나 업데이트되지 않는다. (변경사항이 없음)
그래서 단순히 데이터베이스를 비우거나 해줄 필요 없이 연속으로 테스트를 실행해 볼 수 있다.
*테스트를 해볼 때는 당연히 서비스 할 데이터베이스가 아닌 새로 데이터베이스를 만들어 실수로 삭제하거나 변경되는 일이 없도록 해야한다!
'Backend > Spring' 카테고리의 다른 글
[Spring] AOP (0) | 2021.09.14 |
---|---|
[Spring] db접근기술(JPA, Spring JPA) (0) | 2021.09.14 |
[Spring] DB설치(h2 데이터베이스) (0) | 2021.09.14 |
[Spring] 웹 MVC개발 (0) | 2021.09.13 |
[Spring] Spring Bean2 (0) | 2021.09.10 |