개발 무지렁이

[Spring] JDBC API 로직의 반복과 이를 해결하는 Spring의 JdbcTemplate 본문

Backend/스프링

[Spring] JDBC API 로직의 반복과 이를 해결하는 Spring의 JdbcTemplate

Gaejirang-e 2023. 9. 15. 03:13

🦉 DataSource
: 데이터베이스와 연결할 수 있는 정보가 들어있는 객체를 말한다.
(⚠️ SpringDataSource를 주입해준다)

📜 SpringConfig.java

  @Configuration // Spring Bean으로 관리된다.
  public class SpringConfig {
      DataSource dataSource;

      @Autowired
      public SpringConfig(DataSource dataSource) {
          this.dataSource = dataSource;
      }

      @Bean
      public MemberService memberService() {
          return new MemberService(memberRepository());
      }

      @Bean
      public MemberRepository memberRepository() {
          return new JdbcMemberRepository(dataSource);
      }
  }

📜 JdbcMemberRepository.java

  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 conn) throws SQLException {
          DataSourceUtils.releaseConnection(conn, dataSource);
      }

      private void close(Connection conn, PerparedStatement 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 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(Long id) {
          String sql = "select * from member where id = ?";
          Connection conn = null;
          PreparedStatement pstmt = null;
          ResultSet rs = null;

          try {
              conn = getConnection();
              pstmt = conn.preparedStatement(sql);
              pstmt.setString(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 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(); // db에 DML 쿼리가 날라간다
              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);
          }
      }
  }
🦉 Statement 클래스
: 기본적인 SQL문을 실행하는데 사용하지만,
SQL INJECTION 공격에는 취약하다.
⚠️ 해당 클래스는 2가지 하위 클래스로 나뉜다.

(1) PreparedStatement
: 미리 컴파일된 SQL문을 실행하는데 사용된다.
데이터바인딩파라미터화되어 메서드를 통해 이루어지므로, 다른 연산자가 들어갈 수 없고,
이로 인해, SQL INJECTION 공격을 예방할 수 있다.

(2) CallableStatement
: 저장 프로시저를 실행하는데 사용된다.
데이터베이스에서 미리 정의된 일련의 SQL명령문을 실행하는데 사용된다.

🦉 Statement 클래스의 내장 메서드
.executeQuery(String sql)
: SELECT문을 실행하고 결과집합(ResultSet)을 반환

.executeUpdate(String sql)
: INSERT, UPDATE, DELETE와 같은 DML 쿼리를 실행하고,
영향을 미친 행의 수를 반환

.execute(String sql)
: 임의의 SQL문 실행
데이터베이스에서 지원하지 않는 특수한 작업을 수행하는데 사용

⚠️ Statement.RETURN_GENERATED_KEYS
:INSERT 쿼리를 실행한 후에 자동으로 생성된 key를 알고 싶을 때
🏁 플래그를 사용하여 설정
⚠️ pstmt.getGeneratedKeys() 메서드를 호출해 ResultSet을 반환받고, 여기서 getLong()을 통해 추출한다.

📕 참고 자료 📕
Tistory's Card

🦉 DataSourceUtils
: Spring에서 제공하는 유틸리티 클래스 중 하나로,
데이터베이스 연결/관리 작업을 편하게 수행하도록 하는 클래스를 말한다.(🧩 코드 간소화)
(⚠️ Spring JDBC에서 사용)

.getConnection(DataSource dataSource)
: 데이터베이스 연결을 얻는다

.releaseConnection(Connection conn, DataSource dataSource)
: 데이터베이스 연결을 안전하게 해제 및 반환

.doGetConnection(DataSource dataSource)
: 내부적인 메서드로 실제 데이터베이스 연결을 가져온다.
(개발자가 호출하는 메서드가 아님)

.doReleaseConnection(Connection conn, DataSource dataSource)
: 내부적인 메서드 실제 데이터베이스 연결을 안전하게 해제 및 반환
(개발자가 호출하는 메서드가 아님)

.doBegin(TransactioniInfo txInfo)
: 스프링의 트랜잭션 관리시작

.doCommit(TransactionInfo txInfo)
: 스프링의 트랜잭션 관리커밋

.doRollback(TransactionInfo txInfo)
: 스프링의 트랜잭션 관리롤백

𖠃 🌱 Spring의 JdbcTemplate 기능 제공 + SimpleJdbcInsert
: 데이터베이스에 안전하게 Access하고,
편하게 sql문을 날릴 수 있게 도와주는 클래스를 말한다.

🎯 JDBC API에서 반복 코드 제거
🎯 SimpleJdbcInsert: Insert 쿼리를 짤 필요가 없다.

📜 JdbcTemplateMemberRepository.java

  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));
          // Number: 숫자값을 나타내는 추상클래스
          // executeAndReturnKey(): 데이터베이스에 데이터를 삽입하고, 자동으로 생성된 키를 반환하는 메서드
          // MapSqlParameterSource: 데이터베이스에 삽입할 파라미터를 나타내는 객체

          member.setId(key.longValue());
          return member;
      }

      private RowMapper<Member> memberRowMapper() {
          return (rs, rowNum) -> {
              Member member = new Member();
              member.setId(rs.getLong("id"));
              member.setName(rs.getString("name"));
              return member;
          }
      }

      @Override
      public List<Member> findAll() {
          return jdbcTemplate.query("select * from member", memberRowMapper());
      }

    @Override
      public Optional<Member> findById(Long id) {
          List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
          return result.stream().findAny(); //첫번째로 찾은 요소를 반환
      }
  }
🦉 RowMapper
: JDBCResultSetJava객체로 매핑하는데 사용되는 인터페이스를 말한다.

(데이터베이스 -> 자바객체로의 변환작업을 🧩 간소화)
(T: 변환하려는 자바객체의 타입)
⚠️ mapRow(ResultSet rs, int rowNum), rowNum: 현재행의 인덱스
Comments