[Spring Boot] DB 접근 기술 및 인터페이스 구현체
데이터를 저장하고 삭제하고 수정하고 .. 주고 받으려면 DB에 접근해야한다. 과거에는 jdbc를 이용해서 개발을 했다.
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);
}
}
하지만 지금 시점으로 볼때는 너무 길고 반복되는 코드가 많다. 이를 보완한 라이브러리로는 대표적으로 Mybatis와 JdbcTemplate가 존재한다.
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와 공통점이라면 sql쿼리를 여전히 작성해야 한다는 점이다.
다음은 jpa인데 넘어가기 전에 하나만 보고가자!
@Configuration
public class SpringConfig {
private final DataSource dataSource;
private final EntityManager em;
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(DataSource dataSource, EntityManager em, MemberRepository memberRepository) {
this.dataSource = dataSource;
this.em = em;
this.memberRepository=memberRepository;
}
@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);
}
}
위의 두 구현체의 인터페이스는 MemberRepository로 동일하다. 따라서 스프링 빈으로 등록 해두는 것만으로도 설정만으로도 모듈처럼 구현체를 마음대로 붙였다 바꿨다 할 수가 있다. 이것이 인터페이스를 잘 활용하는 예시가 될 것 같다.
jpa는 반복코드도 제거해주고 기본적인 SQL도 JPA가 직접 만들어서 실행한다. 객체 중심의 설계로 패러다임이 바뀐다고 보면 된다.
추가적으로 기존의 member 도메인에 @Entity 어노테이션을 통해 매핑한다.
@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;
}
}
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();
}
}
추가로 MemberService에 @Transactional 어노테이션을 붙여야한다. JPA를 통한 데이터 변경은 트랜잭션 안에서 실행되야 한다고 한다.
추가적으로 jpql이 있긴하지만 전체적으로 sql쿼리는 보이지 않게 되었다!
여기서 인터페이스만 구현하고 개발을 완료할 수 있게 더 도와주는 마법같은 라이브러리로 스프링 데이터 JPA가 존재한다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
이렇게 끝이나버린다.. 물론 이 라이브러리를 사용하기 위해서는 JPA를 심도 깊게 알아야 활용할 수 있는 쿠폰(?) 같은 것이다. 인터페이스를 통해서 기본적인 crud를 제공하기 때문에 공통된 일반적인 로직은 코드를 작성하지 않아도 되게 된다. 다만 email name.. 이런 것들은 공통사항으로 뺄 수가 없기 때문에 자동으로 생성할수는 없지만 위의 메서드의 이름만으로도 구현이 가능해진다.