복습

sungsimdangmascot·2026년 4월 26일

[Java/JDBC] CLI 게시판 프로젝트 총정리: 전체 코드 및 모듈 간 연결성 완벽 분석

Java와 JDBC를 활용하여 CLI 기반의 게시판을 만드는 프로젝트의 전체 뼈대와 코드를 분석한다. 초기에는 하나의 클래스에 모든 기능이 뭉쳐있었으나, 객체 지향 원칙에 따라 역할을 분리(리팩토링)하여 유지보수가 뛰어난 아키텍처를 완성했다.

아래는 본 프로젝트를 구성하는 핵심 모듈들의 전체 코드와 상세 분석이다.


1. Main.java : 프로그램의 진입점 (스위치)

과거에는 모든 로직이 Main에 있었지만, 리팩토링 후에는 단순히 프로그램을 켜는 스위치 역할만 수행한다.

package org.example;

public class Main {
  public static void main(String[] args) {
    // App 객체를 생성하고 run() 메서드를 호출하여 프로그램 엔진을 가동한다.
    new App().run(); 
  }
}

💡 흐름 및 해석

  • new App().run();: App이라는 총괄 매니저를 고용해서, 바로 업무 시작(run)을 지시한다.
  • 이제 Main은 더 이상 비즈니스 로직(글쓰기, 수정 등)이나 DB 연결에 관여하지 않으며, 오직 시작점(Entry Point)으로서의 책임만 가진다.

2. Article.java : 데이터 전송 객체 (DTO)

데이터베이스 창고에서 꺼낸 게시글 데이터를 자바 프로그램 안에서 안전하게 옮기기 위해 사용하는 '규격화된 택배 상자'다.

package org.example;

public class Article {
  private int id;             // 게시글 번호
  private String regDate;     // 작성일
  private String updateDate;  // 수정일
  private String title;       // 제목
  private String body;        // 내용

  // 1️⃣ DB에서 꺼내올 때 사용하는 생성자 (모든 정보 포함)
  public Article(int id, String regDate, String updateDate, String title, String body) {
    this.id = id;
    this.regDate = regDate;
    this.updateDate = updateDate;
    this.title = title;
    this.body = body;
  }

  // 2️⃣ 자바에서 새로 글을 쓸 때 임시로 사용하는 생성자 (날짜 제외)
  public Article(int id, String title, String body) {
    this.id = id;
    this.title = title;
    this.body = body;
  }

  // (Getter, Setter 생략 - 외부에서 안전하게 데이터를 읽고 쓰기 위한 메서드들)
  public int getId() { return id; }
  public String getTitle() { return title; }
  public String getBody() { return body; }
  public String getRegDate() { return regDate; }
  public String getUpdateDate() { return updateDate; }

  // 3️⃣ 객체 출력 형식 재정의 (자기소개서)
  @Override
  public String toString() {
    return "Article{" +
        "id=" + id +
        ", regDate ='" + regDate + '\'' +
        ", updateDate ='" + updateDate + '\'' +
        ", title='" + title + '\'' +
        ", body='" + body + '\'' +
        '}';
  }
}

💡 흐름 및 해석

  • 생성자 오버로딩 (1️⃣, 2️⃣): 상황에 맞춰 유연하게 캡슐을 조립하기 위해 3개짜리와 5개짜리 조립 기계(생성자)를 모두 구비했다.
  • toString() (3️⃣): 객체를 출력할 때 메모리 주소(예: Article@1a2b3c)가 아닌, 실제 데이터 값을 예쁘게 문자열로 보여주도록 기본 기능을 덮어쓰기(Override) 했다.

3. SecSql.java : 동적 쿼리 조립기 (보안 요원)

사용자의 입력을 직접 SQL에 더하기(+) 방식으로 붙이면 SQL Injection(해킹) 위험이 있다. 이를 방지하기 위해 물음표(?) 기반의 안전한 쿼리를 자동으로 만들어주는 도구다.

package org.example.util;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class SecSql {
  private StringBuilder sqlBuilder; // 쿼리문(시험지)을 덧붙여 저장하는 곳
  private List<Object> datas;       // 물음표(?)에 들어갈 데이터(정답)를 저장하는 곳

  public SecSql() {
    sqlBuilder = new StringBuilder();
    datas = new ArrayList<>();
  }

  // 1️⃣ 쿼리와 데이터를 한 번에 세팅하는 가변인자 메서드
  public SecSql append(Object... args) {
    if (args.length > 0) {
      String sqlBit = (String) args[0];
      sqlBuilder.append(sqlBit + " "); // 첫 번째 인자는 SQL 문장으로 추가
    }
    for (int i = 1; i < args.length; i++) {
      datas.add(args[i]); // 두 번째 인자부터는 데이터 리스트에 추가
    }
    return this; // 메서드 체이닝을 위해 자기 자신을 반환
  }

  // 2️⃣ 완성된 PreparedStatement(안전한 주문서)를 반환
  public PreparedStatement getPreparedStatement(Connection dbConn) throws SQLException {
    PreparedStatement stmt = null;
    
    // INSERT 문이면 방금 생성된 PK(id)를 가져올 수 있게 세팅
    if (sqlBuilder.toString().startsWith("INSERT")) {
      stmt = dbConn.prepareStatement(sqlBuilder.toString(), Statement.RETURN_GENERATED_KEYS);
    } else {
      stmt = dbConn.prepareStatement(sqlBuilder.toString());
    }

    // 모아둔 정답(datas)을 타입에 맞게 물음표(?)에 쏙쏙 끼워 넣음
    for (int i = 0; i < datas.size(); i++) {
      Object data = datas.get(i);
      int parameterIndex = i + 1;

      if (data instanceof Integer) stmt.setInt(parameterIndex, (int) data);
      else if (data instanceof String) stmt.setString(parameterIndex, (String) data);
    }
    return stmt;
  }
}

