TIL_20221123_웹과 데이터베이스

창고·2022년 11월 23일
0

서적 : http://www.yes24.com/product/goods/112373280
Spring + MyBatis + JSP 실습 겸 리마인드용으로 공부 중입니다.

1. JDBC 프로그래밍 준비

(1) MariaDB 설치 및 생성, Project와 연결

  • mariadb maven 키워드 -> build.gradle 의존성 추가
implementation 'org.mariadb.jdbc:mariadb-java-client:3.0.4'
  • 테스트 코드 작성 후 연결 상태 확인 완료
    @Test
    public void test1() {

        int v1 = 10;
        int v2 = 10;

        Assertions.assertEquals(v1, v2);

    }

    @Test
    public void testConnection() throws Exception {

        Class.forName("org.mariadb.jdbc.Driver");

        Connection connection = DriverManager.getConnection(
          "jdbc:mariadb://localhost:4000/testdb",
          "testuser",
          "test1234"
        );

        Assertions.assertNotNull(connection);

        connection.close();

    }

(2) JDBC 프로그래밍을 위한 API와 용어들

  • java.sql.Connection
    • Connection 인터페이스는 DB와 네트워크 상의 연결을 의미
    • DB에 SQL을 실행하기 위해서는 반드시 정상적인 Connection 타입의 객체를 생성해야 함
    • Connection이라는 인터페이스를 활용하고 실제 구현 클래스는 JDBC 드라이버 파일 내부의 클래스를 이용
    • Connection은 반드시 close()해야 함
  • java.sql.Statement/PreparedStatement
    • JDBC에서 SQL을 DB로 보내기 위해서는 해당 타입을 이용
    • SQL 문을 미리 전달하고 나중에 데이터를 보내는 방식 (<-> 같이 보내는 방식은 Statement)
    • SQL Injection 공격을 막기 위해 실제 개발에서는 PreparedStatement를 사용하는 것이 관례
      • setXXX() : setInt(), setString() 등 다양한 타입에 맞게 데이터 세팅 가능
      • executeUpdate() : DML(insert, update, delete)을 실행, 결과를 int 타입으로 반환 (몇개의 행이 영향을 받았는가)
      • executeQuery() : 쿼리 실행(select) 시 사용, ResultSet라는 리턴 타입 이용
  • java.sql.ResultSet
    • int로 결과값을 반환하는 DML과 달리 쿼리를 실행했을 때 DB에서 반환하는 데이터를 읽기 위해서는 ResultSet이라는 인터페이스를 이용
    • 자바 코드에서 데이터를 읽어들이므로 getInt(), getString() 등의 메소드를 이용해서 필요한 타입으로 읽음
    • next() : ResultSet은 데이터를 순차적으로 읽어들이기 때문에 다음 행의 데이터를 읽어들일 수 있도록 이동
  • Connection Pool과 DataSource
    • 미리 Connection들을 생성해서 보관하고 필요할 때마다 꺼내서 쓰는 개념
    • DB 연결에 걸리는 시간과 자우너을 절약할 수 있음
    • javax.sql.DataSource 인터페이스는 Connection Pool을 자바에서 API 형태로 지원하는 것
    • 대표적인 Connection Pool은 HikariCP
  • DAO(Data Access Object)
    • 데이터를 전문적으로 처리하는 객체
    • DB의 접근과 처리를 전담하는 객체를 의미. 주로 VO를 단위로 처리
    • DAO를 호출하는 객체는 DAO가 내부에서 어떤식으로 데이터를 처리하는지 알 수 없도록 구성
    • JDBC 프로그램을 작성한다는 의미 = DAO를 작성한다는 의미
  • VO(Value Object) 혹은 엔티티(Entity)
    • 객체 지향 프로그램에서는 데이터를 객체라는 단위로 처리
    • DB에서는 하나의 데이터를 엔티티(Entity) 라고 하는데 자바 프로그램에서는 이를 처리하기 위해 테이블과 유사한 구조의 클래스를 만들어서 객체로 처리하는 방식을 사용
    • 값을 보관하는 용도라는 의미에서 VO라고 함
    • DTO와 유사한 모습이나 DTO는 각 계층을 오고 가는데에 사용된다면, VO는 DB의 엔티티를 자바 객체로 표현한 것
    • DTO는 자유롭게 데이터를 가공할 수 있는 반면, VO는 데이터 자체를 의미하므로 조회 (getter)만 사용하는 경우가 대부분

