Spring - ORM의 탄생 배경 (JDBC, Query Mapper)

김상엽·2024년 3월 7일
0

Spring

목록 보기
16/26
post-thumbnail

TIL

JDBC(Java Database Connectivity)

  • 문장 그대로 Java 앱과 DB 를 연결시켜주기 위해 만들어진 기술이다.
  • 그렇기 때문에 JPA 도 이 기술을 사용하여 구현되어 있다.
  • JDBC Driver 는 여러타입의 DB 와 연결할 수 있는 기능을 제공한다.
  • JDBC Driver Manager 는 런타임 시점에
    -Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
    -Statement(상태) 를 생성하여 쿼리를 요청하게 해주고
    -ResultSet(결과셋) 을 생성해 쿼리 결과를 받아올 수 있게 해준다.
    - 꼭 사용후에는 각각 close() 를 호출해서 자원 해제를 시켜줘야 한다 (자원 관리)


  • JDBC를 통해서 INSERT와 SELECT를 해보았다 (JPA를 사용했을때 보다 상당히 귀찮다)
public class JDBCTest {

  @Test
  @DisplayName("JDBC DB 연결 실습")
  void jdbcTest() throws SQLException {
    // given

    String url = "jdbc:postgresql://localhost:5432/messenger";
    String username = "tablenext";
    String password = "pass";

    // when
    try (Connection connection = DriverManager.getConnection(url, username, password)) {
      try {
        String creatSql = "CREATE TABLE ACCOUNT (id SERIAL PRIMARY KEY, username varchar(255), password varchar(255))";
        try (PreparedStatement statement = connection.prepareStatement(creatSql)) {
          statement.execute();
        }
      } catch (SQLException e) {
        if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
          System.out.println("ACCOUNT 테이블이 이미 존재합니다.");
        } else {
          throw new RuntimeException();
        }
      }
    }

    // then
		// DB 확인
  }

  @Test
  @DisplayName("JDBC 삽입/조회 실습")
  void jdbcInsertSelectTest() throws SQLException {
    // given
    String url = "jdbc:postgresql://localhost:5432/messenger";
    String username = "tablenext";
    String password = "pass";

    // when
    try (Connection connection = DriverManager.getConnection(url, username, password)) {
      System.out.println("Connection created: " + connection);

      String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
      try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
        statement.execute();
      }

      // then
      String selectSql = "SELECT * FROM ACCOUNT";
      try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
        var rs = statement.executeQuery();
        while (rs.next()) {
          System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
              rs.getString("password"));
        }
      }
    }
  }

  @Test
  @DisplayName("JDBC DAO 삽입/조회 실습")
  void jdbcDAOInsertSelectTest() throws SQLException {
    // given
    AccountDAO accountDAO = new AccountDAO();

    // when
    var id = accountDAO.insertAccount(new AccountVO("new user", "new password"));

    // then
    var account = accountDAO.selectAccount(id);
    assert account.getUsername().equals("new user");
  }
}

JDBC의 문제점, QueryMapper의 등장

  • JDBC 로 직접 SQL을 작성했을때의 문제
    • SQL 쿼리 요청시 중복 코드 발생
    • DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
    • Connection, Statement 등.. 자원 관리를 따로 해줘야함
      • 안해주면 메모리 꽉차서 서버가 죽음
  • 이 문제 해결을 위해 처음으로 Persistence Framework 등장
    • Persistence Framework 는 2가지가 있다.
      • SQL Mapper : JDBC Template, MyBatis 👈 요게 먼저나옴
      • ORM : JPA, Hibernate
  • SQL Mapper (QueryMapper)
    • SQL ↔ Object
    • SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객채화

JDBC Template

  • SQL Mapper 첫번째 주자로 JDBCTemplate 탄생
    • 쿼리 수행 결과와 객채 필드 매핑
    • RowMapper 로 응답필드 매핑코드 재사용
    • Connection, Statement, ResultSet 반복적 처리 대신 해줌
    • But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함
@JdbcTest // Jdbc Slice Test
@AutoConfigureTestDatabase(replace = Replace.NONE) // 테스트용 DB 쓰지 않도록
@Rollback(value = false) // Transactional 에 있는 테스트 변경은 기본적으론 롤백 하도록 되어있다.
public class JDBCTemplateTest {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Test
  @DisplayName("SQL Mapper - JDBC Template 실습")
  void sqlMapper_JDBCTemplateTest() {
    // given
    var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);

