- 스프링부트
- string
- DI
- spring security 6
- 스프링
- mysql
- nginx
- Django
- spring
- session
- @transactional
- java
- ORM
- Docker
- Git
- 문자열
- spring mvc
- SSL
- 프로그래머스
- sql
- spring boot
- springboot
- select
- join
- AWS
- 데이터베이스
- PYTHON
- 자바
- jpa
- 1차원 배열
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
개발하는 자몽
스프링 부트 공부 (9), 스프링 DB 접근 기술 본문
이 글은 아래 강의를 기반으로 작성됩니다.
다음 순서로 진행한다.
- 스프링 통합 테스트
- 스프링 JdbcTemplate
- JPA
- 스프링 데이터 JPA
본래 강의 순서대로라면 "H2 데이터베이스 설치"와 "순수 JDBC"가 있어야 하는데 데이터베이스 설치를 따로 다룰 필요는 없어 보이고, 순수 JDBC는 요즘 실무에서 쓰지 않는 걸로 알고 있기 때문에 넘긴다.
스프링 통합 테스트
이전까지 했던 테스트는 전혀 스프링과 관련이 없는 순수한 Java 코드로만 이루어져 있었다.
순수 Java 테스트
class MemberServiceTest {
MemberService memberService; // = new MemberService();
MemoryMemberRepository memberRepository; //= new MemoryMemberRepository(); //MemberService의 repository 객체와 다름
@BeforeEach
public void beforeEach() { // 테스트 실행 전에
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); //MemberService에 넣어줌 : 같은 repository 사용
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void join() {
// given : 뭔가 주어졌는데
Member member = new Member();
member.setName("hello");
// when : 이거를 실행했을 때
Long saveId = memberService.join(member);
// then : 결과가 이게 나와야 해
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName(), is(equalTo(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));
assertThat(e, is(equalTo("이미 존재하는 회원입니다.")));
/* 이거 때문에 try-catch문 넣기는 좀 그래서 위와 같은 방법을 쓸 수 있음
try{
memberService.join(member2);
fail("예외가 발생해야 합니다.");
} catch(IllegalStateException e) {
assertThat(e.getMessage(), is(equalTo("이미 존재하는 회원입니다.")));
}
*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
→ 가정 : 이전 강의(순수 JDBC)에서 H2 데이터베이스와 연결
이제는 데이터베이스 커넥션 정보를 스프링 부트가 관리하고 있기 때문에 순수한 Java 코드로만 테스트를 할 수 없다.
스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.
@SpringBootTest //스프링 테스트 할 때 사용하는 어노테이션
@Transactional
public class MemberServiceIntegrationTest {
/*
테스트 케이스할 때는 그냥 편하게 field로 주입받기
*/
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void join() {
// given : 뭔가 주어졌는데
Member member = new Member();
member.setName("spring");
// when : 이거를 실행했을 때
Long saveId = memberService.join(member);
// then : 결과가 이게 나와야 해
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName(), is(equalTo(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));
assertThat(e, is(equalTo("이미 존재하는 회원입니다.")));
}
}
스프링을 테스트 할 때는 @SpringBootTest, @Transactional을 추가하면 된다. 이전에는 직접 객체를 생성해서 의존성을 주입했다. 이제는 스프링 컨테이너한테 MemberService와 MemberRepository를 Constructor Injection으로 요청해야 한다. 하지만 테스트 코드를 짤 때는 편하게 Field Injection으로 하면 된다.
@SpringBootTest
위 어노테이션은 스프링 컨테이너와 테스트를 함께 실행한다. 진짜 스프링을 띄워서 테스트를 하는 것이다. (해당 어노테이션에 대한 자세한 설명은 다음 블로그 글을 참고)
@Transactional
테스트는 반복할 수 있어야 하므로 순수 Java 코드로 이루어진 테스트를 할 때는 메모리 DB에 있는 데이터를 다음 테스트에 영향을 주지 않기 위해 @BeforeEach, @AfterEach를 사용했는데 스프링 테스트에서는 @Transcational을 사용하므로 필요 없다.
데이터베이스는 기본적으로 트랜젝션(Transaction) 개념이 존재한다. DB에 데이터를 삽입(insert) 한 후 커밋(commit)을 해줘야 DB에 반영이 된다(보통 auto commit으로 되어있음). 그리고 테스트 후 롤백(rollback)을 하면 DB에 반영되었던 모든 것들이 사라진다. 이러한 일을 @Transactional 어노테이션이 처리해준다.
@Transactional을 테스트 케이스에 적용하면 테스트를 실행할 때 트랜젝션을 먼저 실행한다. 그리고 DB에 데이터를 반영하고 테스트 후 롤백한다. 따라서 DB에 넣었던 데이터는 깔끔하게 다 지워지므로 즉, DB에 반영되지 않는다.
결론적으로 트랜젝션이 롤백되기 때문에 DB에 데이터가 반영이 안 되므로 테스트를 여러 번 반복할 수 있게 된다.
Note
- 단위 테스트 : 순수한 Java 코드이면서 최소한의 단위로 테스트하는 것
- 통합 테스트 : 스프링 컨테이너랑 DB 연동해서 테스트하는 것
대부분 순수한 단위 테스트가 좋은 테스트일 확률이 높다. 단위로 쪼개서 테스트할 수 있고, 스프링 컨테이너 없이 테스트할 수 있도록 하자.
스프링 컨테이너를 꼭 올려야 되는 경우는 잘못된 테스트 설계일 확률이 높다. 필요한 경우도 있지만, 단위 테스트가 가능하도록 하는 게 보통은 좋은 테스트이다.
스프링 JdbcTemplate
Jdbc 사용을 위해 build.gradle에 dependency를 추가해주자.
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
repository 패키지에 JdbcTemplateMemberRepository 클래스를 하나 만든다. JdbcTemplate을 쓰기 위해서는 객체를 하나 생성해야 하는데 DataSource를 Injection 한다.
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) { //스프링에서 자동으로 injection
jdbcTemplate = new JdbcTemplate(dataSource);
}
}
조회 쿼리 - findById, findByName, findAll
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny(); //Optional로 반환
}
@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());
}
결과를 rowMapper로 매핑해줘야 한다. 객체 생성에 대한 것은 여기서 콜백(callback)으로 정리된다. member 객체가 생성돼서 부른 곳으로 넘겨준다.
//member로 객체에 매핑
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
회원가입 - save
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id"); //테이블명 & pk값 -> insert 쿼리
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
관련 내용은 document에 잘 나와있다. 사실 위 코드에 쓰인 영어만 해석해도 부가적인 설명 없이 코드 이해가 가능하다.
JdbcTemplateMemberRepository 전체 코드
package hello.hellospring.repository;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
//@Autowired //생성자가 하나일 때는 @Autowired 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) { //스프링에서 자동으로 injection
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(); //Optional로 반환
}
@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());
}
//member로 객체에 매핑
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
SpringConfig
마지막으로 설정 파일에 들어가서 스프링 빈 등록.
package hello.hellospring;
@Configuration
public class SpringConfig {
private DataSource datasource;
@Autowired
public SpringConfig(DataSource datasource) {
this.dataSource = datasource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new JdbcTemplateMemberRepository(dataSource);
}
}
코드 작성 후 위에서 작성한 스프링 통합 테스트 코드를 실행하여 문제가 없는지 반드시 확인하자.
JPA
순수 Jdbc에서 JdbcTemplate으로 바꾸면서 개발해야 하는 반복적인 코드가 줄었다. 그럼에도 아직은 개발자가 직접 SQL문을 작성해야 한다. 하지만 JPA를 사용하면 SQL문을 JPA가 자동으로 처리해줌으로써 개발 생산성을 크게 높일 수 있다.
JPA 사용은 단순히 SQL문을 만들어주는 것을 넘어서, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
JPA를 사용하기 위해서는 build.gradle에 dependency를 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' //내부에 jdbc 관련 라이브러리를 포함하므로 jdbc 제거해도 됨
application.properties에 JPA 설정을 추가해주자.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
JPA를 사용하면, JPA가 객체를 보고 스스로 테이블을 생성한다. 지금은 이미 생성한 테이블을 사용하므로 ddl-auto기능을 none으로 설정한다.
Entity 매핑
JPA는 ORM(Object-Relational Mapping) 기술로, 어노테이션으로 entity를 매핑해야 한다.
package hello.hellospring.domain;
import javax.persistence.*;
@Entity //jpa가 관리하는 entity
public class Member {
/*
IDENTITY - @GeneratedValue(strategy = GenerationType.IDENTITY)
: 기본키 생성을 데이터베이스에게 위임하는 방식
: id 값을 따로 할당하지 않아도 데이터베이스가 자동으로 AUTO_INCREMENT를 하여 기본키를 생성
출처 - https://velog.io/@gudnr1451/GeneratedValue-%EC%A0%95%EB%A6%AC
*/
// 기본키 자동 생성 : @Id, @GeneratedValue
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// @Column(name="name") : name이 DB에서 username이면 name="username"으로 하면 된다. 지금은 DB에 name으로 되어 있어서 굳이 필요치 않다.
private String name;
//getter, setter 자동생성 단축키 : Alt + Insert
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
ORM 관련 블로그 글
@GeneratedValue 관련 참고 글
Repository 생성
JPA를 사용하려면 entityManager를 주입받아야 한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em; //JPA는 EntityManager로 모든게 동작함, jpa 라이브러리와 스프링이 알아서 다 해준다~
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
}
회원가입 - save
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
JPA가 Insert 쿼리를 만들고, id 처리(setId)까지 다 해준다.
조회 - findById, findByName, findAll
findById는 find를 이용한다. 타입과 식별자 PK값을 넣어주면 조회가 가능하다. (PK 조회)
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id); //pk 조회
return Optional.ofNullable(member);
}
findByName과 findAll은 JPQL(쿼리 언어)을 이용해야 한다. 보통 테이블 대상으로 쿼리문을 작성하는데 JPQL은 객체 대상으로(정확히는 entity를 대상으로) 쿼리문을 작성한다. 이 쿼리문은 SQL로 번역된다.
먼저 findAll부터 보자.
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class) //JPQL 쿼리 언어, 객체(entity)를 대상으로 쿼리를 날림.
.getResultList();
}
"select m from Member m where m.name = :name"
SQL의 경우 조회할 때 * 을 쓰는데, 여기서는 Member 객체, 그 자체를 select 한다. (select m)
findByName은 아래와 같이 작성하자.
@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();
}
위에서 본 것처럼 PK기반이 아닌 것들은 JPQL을 작성해야 한다. 또한 JPA를 사용할 때 주의할 점은 트랜젝션(for 데이터 저장, 변경,...)이 있어야 한다. 서비스에 추가해주자.
/*
JPA를 쓰려면 Transaction이 필요함 -> Service에 추가(회원가입에만 필요해서 거기에 해둬도 되는데.. 일단 여기에)
JPA는 Join 들어올 때 모든 데이터 변경이 Transaction 안에서 실행되어야 함.
*/
@Transactional
public class MemberService {
private final MemberRepository memberRepository; //= new MemoryMemberRepository();
@Autowired
public MemberService(MemberRepository memberRepository) { // Alt + Insert : 생성자 단축키
//MemberService 입장에서 외부에서 객체를 넣어줌 : Dependency Injection(DI)
this.memberRepository = memberRepository;
}
...
}
SpringConfig
마지막으로 SpringConfig에 EntityManager 빈을 등록해주자.
package hello.hellospring;
import hello.hellospring.repository.JpaMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
@Configuration
public class SpringConfig {
private EntityManager em;
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}
이제 위에서 작성해둔 스프링 통합 테스트로 확인해보자.
실행하면 터미널에서 Hibernate와 함께 쿼리문(for DB)들을 확인할 수 있다. Spring Data Jpa를 세팅하면 기본적으로 Hibernate라는 오픈소스 구현체가 사용되기 때문이다. (더 자세한 내용은 아래 블로그 글을 참고)
스프링 데이터 JPA
스프링 데이터 JPA를 사용하면 인터페이스만으로도 개발을 완료할 수 있다. 반복적으로 개발한 CRUD 기능도 다 제공이 된다. 따라서 개발자는 핵심 비즈니스 로직 개발에 집중할 수 있게 되고, 코드의 개발 생산성을 높이고 중복을 줄일 수 있게 된다.
repository 패키지에 인터페이스를 만들자. extends는 JpaRepository와 MemberRepository(인터페이스는 인터페이스를 상속받을 수 있다.). JpaRepository<T, ID> : T는 객체, ID는 PK의 타입이다. → JpaRepository<Member, Long>
findByName
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository { //인터페이스가 인터페이스를 받을 때는 extends
@Override
Optional<Member> findByName(String name); //JPQL : select m from Member m where m.name = ?
}
위 코드에는 Interface만 있는데, Spring Data JPA가 JpaRepository를 상속받고 있으면 자동으로 SpringDataJpaMemberRepository 구현체를 만들어서 스프링 빈에 자동으로 등록해준다.
즉, 내가 스프링 빈에 등록하는게 아닌 Spring Data JPA가 구현체를 만들어서 스프링 빈에 등록해준다. 개발자는 만들어진 것을 설정 파일을 이용하여 가져다 쓰면 된다.
SpringConfig
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) { //injection
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository); // 위에서 injection 받은 걸로 의존관계 세팅
}
}
이렇게 해두면 Spring Data JPA가 만들어 둔 구현체가 스프링 빈으로 자동 등록이 된다.
대충 전체적인 흐름
스프링 컨테이너에서 memberRepository를 찾는데, 지금 SpringConfig에 보면 등록한 게 없음. 그런데 SpringDataJpaMemberRepository 인터페이스가 있어. 이렇게 인터페이스만 만들어 놓고 extends JpaRepository<T, ID>를 하면 Spring Data JPA가 인터페이스(SpringDataJpaMemberRepository)에 대한 구현체를 만든다. 그리고 스프링 빈에 등록을 한다. 따라서 개발자는 injection을 받을 수 있다(SpringConfig).
JpaRepository
JpaRepository를 살펴보면 기본적인 CRUD와 PK 기반 단순 조회는 다 제공이 된다. 자세히 보면 PagingAndSortingRepository를 상속받고 있기 때문에 페이징 처리도 제공되며, Paging...Repository와 CrudRepository를 상속받고 있어서 save와 같은 CRUD가 제공된다. 즉, 공통적인 기능들은 제공이 되므로 가져다 쓰면 된다.
- 인터페이스를 통한 기본적인 CRUD
- 메서드 이름 만으로 조회 기능 제공
- Ex. findByName(), findByEmail()
- 페이징 기능 자동 제공
'Java > Spring' 카테고리의 다른 글
AnnotationConfigApplicationContext와 static class (0) | 2022.08.03 |
---|---|
스프링 부트 공부 (10), AOP (0) | 2022.03.20 |
스프링 부트 공부 (8), 회원관리 예제 - 웹 MVC 개발 (0) | 2022.03.06 |
스프링 부트 공부 (7), 스프링 빈과 의존관계 - 2 (0) | 2022.02.23 |
스프링 부트 공부 (6), 스프링 빈과 의존관계 - 1 (0) | 2022.02.22 |