본문 바로가기
Java/spring

spring 입문 - 스프링 DB 접근

by 티코딩 2024. 1. 25.

ㅇ H2

다운로드 받고
h2 파일에 bin에 들어가 chmod 755 h2.sh 해주고
./h2.sh 해주면

브라우저로 콘솔이 열림.

안뜨면 앞에만 로컬호스트로 바꿔줌.

http://localhost:8082/login.jsp?jsessionid=c399bfc99f7963b481efd3c7b8409fda

 

JDBC URL에 jdbc:h2:~/test 넣고 연결버튼 누르고 나오면 터미널에 홈으로 들어가 ll 입력하면

test.mv.db 가 생성됨.

그다음 다시 DBC URL에 jdbc:h2:tcp://localhost/~/test 로 바꾸고 연결

drop table if exists member CASCADE;
create table member
(
  	 id   bigint generated by default as identity,
     name varchar(255),
     primary key (id)
);

이렇게 해서 member 테이블만들고,

sql 디렉토리만들고 ddl.sql 파일에

똑같이 넣어준다.

 

 

ㅇ 순수 JDBC

h2데이터베이스, JDBC를 사용하기위해 build.gradle 파일에 라이브러리 추가

 

그리고 application.properties에 아래와같이 추가

MemberRepository를 implements 하는 JdbcMemberRepository를 만들어준다.

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

 

그리고 SpringConfig를 수정해준다.

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

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

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

 

이렇게 구현체가 두개인MemberRepository에서 기존에 스프링빈으로 등록했던 MemoryMemberRepository에서 JdbcMemberRepository로 등록을 바꿔주면 바로 구현체가 바뀌고 된다.

이것이 바로 OCP 개방폐쇄 원칙. 확장엔 열려있고 변경엔 닫혀있다.

스프링의 DI를 활용하면 기존 코드를 전혀 손대지않고 설정만으로 구현 클래스를 변경할 수 있다.

localhost:8080에 접속해 JPA라는 회원을 등록하고 H2로 접속해 회원목록을 조회하면

요로코롬 잘 나온다. 그리고 DB에 데이터를 저장했으므로 스프링을 재시작해도 잘 저장이 되있다.

 

ㅇ 스프링 통합테스트

스프링 컨테이너와 DB까지 연결한 통합테스트를 진행해보자.

@SpringBootTest //스프링컨테이너와 테스트를 함께 실행함
@Transactional //테스트케이스에 달면 테스트시작전에 트랜잭을 시작하고,완료 후에 항상 롤백함.
public class MemberServiceIntegrationTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;

    @Test
    void 회원가입() {
        //given : 상황이 주어질때
        Member member = new Member();
        member.setName("spring");

        //when : 이걸 실행(검증)했을때
        Long saveId = memberService.join(member);

        //then : 결과는 이렇게 나와야함
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @Test
    public void 중복_회원_예외(){
        //given : 상황이 주어질때
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when : 이걸 실행(검증)했을때
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then : 결과는 이렇게 나와야함
    }

    @Test
    void findMembers() {
        //given : 상황이 주어질때

        //when : 이걸 실행(검증)했을때

        //then : 결과는 이렇게 나와야함

    }
    }

기존에 MemberServiceTest를 복붙해서 MemberServiceIntegrationTest를 만들고, 애너테이션 두개를 붙혀준다.

@SpringBootTest //스프링컨테이너와 테스트를 함께 실행함
@Transactional //테스트케이스에 달면 테스트시작전에 트랜잭을 시작하고,완료 후에 항상 롤백함. 이건 MemberServiceTest에 있던 afterEach 같은 역할을 해준다.

ㅇ 스프링 JDBCTemplates

스프링 JdbcTemplateMyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준 다. 하지만 SQL은 직접 작성해야 한다

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

이렇게 구현체를 하나 더만들어주고

springconfig에서 구현체등록을 얘로 해준다.

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

 

그러고 다시 통합테스트를 돌려주면 끝!

테스트케이스를 잘 작성하자.

ㅇ JPA

이전까진 개발자가 직접SQL 쿼리를 작성해야하는데, JPA가 자동으로 처리를 해줘서 개발 생산성을 크게 높일수 있다. 

SQL과 데이터 중심의 설계에서 객체 중심의 설계로 전환할 수 있게 됐다. 

실제로 JPA가 글로벌하게 많이 쓰이는 기술이다. JPA도 스프링만큼 넓이와 깊이가 있는 기술이다.

build.gradle 에서 jdbc 라이브러리를 지우고

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

를 넣어주자.

그리고 application.properties에

spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

이렇게 넣어준다. 이미 테이블이 생성되어있으므로 자동으로 테이블을 생성해주는 ddl-auto를 none으로 해준다.

 

다음은 객체를 매핑해줘야한다.

엔티티에 @Entity를 붙혀주면 JPA가 관리해준다.

그리고 PK에 @Id 를 붙혀준다. 그리고 GeneratedValue 해주고 Identity를 해주면 DB가 알아서 생긴 순서대로 붙혀준다.

컬럼이 되는 name에는 @Column 붙혀주고 이름은 username으로 해준다.

 

그리고 JpaMemberRepository를 만들어준다.

JPA는 EntityManager를 통해 동작한다.

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

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

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

pk 기반이 아닌것들은 JPQL을 꼭 작성해줘야한다.

JPA 기술을 스프링 기술로 감싸서 제공하는 기술은 spring data jpa. 이걸사용하면 JPQL도 안써도됨.

JPA를 쓰려면 항상 트랜잭션이란게 있어야함. 그래서 서비스에 @Transactional 을 붙혀준다.

그리고 SpringConfig 에서 설정을 이렇게 바꿔주면 끝

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

    @Autowired
    public SpringConfig(EntityManager em){
        this.em = em;
    }

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

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

ㅇ 스프링 데이터 JPA

스프링 데이터 JPA를 사용하면 리포지토리에 구현 클래스 없이 인터페이스만으로도 개발을 완료할 수 있음. 반복개발해온 CRUD 기능도 스프링 데이터 JPA가 모두 제공함. 실무에서 관계형 데이터베이스를 사용한다면 스프링데이터 JPA는 필수.

리포지토리를 하나 만들어보자. 이번엔 Interface로 만들고 Interface는 다중상속이 가능하니, JpaRepository와 MemberRepository를 받아보자.

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    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);
    }

이렇게 바꾸고 테스트돌리면 잘 된다.

어떻게 구현하지않고도 되는걸까?

위에서 SpringDataJpaMemberRepository가 extends하는 JpaRepository는 웬만한 기본메서드들부터 CRUD를 제공하기 때문이다.

그런데 왜 
Optional<Member> findByName(String name);
이건 따로 구현해줬을까?

비지니스 별로 다 다르기때문에 공통하는게 불가능하다. name으로 찾을수도있지만 email, username, productname 등등 다 달라질수있기때문이다.

springDataJpa에서는 findByName하면

select m from Member m where m.name = ?

이렇게 JPQL을 짜주는 이런 규칙이 있다.

이렇게 인터페이스 이름만으로도 개발이 끝난다.

내가 여태껏 썼던거에 대해 자세히 알게 됐다.

 

'Java > spring' 카테고리의 다른 글

spring security를 알아보자 - 2  (0) 2024.03.12
spring security를 알아보자 - 1  (0) 2024.03.04
Spring 입문 - AOP  (1) 2024.01.25
spring 입문 - 웹 MVC 개발  (1) 2024.01.19
spring 입문 - MVC, 스프링빈 등록까지  (0) 2024.01.19