2. 프로젝트 내 JDBC 구현

(1) VO, ConnectionUtil, DAO 작성, 기능 추가

  • VO
    • 엔티티 개념
@Getter
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TodoVO {

    private Long tno;
    private String title;
    private LocalDate dueDate;
    private boolean finished;

}
  • ConnectionUtil
    • JDBC 드라이버 설정과 HikariCP의 설정을 세팅
public enum ConnectionUtil {

    INSTANCE;

    private HikariDataSource ds;

    ConnectionUtil()  {
        HikariConfig config = new HikariConfig();
        config.setDriverClassName("org.mariadb.jdbc.Driver");
        config.setJdbcUrl("jdbc:mariadb://localhost:4000/testdb");
        config.setUsername("testuser");
        config.setPassword("test1234");
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

        ds = new HikariDataSource(config);
    }

    public Connection getConnection() throws Exception {
        return ds.getConnection();
    }

}
  • DAO
    • sql을 직접 작성한 후 Connection Pool 생성, prepareStatement를 사용해서 데이터 세팅 후 전송 (DML은 executeUpdate, 쿼리는 executeQuery이며 이 경우 resultSet을 추가로 사용)
    public List<TodoVO> selectAll() throws Exception {

        String sql = "select * from tbl_todo";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        List<TodoVO> list = new ArrayList<>();

        while(resultSet.next()) {

            TodoVO vo = TodoVO.builder()
                    .tno(resultSet.getLong("tno"))
                    .title(resultSet.getString("title"))
                    .dueDate(resultSet.getDate("dueDate").toLocalDate())
                    .finished(resultSet.getBoolean("finished"))
                    .build();

            list.add(vo);
        }

        return list;

    }
  • @Cleanup : try-with-resource와 동일하게 메소드가 끝날 시 close()가 호출되는 것을 보장 (코드 가독성 향상)
  • 추가 / 단건 조회 / 전체 조회 / 수정, 삭제
   public void insert(TodoVO vo) throws Exception {

        String sql = "insert into tbl_todo (title, dueDate, finished) values (?, ?, ?)";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, vo.getTitle());
        preparedStatement.setDate(2, Date.valueOf(vo.getDueDate()));
        preparedStatement.setBoolean(3, vo.isFinished());

        preparedStatement.executeUpdate();

    }

    public List<TodoVO> selectAll() throws Exception {

        String sql = "select * from tbl_todo";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        List<TodoVO> list = new ArrayList<>();

        while(resultSet.next()) {

            TodoVO vo = TodoVO.builder()
                    .tno(resultSet.getLong("tno"))
                    .title(resultSet.getString("title"))
                    .dueDate(resultSet.getDate("dueDate").toLocalDate())
                    .finished(resultSet.getBoolean("finished"))
                    .build();

            list.add(vo);
        }

        return list;

    }

    public TodoVO selectOne(Long tno) throws Exception {

        String sql = "select * from tbl_todo where tno = ?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setLong(1, tno);

        @Cleanup ResultSet resultSet = preparedStatement.executeQuery();

        resultSet.next();
        TodoVO vo = TodoVO.builder()
                .tno(resultSet.getLong("tno"))
                .title(resultSet.getString("title"))
                .dueDate(resultSet.getDate("dueDate").toLocalDate())
                .finished(resultSet.getBoolean("finished"))
                .build();

        return vo;
    }

    public void updateOne(TodoVO todoVO) throws Exception {

        String sql = "update tbl_todo set title = ?, dueDate = ?, finished = ? where tno =?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setString(1, todoVO.getTitle());
        preparedStatement.setDate(2, Date.valueOf(todoVO.getDueDate()));
        preparedStatement.setBoolean(3, todoVO.isFinished());
        preparedStatement.setLong(4, todoVO.getTno());

        preparedStatement.executeUpdate();

    }

    public void deleteOne(Long tno) throws Exception {

        String sql = "delete from tbl_todo where tno = ?";

        @Cleanup Connection connection = ConnectionUtil.INSTANCE.getConnection();
        @Cleanup PreparedStatement preparedStatement = connection.prepareStatement(sql);

        preparedStatement.setLong(1, tno);

        preparedStatement.executeUpdate();

    }

