..

[Spring Basic] 6. Spring DB Access Technique

Table of contents

  • H2 Database Installation
  • 순수 JDBC
    • DB 접근 준비
      • Dependency 추가
      • DB 접속정보 추가
    • JDBC
      • Good to know!
      • Configuration for JDBC
    • 우리가 배운것
    • 스프링을 쓰는 이유
  • Spring Integration Test
    • 테스트 생성
    • 테스트는 반복이 가능해야 한다 (Transaction)
  • Spring JdbcTemplate
    • JdbcTemplate 생성
    • RowMapper
    • Configuration for JdbcTemplate
  • JPA
    • Dependency 추가
    • 정보 추가
    • what is JPA?
    • JPA 사용
      • Entity Mapping
      • PK Mapping
      • Column Mapping
    • JPA 동작
      • EntityManager
      • JPQL
      • Transactional
    • Configuration for JPA
    • JPA 내부 동작
  • Spring Data JPA
    • Configuration for JPA
    • 스프링 데이터 JPA 제공 기능
    • 참고

H2 Database Installation

  • 권한 주기 - chmod 755 h2.sh
  • 실행 - ./h2.sh

  • 아래의 파일이 생성 되어있어야 한다

JDBC URL: 에 파일로 접근 (e.g., jdbc:h2:~/test) 하게 되면 웹 어플리케이션과 웹 콘솔이 동시에 접근하려고 하면 접근이 안되고 오류가 날 수도 있다.

  • 소켓을 통해서 접근해야 여러군데에서 접근 할 수 있다.
    • jdbc:h2:~/testjdbc:h2:tcp://localhost/~/test로 변경해서 접속

SQL query

Member Table 생성

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

조회

SELECT * FROM MEMBER

데이터 입력

insert into member(name) values('spring')
  • id를 안넣어줘도 자동으로 unique한 값으로 입력된다.
  • MemoryMemberRepository에 있는 sequence와 같은 역할을 한다.
  • generated by default as identity - null 값 (값이 채워지지 않을 경우)이면 자동으로 값을 채워준다
  • iD가 자동으로 생성됨
  • 위의 쿼리들을 DDL(Data Definition Language)라고 부른다
  • JPA - 객체를 쿼리없이 DB에 저장 할 수 있다.
  • 기존의 MemoryMemberRepository를 jdbcRepository, JpaRepository로 나중에 변경해볼 예정이다

순수 JDBC

  • application에서 db에 저장하는게 기존처럼 메모리에 저장하는게 아니라 DB에 insert, select 쿼리를 날려서 데이터를 넣고, 조회하고, 삭제하는걸 해보자
  • jdbc는 20년전에 하던 레거시 방식

DB 접근 준비

Dependency 추가

build.gradle

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'  
runtimeOnly 'com.h2database:h2'  
testImplementation 'org.springframework.boot:spring-boot-starter-test'  
}

DB 접속정보 추가

src/main/resources/application.properties

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

JDBC

  • 구현을 메모리에 할꺼야 or DB와 연동해서 할꺼야 라는 차이때문에 JdbcMemberRepository를 생성
  • Spring boot로 부터 constructor를 통해서 dataSource를 주입 받아야된다

  • JdbcMemberRepository 생성
    • repository/JdbcMemberRepository (내용이 길으므로 여기서는 코드 생략..)
  • SpringConfig
    • SpringConfig에서 Spring boot가 application.properties를 보고 자체적으로 bean을 생성해준다. 즉 Datasource를 만들어 준다

중요!

  • 오직 JdbcMemberRepository클래스를 만들고 interface를 확장했다.
  • Spring이 제공하는 configuration만 손대서 repository를 바꿔줬다.

Good to know!

Spring framework를 통해서 getConnection을 쓸때는 DataSourceUtils를 통해서 connection을 획득해야한다. 이전에 connection이 매번 다르면 transaction에 걸릴 수 있는데 그럼 db connection을 같은걸로 유지를 해야한다. DataSourceUtils.getConnectino()이 유지를 시켜준다.

private Connection getConnection() {  
	return DataSourceUtils.getConnection(dataSource);  
}

connection을 닫을때도 DataSourceUtils를 활용해서 DataSourceUtils.releaseConnection()으로 닫는다

private void close(Connection conn) throws SQLException {  
	DataSourceUtils.releaseConnection(conn, dataSource);  
}

Configuration for JDBC

SpringConfig에서 설정해주고 DataSource도 넣어준다

ericbyeric/firstspringdemo/SpringConfig.java

@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 MemoryMemberRepository();  
		return new JdbcMemberRepository(dataSource);  
	}  
}

