이번 목표는 2가지다.
- DB 접근 기술 4가지
- 코드 수정없이 Repository 교체하는 방법
Service는 그대로 두고 DB에 접근하는 방법만 교체하면서 동일한 결과를 확인하게 된다.
데이터베이스(DB)를 연결해보자
https://www.h2database.com/html/main.html
Windows Installer를 클릭해 설치하면 된다.
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
H2 Console을 실행시키면 브라우저가 열릴것이다.
에러가 발생한다면 H2를 껐다가 다시 켜서 URL에서 IP부분만 localhost로 입력하면 연결된다.
IP 외에 부분을 수정하면 안된다

이제 JDBC URL을 변경해주자 (jdbc:h2:tcp://localhost/~/test)
연결을 눌러 입장하면 아래 사진과 같은 내용이 보인다

빈공간에 DDL을 작성하여 테이블을 만들자
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
실행을 눌러 테이블이 생성된것을 확인할 수 있다. MySQL을 접해 봤다면 어렵지 않을 것이다.
DB 연결을 유지한 채로 프로젝트를 수정할 것이다.
1. JDBC
의존성을 추가하자
// gradle (기준)
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>runtime</scope>
</dependency>
버전 확인하고 추가하길 바란다.
application.properties에 내용을 추가하자

spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
여기까지가 기본 세팅이다
JDBC 사용을 위한 Repository를 새로 만들자

public class JdbcMemberRepository implements MemberRepository {
private final DataSource 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);
}
}
바뀐 Repository테스트를 위해 새로운 테스트를 만들자
Service에서 Repository를 사용하기 때문에 새로운 Service 테스트를 작성하겠다.

@SpringBootTest
@Transactional // rollback after test
public class MemberServiceIntegrationTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
void join() {
// given
Member member = new Member();
member.setName("spring");
// when
Long result = memberService.join(member);
// then
Member findMember = memberService.findOne(result).get();
Assertions.assertThat(findMember.getName()).isEqualTo(member.getName());
}
@Test
void ExceptionOfJoin() {
// given
Member member1 = new Member();
member1.setName("spring");
memberService.join(member1);
Member member2 = new Member();
member2.setName("spring");
// when, then
Assertions.assertThatThrownBy(() -> memberService.join(member2))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("Already Existence");
}
@Test
void findMembers() {
// given
Member member1 = new Member();
member1.setName("spring1");
memberService.join(member1);
Member member2 = new Member();
member2.setName("spring2");
memberService.join(member2);
Member member3 = new Member();
member3.setName("spring3");
memberService.join(member3);
// when
List<Member> result = memberService.findMembers();
// then
for (Member m : result) {
Assertions.assertThat(m.getName()).containsAnyOf(member1.getName(), member2.getName(), member3.getName());
}
}
@Test
void findOne() {
// given
Member member = new Member();
member.setName("spring");
Long memberId = memberService.join(member);
// when
Member result = memberService.findOne(memberId).get();
// then
Assertions.assertThat(result.getName()).isEqualTo(member.getName());
}
}
스프링 빈도 바꿔주자
@Configuration
public class SpringConfig {
// 1
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);
}
}
이 테스트 코드는 Repository를 바꾸면서 반복해 검증할것이다
검증이 확인됐다면 프로그램을 실행하여 동일하게 진행된다는 것을 확인할 수 있다.
가장 오래된 JDBC 기술이다. 반복되는 코드들이 많고 단순한 기능임에도 라인수가 상당한 것을 알 수있다.
2. JDBC Template
의존성, DB 환경 설정 변경없이 새로운 Repository를 만들어 스프링 빈으로 등록하면 된다
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.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;
};
}
}
스프링 빈을 등록하자
@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);
}
}
테스트 실행 전 H2 DB를 비우고 시작하는게 에러를 방지할 수 있다
truncate tabel member;
JDBC 때 만든 테스트를 다시 실행하여 확인 후에 프로그램을 실행해 동일한 동작을 하는지 확인하자
코드를 보면 반복적인 내용을 줄이고 SQL만 작성하여 전보다 의미있는 내용들로 구성되있는 것을 알 수 있다.
3. JPA
의존성과 환경 설정을 수정해야한다
// gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc' // 주석 || 삭제
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // 추가
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
// maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.2.1</version>
</dependency>
프로젝트의 spring 버전을 확인해야한다
환경 설정 수정
application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
## show sql for jpa
spring.jpa.show-sql=true
## reject auto creating table
spring.jpa.hibernate.ddl-auto=none
도메인 Member 클래스 어노테이션 추가
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
... 이하 생략
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 하기 때문이다
Service에 어노테이션 추가
@Transactional // jpa 사용시 추가해야함
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
... 이하 생략
새로운 Repository 생성
public class JpaMemberRepository implements MemberRepository {
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) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList()
.stream().findAny();
}
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
스프링 빈 수정
@Configuration
public class SpringConfig {
// private final DataSource dataSource;
//
// @Autowired
// public SpringConfig(DataSource dataSource) {
// this.dataSource = dataSource;
// }
private final EntityManager em;
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);
}
}
테스트 코드 수행 후 프로그램을 실행하여 결과를 확인하자
4. Spring Data JPA
JPA를 더욱 편리하게 사용할 수 있도록 만들어졌다. JPA에 대한 이해가 충분하여야 간소화된 부분에 대하여 이해가 가능하다.
레포지토리에 대한 새로운 인터페이스 1개 만들고 스프링 빈을 수정하면 마무리 된다
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
스프링 빈을 수정하자
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
// return new MemberService(memberRepository());
return new MemberService(memberRepository);
}
}
위에서 만든 SpringDataJapMemberRepository는 스프링이 자동으로 주입시켜 준다
마지막으로 테스트 코드로 검증 후에 어플리케이션을 실행하여 결과를 확인하자
현 시장은 JPA가 정말 많이 사용되고 있다. 스프링 사용 방법도 중요하지만 데이터를 다루는 기술도 알아야 하기 때문에 이해가 필요하다. 그러나 앞으로도 개발의 효율을 위해 지속적으로 발전된 기술을 이용하려면 기초를 잘 다져놔야한다.
스프링 가볍게 느껴보자 (3)
마지막으로 AOP에 대해서 가볍게 알아보자 해당 사진은 서비스 기능 중 join() 함수를 실행 한 결과다 두번째 파트에서 DB기술을 변경하면서 마지막에 사용했던 JPA를 사용했을 때의 모습이다. 당연
cloakinghost.tistory.com
'Spring' 카테고리의 다른 글
| 스프링 가볍게 느껴보자 (4) (0) | 2024.01.13 |
|---|---|
| 스프링 가볍게 느껴보자 (3) (0) | 2024.01.11 |
| 스프링 가볍게 느껴보자 (1) (0) | 2024.01.09 |
| API (1) | 2024.01.07 |
| 정적 컨텐츠, MVC와 템플릿 엔진 (0) | 2024.01.07 |