[LG U+ 유레카 4기] WEEK 07 - 미니 프로젝트(2)

Soohwan Lim·2026년 5월 25일

유레카부트캠프

목록 보기
29/31
post-thumbnail

StockTalk - 주식 종목 토론방


1. 프로젝트 개요

항목내용
개발 기간3일
개발 환경Java 17, MySQL 8.0
아키텍처Layered Architecture (MVC 유사 구조)
UIJava Swing
DB 연동JDBC (순수 Java, Spring 미사용)

주식 종목별로 게시글을 작성하고 댓글을 달 수 있는 로컬 환경 토론방이다. 외부 Yahoo Finance API로 실시간 종가를 수집해 DB에 동기화하고, 3개 테이블 간 관계형 구조를 JDBC로 직접 다루는 것에 초점을 맞췄다.


2. 아키텍처

View → Controller → DAO → DB
stocktalk-project/
├── config/
│   ├── DBConnection.java       # 싱글턴 DB 커넥션 + AutoCloseable 자원 반납
│   └── StockAPIClient.java     # 외부 주가 API + Fallback
├── model/
│   ├── dto/  StockDTO, PostDTO, CommentDTO
│   └── dao/  StockDAO, PostDAO, CommentDAO
├── controller/
│   └── StockController.java    # View ↔ DAO 이벤트 중재
└── view/
    ├── MainFrame.java           # 3단 분할 메인 윈도우 (JSplitPane)
    └── WriteDialog.java         # 글쓰기 모달 팝업

DAY05에서 배운 DAO 패턴, DAY26에서 배운 JDBC를 합쳐서 실제 동작하는 앱을 만들었다.


3. ERD

Stock (1) ──────< Post (N) ──────< Comment (N)
CREATE TABLE Stock (
    stock_code VARCHAR(10) PRIMARY KEY,
    stock_name VARCHAR(50) NOT NULL,
    current_price INT DEFAULT 0
);

CREATE TABLE Post (
    post_id    INT AUTO_INCREMENT PRIMARY KEY,
    stock_code VARCHAR(10) NOT NULL,
    title      VARCHAR(100) NOT NULL,
    content    TEXT NOT NULL,
    writer     VARCHAR(30) DEFAULT '익명',
    post_password VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (stock_code) REFERENCES Stock(stock_code) ON DELETE CASCADE
);

CREATE TABLE Comment (
    comment_id   INT AUTO_INCREMENT PRIMARY KEY,
    post_id      INT NOT NULL,
    reply_content VARCHAR(500) NOT NULL,
    reply_writer  VARCHAR(30) DEFAULT '익명',
    created_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES Post(post_id) ON DELETE CASCADE
);

ON DELETE CASCADE를 적용해서 상위 데이터 삭제 시 하위 데이터가 자동으로 정리된다. Stock 삭제 시 관련 Post가, Post 삭제 시 관련 Comment가 연쇄 삭제된다.


4. 주요 구현 내용

DBConnection - 싱글턴 + AutoCloseable

public class DBConnection {
    private static DBConnection instance = new DBConnection();

    private DBConnection() {
        try { Class.forName(DRIVER); } catch (Exception e) { e.printStackTrace(); }
    }

    public static DBConnection getInstance() { return instance; }

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }

    public void close(AutoCloseable... acs) {
        for (AutoCloseable c : acs) {
            if (c != null) try { c.close(); } catch (Exception e) {}
        }
    }
}

드라이버 로드는 생성자에서 한 번만 수행하고, AutoCloseable 가변 인자로 rs/stmt/con을 한 번에 닫는다.

게시글 목록 - LEFT JOIN + GROUP BY로 댓글 수 포함

SELECT p.post_id, p.stock_code, p.title, p.content, p.writer,
       p.created_at, COUNT(c.comment_id) AS reply_count
FROM Post p
LEFT JOIN Comment c ON p.post_id = c.post_id
WHERE p.stock_code = ?
GROUP BY p.post_id
ORDER BY p.created_at DESC

댓글이 없는 게시글도 LEFT JOIN으로 포함하고, COUNT로 댓글 수를 집계한다. 어제 배운 JOIN + GROUP BY가 바로 쓰였다.

게시글 삭제 - 비밀번호 인증

