[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 06. 스프링 DB 접근 기술

Turtle·2024년 6월 8일
0
post-thumbnail

✔️H2 데이터베이스 설치

H2 Database Engine

위의 사이트에서 H2 데이터베이스를 설치

✔️H2 데이터베이스 연동

최초 접속시의 JDBC URL과 DB 생성 이후 접속시의 JDBC URL은 다르다.

  • 최초 접속 시 JDBC URL : jdbc:h2:~/파일명 후 루트 폴더에 ~/파일명.mv.db 파일 생성되었는지 확인
  • 최초 접속 시 저장한 설정 : Generic H2 (Embedded)
  • 이후 접속 시 JDBC URL : jdbc:h2:tcp://localhost/~/파일명
  • 이후 접속 시 저장한 설정 : Generic H2 (Server)

필자는 DB의 이름을 spring으로 하였고 아래와 같이 진행했다.

그럼 연결이 정상적으로 이루어지면서 DB가 만들어지게 된다.

연결을 끊고 기존에 생성했던 spring DB로 다시 연결하고자 한다.

세팅이 완료되었으면 테이블을 생성한다.

drop table if exists member CASCADE;	// 기존에 존재하는 member테이블이 있다면 테이블을 삭제
create table member						// DDL로 테이블 생성
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

📉순수 JDBC

JDBC 환경 설정 - build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'

스프링 부트 데이터베이스 연결 설정 추가 - resources/application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/spring
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa	// 스프링 부트 2.4 버전 패치

❗org.h2.jdbc.JdbcSQLInvalidAuthorizationSpecException: Wrong user name or password [28000-224]

회원가입 후 리다이렉트 되는 시점과 회원 목록을 조회하는 시점에서 위와 같은 에러 메시지가 나오면서 오류가 발생했다. 구글링을 통해 찾아보니 스프링 부트의 패치로 인해 발생하는 문제로 스프링 부트 공식 깃허브의 PR 내역을 보면 확인할 수 있다.

출처 - Spring-Boot Github Repository PR

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;

import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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);
        }
    }
    @Override
    public Optional<Member> findById(Long id) {
        String sql = "select * from member where id = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public List<Member> findAll() {
        String sql = "select * from member";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();
            List<Member> members = new ArrayList<>();
            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                members.add(member);
            }
            return members;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional<Member> findByName(String name) {
        String sql = "select * from member where name = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, name);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            }
            return Optional.empty();
        } 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);
    }
}

코드가 어마무시하게 길다... 이런 DB 기술부터 점진적으로 발전해왔다는걸 느끼고 넘어가자.

💻스프링 통합 테스트

기존에 메모리 저장소를 사용했던 코드를 JDBC에 맞춰서 전부 바꿔줘야 한다.
컴포넌트 스캔을 통한 방식이 아닌 자바 설정 파일로 스프링 빈을 직접 등록했기때문에 코드를 아래와 같이 수정한다.

package hello.hello_spring;

import hello.hello_spring.controller.MemberController;
import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
  	// DataSource 생성자로 넣어주기
    private final DataSource dataSource;

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

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

    @Bean
    public MemberRepository memoryMemberRepository() {
        //return new MemoryMemberRepository(); - 메모리 저장소
        return new JdbcMemberRepository(dataSource);
    }

    @Bean
    public MemberController memberController() {
        return new MemberController(memberService());
    }
}

다형성에 근거하여 설명을 하자면 MemberRepository는 인터페이스이고 이 인터페이스를 구현한 것이 앞서 작성했던 메모리 기반의 MemoryMemberRepository, 그리고 지금 JDBC 기반의 JdbcMemberRepository이다. 따라서 리포지토리를 빈으로 등록하는 부분에서 반환 타입을 MemberRepository로 하고 new 연산자를 사용해서 JdbcMemberRepository 객체를 리턴한다.(다형적 참조에 의해 가능한 코드)

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void join() {
        // given(무언가가 주어졌는데...)
        Member member = new Member();
        member.setName("TEST");

        // when(실행했을 때...)
        Long saveId = memberService.join(member);

        // then(결과가 이렇게 나와야 한다...)
        Member findMember = memberService.findOne(saveId).get();
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    void duplicate() {
        Member member1 = new Member();
        member1.setName("Spring1");
        memberService.join(member1);

        Member member2 = new Member();
        member2.setName("Spring1");
        /*
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        */

        assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    }
}

❓@Transactional 어노테이션의 역할

출처 - Spring documentation(@Transactional)

테스트 코드에 @Transactional을 붙여주면 기본적으로 각각의 테스트 메서드가 끝날 때 트랜잭션을 롤백한다.

📈스프링 JdbcTemplate

package hello.hello_spring;

import hello.hello_spring.controller.MemberController;
import hello.hello_spring.repository.JdbcMemberRepository;
import hello.hello_spring.repository.JdbcTemplateMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private final DataSource dataSource;

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

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

    @Bean
    public MemberRepository memoryMemberRepository() {
        //return new MemoryMemberRepository(); - 메모리 저장소
        //return new JdbcMemberRepository(dataSource); - JDBC 기반의 리포지토리
        return new JdbcTemplateMemberRepository(dataSource);
    }

    @Bean
    public MemberController memberController() {
        return new MemberController(memberService());
    }
}
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
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
    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) -> {
            Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

📈JPA

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

build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
package hello.hello_spring.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

// JPA가 관리하는 엔티티라는 의미에서 엔티티 어노테이션 사용
@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}
package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import jakarta.persistence.EntityManager;

import java.util.List;
import java.util.Optional;

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> resultList = em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
        return resultList.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
        return result;
    }
}
package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Transactional
public class MemberService {
    private MemberRepository memberRepository;
    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public Long join(Member member) {
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
package hello.hello_spring;

import hello.hello_spring.controller.MemberController;
import hello.hello_spring.repository.JpaMemberRepository;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
    //private final DataSource dataSource;
    private EntityManager em;
    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }


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

    @Bean
    public MemberRepository memoryMemberRepository() {
        //return new MemoryMemberRepository(); - 메모리 저장소
        //return new JdbcMemberRepository(dataSource); - JDBC 기반의 리포지토리
        //return new JdbcTemplateMemberRepository(dataSource); - JdbcTemplate 기반의 리포지토리
        return new JpaMemberRepository(em);
    }

    @Bean
    public MemberController memberController() {
        return new MemberController(memberService());
    }
}

📈스프링 데이터 JPA

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

// 스프링 데이터 JPA - 구현 클래스 알아서 생성
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    @Override
    Optional<Member> findByName(String name);
}
package hello.hello_spring;

import hello.hello_spring.controller.MemberController;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.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);
    }

    @Bean
    public MemberController memberController() {
        return new MemberController(memberService());
    }
}

스프링 데이터 JPA에서 구현 클래스를 만들고 의존성 주입 받아 사용하면 된다.

스프링 데이터 JPA는 인터페이스를 통한 기본적인 CRUD 기능을 제공하고 findByName(), fineByEmail()과 같은 메서드 이름만으로 조회 기능 제공한다.(구현 불필요)
페이징 기능 자동 제공

🔒출처

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술

0개의 댓글