minghxx.blog
  • [Spring] 스프링 입문 6) 스프링 DB 접근 기술
    2023년 10월 15일 22시 03분 19초에 업로드 된 글입니다.
    작성자: 민발자
    728x90

    스프링 입문 - 스프링 부트 웹 MVC, DB 접근 기술 

    Session 6 스프링 DB 접근 기술

    1. 순수 JDBC

    1) 환경설정

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

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

    application.properties에 데이터베이스 연결 설정 추가

    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa

     

    2) JDBC 리포지토리 생성

    public class JdbcMemberRepository implements MemberRepository {
        // DataSource ▶ 데이터베이스 커넥션을 받아올때 사용하는 객체, 데이터베이스 커넥션 정보를 바탕으로 객체를 생성하고 DI를 받음 
        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(); // DataSource에서 데이터베이스 커넥션을 받아옴
                pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
                pstmt.setString(1, member.getName()); // 값 넣어줌
                pstmt.executeUpdate(); // DB에 쿼리를 보냄
                rs = pstmt.getGeneratedKeys(); // Statement.RETURN_GENERATED_KEYS 생성된 키를 반환
                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); // 자원 반환
            }
        }
    }

     

    3) 스프링 설정 변경

    @Configuration
    public class SpringConfig {
    	private final 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);
    	}
        
    	// application.properties에서 데이터베이스 설정으로 
    	// 스프링에서 자체적으로 DataSource 빈을 생성해주기 때문에 알아서 주입해줌
    }

     

    4) 스프링 설정 이미지

     

    개방-폐쇄 원칙 OCP ▶ 확장에는 열려있고, 수정, 변경에는 닫혀있다

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


    2. 스프링 통합 테스트

    1) 회원 서비스 스프링 통합 테스트

    @SpringBootTest // 스프링 환경에서 테스트
    @Transactional // 테스트 후 롤백해서 테스트전으로 초기화
    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();
            assertThat(member.getName()).isEqualTo(findMember.getName());
        }
    
        @Test
        public void 중복회원예외() {
            //given
            Member member1 = new Member();
            member1.setName("test");
    
            Member member2 = new Member();
            member2.setName("test");
    
            //when
            memberService.join(member1);
            IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
    }

    @SpringBootTest 스프링 환경에서 테스트, 스프링 컨테이너와 테스트를 함께 실행
    @Transactional 테스트 후 롤백해서 테스트 전으로 초기화, 다음 테스트에 영향을 주지 않음

     

    2) 단위테스트와 통합테스트

    단위테스트 ▶ 순수한 자바코드로 진행

    통합테스트 ▶ 스프링과 DB 모두 같이 실행


    3. 스프링 JdbcTemplate

    1) 스프링 JdbcTemplate

    반복코드를 대부분 제거, SQL 작성은 필요

     

    2) 스프링 JdbcTemplate 회원 리포지토리 작성

    public class JdbcTemplateRepository implements MemberRepository{
        private  final JdbcTemplate jdbcTemplate;
        
        // 생성자가 하나면 @Autowired 생략 가능
        public JdbcTemplateRepository(DataSource dataSource) {
            this.jdbcTemplate = new JdbcTemplate(dataSource);
        }
    
        @Override
        public Member save(Member member) {
            SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); // 쿼리 작성할 필요없이 insert문 만들어줌
            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;
        }
    }

     

    3) 스프링 설정 멤버 리포지토리 변경

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

    4. JPA

    1) JPA

    코드 반복을 제거하고, SQL도 직접 작성해서 실행해준다.

     

    2) 환경설정

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

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        //implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
        }
    }

    스프링 부트에 JPA 설정 추가

    spring.jpa.show-sql=true //jpa가 만든 sql 확인 가능
    spring.jpa.hibernate.ddl-auto=none // 테이블 자동생성 기능 off

    spring.jpa.show-sql ▶ JPA가 생성한 SQL 확인할 수 있게하는 옵션
    spring.jpa.hibernate.ddl-auto entity 정보로 테이블 자동 생성 옵션, 현재 테이블이 이미 생성되어있으므로 꺼준다

     

    3) 엔티티 매핑

    ORM(Object Relational Mapping) ▶ 객체와 관계형 데이터베이스를 맵핑하는 것

    @Entity // jpa가 관리하는 엔티티 선언
    public class Member {
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // pk 생성을 데이터베이스에 위임
        private Long id;
    
        private String name;
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

     @GeneratedValue(strategy = GenerationType.IDENTITY) 아이덴티티 전략, 기본키 생성을 데이터베이스에 위임

     

    3) JPA 멤버 리포지토리 생성

    public class JpaMemberRepository implements MemberRepository{
        // JPA는 EntityManager로 동작
        // data-jpa 라이브러리틑 통해 스프링이 EntityManager를 자동으로 생성하고 DB와 통신을 내부에서 처리해줌
        private final EntityManager em;
        
        // 스프링에서 만들어준 EntityManager를 주입받음
        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); // find(조회타입, 식별자)
            return Optional.ofNullable(member);
        }
    
        @Override
        public Optional<Member> findByName(String name) {
            // jpql 객제지향 쿼리 사용
            // 엔터티를 대상으로 쿼리를 보냄
            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(); // 엔티티를 대상으로 쿼리를 보냄
        }
    }

    pk기반이 아닌 경우 jpql 사용 ▶ 스프링으로 JPA를 감싸서 사용하는 경우(스프링 JPA) jpql 작성도 생략할 수 있다

     

    4) 서비스 계층 트랜잭션 추가

    @Transactional
    public class MemberService {}

    스프링이 해당 클래스 메서드 실행하면 트랜잭션 시작, 정상 종료되면 트랜잭션 커밋, 런타임예외 발생시 롤백

    JPA 통한 모든 데이터 변경을 트랜잭션 안에서 실행

     

    5) 스프링 설정 변경

    SpringConfig 수정

    @Configuration
    public class SpringConfig {
    	private final EntityManager em;
        
    	public SpringConfig(DataSource dataSource, EntityManager em) {
    		this.dataSource = dataSource;
    		this.em = em;
    	}
        
    	@Bean
    	public MemberRepository memberRepository() {
    		return new JpaMemberRepository(em);
    	}
    }

    5. 스프링 데이터 JPA

    1) 스프링 데이터 JPA

    JPA를 편리하게 사용하도록 도와주는 기술

    리포지토리에 구현 클래스 없이 인터페이스 만으로 개발 가능, CRUD 기능도 스프링 데이터 JPA가 모두 제공

     

    2) 스프링 데이터 JPA 멤버 리포지토리 작성

    public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
    	@Override
    	Optional<Member> findByName(String name);
    }

     

    3) 스프링 설정 변경

    SpringConfig 수정

    @Configuration
    public class SpringConfig {
        private final MemberRepository memberRepository;
    
        @Autowired
        public SpringConfig(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        // 스프링 데이터 JPA가 JapRepository를 상속 받고있으면 스프링 컨테이너가 구현체를 생성하고 빈으로 등록해줘서 주입 가능
        // memberRepository를 주입
        
        @Bean
        public MemberService memberService() {
            return new MemberService(memberRepository);
        }
    
    }

     

    스프링 데이터 JPA가 SpringDataJpaMemberRepository를 보고 스프링 빈을 자동 등록

    MemberRepository 구현체를 스프링 컨테이너가 관리하고 있어  생성자로 주입 가능

     

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

    인터페이스를 통한 기본 CRUD, 페이징, save() 등 기능 제공

    728x90
    댓글