    // when
    var id = accountTemplateDAO.insertAccount(new AccountVO("new user2", "new password2"));

    // then
    var account = accountTemplateDAO.selectAccount(id);
    assert account.getUsername().equals("new user2");
  }
}
  • AccountTemplateDAO (Data Access Object)
package me.whitebear.jpastudy.jdbc.template;

import me.whitebear.jpastudy.jdbc.vo.AccountVO;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

@Repository // Java 패키지에 있었다면 JdbcTemplate 생성자 주입받음
public class AccountTemplateDAO {

  private final JdbcTemplate jdbcTemplate;

  public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  //SQL 관련 명령어
  private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD) "
      + "VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)";
  // coalesce 은 Postgresql 용 IFNULL
  private final String ACCOUNT_GET = "SELECT * FROM account WHERE ID = ?";

  //CRUD 기능의 메소드 구현
  //계정 등록
  public Integer insertAccount(AccountVO vo) {
    // 기본 코드 (응답값은 생성된 갯수)
    // return jdbcTemplate.update(ACCOUNT_INSERT, vo.getUsername(), vo.getPassword());

    // id 값을 받아오기 위한 코드
    KeyHolder keyHolder = new GeneratedKeyHolder();

    jdbcTemplate.update(connection -> {
      var ps = connection
          .prepareStatement(ACCOUNT_INSERT, new String[]{"id"});
      ps.setString(1, vo.getUsername());
      ps.setString(2, vo.getPassword());
      return ps;
    }, keyHolder);

    return (Integer) keyHolder.getKey();
  }

  //계정 조회
  public AccountVO selectAccount(Integer id) {
    return jdbcTemplate.queryForObject(ACCOUNT_GET, new AccountRowMapper(), id);
  }

}

MyBatis

  • SQL Mapper 두번째 주자로 MyBatis 탄생
    - 반복적인 JDBC 프로그래밍을 단순화
    - SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!
    - But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적)
    - But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)
public class MyBatisTest {

  // Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
  @Autowired
  AccountMapper accountMapper;

  @Autowired
  AccountMapperV2 accountMapperV2;

  @Test
  @DisplayName("SQL Mapper - MyBatis 실습")
  void sqlMapper_MyBatisTest() {
    // given

    // when
    accountMapper.insertAccount(new AccountMyBatisVO("new user3", "new password3"));
    var account = accountMapper.selectAccount(1);

    // then
    assert !account.getUsername().isEmpty();
  }

  @Test
  @DisplayName("SQL Mapper - MyBatis V2 실습")
  void sqlMapper_MyBatisV2Test() {
    // given

    // when
    accountMapperV2.insertAccount(new AccountMyBatisVO("new user4", "new password4"));
    var account = accountMapperV2.selectAccount(1);

    // then
    assert !account.getUsername().isEmpty();
  }
}
  • AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--
    [템플릿 설명]
    - 해당 파일은 SQL 문을 작성하는 곳입니다.
-->
<mapper namespace="me.whitebear.jpastudy.mybatis.mapper.AccountMapper">

  <select id="selectAccount" resultType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
    SELECT id,
           username,
           password
    FROM account
    WHERE id = #{id}
  </select>

  <insert id="insertAccount" parameterType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
    INSERT INTO account(username, password)
    VALUES (#{username}, #{password});
  </insert>

</mapper>

QueryMapper 문제점, ORM의 탄생

  • QueryMapper 의 DB의존성 및 중복 쿼리 문제점
  • ORM 은 DB의 주도권을 뺏어왔다고 표현해도 과언이 아니다.
  • ORMDAO 또는 Mapper 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버린다.
  • 말이 쉽지…. 객체지향(Object)관계형 데이터베이스(Relation)매핑(Mapping) 한다는건 정말 많은 난관이 있다.

오늘의 회고

JDBC, JDBC Template, MyBatis를 통해 평소에 JPA를 사용하여 했던 DB실습을 해보았는데,
하면 할 수 록 JPA가 굉장히 편하다는걸 느꼈다.
하지만 JPA를 사용하지 않는 기업들이 많기에..
쓰는법을 익히는 김에 등장 배경도 알아보았다.
(꼭 JPA를 사용하는 기업에 취업해야지)

profile
개발하는 기록자

0개의 댓글