public boolean deletePost(int postId, String password) {
    String sql = "DELETE FROM Post WHERE post_id = ? AND post_password = ?";
    try (Connection conn = DBConnection.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        pstmt.setInt(1, postId);
        pstmt.setString(2, password);
        return pstmt.executeUpdate() > 0;  // 0이면 비밀번호 불일치
    } catch (Exception e) { return false; }
}

비밀번호 조건을 WHERE에 포함해서 영향 받은 행이 0이면 불일치로 판단한다. 별도 SELECT 없이 DELETE 한 번으로 인증과 삭제를 동시에 처리한다.

StockAPIClient - Yahoo Finance + Fallback

public static int fetchClosePrice(String stockCode) {
    // 한국 종목은 .KS 접미사 추가 (KOSPI 기준)
    String ticker = stockCode + ".KS";
    String urlString = "https://query1.finance.yahoo.com/v8/finance/chart/" + ticker + "?interval=1d&range=1d";

    try {
        // HTTP GET, 타임아웃 2초
        // 정규식으로 regularMarketPrice 추출 (JSON 라이브러리 없이)
        Pattern pattern = Pattern.compile("\"regularMarketPrice\"\\s*:\\s*([0-9.]+)");
        Matcher matcher = pattern.matcher(json);
        if (matcher.find()) return (int) Math.round(Double.parseDouble(matcher.group(1)));
    } catch (Exception e) { ... }

    // 실패 시 Fallback (하드코딩된 기본값)
    return FALLBACK_PRICES.getOrDefault(stockCode, 0);
}

외부 라이브러리(Gson, Jackson) 없이 정규식으로 JSON에서 필요한 값만 추출했다. API 실패 시 Fallback으로 앱이 항상 동작한다.

try-with-resources 적용

try (Connection conn = DBConnection.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(sql);
     ResultSet rs = pstmt.executeQuery()) {
    while (rs.next()) { ... }
} catch (Exception e) { ... }

AutoCloseable을 구현한 객체는 try-with-resources로 자동 close된다. 명시적 finally 블록이 필요 없다.

Controller - View와 DAO 중재

public List<StockDTO> getAllStocks() {
    List<StockDTO> stocks = stockDAO.selectAll();
    for (StockDTO stock : stocks) {
        int price = StockAPIClient.fetchClosePrice(stock.getStockCode());
        stockDAO.updatePrice(stock.getStockCode(), price);
        stock.setCurrentPrice(price);  // DTO도 갱신해야 JList에 즉시 반영
    }
    return stocks;
}

View는 Controller만 알면 되고, Controller는 DAO를 통해 DB와 통신한다. View가 직접 DAO를 호출하지 않아서 계층 간 의존이 단방향으로 유지된다.


5. 역할 분담

담당작업 범위
A(나) (Model/Infrastructure)ERD 설계, DDL, DBConnection, StockAPIClient, StockDAO, PostDAO, CommentDAO
B (View/Controller)Swing UI, ActionListener, StockController, 예외 처리 모달

6. 기술적 의사결정

PreparedStatement 강제 사용: 모든 쿼리에서 문자열 연결을 금지하고 setXxx() 바인딩만 허용. SQL Injection 원천 차단.

API Fallback 전략: 네트워크 실패 시 하드코딩된 기본값으로 대체해 앱이 항상 동작.

비밀번호는 학습 목적의 평문 저장. 실제 서비스라면 bcrypt 등 단방향 해시를 적용해야 한다.


7. 개선 여지

  • 비밀번호 bcrypt 해싱
  • Connection Pool 도입 (HikariCP)
  • 게시글 페이지네이션 (LIMIT / OFFSET)
  • 단위 테스트 (JUnit)

8. 리뷰

이번 프로젝트에서 가장 배운 것은 계층 분리의 실질적인 효과다. DB를 MySQL에서 다른 DB로 바꿔도 DAO 구현체만 교체하면 Controller와 View는 건드리지 않아도 된다. DAY05 때는 이게 당연해 보였는데, 실제로 JDBC로 구현하면서 왜 인터페이스를 쓰는지 체감이 됐다.

외부 API 연동에서 Fallback 설계도 중요했다. 네트워크 상태에 따라 앱이 죽는 건 사용자 경험을 망친다. 실패를 예상하고 대안을 마련하는 것이 실제 서비스 개발의 기본이라는 걸 느꼈다.


9. 키워드 정리

JDBC 미니 프로젝트

profile
developer

0개의 댓글