H2 database 연결
jdbc:h2:tcp://localhost/D:\H2\test 이것이 나의 jdbc url 이다 저기에 test.mv.db 가 생성 될것이다. 이렇게 접속을 하는 이유는 소켓을 통해 접속하게 됨으로서 여러곳에서 접근을 가능하게 해주기 때문이다.
drop table if exists member CASCADE;
create table memeber
(
id bigint generated by default as identity,
name varchar(255),
primary key(id)
);
자바 도메인 member 클래스와 동일하게 하나 만들어준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository
{
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource)
{
this.dataSource = dataSource;
}
private Connection getConnection()
{
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection connection) throws SQLException
{
DataSourceUtils.releaseConnection(connection,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();
}
}
@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());// 위의 values와 매칭이된다. parameter index 1은
pstmt.executeUpdate(); //실제 쿼리가 날라감
rs = pstmt.getGeneratedKeys();// 자동으로 key를 꺼내준다.
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 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);
}
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);
}
}
}
Jdbc 사용법이다. 잘안쓴다고 했다. 이제 메모리 대신에 db를 사용하게 되었으므로 설정 값을 달리 해줘야한다.
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
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.sql.DataSource;
@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 MemoryMeberRepository();
return new JdbcMemberRepository(dataSource);
}
}
기존에 인터페이스를 만들어줬으니 간단하게 JdbcMemberRepository 만 교체 해서 써주면 된다. 이러한 특성을 다형성이라고 한다.
개방-폐쇄 원칙 (OCP, Open Closed Principle) 확장에는 열려있고, 수정,변경에는 닫혀있다. 물론 조립하는 코드들은 수정을 해야지만 실제 에플리케이션을 동작하게 하는 코드는 하나도 수정 하지 않아도 된다.
단위 테스트가 더 좋은 확률이 높다고 하셨다. 하지만 지금부터는 통합테스트 , 순수 자바 코드가 아닌 스프링 과 함께 동작하는 테스트를 해볼 것이다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMeberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.assertThrows;
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest
{
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입()
{
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member findMemeber = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMemeber.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.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
기존에 것처럼 직접 객체를 만들어서 넣어주는것이 아니라 @Autowired 를 통한 spring이 inject 해준다. 그리고 기존 것은 db에 넣어줬으면 매번 테스트 실행 전 후 로 무엇인가를 해주어야 만 했었는데 그럴 필요가 없어졌다.
@Transactional 의 여기서의 역할은 각 테스트가 실행될떄마다 commit 을 안하고 그전의 상황으로 rollback 을 해주는데 역할이 있다. @Test 밑에 @Commit을 하면 db에 반영시킬수 있다.
Jdbctemplate 을 이용해서 repository를 재구성했다. 기존 jdbc보다 훨씬 간결해졌다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Autowired
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;
};
}
}
콜백함수를 통해 객체를 생성해주어서 리턴하는 방식이다. 이떄 RowMapper는 FunctionalInterface 로써 람다식을 통해 RowMapper interface안에 있는 mapRow함수를 구현해준 객체를 return 받는다. mapRow 는 해당되는 row를 객체로 return 을 해준다.
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 entityManager; // Jpa는 이것으로 모두 동작한다.
public JpaMemberRepository(EntityManager entityManager)
{
this.entityManager = entityManager;
}
@Override
public Member save(Member member)
{
entityManager.persist(member);
return member;
}
@Override
public Optional<Member> findById(Long id)
{
Member member = entityManager.find(Member.class, id);
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name)
{
List<Member> result = entityManager.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()
{
return entityManager.createQuery("select m from Member m",Member.class)
.getResultList();
}
}
JPA 는 기본적으로 인터페이스이다. 지금은 오픈소스로 나와있는 hibernate라는 구현체를 통해 쓰고 있다. pk 기반으로 조회 할경우는 간단히 find를 쓰면 되지만 나머지는 일종의 SQL비슷한 쿼리를 짜주어야 한다. 하지만 기존의 SQL보다 훨씬 간단하다. column하나하나 읽어오는 방식이아닌 객체 Member 하나를 읽어온다.
Spring JPA
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long> , MemberRepository
{
Optional<Member> findByName(String name);
}
JPA를 spring으로 한번 감싼 형태라 설명했다. interface만으로 개발이 끝난다. SpringDataJpaMemberRepository가 알아서 구현이 된 객체를 생성해줘서 필요한곳에 inject해주기 떄문이다. 기본적인것 ,pk 같이 공통적인것 이 아닌 경우에는 메소드 이름을 정해진 규칙에따라 적으면 이것 역시 구현할 필요없이 알아서 만들어준다.
package hello.hellospring;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig
{
// private DataSource dataSource;
// @Autowired
// public SpringConfig(DataSource dataSource)
// {
// this.dataSource = dataSource;
// }
// private EntityManager em;
//
// @Autowired
// public SpringConfig(EntityManager em)
// {
// this.em = em;
// }
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository)
{
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService()
{
return new MemberService(memberRepository);
}
// @Bean
// public MemberRepository memberRepository()
// {
//// return new MemoryMeberRepository();
//// return new JdbcMemberRepository(dataSource);
//// return new JdbcTemplateMemberRepository(dataSource);
//// return new JpaMemberRepository(em);
//
// }
}
인터페이스는 여러개의 인터페이스를 상속 받을 수 있기에 MemberRepository 에 알맞게 inject가 가능하다. 서비스에게 memberRepository 를 넣어주면 서비스 단도 이용할 수 있게 된다.
'WEB > Spring' 카테고리의 다른 글
김영한 (스프링 핵심원리 1) 객체지향설계와 스프링 (0) | 2021.02.08 |
---|---|
김영한 (스프링 부트 5) (0) | 2021.02.08 |
김영한 (스프링부트 입문) 3 (1) | 2021.01.31 |
김영한 (스트링부트 입문) 2 (1) | 2021.01.24 |
김영한 (스프링부트 입문) 1 (0) | 2021.01.24 |