Spring에서 DB에 접근하는 방법은 여러 개가 있는데, 전에 봤던 순수 JDBC를 쓸 수도 있지만, JdbcTemplate이나 JPA, 스프링 데이터 JPA를 이용하는 방법도 있다. 각각의 차이는 다음과 같다.
- JDBCTemplate을 이용하면 반복된 코드가 많이 줄어들지만, SQL을 직접 작성해야 한다.
- JPA를 이용하면 기본적인 CRUD하는 데에 내가 쿼리를 직접 작성할 필요가 없다. (select 할 때만 JPQL을 짜야 한다)
- 스프링 데이터 JPA까지 오니까 아예 구현 클래스를 작성할 필요 없이 인터페이스만으로 개발이 끝나 버렸다.
웹 MVC, 데이터 접근 기술, ORM 기술도 되게 중요하다.
1. JDBCTemplate 이용하는 법
다음의 코드를 JdbcTemplateMemberRepository 자바 클래스로 생성해준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
//@Autowired 생성자가 딱 하나만 존재하고, spring bean으로 등록되어 있는 거라면 autowired는 생략해도 된다.
public JdbcTemplateMemberRepository(DataSource dataSource) {
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(), 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;
};
}
}
스프링 JdbcTemplate이나 MyBatis 같은 라이브러리는 중복 코드를 줄여주게 된다. (여전히 SQL문은 사용된다)
위 코드처럼 작성해준 후에는 Spring container에 등록한 내용을 바꿔끼워주면 된다.
@Configuration
public class SpringConfig {
...
@Bean
public MemberRepository memberRepository(){
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
2. JPA 이용하는 방법
JPA를 이용하기 위해서는 build.gradle에 관련 라이브러리를 추가해야 한다. (starter-data-jpa 라이브러리 추가함) spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함하기 때문에 jdbc는 제거해도 된다.
dependencies {
implementation 'org.springframework.boot:spring-boot-devtools'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' //template engine
implementation 'org.springframework.boot:spring-boot-starter-web' //web 버전으로 선택했던 사항
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
그리고 나서 application.properties에서 스프링부트에 JPA 설정을 추가해주어야 한다.
...
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
이후 domain/member.java에서 JPA 엔티티 매핑을 해준다.
package hello.hellospring.domain;
import javax.persistence.*;
//JPA는 자바 표준 ORM(객체와 관계형 DB를 매핑하는 것) 인터페이스, hibernate 등이 구현체로 각 업체에서 만든 것들.
@Entity //JPA가 관리하는 entity가 되는 것
public class Member {
@Id //pk 값이라는 뜻
@GeneratedValue(strategy = GenerationType.IDENTITY) //DB가 알아서 자동생성 해주는 것을 IDENTITY라고 함
private Long id;
//@Column(name = "username") //DB column의 username과 매핑
private String name;
public Long getId() {
...
@Entity 어노테이션을 통해서 JPA가 관리하는 entity로 등록하고, pk 값인 id에 @Id를 이용해서 명시해준 후, @GeneratedValue(strategy = GenerationType.IDENTITY) 를 통해서 데이터가 추가될 때마다 자동으로 하나씩 올려서 저장되도록 해준다. 또, DB 테이블에 세팅된 이름이랑 다른 컬럼의 경우는 @Column(name="")를 통해서 매핑해줄 수도 있다.
※Entity란?
테이블과 매칭되는 개념으로, ORM을 이루는 기반 개념 중 하나이다. 객체와 RDB 간에 연결시켜주는 존재이다.
JpaMemberRepository 자바 클래스를 생성해서 기능을 만들어 준다.
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; //jpa 라이브러리 추가 시 스프링 부트가 Entity Manager 이라는 것을 자동 생성.따라서 우리는 만들어진 것을 injection 받으면 된다.
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member); //member를 영구 저장
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);//find할 대상이 되는 type과 식별자 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(); //객체(정확히는 entity) 대상으로 쿼리를 날림. (jpql) -> Member entity를 조회해 (as m) + member 그 자체를 select
}
}
JPA를 사용하면 EntityManager이라는 것의 의존성을 주입시켜주어야 한다. 엔티티 매니저는 관계형 DB의 엔티티를 관리하고 검색할 수 있도록 하는 데 사용된다. Entity instance의 수명 주기를 관리하는 API이다. JPA 라이브러리를 추가하면 스프링에서는 자동으로 EntityManager를 생성하게 되므로, 우리는 이것의 의존성만 주입시켜주면 된다. 이후에 JPA를 통해서 DB를 찾는 기능들을 정의해주면 되는데, 이 때 SQL이 아닌 JPQL을 이용하게 된다.
서비스 계층에 트랜잭션도 추가해준다.
...
import org.springframework.transaction.annotation.Transactional
...
@Transactional
public class MemberService {...}
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다. JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
그리고 마지막으로 스프링 컨테이너에서 JpaMemberRepository로 바꿔끼면 된다.
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class SpringConfig {
private final EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
//return new JdbcMemberRepository(dataSource);
//return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
3. 스프링 데이터 JPA
스프링 부트 + JPA의 조합만으로도 개발 생산성이 늘고 코드의 길이도 줄어들게 된다. 여기에 Spring Data JPA까지 사용하면 리포지토리에 구현 클래스 없이 인터페이스 만으로도 개발을 완료할 수 있다. 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공되기 때문에, 간편하고 짧게 정리된 코드로 개발이 가능하다. 따라서 개발자는 핵심 비즈니스 로직을 개발하는 데 집중할 수 있게 된다. 하지만, 데이터 JPA는 JPA를 기반으로 한 기술이기 때문에 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 공부하는 것이 좋다.
SpringDataJpaMemberRepository라는 인터페이스를 생성해서 다음 코드를 적어준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
//JPQL로 'select m from Member m where m.name = ?'을 짜게 되고 이를 SQL로 돌리게 됨.
@Override
Optional<Member> findByName(String name);
}
* 인터페이스가 인터페이스를 상속받을 때는 implements가 아닌 extends를 사용하며, interface는 다중 상속이 가능하다.
**JPA에는 JpaRepository<엔티티 이름, 엔티티의 pk 컬럼명>을 상속받아야 한다.
***JpaRepository 안에는 이미 findAll 이나 save 메소드 등이 정의되어 있기 때문에 별도로 정의해 줄 필요 없이 사용이 가능하다.
**** Optional<Member> findByNameAndId(String name, Long id); 와 같은 형태로 인터페이스를 정의해주면 찾을 항목을 커스텀하여 찾아줄 수 있다. 인터페이스에 선언된 이름 만으로도 기능을 생성할 수 있다. And 말고도 Or등을 사용할 수도 있다.
***** Spring data JPA가 JpaRepository를 상속받은 SpringDataJpaMemberRepository가 존재하는 것을 확인하면 자동으로 Bean에 등록해주기 때문에 편리하게 사용이 가능하다.
이후에 스프링 데이터 JPA를 사용하도록 스프링 설정을 변경해주면 된다.
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
앞서 말한 것처럼 스프링 데이터 JPA가 SpringDataJpaMemberRepository를 컨테이너에 빈으로 자동 등록해주기 때문에 repository를 바꿔끼는 코드를 생략해줘도 된다.
스프링 데이터 JPA는 다음과 같이 다양한 기능을 제공한다. CRUD 뿐만 아니라 PagingAndSortingRepository, JpaRepository 관련한 기능들도 있다. findByName() , findByEmail() 처럼 인터페이스 내에 정의된 메서드 이름 만으로도 조회가 가능한 기능들을 제공하고 있다.
당연히 JPA와 스프링 데이터 JPA만으로 해결할 수 없는 쿼리들이 존재한다. 복잡한 동적 쿼리의 경우에는 Querydsl이라는 라이브러리를 사용할 수 있다. 이 조합으로도 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, JdbcTemplate를 사용하면 된다.
'TIL > Java | Spring Boot' 카테고리의 다른 글
[Spring Boot] IoC, DI, 그리고 컨테이너 (0) | 2022.01.21 |
---|---|
[Spring Boot] AOP (0) | 2022.01.20 |
[Spring Boot] 스프링 DB 접근 기술 (0) | 2022.01.14 |
[Spring Boot] 회원 관리 예제 - 웹 MVC 개발 (0) | 2022.01.11 |
[Spring Boot] 스프링 빈과 의존관계 (0) | 2022.01.09 |
댓글