3. 웹 MVC와 JDBC의 결합

(1) ModelMapper 라이브러리 추가, DTO, Service 구현

  • DTO <-> VO 변환은 ModelMapper 라이브러리를 이용해 처리
  • MapperUtil
public enum MapperUtil {

    INSTANCE;

    private ModelMapper modelMapper;

    MapperUtil() {
        this.modelMapper = new ModelMapper();
        this.modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.STRICT);
    }

    public ModelMapper get() {
        return modelMapper;
    }

}
  • DTO
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoDTO {

    private Long tno;
    private String title;
    private LocalDate dueDate;
    private boolean finished;

}
  • Service
@Log4j2
public enum TodoService {

    INSTANCE;

    private TodoDAO dao;
    private ModelMapper modelMapper;

    TodoService() {

        dao = new TodoDAO();
        modelMapper = MapperUtil.INSTANCE.get();

    }

    public void register(TodoDTO todoDTO) throws Exception {

        TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);

        log.info(todoVO);
        dao.insert(todoVO); // int를 반환하므로 이를 이용해서 예외 처리도 가능
    }

(2) 컨트롤러, 서비스 객체 연동

  • Controller
@WebServlet(name = "todoListController", value = "/todo/list")
@Log4j2
public class TodoListController extends HttpServlet {

    private TodoService todoService = TodoService.INSTANCE;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        log.info("--todo list.......--");

        try {
            List<TodoDTO> dtoList = todoService.listAll();
            req.setAttribute("dtoList", dtoList); // setAttribute를 통해 TodoService가 반환하는 데이터 저장 후
            req.getRequestDispatcher("/WEB-INF/todo/list.jsp").forward(req, resp); // list.jsp로 전달
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new ServletException("list error!");
        }

    }

}
  • 실제 개발 DAO -> Service -> Controller 순
  • JSP 파일 생성
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
    <title>Todo List</title>
</head>
<body>
<h1>COCOBALL TODO LISI</h1>

<ul>
    <c:forEach items="${dtoList}" var="dto">
        <li>${dto}</li>
    </c:forEach>
</ul>

</body>
</html>

(3) 코드 개선 사항들

  • 웹 MVC 구조 이용 시 좀 더 확실하게 책임과 역할을 구분해서 작업을 진행할 수 있으나 여러 개의 코드를 만들어야 하는 단점이 분명 존재
  • 여러 개의 컨트롤러를 작성하는 번거로움
    • TodoDAO나 TodoService와 달리 HttpServlet을 상속하는 여러 개의 컨트롤러를 작성해야 하는 불편함이 있음
  • 동일한 로직의 반복적인 사용
    • 게시물의 조회 / 수정 작업은 둘 다 GET 방식으로 동작하나 결과를 보여주는 JSP만 다른 형탱니 상황. 결국 동일 코드를 여러 번 작성하는 번거로움 발생
  • 예외 처리의 부재
    • 예외가 발생하면 어떤 식으로 처리해야하는지에 대한 설계가 없었음
  • 반복적인 메소드 호출
    • HttpServletRequest나 HttpServletResponse를 이용해서 TodoDTO를 구성하는 작업 등이 동일한 코드들로 작성 -> 이에 대한 개선이 필요하며 Long.parseLong() 과 같은 코드들도 많이 반복됨
  • 이와 같은 단점에 대한 고민은 추후 프레임워크의 형태로 이어지며 스프링 프레임워크를 배우면 이 문제들을 어떻게 해결하는지 알게 될 수 있음
profile
공부했던 내용들을 모아둔 창고입니다.

0개의 댓글