Spring이 application.properties의 설정 내용을 보고 자체적으로 DataSource bean을 만들어 주고 SpringConfig constructor에 주입을 해준다

우리가 배운것!

오직 JdbcMemberRepository라는 클래스를 MemberRepository interface를 확장해서 만들고 SpringConfig.java파일 이외에 다른 어떤 코드도 손대지 않고 Repository를 변경했다

  • MemberService는 MemberRepository를 의존하고 있다
  • MemberRepository는 구현체로 MemoryMemberRepository와 JdbcMemberRepository가 있다

  • 기존에는 memberRepository를 Spring bean으로 등록을 했는데 jdbcmemberRepository를 등록 해서 바꿔 낀다.

SOLID

  • 개방-패쇄 원칙 (OCP, Open-Closed Principle)
  • 확장(기능을 추가)에는 열려있고, 수정, 변경에는 닫혀있다 이렇게 기능을 완전히 변경을 해도 application 전체를 수정 할 필요가 없다 (조립하는 코드 e.g., SpringConfig.java 는 어쩔수 없이 수정을 해야한다)’’

스프링을 쓰는 이유

객체지향의 진짜 매력은 상속 이런거 보다 구현체를 바꾸면서도 기존 코드를 바꾸지 않고 바꿀 수 있는것! 다형성을 활용한다는 의미는 인터페이스는 두고 구현체를 바꿔 끼울 수가 있는것 스프링 컨테이너가 다형성을 활용하기 편하게 지원을 해준다. DI(Dependency Injection) 덕분에 굉장히 편하게 사용 가능


Spring Integration Test

이제 DB까지 연결이 되니 그럼 테스트도 DB까지 연결이 되어야 한다. 이전에 했던 test들은 스프링과는 관련이 없고 순수하게 자바코드로만 테스트를 했다.

지금은 순수한 자바 코드로만 테스트를 할 수 없다. db connection 정보도 Spring boot가 들고 있다. 그래서 이제 테스트를 Spring과 엮어서 진행 할 예정이다.

테스트 생성

test/java/ericbyeric/firstspringdemo/service/MemberServiceIntegrationTest.java

@SpringBootTest  
@Transactional  
class MemberServiceIntegrationTest {  
	  
	@Autowired MemberService memberService;  
	@Autowired MemberRepository memberRepository;  
	  
	// @BeforeEach  
	// public void beforeEach(){  
	// memberRepository = new MemoryMemberRepository();  
	// memberService = new MemberService(memberRepository);  
	// }  
	//  
	// @AfterEach  
	// public void afterEach() {  
	// memberRepository.clearStore();  
	// }  
	  
	@Test  
	void join() {  
		// given  
		Member member = new Member();  
		member.setName("hello");  
		  
		// when  
		Long saveId = memberService.join(member);  
		  
		// then  
		Member findMember = memberService.findOne(saveId).get();  
		Assertions.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));  
		Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");  
		/*  
		try {  
		memberService.join(member2);  
		fail();  
		} catch (IllegalStateException e){  
		Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");  
		}  
		*/  
		//then  
	}  
	  
	@Test  
	void findMembers() {  
	}  
	  
	@Test  
	void findOne() {  
	}  
}

@SpringBootTest

  • 스프링 컨테이너와 테스트를 함께 실행한다

@Transactional

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

이전에는 memberRepository를 직접 만들어서 넣어주었다. 테스트는 제일 끝단에 위치한 작업이기 때문에 제일 편한 방법을 사용해도 무관하다.

이전 구현방식

MemberService memberService;  
MemoryMemberRepository memberRepository;  
  
@BeforeEach  
public void beforeEach(){  
	memberRepository = new MemoryMemberRepository();  
	memberService = new MemberService(memberRepository);  
}

여기서 beforeEach()를 지우고 필드주입으로 설정해주자 아래처럼

@Autowired MemberService memberService;  
@Autowired MemberRepository memberRepository;

@Transactional덕분에 아래 코드가 필요 없다

@AfterEach  
public void afterEach() {  
	memberRepository.clearStore();  
}

테스트는 반복이 가능해야 한다 (Transaction)

테스트는 반복이 가능해야한다. 그러나 같은 이름이 있는경우 테스트 진행 불가.. @BeforeEach를 활용해서 Delete쿼리를 날려 주어야 하나..? 번잡하다 No!

Database는 transaction이라는 개념이 있다

  • DB에 data insert query를 날리고 commit을 마지막에 해주어야 DB에 반영이 된다

test끝나고 rollback을하면? -> @Transactional 어노테이션을 테스트에 달면 테스트를 실행하고 DB에 insert query를 넣은 다음에 테스트가 끝나면 rollback을 해준다!

  • rollback의 의미는 commit을 수행하지 않는다는 뜻 -> 다음 테스트를 반복해서 진행 할 수 있다!