💡 필수 암기 개념

  • Object... args (가변인자): 들어오는 인자의 개수를 제한하지 않고 배열 형태로 모두 받아내는 마법의 문법.
  • instanceof: 주머니에 든 데이터(Object)가 진짜 숫자인지(Integer), 글자인지(String) 타입을 검사하여 창고 관리인에게 정확한 형태로 전달한다.

4. DBUtil.java : DB 자동화 공장

매번 try-catch를 작성하고 rs, pstmt를 닫아주는 반복적인 노가다를 없애기 위해 만든 핵심 유틸리티 클래스다.

package org.example.util;

import org.example.exception.SQLErrorException;
import java.sql.*;
import java.util.*;

public class DBUtil {
  
  // 1️⃣ 조회를 위한 공용 메서드 (Map을 사용해 어떤 형태의 테이블이든 다 받아냄)
  public static List<Map<String, Object>> selectRows(Connection dbConn, SecSql sql) {
    List<Map<String, Object>> rows = new ArrayList<>();
    PreparedStatement stmt = null;
    ResultSet rs = null;

    try {
      stmt = sql.getPreparedStatement(dbConn);
      rs = stmt.executeQuery();
      
      // ResultSetMetaData: DB 창고의 칸(컬럼) 이름과 개수를 알아내는 똑똑한 도구
      ResultSetMetaData metaData = rs.getMetaData();
      int columnSize = metaData.getColumnCount();

      while (rs.next()) {
        Map<String, Object> row = new HashMap<>();
        for (int i = 0; i < columnSize; i++) {
          String columnName = metaData.getColumnName(i + 1);
          Object value = rs.getObject(columnName);
          row.put(columnName, value); // 라벨(컬럼명)과 내용물(데이터)을 매핑하여 저장
        }
        rows.add(row);
      }
    } catch (SQLException e) {
      throw new SQLErrorException("SQL 예외 발생", e); // 🚨 커스텀 에러로 변환
    } finally {
      // 🧹 깔끔한 자원 해제 (반복되는 노가다를 여기서 한 번에 처리)
      try { if (rs != null) rs.close(); } catch (SQLException e) {}
      try { if (stmt != null) stmt.close(); } catch (SQLException e) {}
    }
    return rows;
  }

  // 2️⃣ 데이터 삽입 (INSERT) 후 생성된 ID 반환
  public static int insert(Connection dbConn, SecSql sql) {
    int id = -1;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
      stmt = sql.getPreparedStatement(dbConn);
      stmt.executeUpdate();
      rs = stmt.getGeneratedKeys(); // 생성된 번호표 가져오기
      if (rs.next()) id = rs.getInt(1);
    } catch (SQLException e) {
      throw new SQLErrorException("SQL 예외 발생", e);
    } finally { /* 자원 해제 생략 */ }
    return id;
  }
}

5. 🚀 모듈 간의 완벽한 연결성 (아키텍처 흐름)

이 프로그램은 각 클래스들이 다음과 같은 유기적인 순서로 협력하며 작동한다.

  1. 시작 (Main): new App().run();을 통해 프로그램을 메모리에 올리고 제어권을 넘긴다.
  2. 명령 수신 (App): 사용자가 article list를 입력하면, App은 이 요청을 확인하고 DB 통신용 전화선(Connection)을 챙긴다.
  3. 보안 쿼리 작성 (SecSql): App은 날것의 SQL이 아닌, SecSql 객체를 만들어 sql.append("SELECT * FROM article"); 형식으로 안전한 주문서를 작성한다.
  4. 배달 위임 (DBUtil): App은 본인이 직접 try-catch를 하지 않고, 작성한 SecSqlConnectionDBUtil.selectRows() 공장에 던져버린다.
  5. DB 통신 및 매핑 (DBUtil ➡️ DB):
    • DBUtil이 DB와 통신하여 ResultSet을 받는다.
    • 컬럼 정보를 스스로 읽어내어(MetaData) Map<String, Object> 형태로 유연하게 포장한다.
  6. 조립 및 출력 (App ⬅️ Article):
    • DBUtil이 넘겨준 Map 데이터를 App이 다시 받아, 우리가 규격화해둔 Article 캡슐 안에 쏙쏙 옮겨 담는다.
    • 최종적으로 화면에 예쁘게 출력한다.

결론: App은 오직 "무엇을 할지(명령어 분기, 데이터 출력)"만 신경 쓰고, "어떻게 안전하게 통신할지(SQL 인젝션 방어, 자원 해제)"SecSqlDBUtil이 전담하는 완벽한 역할 분담(관심사의 분리)이 완성된 것이다.


profile
성심당마스코트

0개의 댓글