[Spring] 입문5
Java & Spring

[Spring] 입문5

스프링 DB 접근 기술

이전까지는 메모리에 데이터를 저장하는 방식으로 구현했다. 하지만 이렇게 하면 서버가 내려갈 경우 모든 데이터가 사라진다는 단점이 있다. 데이터베이스를 만들어서 서버가 내려가도 없어지지 않도록 구현해보자.

 

아래 소주제들중 일부를 보면 순수 JDBC -> 스프링 JdbcTemplate -> JPA -> 스프링 데이터 JPA 이렇게 발전했다고 볼 수 있다. 이전에 사용되었던 기술들은 알아두면 좋으니 간단하게 살펴보는 정도로만 확인해본다.

완료 후 폴더 구성

 


H2 데이터베이스 설치 (Mac M1 기준)

H2 데이터베이스는 간단하고 가벼운 RDBMS다. H2 데이터베이스가 뭔지 자세한 내용이 중요한게 아니니 설명은 생략한다.

 

1. DB 접근 기술을 알기전에 DB 먼저 있어야 하므로 가벼운 H2 데이터베이스를 먼저 설치해준다.

 

https://www.h2database.com/html/main.html

 

H2 Database Engine

H2 Database Engine Welcome to H2, the Java SQL database. The main features of H2 are: Very fast, open source, JDBC API Embedded and server modes; in-memory databases Browser based Console application Small footprint: around 2 MB jar file size     Suppor

www.h2database.com

 

2. 다운 받은 압축 파일을 풀어주고 h2/bin 폴더에 들어간다. 아래와 같은 파일들이 있는 것을 확인할 수 있다.

3. h2.sh라는 파일이 있는 것을 확인하고 권한 설정을 해준다. (mac만!)

chmod 755 h2.sh

3. 이제 실행 해준다.

./h2.sh

윈도우 사용자는
./h2.bat

3. 조금 기다리면 화면이 뜬다. (localhost로 뜨지 않으면 ip를 localhost로 바꿔준다.)

4. 연결 버튼을 누른다. 위에 JDBC URL에 적혀있는 것은 ~/test 폴더에 파일을 만든다는 의미이다. 처음에만 그렇게 들어가고 이후부터는 해당 부분에 jdbc:h2:tcp://localhost/~/test 라고 적어줘야한다.

5. 새로운 테이블을 생성한다.

drop table if exists member CASCADE;
     create table member
     (
         id   bigint generated by default as identity,
         name varchar(255),
         primary key (id)
);
  • java에서 long -> sql에서 bigint
  • generated by default as identity : 값이 들어오지 않는다면 자동으로 채워주는 설정
  • 아래와 같이 데이터도 몇개 넣어줬다.
insert into member (name) values ('spring2');

이제 Spring의 DB 접근 기술에 대해 공부할 준비가 되었다.


순수 JDBC

순수 JDBC로 개발하는 방식은 20년전에 개발하던 방식으로 현재는 거의 쓰이지 않고 있다고 한다. 옛날에는 그랬구나 정도만 생각하고 넘어가자.

  1. build.gradle에 설정 추가
    dependencies {
        ...
        implementation 'org.springframework.boot:spring-boot-starter-jdbc' // db와 붙을때 꼭필요
        runtimeOnly 'com.h2database:h2' // h2 클라이언트
    }
  2. application.properties에 설정 추가
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa​
  3. JdbcMemberRepository.java 생성해서 개발 (회원을 jdbc와 연동해서 저장하겠다~)

스프링 통합테스트

이전에는 순수 java 코드를 가지고 테스트를 했지만 db가 연결되는 순간 Db에 대한 정보를 spring이 들고 있기 때문에 순수 java가 아닌 spring 통합테스트를 해야한다.

-> 단위테스트를 하는게 가장 좋음. 통합테스트를 해야하는 경우가 있을 수 있지만 대부분은 단위테스트

package hello.hellospring.service;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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;

@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
    // test code에서는 편하게 필드 기반 injection
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository; // clear를 해주기 위해 가져옴

    @Test
    public void 회원가입() {
        //Given
        Member member = new Member();
        member.setName("hello");
        //When
        Long saveId = memberService.join(member);
        //Then
        Member findMember = memberRepository.findById(saveId).get();
        assertEquals(member.getName(), findMember.getName());
    }

    // 예외가 잘 처리되는지도 확인해야함
    @Test
    public void 중복_회원_예외() {
        //Given
        Member member1 = new Member();
        member1.setName("spring5");
        Member member2 = new Member();
        member2.setName("spring5");
        //When
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));//예외가 발생해야 한다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}

