철학과 학생의 개발자 도전기
[Spring] 스프링 입문 - 섹션 6. 스프링 DB 접근 기술 본문
H2 데이터베이스 설치
실습을 위해 1.4.2 버전의 H2 데이터베이스를 설치한다. 그 후 실행시키면 다음처럼 창이 나온다.
연결을 누르면 test.mv.db파일이 생성되고 아래와 같은 화면이 나온다.
한번 연결한 이후에는 URL을 아래처럼 바꿔서 연결하면 된다. 웹 애플리케이션과 충돌이 안 생기도록 하는 것이라는데 정확한 원리는 필요할 때 찾아보려고 한다.
이제 DB에 member 테이블을 만들어보자.
다음 코드를 입력하고 실행을 누르면 왼쪽 목록에 MEMBER 테이블이 생긴 것을 볼 수 있다.
순수JDBC
과거의 유산인 JDBC를 이용해서 DB에 연결하는 실습을 해 볼 것이다. 현재는 전혀 사용하지 않는 방식이니 가볍게 들으려고 한다. 우선 필요한 설정부터 해준다.
그리고 JdbcMemberRepository 클래스를 만들어서 구현해주면 된다.
dataSource를 주입해주고 메소드들을 구현해주면 된다. 자세한 내용은 지금 굳이 알 필요는 없을 것 같다. 혹시라도 과거 코드를 봐야할 필요가 있을 때 찾아보면 된다.
중요한 것은 아래 화면이다.
SpringConfig 파일을 위처럼 수정만 해주면 메모리에 올려서 사용하던 객체를 바로 DB 버전으로 바꿀 수 있다. 정말 편리한 기능이다.
스프링 통합 테스트
이제 DB에 연결된 버전의 코드도 테스트를 해야한다.
@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("이미 존재하는 회원입니다.");
}
}
@SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional: 테스트를 한 후 DB를 테스트 전으로 롤백한다. 이로 인해 다음 테스트에 영향을 주지 않게 된다.
참고로 테스트는 단위 테스트로 쪼개서 하는 것이 보통 더 좋다고 한다.
스프링 JDBC Template
JdbcTemplate은 순수 JDBC에서 본 많은 중복들을 제거해준다고 한다.
repository 폴더에 JdbcTemplateMemberRepository 클래스를 만들어보자.
package hello.hellospring.repository;
import hello.hellospring.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.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;
};
}
}
@Autowired는 생성자가 하나일 때 생략가능하다. SQL 쿼리문은 직접 작성해주어야한다. 하지만 save 메소드처럼 자바로 쿼리문과 같은 역할을 수행하는 코드를 짤 수도 있다.
그 후 service 폴더에 있는 SpringConfig 클래스 새로 만든 repository를 등록해주면 된다.
테스트 코드도 잘 동작하는 모습이다. 혹시 위 코드대로 입력했는데 오류가 발생한다면 h2 데이터베이스를 실행시켰는지 확인해보자! 내가 겪었던 오류이다 하하
이제 JPA를 이용해서 SQL 쿼리문까지 자바코드로 대체해보자.
JPA
JPA는 반복적인 코드도 줄여주고 SQL 쿼리문도 만들어준다. 이로 인해 객체 중심 설계에 더 집중할 수 있다. 그러나 JPA도 엄청나게 방대한 내용을 담는 개념이므로 다른 글들과 마찬가지로 가볍게 맛보기로 배워본다.
라이브러리부터 추가해보자.
build.gradle에서 기존에 사용한 jdbc를 jpa로 바꿔준다. 그리고 코끼리 새로고침을 누르면 새로운 라이브러리를 다운받는다.
그 다음 application.properties 파일에 설정을 두가지 해준다.
JPA는 인터페이스이고 실제 구현은 여러 업체들이 경쟁한다고 한다. 지금은 hibernate를 사용한다.
JPA는 ORM이므로 객체와 DB를 맵핑해주어야한다.
package hello.hellospring.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;
}
}
@Entity를 통해 엔터티임을 알려주고 @Id로 id값을 지정한다. @GeneratedValue에서 위와 같이 설정하면 DB에서 자동으로 설정되는 id 값이라고 한다.
이제 JpaMemberRepository를 만들어서 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();
}
}
em.persist, em.find처럼 아주 간단하게 쿼리문을 대체할 수 있다. em.createQuery를 사용하려면 JPQL을 사용해야한다. 객체를 대상으로 쿼리를 날리면 SQL로 변환된다고 한다.
그리고 JPA를 사용하기 위해서 MemberService위에 @Transactional을 붙여줘야한다.
마지막으로 스프링이 JPA를 사용하도록 설정해주고 테스트코드를 돌려보자.
잘 동작한다!
스프링 데이터 JPA
스프링 데이터 JPA는 편의성의 최종판이다. 중복을 제거하고 생산성을 늘려주기 때문에 관계형 데이터베이스를 사용한다면 필수적인 기술이다. 하지만 JPA에 대한 이해가 선행되어야 제대로 활용할 수 있다.
스프링 데이터 JPA를 사용하는 리포지토리를 만들어보자. 특이한 점은 클래스가 아니라 인터페이스로 생성할 수 있다는 점이다.
그리고 Spring Config에 다음과 같이 설정해준다.
그리고 테스트를 돌려보면 잘 동작한다! 구현한 것이 없는데 어떻게 기능이 동작하는 것일까?
스프링 데이터 JPA에서 findAll, save 등의 기능을 기본적으로 제공한다. 따로 구현할 필요없이 메소드 명만 맞추면 된다. 하지만 기본으로 제공하지 않는 것은 따로 구현해줘야한다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 부트와 AWS 스터디 - 1주차 (0) | 2022.11.19 |
---|---|
[Spring] 스프링 입문 - 후기 (0) | 2022.11.05 |
[Spring] 스프링 입문 - 섹션 5. 회원 관리 예제 - 웹 MVC 개발 (0) | 2022.10.09 |
[Spring] 스프링 입문 - 섹션 4. 스프링 빈과 의존관계 (0) | 2022.10.08 |
[Spring] 스프링 입문 - 섹션 3. 회원 관리 예제 - 백엔드 개발 (0) | 2022.10.02 |