@Test 어노테이션 밑에 @Commit을 달면 commit을 test에서 수행해 버린다.

단위 테스트 - 순수하게 자바코드로 하면서 최소한의 단위로 테스트

통합 테스트 - Spring containerDB도 연동해서 테스트


Spring JdbcTemplate

JdbcTemplate is SQL mapper

result, set, connection.. 반복적.. 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. (중복 제거) 하지만 SQL은 직접 작성해야 한다.

JdbcTemplate은 실무에서도 많이 사용함

JdbcTemplate 생성

jdbcTemplate은 injection을 받을수 는 없다

Datasource를 받아야해

public class JdbcTemplateMemberRepository implements MemberRepository {  
  
	private final JdbcTemplate jdbcTemplate;  
	  
	public JdbcTemplateMemberRepository(DataSource dataSource) {  
		jdbcTemplate = new JdbcTemplate(dataSource);  
	)  
}

Spring에서 권장하는 방법이며 Spring이 dataSource를 자동으로 injection해준다

RowMapper

RowMapper는 데이터베이스의 반환 결과인 ResultSet객체로 변환해주는 클래스이다.

RowMapper

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

// 람다형
	return (rs, rowNum) -> {  
		Member member = new Member();  
		member.setId(rs.getLong("id"));  
		member.setName(rs.getString("name"));  
		return member;  
	};  
  
}

findById, findByName, findAll

@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 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());  
}
  • jdbc를 template method를 써서 줄이고 줄인 결과

Design pattern중에 template method pattern이 있다. 그게 많이 들어가 있어서 jdbctemplate이라고 부른다

Configuration for JdbcTemplate

SpringConfig에서 설정해주고 DataSource도 넣어준다

ericbyeric/firstspringdemo/SpringConfig.java

@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 MemoryMemberRepository();  
		// return new JdbcMemberRepository(dataSource);  
		return new JdbcTemplateMemberRepository(dataSource);  
	}  
  
}

JPA

Jdbc에서 jdbcTemplate으로 바꿨을때 반복적인 업무가 확 줄었다

그래도 SQL퀄리는 개발자가 직접 작성했어야 했다

JPA는 쿼리도 자동으로 처리해 준다 우리가 객체를 MemoryRepository에서 memory 넣듯이 jPA에 넣으면 JPA가 중간에서 DB에 sql날리고 데이터를 가져오는것들을 다 처리한다

JPA를 사용하면 SQL보다 객체 중심으로 고민 할 수 있다

Dependency 추가

build.gradle

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

정보 추가

src/main/resources/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

spring.jpa.show-sql=true

  • JPA가 날리는 SQL을 볼 수 있다

spring.jpa.hibernate.ddl-auto=none

  • create으로 세팅해주면JPA가 객체를 보고 테이블을 전부 만든다. 우리는 이 기능을 끄고 시작

what is JPA?

JPA(Java Persistence API) is interface (자바 진영의 표준 인터페이스) 그 구현체로 hibernate 같은 벤더 기술들이 있다 JPA는 객체ORM(Object Relational Mapping)이라는 기술이다

  • 매핑은 Annotation을 통해서 한다.

JPA사용

Entity Mapping(엔티티 매핑)

@Entity

@Entity
public class Member{
...
}

PK Mapping

@id, @GeneratedValue

Identity Strategy (아이덴티티 전략)

  • DB에 값을 넣으면 DB가 id를 자동으로 생성해 주는것
@Entity
public class Member{

	@Id, @GeneratedValue(strategy = GenerationType.IDENTITY)  
	private Long id;  
	private String name;
}

Column Mapping

@Column

어노테이션으로 DB와 매핑을 한다

@Entity
public class Member{

	@Id, @GeneratedValue(strategy = GenerationType.IDENTITY)  
	private Long id;  
	@Column(name="name")
	private String name;
}

DB의 Column이름이 “username”이면 @Column(name="username") 이라고 넣으면 된다.

위의 정보들로 insert, select, delete문들을 만들 수 있다.

JPA 동작

EntityManager

JPA는 모든게 EntityManager를 통해서 동작을 한다

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'를 통해서 라이브러리를 받으면 Spring boot에서 자동적으로 EntityManager를 생성해준다. 우리는 생성된 EntityManager를 injection받으면 된다.

따라서 JPA를 사용하려면 EnrityManager를 주입 받아야 한다.

repository/JpaMemberRepository.java

public class JpaMemberRepository implements MemberRepository{  
	  
	private final EntityManager em;  
	  
