본문 바로가기

TIL

TIL 3/21 - JDBC(UPDATE, DELETE), DAO, Service Layer

<entry key="updateMenu">
        UPDATE
            tbl_menu
        SET
            menu_name = ?
          , menu_price = ?
        WHERE
            menu_code = ?
</entry>
<entry key="deleteMenu">
        DELETE
        FROM tbl_menu
        WHERE menu_code = ?
</entry>

쿼리 매핑 파일에 플레이스홀더와 함께 쿼리를 작성해주자.

public static void main(String[] args) {
        Connection conn = getConnection();
        Scanner sc = new Scanner(System.in);
        PreparedStatement pstmt = null;
        int result = 0;
        Properties prop = new Properties();
        try {
            prop.loadFromXML(new FileInputStream("src/main/java/com/ino/mapper/menu-query.xml"));
            String query = prop.getProperty("deleteMenu");
            pstmt = conn.prepareStatement(query);
            System.out.print("input menuCode: ");
            int menuCode = sc.nextInt();
            pstmt.setInt(1, menuCode);
            result = pstmt.executeUpdate();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(pstmt);
            close(conn);
        }
    }

실행부는 이전과 똑같다

쿼리에서 값을 꺼낼때, ALIAS 도 가능하며 함수를 사용할 경우 함수까지 모두 기재해주어야한다.

<entry key="selectLastMenuCode">
        SELECT
            MAX(menu_code) AS last_menu_code
        FROM
            tbl_menu
    </entry>
// 예시
lastMenuCode = rset1.getInt("last_menu_code");
 // 구현

DAO

Data Access Object

public class MenuDAO {
    // 마지막 메뉴 번호 조회용

