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

2022. 3. 1. 18:38·Spring

 

 

Spring 입문 (5) - 회원 관리 예제 (웹 MVC 개발)

홈 화면 추가 홈 컨트롤러 추가 (controller>HomeController) index.html 파일이 static 디렉토리에 존재하지만 컨트롤러가 우선순위가 높기 때문에 컨트롤러의 @GetMapping 메서드가 실행된다 package hello.hell..

suaring.tistory.com

지금까지 구현한 웹은 메모리에서 구현했기 때문에 서버를 재시작하면 데이터가 저장되지 않고 날아간다. 실무에서는 데이터베이스에서 데이터를 저장하고 관리한다. 스프링에서 데이터베이스에 연결하는 방법을 알아보자.

 

H2 데이터베이스 설치

  • H2 데이터베이스 : 개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공 (다운로드 링크)
  • 다운로드 후 윈도우의 경우 h2.bat 파일을 실행한다. 

  • 다음과 같은 h2 콘솔화면이 뜨면 그대로 연결 버튼을 클릭해 h2 폴더가 있는 위치에 test.mv.db 파일이 생성되었는지 확인한다.
  • 생성이 되었으면 URL에 jdbc:h2:tcp://localhost/~/test를 입력하여 접속한다.

  • 접근 후 데이터를 저장하기 위한 member 테이블을 저장한다.
  • bigint : long 타입
  • generated by default as identity : null 값이 들어오는 경우 DB가 자동으로 값을 채워주는 옵션

 

순수 jdbc

자바와 DB를 연동하기 위해 JDBC API를 이용한다. 코드가 매우 복잡하고 지금은 좋은 라이브러리들이 제공되어 잘 사용되지 않는 방법이다.

 

환경 설정

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
  • h2database : 데이터베이스 클라이언트
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

 

스프링 부트 데이터베이스 연결 설정 추가
  • resources>application.properties
  • 설정 파일에 접속 경로와 h2 드라이버를 추가해준다.
  • 스프링 2.4부터는 username을 꼭 설정해주어야 한다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa

 

JdbcRepository 구현

JdbcMemberRepository
  • 코드의 길이가 매우 길며 반복이 많고 복잡한 로직으로 구성되어 있다.
  • 먼저 Connection 객체를 이용해 DB와 연결하고 PreparedStatement의 객체에서 query문을 DB서버로 보낸다.
  • 예외 처리 부도 굉장히 많고 자원도 다 해제해주어야 하는 번거로운 과정이 필요하다.
public class JdbcMemberRepository implements MemberRepository {

    private final DataSource dataSource;

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

    @Override
    public Member save(Member member) {

        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
  
  // 생략
  
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }

    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
    {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (conn != null) {
                close(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

 

스프링 설정 변경

SpringConfig
  • 메모리로 구현했던 MemoryMemberRepository 대신에 새로 만든 JdbcMemberRepository를 리턴하도록 해준다.
  • DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체이다. 스프링 부트는 데이터베이스 커넥션 정보(properties 파일)를 바탕으로 DataSource를 생성하고 스프링 빈으로 등록한다.
  • @Configuration도 스프링에서 관리해주기 때문에 DI로 dataSource를 넣어준다.
  • MemberRepository를 구현하는 새로운 클래스를 만들고 SpringConfig 파일만 수정해줌으로써 메모리 기반에서 DB 기반으로 교환해줄 수 있다. (객체지향의 다형성 활용 → 매우 편리)
@Configuration
public class SpringConfig {

    private DataSource dataSource;

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

	...

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

 

출력 결과
  • DB에 이름을 "spring"과 "spring2"로 insert하고 회원 가입 창에서 "jpa"와 "jpa2"를 입력한다.
  • jpa와 jpa2는 h2의 information 스키마 중 sequences라는 스키마가 id를 33부터 설정한다.

 

 

 

구현 방식

인프런 김영한님 스프링 입문 강의 자료
인프런 김영한님 스프링 입문 강의 자료

  • 객체지향의 SOLID 원칙 중 OCP(Open-Closed Principle)이 잘 지켜진다. (다형성을 이용)
    • OCP : 확장에는 열려있고, 수정, 변경에는 닫혀있다.
  • 스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고 설정만으로 구현 클래스를 변경할 수 있다.
  • 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.

 

스프링 통합 테스트

  • 스프링 통합 테스트 : 스프링 컨테이너와 DB까지 연결하여 수행하는 테스트
  • @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행
  • @Transactional : 테스트 시작 전에 트랜잭션을 시작하고 테스트 완료 후 항상 롤백을 수행해준다. 이렇게 해주면 다음 테스트에 영향을 주지 않아 반복적인 테스트 수행이 가능하다.
  • 테스트 코드는 다른 곳에서 사용할 것이 아니므로 필드 기반으로 @Autowired 받아 사용하는 것이 편하다.

 

테스트 코드

MemberServiceIntegrationTest
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    
    @Test
    void 회원가입() {
   
    // 생략
    
    }
    
    @Test
    void 중복회원예외() {
    
    // 생략
    
    }
}

 

@Transactional의 필요성

  • @Transactional을 사용하지 않고 테스트를 돌리면 오류가 발생한다.

  • DB에 spring이라는 회원 데이터가 존재하기 때문에 테스트가 수행되지 않는다.
  • DB를 비워주면 잘 돌아가지만 한 번 수행 후에는 같은 오류가 발생한다.
  • 테스트는 반복 수행할 수 있어야하기 때문에 @Transaction을 사용한다.

 

스프링 JdbcTemplate

  • 순수 JDBC와 동일한 환경설정을 해주면 된다.
  • 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 sql문은 직접 작성해주어야 한다.

 

JdbcTemplateRepository 구현

JdbcTemplateMemberRepository
public class JdbcTemplateMemberRepository implements MemberRepository {

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        this.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;
        };
    }
}

 

스프링 설정 변경

SpringConfig
  • JdbcTemplateRepository를 반환하도록 해준다.
@Bean
    public MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }

 

JPA

