[스프링 입문] Section 6-2. 스프링 DB 접근 기술(2)

 

본 포스팅의 내용은

인프런 김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 기반으로 작성했습니다.

 
 

🌱 스프링 JdbcTemplate

순수 JDBC와 동일한 환경설정을 하면 된다.

스프링 JdbcTemplate과 MyBatis와 같은 라이브러리는 순수 JDBC  API에서 본 반복 코드(Connection, ResultSet 등...)를 대부분 제거한다. 하지만 SQL은 직접 작성해야 한다. 

 

순수 JDBC와 다르게 DataSource를 직접 사용하는 것이 아닌, DataSource를 감싼 JdbcTemplate 인스턴스를 사용해서 DB에 접근한다.

 

Insert시 테이블과 컬럼명 그리고 값만 넘겨주면 SimpleJdbcInsert가 Insert 쿼리를 자동으로 생성한다. 그 외에  조회는 쿼리를 직접 작성해야 하고 RowMapper를 정의해서 DB의 데이터를 자바 객체로 매핑해야 한다. 

 

JdbcTemplateMemberRepository

public class JdbcTemplateMemberRepository implements MemberRepository{
    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        // insert할 테이블과 auto generated key 명시
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        // key - value
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        // 테이블 이름과 컬럼 이름과 값만 있으면 insert문을 만들 수 있듯, 필요한 정보만 SimpleJdbcInsert에 넘겨준다.
        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();
    }
    ```
}

SpringConfig

@Configuration
public class SpringConfig {
    private final DataSource dataSource;

    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}

 

🍃 JPA

JPA란 자바 ORM 기술에 대한 API 표준 명세를 의미한다.

JPA는 ORM을 사용하기 위한 인터페이스를 모아둔 것으로, JPA를 사용하기 위해선 JPA를 구현한 Hibernate, EclipseLink, DataNucleus 같은 ORM 프레임워크를 사용해야 한다.

 

ORM이란 Object Relational Mapping의 약자로 객체와 DB의 테이블이 매핑을 이루는 것을 말한다.

즉, 클래스가 테이블이 되도록 매핑 시켜주는 것으로 ORM을 이용하면 SQL 쿼리가 아닌 코드로 데이터를 조작할 수 있다.

 

예를 들어, User 테이블의 데이터를 출력하기 위해선 SELECT * FROM User; 쿼리를 날려야 하지만

ORM을 사용하면 findAll() 메서드 호출로 데이터 조회가 가능하다.

 

쿼리를 직접 작성하지 않고 메서드 호출만으로 쿼리가 수행되다 보니, ORM을 사용하면 생산성이 매우 높아진다.

하지만 쿼리가 복잡해지면 ORM으로 표현하는 데 한계가 있고, 성능이 로우 쿼리에 비해 느리다는 단점이 있다.

그래서 JPQL, QueryDSL 등을 사용하거나 한 프로젝트 내에서 Mybatis와 JPA를 같이 사용하기도 한다.

JPA 참고 블로그

 

 

JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행한다. 또한 JPA를 사용하면 SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.

 

 

build.gradle에 JPA 관련 라이브러리 추가

dependencies {
   ```
   implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
   ```
}

application.properties에 JPA 설정 추가

# JPA가 생성하는 SQL을 출력
spring.jpa.show-sql=true 
# JPA는 테이블을 자동 생성하는 기능을 제공한다. none : 기능 끄기
spring.jpa.hibernate.ddl-auto=none

 

DB의 Member 테이블과 매핑될 Member 클래스에 @Entity 어노테이션을 붙이면 JPA가 관리하는 Entity(객체)가 된다.

@Data
// @Entity : JPA가 관리하는 Entity
@Entity
public class Member {
    // @Id : PK 매핑
    // @GeneratedValue : Identity 전략 매핑 (sequence 같은 컬림)
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

 

JPA는 모든 것인 EntityManager로 동작한다. gradle에 JPA 라이브러리를 추가하면 스프링 부트가 자동으로 EntityManager를 빈으로 등록해서 관리한다.

 

findByName처럼 PK가 아닌 컬럼으로 조회하고 싶다면 객체 기반의 쿼리인 JPQL을 사용해야 한다.

public class JpaMemberRepository implements MemberRepository {
    /**
     * JPA는 EntityManager로 모든것이 동작함
     * <p>
     * build.gradle에 data-jpa 라이브러리를 추가하면 spring boot가 자동으로 EntityManager를 빈으로 관리함
     */
    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) {
        Member member = em.find(Member.class, id);

        return Optional.ofNullable(member);
    }

    @Override
    public Optional<Member> findByName(String name) {
        List<Member> members = em.createQuery("select m from Member as m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();

        return members.stream().findAny();
    } 
    ```
}

 

참고로 JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다. 따라서 Service에 @Transactional 어노테이션을 반드시 추가해야 한다.

 

🖥️ 스프링 데이터 JPA

스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고 개발할 코드도 확연히 줄어든다.

여기에 더해 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.

 

지금까지 반복 개발해온 기본 CRUD 기능을 스프링 데이터 JPA가 모두 제공하므로 개발자는 핵심 비즈니스 로직을 개발하는데 집중할 수 있다.

 

설정은 JPA 설정을 그대로 유지하면 된다.

 

 

개발자는 아래와 같이 인터페이스만 정의하면 된다.

인터페이스만 있는데 구현체는 어디에 있는가?

만약 인터페이스가 JpaRepository를 상속받고 있으면 스프링 데이터 JPA가 자동으로 구현체를 만들고 빈에 등록한다.

 

스프링 데이터 JPA가 기본적은 CRUD 메서드는 제공하지만 findByName과 같은 특수한 메서드는 어떻게 할까?

메서드 이름에 규칙이 있다. findBy(컬럼명)으로 추상 메서드를 선언하면 스프링 데이터 JPA가 JPQL select m from Member as m where m.name = :name을 자동으로 생성하고 SQL로 번역돼서 쿼리문이 실행된다.

 

SpringDataJpaMemeberRepository

/**
 * JpaRepository<T, ID> : T는 entity, ID는 entity의 PK 타입
 */
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    @Override
    Optional<Member> findByName(String name);
}

SpringConfig

@Configuration
public class SpringConfig {
    private final MemberRepository memberRepository;

    public SpringConfig(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로도 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.