스프링 JdbcTemplate

  • 순수 Jdbc와 동일한 환경 설정
  • 순수 JDBC에 있는 반복적 코드를 제거해주지만 sql은 그대로 써야함
  1. JdbcTemplateMemberRepository.java 추가
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    import javax.sql.DataSource;
    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;
    
    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());
            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 new RowMapper<Member>() {
                @Override
                public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
                    Member member = new Member();
                    member.setId(rs.getLong("id"));
                    member.setName(rs.getString("name"));
                    return member;
                }
            };
        }
    }​
  2. SpringConfig 파일에서 아래와 같이 변경
    @Bean
    public MemberRepository memberRepository() {
        // return new MemoryMemberRepository();
         return new JdbcTemplateMemberRepository(dataSource);
    }​

JPA

  • jpa에서는 위에서 없어지지 않았던 sql문도 만들어준다!
  • sql과 데이터 중심 -> 객체 중심의 설계로 패러다임 변화
  1. build.gradle에서 jdbc -> jpa 변경
    // jpa, jdbc 모두 포함함
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'​
  2. application.properties에 추가
    spring.jpa.show-sql=true // jpa가 날리는 query 보기
    spring.jpa.hibernate.ddl-auto=none // 테이블 자동 생성 기능 끄기​
  3. jpa를 사용하기 위해 entity로 매핑해줘야함
    // domain/Member.java
    import javax.persistence.Entity;
    
    @Entity // -> jpa가 관리하는 entity야! 라고 명시
    public class Member {
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // primary key & 자동으로 생성되는 identity
        private Long id;
        private String name;
    ...​
  4. JpaMemberRepository.java 파일 생성
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import java.util.List;
    import java.util.Optional;
    import javax.persistence.EntityManager;
    
    public class JpaMemberRepository implements MemberRepository {
    
        // jpa는 entitymanager로 모든게 동작
        // build.gradle에서 jpa설정 해둠으로써 spring boot에서 entityManager를 자동 생성해줌
        // 여기선 그렇게 만들어진 것을 injection 받음
        private final EntityManager em;
    
        public JpaMemberRepository(EntityManager em) {
            this.em = em;
        }
    
        @Override
        public Member save(Member member) {
            em.persist(member); // jpa가 insert query 다 만들어서 디비에 넣어줌
            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) {
            // pk가 아닌 값으로 조회할 때는 jpql이라는 query언어를 써야함
            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() {
            List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
            return result;
        }
    }​
  5. jpa를 사용할 때 @Transactional 필수
    @Transactional
    public class MemberService {
    ...​
  6. SpringConfig 파일에서 변경
    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 JpaMemberRepository(em);
        }​
        ...

스프링 데이터 JPA

위의 jpa에서 더 나아가 기본 crud도 제공, 레포지토리에 구현 클래스 없이 인터페이스 만으로도 개발 가능

기본적인 findById, findAll, save 등은 제공이 됨

  1. interface로 SpringDataJpaMemberRepository 생성
    package hello.hellospring.repository;
    
    import hello.hellospring.domain.Member;
    import java.util.Optional;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    
        @Override
        Optional<Member> findByName(String name);
    }​
  2. SpringConfig 파일 변경
    package hello.hellospring;
    
    import hello.hellospring.repository.JdbcMemberRepository;
    import hello.hellospring.repository.JdbcTemplateMemberRepository;
    import hello.hellospring.repository.JpaMemberRepository;
    import hello.hellospring.repository.MemberRepository;
    import hello.hellospring.repository.MemoryMemberRepository;
    import hello.hellospring.service.MemberService;
    import javax.persistence.EntityManager;
    import javax.sql.DataSource;
    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 MemberRepository memberRepository;
    
        @Autowired
        public SpringConfig(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    
        @Bean
        public MemberService memberService() {
            return new MemberService(memberRepository);
        }
    
    //    @Bean
    //    public MemberRepository memberRepository() {
    //         return new MemoryMemberRepository();
    //         return new JpaMemberRepository(em);
    //    }
        /*
        장점: Memory -> db로 변경하고 싶으면 return new DbMemberRepository()로만 바꿔주면 된다!
        */
    }​

 

728x90
반응형

'Java & Spring' 카테고리의 다른 글

[Spring] 핵심원리 기본편 - 객체 지향 설계와 스프링  (0) 2022.05.17
[Spring] 입문6 (끝!)  (0) 2021.11.03
[Spring] 입문4  (0) 2021.10.09
[Spring] 입문3  (0) 2021.10.09
[Spring] 입문2  (0) 2021.08.16