개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
-> 개발 중 테스트할 때는 DB를 직접 이용해서 테스트하지 않는다.
-> 이때 간편하게 테스트 용도의 DB로 h2를 많이 사용한다.
https://www.h2database.com/html/download-archive.html
1. 해당 url에서 h2(1.4.200 버전을 사용)를 다운 받고 설치 프로그램을 실행
2. /H2/bin 로 이동해서 ./h2.bat을 실행 -> ./h2.bat을 종료하면 h2에 접속이 되지 않음
3. 데이터베이스 파일 생성 방법
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/test
driver-class-name: org.h2.Driver
username: sa
4. ![](https://velog.velcdn.com/images/99winnmin/post/6868b5c1-35db-4356-b11e-4392ad01a910/image.png)
## 순수 JDBC
내가 기존에 알기로도 JDBC는 굉장히 옛날에 쓰던 방식이라고 알고 있었다.
아니나 다를까 김영한 강사님도
주의! 이렇게 JDBC API로 직접 코딩하는 것은 20년 전 이야기이다. 따라서 고대 개발자들이 이렇게
고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자
라고 한다...ㅋㅋㅋ 다음코드는 옛날 방식으로 쓰던 JDBC API 방식이다.
+ JdbcMemberRepository.java
```java
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null; // 결과를 받음
try {
conn = getConnection(); // 연결
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS); // sql문장을 담음
pstmt.setString(1, member.getName());
pstmt.executeUpdate(); // 실제 sql문장이 날라가는 시점점
rs = pstmt.getGeneratedKeys(); // key 값을 리턴받음
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs); // 썻던 자원들 반환해야함
}
}
+ 그 밖에 override되는 메서드들
}
이전에는 MemoryRepo를 사용했었는데 JdbcRepo를 사용하려면 어떻해야할까?
스프링 설정을 바꾸면 된다!
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
// return new MemoryMemberRepository();
// 이전에 썻던 memoryRepo에서 Jdbc로 넘어감
return new JdbcMemberRepository(dataSource);
}
}
스프링 컨테이너와 DB까지 연결한 통합 테스트
@SpringBootTest
@Transactional // DB에 query를 날리고 반영하지 않고 rollback 시켜버림 -> DB에 데이터가 안들어감!
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("ryu99");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외(){
// given
Member member1 = new Member();
member1.setName("ryu1");
Member member2 = new Member();
member2.setName("ryu1");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
절대 그렇지 않다. db와 spring이 연동되는 테스트는 시간이 더 오래걸리기 때문에 이전에 테스트 방식을 단위 테스트라고 한다. 순수 java로만 테스트하는 방식(단위 테스트)이 더 좋은 테스트로 볼 수 있다.
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분
제거해준다. 하지만 SQL은 직접 작성해야 한다.
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired // 생성자가 단 한개면 빈으로 등록될때 @Autowired 생략해도됨
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}// spring에서 해당 스타일을 권장
@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;
};
}
}
이후 마찬가지로 SpringConfig객체에서 Repo를 바꿔주고 test를 진행하면 된다.
강사님 말에 따르면 현업에서 product 코드보다 test 코드를 짜는 비중이 훨씬 높다고 했는데 왜 그러는지 조금씩 알것같다...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
를 추가jpa:
show-sql: true
hibernate:
ddl-auto: none
다른 repo와 달리 양이 많이 줄어든 것을 볼 수 있다.
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em; // JPA는 em로 모든 것이 동작, springboot가 엔티티랑 db랑 다 연결시켜줌
// 결과적으로 생성된 em이 DB연결부터해서 이것저것 다해줌
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id); // pk인 경우에만 가능
return Optional.ofNullable(member);
}
@Override
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();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
// select m from Member m : JPQL 언어
}
}
또한 jpa는 entitiy로써 관리되기 때문에 domain 객체들을 다음과 같이 설정해줘야한다.
@Entity // JPA가 관리하는 Entity임을 명시
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // pk값 자동으로 생성해준다는 strategy 설정
private Long id;
@Column(name = "name") // DB에는 username으로 설정되있어서 매핑시켜줌
private String name;
}
스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어듭니다.
여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이
인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터
JPA가 모두 제공합니다.
스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 개발이 정말
즐거워집니다. 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어듭니다.
따라서 개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있습니다.
실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수 입니다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
// JPQL : select m from Member m where m.name = ? 을 자동으로 생성해줌
}
SpringDataJpaMemberRepository
를 스프링 빈으로 자동 등록해줌@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
findByName()
, findByEmail()
처럼 메서드 이름 만으로 조회 기능 제공참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
@Component // 여기서 스프링 빈으로 등록하든지 config에서 빈으로 등록하든지!
@Aspect // AOP로 등록
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))") // hellospring 하위에 전부 적용하겠다
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
System.out.println("START: "+joinPoint.toString());
try{
return joinPoint.proceed();
}finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: "+joinPoint.toString()+" "+timeMs + "ms");
}
}
}
해결
프록시란 코드를 복제해서 흉내내는 느낌! 처음에 실제 memberService가 실행되는게 아니라 가짜가 실행되는 것임, joinPoint.proceed가 실행되야 진짜가 실행됨
출처 : [Inflearn]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술