김영한 님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard
JDBC : 어플리케이션 서버와 DB를 연결할 때 필요한 기술
Spring JDBC Template : 어플리케이션에서 DB로 SQL을 편리하게 보낼 수 있음
JPA : JPA라는 기술이 SQL을 직접 만들어서 DB에 쿼리를 날려준다
SpringData JPA : 스프링에서 JPA를 편리하게 사용할 수 있게한 기술
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
id bigint generated by default as identity
: DB가 자동으로 id 값을 채워줌
insert into MEMBER(NAME) values('spring')
: 데이터 삽입
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
build.gradle
에 위의 코드 추가
Java는 DB와 연결하려면 JDBC 드라이버가 필수적으로 필요
DB가 제공하는 클라이언트가 h2database
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
application.properties
에 스프링부트 데이터베이스 연결 설정 추가
spring이 DB와 연결하는 작업을 처리해주기 때문에 사용만 하면 된다
회원을 저장하는 역할은 MemberRepository
가 수행
이 때, 구현을 메모리에 할 것인지, DB와 연동하여 JDBC로 할 것인지를 결정
JdbcMemberRepository
는 MemberRepository
를 구현한 구현체
< JdbcMemberRepository >
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
}
코드가 너무 길기 때문에 게시물 가장 아래쪽에 save()와 관련된 코드만 따로 작성
DB와 연결하려면 DataSource
가 필요
javax.sql.DataSource
DataSource
는 spring에게 주입 받아야 함
개방-폐쇄 원칙(OCP, Open-Closed Principle)
확장에는 열려있고, 수정, 변경에는 닫혀있다
JDBC 기반의 MemberRepository를 만들었지만 기존 코드에 변화를 주지 않았음
스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다
데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다
< SpringConfig >
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
스프링 빈으로 등록되는 것을 MemoryMemberRepository
에서 JdbcMemperRepository
로 변경
이렇게 되면 기존 MemberService
에서는 MemberRepository
객체를 DI 받기 때문에 MemberService
의 코드를 변경하지 않아도 JdbcMemperRepository
가 MemberService
의 MemberRepository
에 주입된다
DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체
스프링부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어두기 때문에 DI를 받을 수 있다
< JdbcMemberRepository >
@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();
// RETURN_GENERATED_KEYS : DB에 insert 했을 때, key 값을 얻는다
pstmt = conn.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
// 1번은 sql의 (?)가 있는 부분을 의미하고, (?)에 member.getName()이 들어간다
pstmt.setString(1, member.getName());
// DB에 실제 쿼리가 날라가는 부분
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);
}
}
기존 MemberServiceTest 코드 복붙 후 코드를 수정하면서 진행
@SpringBootTest
: 스프링 컨테이너와 테스트를 함께 실행한다는 의미
< 수정 전 >
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
< 수정 후 >
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
직접 객체를 생성하는 것이 아니라 스프링 컨테이너에서 객체를 가져오도록 변경
수정 전에는 MemoryMemberRepository
로 객체를 생성했는데 수정 후에는 MemberRepository
로 객체를 주입받는다
< 수정 전 >
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
< 수정 후 >
@Transactional
class MemberServiceIntegrationTest {
}
DB에 데이터를 반영하려면 commit을 해야함 ( AutoCommit 제외 )
테스트 종료 후, 롤백시키면 데이터가 반영되지 않는다
@Transactional
: 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다 ( DB에 삽입했던 데이터가 지워진다 )
DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다
참고 : @Commit
이 붙으면 테스트 종료 후 commit
순수 Jdbc와 동일한 환경설정을 하면 된다
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다
단> SQL은 직접 작성해야 한다
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());
// 실행 부분
Number key = jdbcInsert.executeAndReturnKey(new
MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
SimpleJdbcInsert
는 위에서부터 4줄의 코드를 작성하면 쿼리를 작성할 필요가 없다
테이블명과 기본키를 지정해주면 INSERT 문을 만들어준다
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memeberRowMapper(), id);
return result.stream().findAny();
}
작성한 쿼리가 실행되고, 결과는 memberRowMapper()를 통해 객체로 mapping하여 넘겨줌
memberRowMapper()은 따로 작성해주어야 함
private RowMapper<Member> memeberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다
JPA를 사용하면 넣으면 JPA가 중간에서 DB에 SQL을 날리거나 데이터를 가져오는 것을 전부 처리해줌
JPA는 자바의 표준 인터페이스이고 Hibernate 라는 오픈소스 구현체가 사용됨
JPA는 ORM 기술
@Entity
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함하기 때문에 이전에 작성했던 jdbc는 제거해도 된다
JPA는 EntityManager
로 동작
build.gradle에 data-jpa가 추가되면 스프링이 자동으로 EntityManager
를 생성
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
show-sql : JPA가 생성하는 SQL을 출력한다
ddl-auto : 객체를 보고 테이블을 자동으로 생성해주는 기능
none 를 사용하면 해당 기능을 끈다
create 를 사용하면 Entity 정보를 바탕으로 테이블을 직접 생성
< Member >
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
JPA를 사용하기 위해서는 @Entity
어노테이션을 이용해서 Entity를 Mapping 해야함
@Entity
javax.persistence.Entity
이 어노테이션이 붙으면 JPA가 관리하는 Entity가 된다
PK를 mapping 해줘야 함
@Id
: 객체의 PK를 의미
@GeneratedValue
: PK 값을 위한 자동 생성 전략을 의미
GenerationType.IDENTITY
: DB가 Id를 자동으로 생성해줌
String name에 @Column(name = "username")
을 붙이면 name이 DB 테이블의 username이라는 이름의 column이 된다
< JpaMemberRepository >
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
}
EntityManager
spring이 현재 DB와 연결까지 해서 자동으로 생성
properties에서 설정한 정보들과 DB 연결 정보를 내부에 가지고 있음
또 내부적으로 DataSource를 가지고 있어서 DB와 통신하는 것을 EntityManager 내부에서 다 처리해줌
만들어진 EntityManager를 Injection 받아서 사용하면 된다
< JpaMemberRepository >
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
// 조회할 type과 식별자를 parameter로 넣어준다
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
// JPQL : 객체 지향 쿼리, 객체를 대상으로 쿼리를 날린다
// 그러면 날린 쿼리가 SQL로 번역이 됨
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() {
// 이것도 JPQL
// Entity를 대상으로 쿼리를 날림
// 객체 자체를 select
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
< SpringConfig >
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
// 원래는 @PersistenceContext가 필요
// 없어도 Spring에서 DI를 해줌
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}
JPA를 사용해 데이터를 저장하거나 변경하려면 항상 @Transactional
이 있어야 함
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야함
MemberService에 @Transactional
추가
메서드가 정상 종료되면 트랜잭션을 commit한다
만약 런타임 예외가 발생하면 rollback한다
SpringData JPA는 JPA를 편리하게 사용하도록 도와주는 기술
SpringData JPA를 사용하면, repository에 구현 클래스 없이 인터페이스 만으로 개발할 수 있음
SpringData JPA가 repository 인터페이스를 스프링 빈으로 자동 등록해준다
기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공한다
설정은 이전 JPA 설정과 동일하게 사용
< SpringDataJpaMemberRepository >
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
JpaRepository
를 extends하는 인터페이스가 있으면 SpringData JPA가 자동으로 인터페이스에 대한 구현체를 만들어주고 스프링 빈까지 등록해준다
이렇게 등록된 스프링 빈이 Injection 된다
< 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);
}
}
JpaRepository
를 extends하는 인터페이스가 있으면 등록한 객체가 없어도 Injection 받을 수 있다인터페이스를 통한 기본적인 CRUD를 제공
findByName() , findByEmail() 처럼 메서드 이름만으로 조회 기능 제공
페이징 기능 자동 제공