우선 목표는 MemberRepository를 구현하는 DB전용 Repository 클래스를 구현하고 대체시키는 것이다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc' //jdbc
runtimeOnly 'com.h2database:h2' //h2 DB
build.gradle에 jdbc와 db관련 라이브러리 추가 후 갱신.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.drive-class-name=org.h2.driver
spring.datasource.username=sa
resources/application.properties 에서 DB 연결관련 설정들 추가.
(커넥션 풀 사용하기 위한 것)
Servlet-JSP에서 활용한 것.
JdbcRepository에서 dataSource 객체를 통해 연결을 받아오고, pstmt 등을 통해 쿼리작성을 하여 결과를 받아오도록 구현한다.
구시대 방식이며 현재는 사용하지 않음.
순수 JDBC의 복잡한 중복 코드를 대폭 감소시켜주는 템플릿 패턴이다.
먼저 SpringConfig에서 DB 연결시 사용할 DataSource 객체를 스프링 빈으로 등록한다. 이때 @Bean을 활용하는 것이 아닌 멤버 변수로 DataSource 를 생성하고. SpringConfig 클래스의 생성자를 활용해 스프링 빈으로 등록함에 유의한다.
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
// 생성자 이용 의존성 주입.
// 생성자 하나면 Autowired 어노테이션 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {//insert
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) {//id로 찾기
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {//모든 데이터 조회
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public Optional<Member> findByName(String name) {//이름으로 찾기
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
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("select *f rom member", memberRowMapper()); 로 간단해진다.
JDBCTemplate은 반환형으로 기본형만 제공하므로. 원하는 자료형으로 결과를 도출하기 위해서 RowMapper라는 인터페이스를 사용한다.
SQL 작성은 직접 해야하지만 JDBC 사용이 훨씬 간단해진다고만 알아두자.
JPA는 JDBC-Template 처럼 반복 코드를 줄여주며, 기본적인 SQL도 JPA가 직접 만들어준다.
JPA는 ORM(Object Relational Mapping) 으로서 객체와 관계형 데이터의 엔티티를 매핑하여 사용한다.
먼저 build.gralde 에서 JPA 라이브러리를 추가한다. JPA 라이브러리에는 JDBC도 포함되어있다.
resources/application.properties 에서 jpa 설정을 추가한다.
show-sql은 JPA가 생성하는 SQL을 출력하며.
ddl-auto는 DB의 엔티티 정보를 바탕으로 테이블을 자동 생성할지 여부를 결정한다.
우리는 domain을 미리 생성해놨기에 위 기능을 끈다.
도메인에서 @Entity 어노테이션을 통해 클래스와 테이블을 매핑한다.
@Id 로 DB의 기본키 정보를 알려주고
@GeneratedValue는 값을 자동으로 설정해주도록 할 때 사용한다.
우리는 Id를 기본키로 하며. 기존값과 다르게 자동생성하게 할 것이기에 위처럼 설정하였다.
JPA를 통한 DB수정은 모두 트랜잭션 안에서 수행되야 한다.
따라서 직접 DB수정을 수행하는 Service에서 @Transactional 어노테이션을 추가한다.
설정을 마치고 엔티티와 도메인을 매핑하였다면, EntityManager 정의되며. 이를 SpringConfig에서 직접 스프링빈으로 등록한다.
JPA는 레포지토리에서 이 EM을 주입받은 후 모든 DB접근에서 EM객체를 사용한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em;
@Autowired
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);
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();
}
}
리포지토리 생성자에서 스프링빈으로 등록되어있는 EntityManager를 주입받는다.
코드를 보면 Insert, Update, Delete, 기본키 기준 Select 같은 경우는 쿼리문도 작성하지 않고 EntityManager의 내장함수를 통해 해결한다.
JPA가 쿼리를 작성해줘 매우 간편해진 모습이다.
스프링 데이터 JPA는 JPA를 보다 편하게 사용하도록 도와주는 기술이며, 라이브러리 추가. JPA 설정. 엔티티와 객체 매핑은 모두 동일하다.
부모 인터페이스를 구현할 필요없이 자식 인터페이스만 생성하면 기본 CRUD 및 여러 기능의 메서드를 자동으로 제공한다.
스프링 데이터 JPA는 부모 리포지토리 인터페이스와 JpaRepository 를 구현하는 인터페이스 레포지토리를 만들어 사용한다.
JpaRepository는 자동으로 빈으로 등록되므로. 부모 리포지토리를 빈으로 수동등록한다.
public interface SpringDataJpaMemberRepository
extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
JpaRepository<객체자료형, 키자료형> 과 부모리포지토리를 상속하는 인터페이스를 만들기만 하면 끝난다.
스프링 데이터 JPA는 findAll, save, delete 와 같은 내장함수를 기본적으로 제공한다.
이전에 한 테스트는 메모리 리포지토리를 통한 java 내부 메모리에서의 테스트이다.
@SpringBootTest 어노테이션을 추가하여 스프링부트를 얹은 테스트를 진행시킨다.
@Transactional 어노테이션을 추가하면 테스트 결과를 실제 DB에 반영하지 않고 롤백시킨다.
@Autowired로 MemberService와 MemberRepository 를 주입한다.
해당 부분은
private final MemberService memberService;
private final MemberRepository memberRepository;
@Autowired
MemberServiceIntegrationTest(MemberService memberService, MemberRepository memberRepository) {
this.memberServce = memberService;
this.memberRepository = memberRepsoitory;
}
과 동일하다.
AOP(Aspect Oriented Programming) 은 공통관심사항과 핵심비즈니스로직을 분리하기 위해 사용한다.
예를 들어 모든 비즈니스 로직의 소요시간을 출력한다 가정하자. 비즈니스로직 앞 뒤에 currentTimeMills() 로 측정해야하여 코드가 매우 지저분해질 것이다.
@Aspect
@Component
public class TimeTraceAop {
@Around("execution(* hello.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");
}
}}
따라서 공통관심사항을 AOP로 분리한다.
적용 대상을 @Around 어노테이션을 통해 설정할 수 있으며.
AOP는 객체의 실제 메서드 호출 전 후에 proxy를 주입하여 공통로직을 수행한다.