H2 데이터베이스 설치
이 전까지는 메모리를 사용했지만 이제부터 데이터베이스를 사용하기 위해 H2를 설치합니다.
강의에서는 맥을 사용했는데 저는 윈도우라 조금 다른 부분이 있더라구요.
H2 설치 홈페이지에서 "Window Installer"를 받아주면 Program Files (x86) 폴더에 H2폴더가 생깁니다.
여기 경로에 들어가서 강의에선 h2.sh를 실행하라고 하는데 윈도우는 h2.bat을 실행해야합니다.
실행되면 아래와 같은 화면이 뜹니다.
그리고 충돌로 인한 오류를 방지하기 위해 JDBC URL을 jdbc:h2:tcp://localhost/~/test로 바꿔줍니다.
그리고 아래와 같이 우리가 기존에 만들었던 멤버 테이블을 생성합니다.
select * from member;
를 입력하여 저장된 ID와 NAME을 확인할 수 있습니다.
insert into member(name) values('spring');
를 하면 아래와 같이 멤버가 추가된 것을 확인할 수 있습니다.
순수 JDBC
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);
}
}
JdbcMemberRepository는 이런 식으로 작성했는데 솔직히 이해를 잘 못했습니다...
그리고 리포지토리를 메모리에서 DB로 바꿔줬기 때문에 SpringConfig의 내용을 아래와 같이 바꿉니다.
@Configuration
public class SpringConfig {
private DataSource dataSource;
@Autowired
private 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);
}
}
이렇게 바꿔주면 다른 파일 변경없이 바로 DB로 연결됩니다.
이러한게 가능한건
개방-폐쇄 원칙(OCP, Open-Closed Principle) : 확장에는 열려있고, 수정, 변경에는 닫혀있다.
라는 기능 때문이다.
스프링 통합 테스트
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//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("이미 존재하는 회원입니다.");
}
}
테스트 코드는 위와 같다.
이 전까지는 메모리를 사용했지만 이제는 DB를 사용하기 때문에 스프링 컨테이너와 테스트를 함께 실행하기 위한 @SpringBootTest 애너테이션을 사용한다.
테스트를 할 때 DB에 데이터가 남지 않도록 하기 위해 @Transactional 애너테이션을 사용해서 테스트 완료 후 항상 롤백해준다.
Jdbc Template
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
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 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 (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
이 전에 해보았던 순수 Jdbc의 반복 코드를 제거한게 Jdbc Template입니다.
SQL은 직접 작성해야합니다.
순수 Jdbc보다는 매우 간단해보이지만 깊게 들어가면 이 코드도 복잡하게 설명된다고 합니다.
이번에도 동일하게 SpringConfig를 변경하면 연결이 됩니다.
JPA
JPA는 Jdbc Template에서 직접 만들어야했던 sql도 JPA가 직접 만들어줍니다.
JPA를 사용하면 개발 생산성을 크게 높일 수 있습니다.
JPA를 사용하기 위한 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
JPA 엔티티
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member);
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
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();
}
}
JPA 회원 리포지토리
package hello.hello_spring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@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;
}
}
import org.springframework.transaction.annotation.Transactional
@Transactional
public class MemberService {}
이건 서비스 계층에 트랜잭션 추가한건데
org.springframework.transaction.annotation.Transactional 를 사용해야합니다.
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋합니다.
만약 런타임 예외가 발생하면 롤백한다. JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 합니다.
JPA도 마찬가지로 SpringConfig를 변경해줘야합니다.
스프링 데이터 JPA
스프링 부트와 JPA를 사용했을 때 생산성이 매우 증가하고 개발할 코드가 매우 줄어든다고 합니다.
JPA에 대한 이해가 어려우니 이건 더 쉽지 않네요..
JPA는 따로 조금 더 공부해보도록 하겠습니다..!
'WINK-(Web & App) > Spring Boot 스터디' 카테고리의 다른 글
[2025 1학기 스프링부트 스터디] 김민서 #7주차 (0) | 2025.05.24 |
---|---|
[2025 1학기 스프링 부트 스터디] 정다은 #7주차 (0) | 2025.05.24 |
[2025 1학기 스프링부트 스터디] 최비성 #7주차 (0) | 2025.05.22 |
[2025 1학기 스프링부트 스터디] 최비성 #6주차 (0) | 2025.05.22 |
[2025 1학기 스프링부트 스터디] 장민주 #6주차 (1) | 2025.05.20 |