h2 데이터베이스
- 다운로드 및 설치
- https://www.h2database.com/html/download-archive.html
(최근에 나온 버전을 설치하면 일부 기능이 정상 동작하지 않을수 있어 1.4.200버전을 설치하기를 권장한다.)
- https://www.h2database.com/html/download-archive.html
- 권한 주기 (mac 사용자만)
- chmod 755 h2.sh
- 실행하기
- window : h2.bat
- mac : ./h2.sh
- 데이터베이스 파일 생성
- Embedded 설정
- jdbc:h2~/test (최초 한번)
- 파일 생성 확인 : ~/test.mv.db ➡ 홈 디렉토리(User\사용자이름)에서 확인!
- 접속하기
- jdbc:h2:tcp://localhost/~/test
- jdbc:h2:tcp://localhost/~/test
테이블 생성
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity, # id 자동 생성
name varchar(255),
primary key (id)
);
데이터 삽입
insert into member (name) values ('spring')
insert into member (name) values ('spring2')
Jdbc
환경 설정
👉🏻 jdbc, h2 데이터베이스 관련 라이브러리 추가
📁 build.gradle
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
👉🏻 스프링 부트 데이터베이스 연결 설정 추가
📁 resources/application.properties
spring.datasource.url= jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
Jdbc 리포지토리 구현
JDBC API로 직접 코딩하는 것은 20년 전 이야기로, 참고만 하고 넘어가자
👉🏻 Jdbc 회원 리포지토리
📁 repository/JdbcMemberRepository
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource; // DB 연동을 위한 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();
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
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);
}
}
@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();
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);
}
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);
}
}
- 회원 저장하는 역할은 MemberRepository가 하지만 그 구현을 메모리에서 할 것이냐, DB에 연동해서 JDBC로 할 것이냐의 차이다.
- DB에 연동하려면 DataSource라는 것이 필요하다.
👉🏻 스프링 설정 변경
📁springconfig
@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);
}
}
- DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다.
- 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 둔다. 따라서 DI를 받을 수 있다.
💡 구현 클래스 이미지
💡 스프링 설정 이미지
- 데이터를 메모리가 아닌 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
- 이렇게 스프링의 DI(Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
- 개방-폐쇄 원칙 (OCP, Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다는 원칙
스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보도록 하겠다.
👉🏻 회원 서비스 스프링 통합 테스트
📁 test/serviece/MemberServiceIntegrationTest
@SpringBootTest // 스프링 컨테이너와 테스트 함께 실행
@Transactional // 테스트 완료 후 롤백 진행
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@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)); // 중복회원일 경우 IllegalStateException 예외가 발생하는지 확인
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); // 에러 메세지 확인
}
}
- @SpringBootTest
- 스프링 컨테이너와 테스르를 함께 실행한다.
- @Transactional
- 테스트 케이스에 @Transactional 어노테이션이 있으면, 테스트 시작 전 트랜잭션을 먼저 시작하고, 테스트 완료 후에 항상 롤백한다. 롤백 시 DB에 데이터가 반영되지 않는다.
- 즉, DB에 데이터가 남지 않아 다음 테스트를 반복해서 실행 할 수 있도록 한다.
❓ 트랜잭션
- 데이터베이스의 상태를 변경하는 작업 또는 한번에 수행되어야 하는 연산들
- commit을 자동으로 수행해준다.
- 예외 발생 시 rollback 처리를 자동으로 수행해준다.
- @Transactional
- 선언적 트랜잭션
- 적용된 범위에서는 트랜잭션 기능이 포함된 프록시 객체가 생성되어 자동으로 commit 혹은 rollback을 진행해준다.
JdbcTemplate
- 스프링 JdbcTemplate와 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다.
- SQL은 직접 작성해야 한다.
환경 설정
- 위 Jdbc와 동일하게 환경설정을 해준다.
JdbcTemplate 리포지토리 구현
📁 repository/JdbcTemplateMemberRepository
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// @Autowired // 생성자가 하나일 경우 @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(), 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());
}
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
@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);
}
}
JPA
- JPA는 기존의 반복 코드는 물론이고, 기본적이 SQL도 JPA가 직접 만들어서 실행해준다.
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환 할 수 있다.
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
환경 설정
👉🏻 Jpa, h2 데이터 베이스 관련 라이브러리 추가
📁 build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
/* h2 데이터베이스 관련 라이브러리 */
runtimeOnly 'com.h2database:h2'
/* jpa 관련 라이브러리 */
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
/* jdbc 관련 라이브러리 */
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함한다.
따라서 jdbc 관련 라이브러리는 제거해도 된다.
👉🏻 스프링 부트에 JPA 설정 추가
📁 resources/application.properties
spring.datasource.url= jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
# jpa가 생성한 sql을 볼 수 있음
spring.jpa.show-sql=true
# 자동으로 테이블 생성해주는 기능 off
spring.jpa.hibernate.ddl-auto=none
- show-sql : JPA가 생성하는 SQL을 출력한다.
- ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none을 사용하면 해당 기능을 끈다.
- creat를 사용하면 엔티티(Entity) 정보를 바탕으로 테이블도 직접 생성해준다.
👉🏻 JPA 엔티티 매핑
📁 domain/Member
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
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;
}
}
- JPA는 ORM이라는 기술로 객체와 관계형 데이터베이스를 매핑한다. 이때 매핑은 어노테이션으로 한다.
- @Entity : JPA가 관리하는 엔티티라는 의미
- @Id @GneratedValue(strategy = GenerationType.IDENTITY) : @Id는 식별자(PK)로, id값은 데이터에 값을 저장할 때 DB가 알아서 생성해주도록 했다. 이러한 방식을 IDENTITY라 한다.
- 이렇게 어노테이션을 가지고 데이터베이스와 매핑을 진행 한 뒤, 그 정보를 통해 알아서 SQL 쿼리를 만들어주는 식으로 JPA가 동작한다.
❓ ORM (Object-Relation Mapping)
- DB 데이터 ⬅ mapping ➡ Object 필드
- 객체를 통해 간접적으로 디비 데이터를 다룬다.
- 객체와 디비의 데이터를 자동으로 매핑해준다.
- SQL 쿼리가 아니라 메소드로 데이터를 조작할 수 있다.
- 객체간 관계를 바탕으로 SQL을 자동으로 생성한다.
JPA 리포지토리 구현
📁 repository/JpaMemberRepository
public class JpaMemberRepository implements MemberRepository {
// JPA 사용을 위해 EntityManager 주입 받음
private final EntityManager em;
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);
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() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
- JPA는 EntityManager를 통해 모든 것을 동작한다.
- build.gradle에서 data-jpa 라이브러리를 받으면 스프링 부트가 EntityManager를 자동으로 만들어준다.
- 따라서 JPA를 쓰려면 만들어진 EntityManager를 주입 받아주면 된다.
- save
- persist()는 영구저장 한다는 의미로, 객체(member)를 저장
- SQL 작성 필요 x
- findById
- find(조회타입, 식별자) 메소드를 호출해 조회
- SQL 작성 필요 x
- findByName, findAll
- JPQL이라는 객체지향쿼리 언어를 사용한다. (테이블이 아닌 객체를 대상으로 쿼리 작성)
👉🏻 서비스 계층에 트랜잭션 추가
📁 service/MemberService
@Transactional
public class MemberService {...}
- @Transactional
- JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
- 따라서 서비스 계층에 트랜젝션 어노테이션 달아준다.
👉🏻 JPA를 사용하도록 스프링 설정 변경
📁 SpringConfig
@Configuration
public class SpringConfig {
// private final DataSource dataSource;
// public SpringConfig(DataSource dataSource) {
// this.dataSource = dataSource;
// }
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 JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
👉🏻 회원가입 통합 테스트 실행
- 알아서 SQL 쿼리가 작성된 것을 확인 할 수 있다.
스프링 데이터 JPA
스프링 부트와 JPA만 사용해도 개발 생산성이 많이 증가하고, 개발해야 할 코드도 확연히 줄어든다.
더 나아가 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들을 확연하게 줄일 수 있는 마법같은 스프링 데이터 JPA를 알아보자. ( 스프링 데이터 JPA는 JPA를 편리하게 사용할 수 있도록 도와주는 기술이므로, JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습하는 것을 권장함 )
- 스프링 데이터 JAP를 사용하면, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다.
- 스프링 데이터 JPA는 기본 CRUD 기능을 모두 제공한다.
환경 설정
- 앞의 JPA 설정을 동일하게 사용한다.
스프링 데이터 JPA 리포지토리 구현
👉🏻 스프링 데이터 JPA 회원 리포지토리
📁 repository/SpringDataMemberRepository
public interface SpringDataMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
// JPQL : select m from Member m where m.name = ?
@Override
Optional<Member> findByName(String name);
}
인터페이스를 만들고 스프링 데이터가 제공하는 JpaRepository를 해놓으면 스프링 데이터 JPA가 인터페이스에 대한 구현체를 만들어 스프링 빈에 등록한다.
👉🏻 스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
📁 SpringConfig
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
- 스프링 데이터 JPA가 스프링 빈에 등록해 놓은 객체를 주입받아 주면 된다.
👉🏻 회원가입 통합 테스트 실행
스프링 데이터 JPA 제공 클래스
- 스프링 데이터 JAP 제공 기능
- 인터페이스를 통한 기본적인 CRUD(Creat, Read, Update, Delete) 기능 제공한다.
- findByName(), findByEmail() 처럼 메소드 이름 만으로 조회 기능 제공한다.
- 페이징 기능 제공한다.
Reference
https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h
'🌱 Spring > Core' 카테고리의 다른 글
[기본] #3 객체 지향 원리 적용 (0) | 2022.02.22 |
---|---|
[기본] #2 회원, 주문, 할인 도메인 개발 및 테스트 (0) | 2022.02.21 |
[기본] #1 객체 지향 설계와 스프링 (0) | 2022.02.18 |
[입문] #7 AOP (0) | 2022.02.17 |
[입문] #5 웹 MVC 개발 (0) | 2022.02.15 |
[입문] #4 스프링 빈과 의존관계 (0) | 2022.02.15 |
[입문] #3 회원관리 예제 만들기 (0) | 2022.02.15 |
[입문] #2 정적웹, MVC, API (0) | 2022.02.14 |