순수 JDBC
과거에 사용했으나 현재는 직접 코드를 하나하나 구현해야한다는 단점 때문에 사용하지 않는다.
활용방법.
build.gradle 파일에 다음 코드를 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
application.properties에는 다음 코드를 넣는다.
url정보를 스프링에게 전달해서 스프링이 데이터소스 커넥션을 해놓는다.
spring.datasource.url = jdbc:h2:tcp://localhost/~/test #h2 DB에 있는 jdbc url
spring.datasource.driver-clase-name =org.h2.Driver
spring.datasource.username=sa
이러면 스프링부트가 datasource라는 접속 정보를 우선 받아놓는다.
public class jdbcMemberRepository implements MemberRepository{
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
//dataSource.getConnection();을 통해 진짜 데이터베이스와 연결된 소켓을 얻을 수 있다.
}
...
세부구현
}
주로 Connection conn = dataSource.getConnection(); 해당 코드를 사용하여 코드를 활용한다.
마지막으로 SpringConfig에서 Bean 설정을 통해 MemberRepository의 구현체를 JdbcMemberRepository로 바꿔주자.
@Bean
public MemberRepository memberRepositoty(){
return new JdbcMemberRepository(dataSource);
}
Jdbc Template (실무에서도 자주씀)
- 순수 jdbc에서 반복적인 코드를 제거해준다.
- 물론 이 과정에서 Sql 문은 직접 작성을 해줘야한다.
public JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplate jdbcTemplate(DataSource dataSource){
jdbcTemplate = new jdbcTemplate(dataSource);
}
...
//세부 구현
}
JdbcTemplate 객체를 선언하고 해당 객체에 DataSource를 Injection 받아야한다.
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?",memberRowMapper(),id);
return result.stream().findAny();
}
위 코드는 jdbcTemplate에 다음과 같은 쿼리 문을 날리고 memberRowMapper()라는 메서드를 통해 row를 MemberList로 받는다.
result는 이미 id에 대응하는 set이므로 해당 set에서 먼저 탐색되는 순서대로 바로바로 반환해준다.
Q. 이러면 findAny()를 썼는데 객체가 하나만 반환되나?
A. id가 Pk라서 상관이 없었습니다~
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
ResultSet rs에서 id, name 을 행을 객체 Member를 변환해서 반환한다.
rs : Resultset
rowNum : 행의 개수. 해당 값만큼 작업을 반복한다.
이어서 SpringConfig에서 MemberRepository 설정을 변경해주자.
RowMapper<>란?
<>안에 들어간 객체의 row들을 반환하는 반환형 jdbcTemplate.query("쿼리문", RowMapper<> (), 인자); 형태의 코드에서 관련 쿼리의 row들을 반환하는 방식을 RowMapper를 통해 진행하는 듯하다.
즉, ResultSet을 객체로 변환하는 역할을 하는 클래스이다.
https://code-lab1.tistory.com/277
java : stream findAny 등등..
스프링 통합 테스트
순수 자바 코드가 아닌 스프링부트 테스틀 진행하기 위해선
2개의 어노테이션을 사용해주어야한다.
@SpringBootTest : 스프링 컨테이너를 활용한 테스트 (순수 자바 코드가 아닌 DI, DB연결 등의 작업을 요하는 테스트)
- 반복적인 테스트를 위해서 DB를 비워줘야한다. 테스트의 순서끼리 상관 관계가 있으면 안되고 테스트는 반복적으로 실행할 수 있어야하기 때문이다.
@Transactional : 테스트가 끝나면 roll back을 해줌. 테스트가 끝나면 끝!
즉, @AfterEach 어노테이션으로 리포지토리를 비워줄 필요가 없다.
테스트는 따로 가져다 쓸게 아니라 쓰고 말 것이기 때문에 DI를 할때 그냥 가장 편한 방법으로 진행해주면 된다.
* 가급적이면 순수한 단위 테스트, 스프링 컨테이너가 없이 테스트를 하는게 좋은 테스트일 확률이 높다. 컨테이너까지 올려서 통합테스트를 진행하는것보단 전자가 조금 더 나은 테스트라고 생각하신다고 한다.
JPA
- JdcbTemplate을 사용해도 sql은 결국 개발자가 직접 작성해야한다. 하지만 jpa를 사용하면 개발자가 sql을 작성할 필요가 없다.
- sql 보다 객체중심의 설계로 패러다임을 전환하고 개발 생산성을 향상 시킬 수 있다.
우선 build.gradle에서 아래 코드를 추가한다. 이건 jpa, jdbc 다 포함한다고 한다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
이어서 application.properties에서도 아래 설정을 추가한다.
spring.jpa.show.sql=true
spring.jpa.hibernate.ddq-auto=none
spring.jpa.show.sql=true : jpa가 날리는 sql을 볼 수 있다.
spring.jpa.hibernate.ddq-auto=none : jpa가 테이블도 직접 생성해준다. 그러나, 우리는 만들어진 테이블을 사용할 계획이기에 none으로 설정한다.
jpa는 자바 진영의 표준 인터페이스고 구현체는 hibernate이다.
우리는 수많은 구현체중 가장 대표적인 hibernate를 사용하는 것이다.
JPA 활용하기. (도메인 객체)
- jpa가 관리하는 entity 클래스에 @Entity 어노테이션을 붙여준다.
- primary key를 설정해줘야한다. 이를 설정하기 위한 코드를 작성해줘야한다.
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)//DB가 알아서 생성 : identitity
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;
}
}
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // DB가 알아서 id를 증가시키며 pk로 활용한다.
이어서 repository를 구현해줘야한다.
EntityManager 변수를 선언하고 이를 활용한다.
public class JpaMemberRepository implements MemberRepository{
//db랑 통신을 얘가 다함
private final EntityManager em;
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) {
//pk를
Member member = em.find(Member.class,id);
return Optional.ofNullable(member);
}
// 세부 구현...
}
em.persist(member) : 전달 받은 member객체를 저장하는 메소드
em.find( 타입 , 식별자 ) : 식별자로 해당 타입의 내용을 반환하는 메소드
이러면 해당 row를 조회하고 저장할 수 있다.
또한, jpa 는 jpql이라는 쿼리문을 사용해줘야할때가 있다.
언제 사용하지?
- PK기반으로 찾는것이 아닌 다른 쿼리문들에 대해서 작성을 해야한다.
즉, 여기서는 pk가 id이기 때문에 findByName()과 findAll() 은 jpql을 활용해야한다.
테이블 대상으로 쿼리를 날리는 것이 아닌 객체를 대상으로 쿼리를 날리는 것이다. 그것이 sql로 번역이 된다.
@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) //name이라는 parameter를 name으로 설정
.getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m",Member.class)
.getResultList();
}
멤버 엔티티를 조회해.
jpql은 select를 하는데 엔티티 member 자체를 select 한다.
sql은 id name,, 등을 mapping해줘야한다.
SpringConfig 변경
@Configuration
public class SpringConfig{
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em){
this.em = em;
}
//...
@Bean
public MemberRepository memberRepository(){
return new JpaMemberRepository(em);//em을 인자로 받아야한다.
}
}
EntityManager 타입을 활용해 DI를 주입 받아야한다.
번외
@Commit 어노테이션을 통해 @Transactional로 DB를 롤백시키는 과정이 아닌 DB에 정보를 반영할 수 있다.|
스프링 데이터 JPA
스프링부트, JPA를 넘어서 스프링 데이터 JPA라는 개념이 등장한다. 단순 인터페이스만으로 기본 기능을 구현할 수 있으며 CRUD와 같은 기본 기능은 애초에 스프링 데이터 JPA가 모두 담당한다.
따라서 개발 과정이 단순해지고 개발자는 핵심 비즈니스 로직에 집중할 수 있다.
단, 이 기술은 JPA를 모르는채로 사용하는 것이 아닌, 기초적인 기술을 알고난 후에 필히 사용하기를 바란다.
새로운 스프링데이터 JPA 리포지토리 interFace를 만들어주자.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>,MemberRepository {
@Override
Optional<Member> findByName(String name);
}
- Interface는 다중 상속이 가능하다. 따라서, 해당 인터페이스는 MemberRepository와 JpaRepository에 상속을 받는다.
- JpaRepository : 이는 org.springframework.data.jpa에서 제공하는 클래스로 CRUD 기본 기능을 제공한다.
실제로 위의 코드에는 save, findById(), findAll() 메소드가 존재하지 않는다. JpaRepository에서 기본으로 해당 기능을 제공하기 때문이다. 그러나, findByName(String name)은 따로 구현?선언?을 해주어야 한다.
Member 객체의 primary key가 Id이기에 findById는 자동으로 구현이 된다.
그러나 Name은 primary key가 아니므로 따로 구현이 아닌 '선언'을 해주면 된다. 그 이유는 스프링 데이터 JPA에서 해당 이름의 형식 findBy + {Name}을 따라서 자동으로 인지하고 메소드를 만들기 때문이다.
Ex : findByNameAndId(String name, Long id) 이런식의 구현도 가능하다.
SpringConfig 변경
@Configuration
public class SpringConfig{
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository);
}
}
위에서 만든 JpaRepository를 extends하는 SpringDataJpaMemberRepository 만들면 스프링 컨테이너에 구현체를 만들고 자동으로 등록이 된다.
따라서 따로 memberRepository를 Bean으로 등록하지 않고 따로 지역 변수를 선언하고 SpringConfig로 주입받는 방식으로 진행해준다.
참고 : 실무에선 주로 JPA, 스프링 데이터 JPA를 주로 사용하고 복잡한 동적쿼리는 Querydsl 라이브러리를 사용한다.
이건... 조금 어렵다. 이어서 나오는 실무 강의에서 제대로 보충해야겠다.
AOP
모든 메소드의 호출 시간, 소요시간을 확인하고 싶은 상황이라 하자.
메소드에 다음과 같은 코드를 추가해서 시간을 확인할 수 있다.
public Long join(Member member){
//같은 이름의 중복회원
long start = System.currentTimeMills();
try{
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
} finally {
long finish = System.currentTimeMills();
long timeMS = finish - start;
System.out.println(timeMS);
}
}
이런 방법으로 모든 메소드의 시간을 확인하면 그게 효율적일까? 절대 아니다.
시간을 측정하는 로직은 핵심 비즈니스 로직이 아니라 공통 관심 사항이다.
이는 유지보수도 어렵다.
따라서 이런 상황에 효율적인 로직이 있으면 좋지 않을까? 그래서 등장한 개념이 바로 AOP이다.
AOP란?
- Aspect Oriented Programming의 약자로 공통 관심 사항과 핵심 관심 사항을 분리한다.
Aop 디렉토리를 만들고 원하는 공통 기능에 대한 코드를 작성하자.
@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");
}
}
}
@Around("execution(* hello.hellospring..*(..))")
hello.hellospring.패키지 하위에선 다 적용하라
이러면 메소드 ,클래스 별로 호출이 될때마다 시간이 뜨기때문에 어디서 시간이 밀리고 있는지를 알 수 있다.
Bean 등록
- 컴포넌트 스캔
- 직접 등록
두 가지 다 가능하다. 다만 이런 특별한 경우는 일반적으로 직접 Bean을 등록하는 경우가 더 많다.
두번 등록하면 어떻게 될까?
'BE > Spring - Inflearn 김영한' 카테고리의 다른 글
스프링 핵심원리 - 기본편 섹션2 (0) | 2023.03.23 |
---|---|
스프링 핵심 원리 - 기본편 1 (0) | 2023.03.16 |
Spring 입문 인프런(무료강의) 김영한4 (0) | 2023.03.10 |
Spring 입문 인프런(무료강의) 김영한3 (0) | 2023.03.10 |
Spring 입문 인프런(무료강의) 김영한2 (0) | 2023.03.10 |