	public JpaMemberRepository(EntityManager em) {  
		this.em = em;  
}

save

@Override  
public Member save(Member member) {  
	em.persist(member);  
	return member;  
}
  • JPA가 insert쿼리를 만들어서 객체를 DB에 집어넣고 setId까지 해준다.

findById

@Override  
public Optional<Member> findById(Long id) {  
	Member member = em.find(Member.class, id); // (조회할 타입, 식별자)  
	return Optional.ofNullable(member);  
}
  • id가 PK(Primary Key)인 경우 이렇게 조회 가능

JPQL

findByName

@Override  
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();  
}  
  
@Override  
	public List<Member> findAll() {  
		List<Member> result = em.createQuery("select m from Member m", Member.class)  
			.getResultList();  
			return result;  
}
  • PK기반이 아닌 것들은 JPQL이라는 객체지향 쿼리언어를 사용해야 한다
  • 보통 DB의 table을 대상으로 쿼리를 날린다.
  • JPQL은 객체를 대상으로 쿼리를 날린다. 그럼 이게 SQL로 번역이 된다.

Transactional

데이터를 저장하거나 지울때 서비스쪽에 @Transactional 어노테이션을 걸어 주어야 한다.

service/MemberService.java

@Transactional  
public class MemberService {
...
}
  • JPA는 모든 join과 같은 메소드가 Transaction안에서 수행이 되어야 한다.

Configuration for JPA

ericbyeric/firstspringdemo/SpringConfig.java

@Configuration  
public class SpringConfig {  
  
//	private final DataSource dataSource;  
	private final EntityManager em;  
	  
	@Autowired  
	public SpringConfig(EntityManager em) {  
	this.em = em;  
	}
/*
	@Autowired  
	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);  
		return new JpaMemberRepository(em);
	}  
  
}
  • Spring에서 EntityManager를 자동으로 DI 해준다

또 다른 방법으로 EntityManager를 받는 방법

@Configuration  
public class SpringConfig {  
  
	@PersistenceContext
	private final EntityManager 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);
	}  
  
}
  • @PersistanceContext

JPA 내부 동작

기본적으로 Spring JPA를 세팅하면 내부에서 JPA의 구현체인 Hibernate라는 오픈소스가 사용이 된다.

HIbernate log

Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_ where member0_.name=?
Hibernate: insert into member (id, name) values (default, ?)
Hibernate: select member0_.id as id1_0_, member0_.name as name2_0_ from member member0_ where member0_.name=?

Spring Data JPA

구현 클래스를 작성할 필요 없이 인터페이스 자체만으로 개발이 끝나 버린다

public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {  
  
	@Override  
	Optional<Member> findByName(String name);  
}
  • 보면 interface만 있고 구현체도 없는데 어떻게 사용할 수 있을까?
  • 다른 메소드들은 어디있지?

Configuration for JPA

Spring Data JPA는 인터페이스가 JpaRepository를 가지고 있으면 자동으로 해당 인터페이스에 대한 구현체를 만들어서 Spring Bean에 등록해 놓는다.

ericbyeric/firstspringdemo/SpringConfig.java

@Configuration  
public class SpringConfig {  
  
	private final MemberRepository memberRepository;  
  
	@Autowired  
	public SpringConfig(MemberRepository memberRepository) {  
	this.memberRepository = memberRepository;  
	}  

	@Bean  
	public MemberService memberService() {  
//		return new MemberService(memberRepository());  
		return new MemberService(memberRepository);
	}  
	  
	@Bean  
	public MemberRepository memberRepository() {  
		// return new MemoryMemberRepository();  
		// return new JdbcMemberRepository(dataSource);  
		// return new JdbcTemplateMemberRepository(dataSource);  
		// return new JpaMemberRepository(em);
		
	}  
  
}
  • memberService도 memberRepository를 DI로 받을수 있고 memberRepository()는 필요가 없어진다.

스프링 데이터 JPA 제공 기능

  • 인터페이스를 통한 기본적인

findByName() , findByEmail() 처럼 메서드 이름 만으로 조회 기능 제공 페이징 기능 자동 제공 메소드 이름이 주어지면 그에 따라 쿼리를 짜는 방식이 있다

e.g.,) findByName() select m from Member m where m.name = ? 이라는 JPQL이 SQL로 번역되어서 실행 된다. e.g.,) findByNameAndId(String name, Long id);

단순한 작업들은 Spring Data JPA에서 인터페이스만으로 끝낼 수 있다. 보통 실무에서 단순작업이 80% 복잡한 작업이 20%..

참고

Querydsl

실무에서는 JPA와 Spring Data JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 Native 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.