H2 데이터베이스 설치
1. 1.4.200 버전 h2 데이터베이스 설치
2. h2.bat 실행
3. 데이터베이스 파일 생성 : 내 홈에 있는 경로로 파일 경로 설정
4. 홈 디렉토리에 test.mv.db 파일 생성 확인
5. 파일 확인 이후부터는 jdbc:h2:tcp://localhost/~/test 로 접속
파일을 직접 접근하는 것이 아니라, 소켓을 통해 접근하게 되므로 여러 곳에서 충돌없이 접근 가능
6. Member 테이블 생성/조회/삽입
// 생성
create table member
(
id bigint generated by default as identity, // DB가 id 값 자동 생성
name varchar(255),
primary key (id)
);
// 조회
select * from member
// 삽입
insert into member(name) values('spring')
7. 파일로 사용했던 sql문 관리
// sql/ddl.sql
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
순수 JDBC
- 이전의 데이터 저장 방식
- JDBC API로 직접 코딩하는 것은 20년 전 이야기
1. 환경 설정 : build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
// build.gradle
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database: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. Jdbc 리포지토리 구현
// main/java/hello/hellospring/repository/JdbcMemberRepository.java
public class JdbcMemberRepository implements MemberRepository { // DB와 연동해서 Jdbc에 구현 (고전 스타일)
private final DataSource dataSource; // DB에 붙기 (연결을) 위해 사용
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(); // DB 연결
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); // 문장 제시
pstmt.setString(1, member.getName()); // values(?)의 ?와 매칭되어 그 안에 이름을 넣어줌
pstmt.executeUpdate(); // DB에 쿼리 보냄
rs = pstmt.getGeneratedKeys(); // 위에 넣었던 옵션인 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 { // 자원 반환 (release)
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(); // 조회의 경우 executeQuery 로 DB에 쿼리 보냄
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); // DataSourceUtils 를 통해 연결 획득
}
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); // DataSourceUtils 를 통해 반환
}
4. 스프링 설정 변경 : 메모리에서 Jdbc로 바꿔줌
// main/java/hello/hellospring/SpringConfig.java
@Configuration
public class SpringConfig {
// DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체
// 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어줌
// 그래서 DI를 받아 제공해줄 수 있음 (스프링이 제공해주는 datasource 를 제공 받아 주입)
private final DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
...
@Bean
// 메모리에서 Jdbc로 변경
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
}
6. 확인
- 스프링은 스프링 컨테이너의 지원으로 객체 지향의 다형성을 활용할 수 있으므로 MemberService 코드의 변화 없이
인터페이스를 두고, 쉽게 구현체를 바꿔 낄 수 있음 (메모리 → Jdbc)
즉, 스프링의 DI를 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있음!
스프링 통합 테스트
- 스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행
- H2 콘솔에서 Member 테이블을 delete 한 후 진행
delete from member;
- Test 시작 전에 @Transactional을 시작하고 DB에 쿼리를 다 날린 후, Test가 끝날 때 그 데이터를 반영하지 않도록 Rollback
이를 통해 기존처럼 지우는 코드 없이, 다음 테스트를 반복해서 실행할 수 있도록 함
// test/java/hello/hellospring/service/MemberServiceIntegrationTest.java
@SpringBootTest // 스프링 컨테이너와 테스트를 함께 실행
@Transactional // test 가 끝난 후, DB에 반영되지 않도록 rollback 하여 검증을 계속 다시 할 수 있도록 함
class MemberServiceIntegrationTest {
// 스프링 컨테이너로부터 받아옴
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository; // 구현체가 SpringConfig 로부터 올라옴
@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("이미 존재하는 회원입니다.");
}
}
- MemberServiceIntegrationTest를 통합 테스트라고 하며, MemberServiceTest를 단위 테스트라고 함
단위 테스트가 통합 테스트보다 스프링 컨테이너 없이 테스트하므로 훨씬 빠르며 좋은 테스트일 확률이 높음
스프링 JdbcTemplate
- 순수 Jdbc와 동일한 환경설정을 하면 되며,
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해 줌 - 실무에서도 많이 사용됨
// main/java/hello/hellospring/repository/JdbcTemplateMemberRepository.java
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// 생성자가 딱 하나만 있으면 @Autowired 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) { // 스프링이 DataSource 자동으로 DI
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(); // Optional 로 반환
}
@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 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;
};
}
}
// main/java/hello/hellospring/SpringConfig.java
@Configuration
public class SpringConfig {
...
@Bean
// MemberRepository는 인터페이스, MemeoryMemberRepository는 구현체
// 메모리에서 Jdbc로 변경
// Jdbc에서 JdbcTemplate로 변경
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
}
- 스프링 통합 테스트를 통해 검증하면 됨
JPA
- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있음
- JPA를 사용하면 개발 생산성을 크게 높일 수 있음
1. build.gradle 파일에 JPA, h2 데이터베이스 관련 라이브러리 추가
spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함하므로 jdbc는 주석 처리
// build.gradle
dependencies {
...
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
2. 스프링 부트에 JPA 설정 추가
show-sql : JPA가 생성하는 SQL을 출력
ddl-auto : JPA는 객체(Member)를 보고 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끔
+) ddl-auto=create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해 줌
// application.properties
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
3. JPA 엔티티 매핑
JPA는 인터페이스이며 구현체로 Hibernate 사용
JPA는 ORM 기술 (O : Object, R : Relational, M : Mapping)
// main/java/hello/hellospring/domaion/Member.java
@Entity // JPA 가 관리하는 엔티티
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // PK 매핑 - DB가 알아서 생성
private Long id; // 데이터 구분을 위해 시스템이 정한 id
private String name;
// Getter, Setter
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;
}
}
4. JPA 회원 리포지토리
저장, 조회, 업데이트는 자동으로 되므로 SQL을 짤 필요 없으며, findbyName이나 여러 개의 리스트를 가지고 사용할 때만 사용
// main/java/hello/hellospring/repository/JpaMemberRepository.java
public class JpaMemberRepository implements MemberRepository {
// 스프링부트가 엔티티매니저를 자동 생성
// application.properties, DB 등의 정보를 통해 자동으로 생성
private final EntityManager em;
public JpaMemberRepository(EntityManager em) { // 만들어진 엔티티매니저를 주입
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member); // persist 를 통해 영속, 영구 저장 - JPA가 모든 것을 해 줌
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) {
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;
*/
// 인라인으로 변경
// JPQL 쿼리 언어 > 객체 (Member Entity) 자체를 대상으로 쿼리 (id나 name이 아닌 그 자체로) > SQL 로 번역이 됨
// return em.createQuery("select m from Member as m", Member.class).getResultList();
return em.createQuery("select m from Member m", Member.class).getResultList();
}
}
5. 서비스 계층에 트랜잭션 추가
JPA를 사용하기 위해서는 항상 트랜잭션이 있어야 함
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋
만약 런타임 예외가 발생하면 롤백
그러므로 데이터 저장, 변경에 트랜잭션이 필요하며, JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행
// main/java/hello/hellospring/service/MemberService
@Transactional
public class MemberService {
...
}
6. JPA를 사용하도록 스프링 설정 변경
// main/java/hello/hellospring/SpringConfig.java
@Configuration
public class SpringConfig {
// EntityManager 를 주입 받음 (DI)
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
// MemberService와 MemberRepository를 스프링 빈에 등록한 후,
// 스프링 빈에 등록되어 있는 MemberRepository를 MemberService 주입
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
// MemberRepository는 인터페이스, MemeoryMemberRepository는 구현체
// 메모리에서 Jdbc로 변경
// Jdbc에서 JdbcTemplate로 변경
// JdbcTemplate에서 JPA로 변경
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
7. 통합 테스트 실행
스프링 데이터 JPA
- 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술
- 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있음
- 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공
- 스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면
개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있음
1. 스프링 데이터 JPA 회원 리포지토리
// main/java/hello/hellospring/repository/SpringDataJpaMemberRepository.java
// 스프링 데이터 JPA 가 자동으로 구현체를 만들어 주어 SpringDataJpaMemberRepository 를 스프링 빈에 자동으로 등록
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
// JPQL : select m from Member m where m.name = ?
@Override
Optional<Member> findByName(String name);
}
2. 스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
// main/java/hello/hellospring/SpringConfig.java
@Configuration
public class SpringConfig {
// 스프링 데이터 JPA
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) { // 스프링 데이터 JPA가 만들어 놓은 구현체가 주입되어 등록
this.memberRepository = memberRepository;
}
// JPA에서 SpringDataJPA로 변경
@Bean
public MemberService memberService() {
return new MemberService(memberRepository); // 주입 받은 것을 등록
}
}
3. 통합 테스트 실행
- 스프링 데이터 JPA 제공 클래스
1) 스프링 데이터 JPA가 객체를 생성해서 SpringDataJpaMemberRepository 를 스프링 빈으로 자동 등록
2) JpaRepository와 상위 레포지토리 (PagingAndSortingRepository, CrudRepository) 에서 기본 CRUD 메서드 제공 - 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용
Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있음 - 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용
참고 영상
'Java-Spring > 스프링 입문' 카테고리의 다른 글
스프링 입문 - 목차 (0) | 2023.07.03 |
---|---|
[스프링 입문] AOP (0) | 2022.02.10 |
[스프링 입문] 회원 관리 예제 - 웹 MVC 개발 (0) | 2022.01.21 |
[스프링 입문] 스프링 빈과 의존관계 (0) | 2022.01.20 |
[스프링 입문] 회원 관리 예제 - 백엔드 개발 (0) | 2022.01.19 |