본문 바로가기
Spring/스프링 입문

인프런 스프링 입문 강의 정리 #6

by 예린lynn 2023. 11. 17.
728x90

1. 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.5 MB jar file size     Supp

www.h2database.com

 

위 사이트에 들어가 1.4.200 버전을 설치한 후 h2/bin/h2.bat를 실행한다.

 

-테이블 형성

id : bigint 타입, null일 경우 자동으로 id 값을 채워준다

name : varchar 타입

PK(primary key) : id

 

-테이블에 값 생성

 

name이 spring인 값을 입력한다.

id의 값은 넣지 않았기 때문에 자동으로 생성된다.

 

 

-테이블 값 관리

sql 패키지를 만들고, 그 안에 ddp.sql 파일을 생성한다.

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

 

2. 순수 JDBC

-예전에 사용했던 방식

 

3. 스프링 통합 테스트

스프링 컨테이너, DB까지 연결한 통합 테스트를 진행한다. 

test/java/service 파일에 MemberServiceIntegrationTest 클래스를 생성한다.

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
 @Autowired MemberService memberService;
 @Autowired MemberRepository memberRepository;
 
 @Test
 public void 회원가입() throws Exception {
 //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 중복_회원_예외() throws Exception {
 //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("이미 존재하는 회원입니다.");
 }
}

@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.

@Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

DB는 트랜잭션을 보장하기 때문에 커밋을 해줘야 DB에 반영이 된다.

만약 테스트를 끝난 다음에 롤백을 하면 DB에 데이터가 반영이 되지 않는다.

이때 @Transactional이 테스트 시작 전에 트랜잭션을 실행하고 테스트 종류 후 롤백을 실행한다.

 

cf) 순수한 테스트인 @MemberServiceTest가 통합 테스트인 @MemberServiceIntegrationTest보다 더 좋은 테스트일 확률이 높다. 

 

4. 스프링 JdbcTemplate

순수 Jdbc와 동일한 환경설정을 하면 된다.

JDBC API에서 본 반복 코드를 대부분 제거해준다는 장점이 있지만, SQL은 직접 작성해야 한다.

 

src/main/java/repository 파일에 JdbcTemplateMemberRepository 클래스를 생성한다.

import hello.hellospring.domain.Member;
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.sql.ResultSet;
import java.sql.SQLException;
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()); //자동으로 intert문을 만들어준다
 
 Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
 member.setId(key.longValue());
 return member;
 }
 
 //Jdbd Template에서 sql 쿼리를 보내서 결과 매핑 후 List로 받아 Optional로 바꿔서 변환한다.
 @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;
 };
 }
}

 

JdbcTemplate를 사용하도록 SpringConfig 클래스에서 스프링 설정을 변경해야 한다.

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.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(memberRepository());
 }
 
 @Bean
 public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
 return new JdbcTemplateMemberRepository(dataSource);
 }
}

 

5. JPA 

  • 기존의 반복 코드는 물론이고, 기본적인 SQL을 직접 만들어서 실행한다
  • SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환한다
  • 개발 생산성을 높일 수 있다

build.gradle 파일을 수정해준다. 

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

 

application.properties 파일에 코드를 추가한다.

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

 

Member.java 파일을 수정한다.

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

@Entity //JPA가 관리하는 Entity
public class Member {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY) //identity id 자동 생성
 private Long id;
 
 @Column(name="username")
 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;
 }
}

 

main/java/repository에 JpaMemberRepository 클래스를 추가한다.

import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;

public class JpaMemberRepository implements MemberRepository {
 private final EntityManager em; //JPA를 쓰기 위해 EntitiyManager 주입
 
 public JpaMemberRepository(EntityManager em) {
 this.em = em;
 }
 
 public Member save(Member member) {
 em.persist(member); //persist는 영속한다는 의미
 return member;
 }
 
 public Optional<Member> findById(Long id) {
 Member member = em.find(Member.class, id);
 return Optional.ofNullable(member);
 }
 
 public List<Member> findAll() {
 return em.createQuery("select m from Member m", Member.class)
 .getResultList();
 }
 
 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();
 }
}

findByName 코드는 'select m from member m where m.name = ?' 으로 이해하면 된다.

 

JPA를 사용할 때는 항상 @Transactional이 필요하다. MemberService 클래스에 추가해준다.

import org.springframework.transaction.annotation.Transactional

@Transactional
public class MemberService {}

 

JPA를 사용하도록 SpringConfig에서 설정을 변경한다.

import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;

@Configuration
public class SpringConfig {
 private final DataSource dataSource;
 
 private final EntityManager em;
 
 @Autowired
 public SpringConfig(DataSource dataSource, EntityManager em) {
 this.dataSource = dataSource;
 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);
 }
}

 

 

6. 스프링 데이터 JPA

  • JPA를 편리하게 사용하도록 도와주는 기술이다.
  • 개발 생산성이 증가하고, 개발해야 할 코드가 줄어든다.
  • 리포지토리에 구현 클래스 없이 인터페이스만으로도 구현이 가능하다.

src/main/java/repository에 SpringDataJpaMemberRepository 인터페이스를 생성한다.

import org.example.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

//Member에서 id의 type이 Long
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {

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

 

-스프링 데이터 JPA

  • 인터페이스를 통한 기본적인 CRUD 제공
  • findByName(), findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공
  • 페이징 기능 자동 제공

cf) 실무에서는 JPA와 스프링 데이터 JPA를 기본적으로 사용한다.

     복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다.

728x90