  • JPA(Java Persistence API)는 자바 진영의 ORM(Object-Relational Mapping) 기술 표준으로 인터페이스의 모음이다.
  • JPA는 인터페이스로, 구현하는 여러 업체들이 있다. (e.g. hibernate)
  • 기존의 반복 코드에 더불어 sql 쿼리도 직접 생성해준다.
  • sql과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
  • 개발 생산성을 크게 높일 수 있다.

 

환경 설정

build.gradle
  • jdbc 라이브러리 대신 data-jpa 라이브러리를 가져온다. (JDBC와 JPA를 모두 포함하는 라이브러리)
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

 

application.properties
  • show-sql=true : JPA가 생성하는 sql문을 볼 수 있다.
  • JPA는 객체를 보고 테이블을 생성해주는 기능이 있는데, 현재 테이블을 생성해 놓은 상태이므로 ddl-auto를 none으로 설정한다. (create로 설정하면 테이블도 자동으로 생성)
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

 

Member 클래스
  • @Entity : JPA가 관리해주며 테이블과 매핑할 수 있다.
  • @Id : pk로 id를 설정해준다.
  • @GenerateValue~ : DB에 값을 넣으면 id를 자동으로 생성해주는 것을 identity 전략이라고 한다.
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;    // 데이터를 구분하기 위해 시스템이 저장하는 id
    private String name;

 

MemberService
  • JPA를 사용하기 위해서는 데이터를 변경하는 서비스 계층의 메서드에 @Transaction을 사용해줘야 한다.
  • 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 런타임 오류 발생 시 롤백한다.
  • JPA를 이용한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
@Transactional
public class MemberService {
...
}

 

JpaRepository 구현

JpaMemberRepository
  • data jpa 라이브러리를 다운받으면 스프링 부트가 EntityManager 객체를 생성해준다. (DI 이용)
  • em.persist()와 em.find() 메서드를 이용해 쉽게 Member 인스턴스를 저장하고 pk 기반의 검색을 수행할 수 있다.
  • pk 기반이 아닌 findByName의 경우에는 jpql을 작성해줘야 한다.
public class JpaMemberRepository implements MemberRepository {

    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> 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();
    }
}

 

스프링 데이터 JPA

  • 반복 개발해온 CRUD와 단순 조회 기능이 모두 제공된다.
  • 리포지토리의 구현 클래스 없이 인터페이스 만으로 개발할 수 있다.

 

SpringDataJpaRepository 구현

SpringDataJpaMemberRepository
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // JPQL select m from Member m where m.name = ?
    @Override
    Optional<Member> findByName(String name);
}

 

스프링 설정 변경

SpringConfig
@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository);
    }
}
  • 스프링 데이터 JPA가 인터페이스의 구현체를 만들어서 스프링 빈으로 자동 등록해준다. → injection해서 사용 가능
  • CRUD와 단순 조회 기능이 모두 제공되므로 따로 구현을 해주지 않아도 된다.
  • 공통화할 수 없는 기능은 메서드 이름(e.g. findByName(), findByEmail(), ...)을 명시하면 그에 따라 JPQL을 생성하여 조회 기능을 제공해준다.

 

스프링 데이터 JPA 제공 클래스

  • save, delete, findOne과 같은 기본적인 CRUD 기능을 제공해준다.

 

※참고※

실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하여 처리한다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 이용해서 직접 쿼리문을 작성하거나 JdbcTemplate을 사용하면 된다.


강의 링크

 

[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저작자표시 비영리 변경금지 (새창열림)

'Spring' 카테고리의 다른 글

[Spring] 스프링 입문 (7) - AOP  (0) 2022.03.01
[Spring] 스프링 입문 (5) - 회원 관리 예제 (웹 MVC 개발)  (0) 2022.02.28
[Spring] 스프링 입문 (4) - 스프링 빈과 의존 관계  (0) 2022.02.27
'Spring' 카테고리의 다른 글
  • [Spring] 핵심 원리 기본 (1) - 객체 지향 설계와 스프링
  • [Spring] 스프링 입문 (7) - AOP
  • [Spring] 스프링 입문 (5) - 회원 관리 예제 (웹 MVC 개발)
  • [Spring] 스프링 입문 (4) - 스프링 빈과 의존 관계
Sue
Sue
개발 공부 로그
  • Sue
    Sue's devlog
    Sue
  • 전체
    오늘
    어제
    • 분류 전체보기 (122)
      • Algorithm (2)
      • WEB (8)
      • Java & Kotlin (83)
      • Spring (26)
      • Database (1)
      • Infra (0)
      • Git (1)
      • devlog (1)
  • 인기 글

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Sue
[Spring] 스프링 입문 (6) - 스프링 DB 접근 기술
상단으로

티스토리툴바