    private Properties prop = new Properties();
    public MenuDAO() {
        try {
            prop.loadFromXML(new FileInputStream("src/main/java/com/ino/mapper/menu-query.xml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public int selectLastMenuCode() {
        Connection conn = getConnection();

        PreparedStatement pstmt1 = null;
        ResultSet rset1 = null;
        int lastMenuCode = 0;

        String query1 = prop.getProperty("selectLastMenuCode");

        try {
            pstmt1 = conn.prepareStatement(query1);
            rset1 = pstmt1.executeQuery();
            if(rset1.next()) {
                lastMenuCode = rset1.getInt("last_menu_code");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset1);
            close(pstmt1);
            close(conn);
        }

        return lastMenuCode;
    }
    // 카테고리 전체 정보 조회용

    public List<CategoryDTO> getCategory(Connection conn) {
        PreparedStatement pstmt2 = null;
        ResultSet rset2 = null;
        List<CategoryDTO> categoryDTOList = new ArrayList<>();
        String query2 = prop.getProperty("selectCategoryList");
        try {
            pstmt2 = conn.prepareStatement(query2);
            rset2 = pstmt2.executeQuery();
            while(rset2.next()) {
                CategoryDTO c = new CategoryDTO(rset2.getInt("category_code"), rset2.getString("category_name"));
                categoryDTOList.add(c);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset2);
            close(pstmt2);
            close(conn);
        }
        for(CategoryDTO c: categoryDTOList) {
            System.out.println(c.toString());
        }
        return categoryDTOList;
    }

    public int insertMenu(Connection conn, MenuDTO menu) {
        menu.setMenuCode(selectLastMenuCode() + 1);
        PreparedStatement pstmt3 = null;
        int result = 0;

        String query3 = prop.getProperty("insertMenu");

        try {
            pstmt3 = conn.prepareStatement(query3);
            pstmt3.setInt(1, menu.getMenuCode());
            pstmt3.setString(2, menu.getMenuName());
            pstmt3.setInt(3, menu.getMenuPrice());
            pstmt3.setInt(4, menu.getCategoryCode());
            pstmt3.setString(5, menu.getOrderableStatus());
            result = pstmt3.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(pstmt3);
            close(conn);
        }
        return result;
    }
}

DB와의 연결에 대한 책임만을 담당하게 해주자.

  • PreparedStatement를 쿼리 변경을 통해 재사용하지 않는 이유

    쿼리 변경과 재사용
    쿼리 변경:

    PreparedStatement는 생성 시에 SQL 쿼리가 컴파일되므로, 쿼리를 변경하려면 기존의 PreparedStatement를 닫고 새로운 것을 생성해야 합니다.
    즉, 쿼리 구조가 변경되면 PreparedStatement를 재사용하는 것이 아니라, 새로 생성하는 것이 좋습니다.
    이유:

    성능: 쿼리 구조가 변경되면, 기존의 쿼리는 더 이상 유효하지 않습니다. 기존의 PreparedStatement를 계속 사용하면 성능이 저하될 수 있습니다.
    보안: 쿼리를 변경하면서 매개변수의 바인딩이 올바르게 이루어지지 않을 위험이 있습니다.
    예시
    아래는 쿼리를 변경하고 새로운 PreparedStatement를 생성하는 예시입니다:

       java
    
      import java.sql.Connection;
      import java.sql.DriverManager;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
    
      public class DynamicPreparedStatementExample {
      public static void main(String[] args) {
      String url = "jdbc:mysql://localhost:3306/mydatabase";
      String user = "username";
      String password = "password";
         try (Connection conn = DriverManager.getConnection(url, user, password)) {
              // 첫 번째 쿼리
              String sql1 = "INSERT INTO users (username, email) VALUES (?, ?)";
              try (PreparedStatement pstmt = conn.prepareStatement(sql1)) {
                  pstmt.setString(1, "user1");
                  pstmt.setString(2, "user1@example.com");
                  pstmt.executeUpdate();
              }
    
              // 두 번째 쿼리
              String sql2 = "UPDATE users SET email = ? WHERE username = ?";
              try (PreparedStatement pstmt = conn.prepareStatement(sql2)) {
                  pstmt.setString(1, "new_email@example.com");
                  pstmt.setString(2, "user1");
                  pstmt.executeUpdate();
              }
    
              System.out.println("사용자가 성공적으로 추가 및 업데이트되었습니다.");
          } catch (SQLException e) {
              e.printStackTrace();
          }
      }
    

    }
    결론
    쿼리 변경 시 새로운 PreparedStatement 생성: 쿼리 구조가 변경될 경우, 기존의 PreparedStatement를 재사용하지 않고 새로운 것을 생성하는 것이 가장 바람직합니다.
    효율성 유지: 이를 통해 성능을 최적화하고, SQL 인젝션과 같은 보안 문제를 예방할 수 있습니다.
    자원 관리: PreparedStatement 사용 후에는 항상 close() 메서드를 호출하여 자원을 해제하는 것을 잊지 마세요. try-with-resources 문을 사용하면 자동으로 관리할 수 있습니다.

  • Connection 객체를 파라미터로 넘기는 이유

    1. 트랜잭션 관리
      트랜잭션의 일관성: 여러 DAO 메서드가 하나의 트랜잭션으로 묶여야 하는 경우, 동일한 Connection 객체를 사용하여 트랜잭션의 일관성을 유지할 수 있습니다. 예를 들어, 여러 데이터베이스 작업이 모두 성공해야만 트랜잭션을 커밋하고, 하나라도 실패할 경우 롤백할 수 있습니다.

    2. 자원 관리
      Connection의 재사용: Connection 객체는 생성하는 데 비용이 크기 때문에, DAO 메서드 간에 동일한 Connection을 재사용함으로써 성능을 향상시킬 수 있습니다. Connection Pool을 사용하면, 미리 생성된 연결을 효율적으로 관리할 수 있습니다.

    3. 유연한 설계
      테스트 용이성: Connection을 파라미터로 받는 방식은 테스트 코드에서 모의 객체(mock object)를 사용하기 용이합니다. 실제 데이터베이스에 연결하지 않고도 DAO 메서드를 테스트할 수 있습니다.

    4. 의존성 주입
      의존성 관리: DAO 메서드가 Connection을 직접 생성하는 대신 외부에서 주입받도록 설계함으로써, 의존성 관리가 더 용이해집니다. 이는 객체 간의 결합도를 낮추고 코드의 유지보수성을 높입니다.

    5. 일관된 API 제공
      API의 일관성: DAO 메서드가 Connection 객체를 인자로 받으면, 호출하는 측에서 트랜잭션 관리나 연결 관리를 명확하게 할 수 있습니다. 이는 코드의 가독성을 높여줍니다.
      예시
      다음은 Connection을 파라미터로 받는 DAO 메서드의 간단한 예시입니다:

      결론
      DAO에서 Connection을 파라미터로 넘기는 방식은 트랜잭션 관리, 자원 효율성, 유연한 설계, 테스트 용이성 등 여러 가지 장점을 제공합니다. 이는 코드의 가독성을 높이고, 유지보수를 용이하게 하며, 성능을 개선하는 데 기여합니다.

      import java.sql.Connection;
      import java.sql.PreparedStatement;
      import java.sql.SQLException;
      
      public class UserDAO {
      public void addUser(Connection conn, String username, String email) throws SQLException {
      String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
      try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
      pstmt.setString(1, username);
      pstmt.setString(2, email);
      pstmt.executeUpdate();
      }
      }
      }

autocommit여부

conn.getAutoCommit() 을 통해 알 수 있고,

끈 상태로 작업을 하는게 좋다.

DriverManager를 통해 커넥션을 가져올때, conn.setAutoCommit(false) 를 설정하여 오토커밋을 해제 가능하다.

int result = dao.insertMenu(conn, menu);

        if (result > 0) {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.commit();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        } else {
            try {
                if (conn != null && !conn.isClosed()) {
                    conn.rollback();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

DAO로부터 쿼리 결과를 받아 정상적으로 삽입이 될 경우에만 commit, 아니면 rollback을 진행해주자.

Service

비즈니스 로직 처리 및 트랜잭션 관리 담당

  • 사용자의 요청에 따라 순차적으로 실행해야 되는 작업들을 하나로 묶어 관리

  • 중간과정에 문제 발생시 rollback이 필요하므로 하나의 트랜잭션으로 묶어 관리

  • 처리 과정

    Connection 생성

    순차적으로 작업 실행

    트랜잭션 처리가 필요할 경우 트랜잭션 처리

    Connection 반납

비즈니스 로직 : db - UI 간 정보교환을 위한 규칙이나 알고리즘 의미

'TIL' 카테고리의 다른 글

TIL 3/25 - CI  (0) 2025.03.25
TIL 3/24 - MVC  (0) 2025.03.24
TIL - 3/20 - JDBC  (0) 2025.03.20
TIL 3/19 - 정규화, JDBC  (0) 2025.03.19
TIL 3/18 - DB 모델링, 이상(Anomaly), 정규화  (0) 2025.03.18