해당 글은 DEPth IT 연합 프로젝트 동아리 활동의 일환인 서버 스터디에 관련되어 있습니다.🐧
Spring JDBC Template
spring JDBC Template은 순수 JDBC와 설정은 동일합니다. 하지만 JDBC API에서의 반복 코드를 효율적으로 줄여서 코드의 줄을 대폭 줄였습니다. 이때 JDBC Template은 템플릿 메소드 패턴을 주로 사용해서 반복 코드를 줄일 수 있었습니다.
⚙️ 템플릿 메서드 패턴
여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여서 상위 클래스에서 정의하고, 하위 클래스마다 자세한 세부 동작을 다르게 구현하는 패턴입니다. 상속이라는 기술을 극대화 하여, 알고리즘의 뼈대를 맞추는데 초점을 맞춥니다.
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
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 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) -> { //자바 8람다 스타일
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
실제로 코드를 보게 되면 이전에 Jdbc API를 사용할 때와 비교하면 코드의 양이 매우 적어진 것을 볼 수 있습니다. 하지만 아직까지는 SQL문은 직접 작성해야합니다. 해당 JdbcTemplate을 사용할 때는 JdbcTemplate 인스턴스를 사용해서 DataSource를 사용해야 합니다.
또한 위의 코드를 보면 save 메서드에서 insert 쿼리를 자동으로 생성하는 것을 볼 수 있습니다. PK를 이용해서 insert 쿼리를 자동으로 생성합니다.
해당 JdbcTemplateMemberRepository도 설정에서 바꾸어서 바로 다른 코드의 수정 없이 사용할 수 있습니다. 또한 이전에 만든 통합테스트도 사용할 수 있습니다.
JPA
JPA는 Jdbc Template에서 SQL을 작성해야하는 부분까지 JPA에서 담당합니다. 따라서 개발자는 SQL에서 객체 중심의 설계로 패러다임을 전환할 수 있습니다. 따라서 해당 기술을 사용하면 생산성을 높일 수 있습니다.
먼저 JPA를 사용하기 위해서 build.gradled에서 라이브러리를 추가해 줍니다. 아래 코드를 추가해 줍니다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
해당 코드는 JPA와 Jdbc 모두 포함하는 라이브러리입니다. 따라서 이전에 Jdbc를 사용하기 위해서 넣었던 라이브러리는 삭제를 해도 됩니다.
이후에는 application.properties에 JPA와 관련된 설정을 추가합니다. 코드는 아래와 같습니다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
순서대로 JPA에서 만드는 SQL문을 볼 수 있게 해주는 설정과 JPA는 엔티티들을 보고 자동으로 테이블 또한 만드는 기능이 있습니다. 하지만 이미 DB에 테이블이 만들어져 있으며 해당 DB를 사용할 것이기 때문에 auto=none으로 하여서 해당 기능을 꺼주는 설정을 넣습니다.
ORM
ORM = Object Relational Mapping으로 어플리케이션과 DB의 연결시에 SQL 언어가 아니라 개발 언어로 데이터베이스를 접근할 수 있게 해주는 툴입니다. 따라서 직관적인 이해를 돕고 개발 생산성이 증진됩니다.
JPA는 이러한 ORM을 사용하기 위한 인터페이스를 모아 놓은 것입니다. JPA를 사용하기 위해서는 JPA를 구현한 ORM 프레임워크를 사용해야합니다.
따라서 Mapping을 하기 위해서 어노테이션 @Entity를 사용합니다. 이러면 JPA가 관리하는 객체가 됩니다. 이때 PK를 함께 mapping해주어야 합니다.
🔎 @GeneratedValue(strategy = GenerationType.IDENTITY)는 DB가 자동으로 생성해주는 것을 의미합니다.
Jpa Memory Repository
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member);
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
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();
}
}
JPA에서는 그 전과는 다르게 EntityManager를 사용합니다. 스프링 부트가 현재 사용하는 DB랑 연결까지 전부 진행시킨 Entity Manager를 생성합니다. Entity Manager에 데이터 소스들이 전부 포함되어 있어 내부에서 DB와 통신합니다.
단순히 저장을 하거나 PK를 기반으로 조회를 하는 것은 간단하지만 그렇지 않은 경우에는 jpql을 사용해야합니다. JPQL은 객체지향 쿼리언어입니다. 테이블이 아니라 엔티티를 대상으로 쿼리합니다. 따라서 위 코드에서도 m이라는 Member를 나타내는 단어를 쓰고 있습니다.
또한 트랜잭션도 중요합니다. JPA는 모든 데이터의 변경이 트랜잭션 안에서 실행되어야 합니다. 따라서 MemberService 메서드에 트랜잭션 어노테이션을 추가해야 합니다.
이후 설정을 바꾸고 통합테스트에서 테스트를 진행하면 되는데 설정을 바꿀때 Source처럼 EntityManager를 받아서 사용합니다.
Spring Data JPA
개발자는 인터페이스 만을 구현하고 알맞은 인터페이스를 상속해주면 스프링 데이터 JPA가 구현해야 하는 기능들을 제공합니다. DB를 이용하는 것이기 때문에 공통적으로 사용하는 CRUD에 관한 기술 및 간단한 조회 같은 기능들은 이미 자체적으로 구현이 되어 있습니다. 따라서 인터페이스만 구현해도 가능합니다. 하지만 자체적으로 구현이 되어 있지 않거나 각 사용자마다 다른 로직인 기능들도 있습니다. 이때는 메서드의 이름, 반환값, 파라미터의 값등을 종합하여서 스프링 데이터 JPA가 JPQL문을 작성해서 처리합니다.
따라서 자동으로 구현을 하고 빈에 등록해서 관리합니다. 코드는 아래와 같습니다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}