<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>이희백의 개발 블로그</title>
    <link>https://heebaek.tistory.com/</link>
    <description>이희백의 개발 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 11 Sep 2026 14:04:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>heebaek</managingEditor>
    <item>
      <title>사용자 100명이 들어오면 DB부터 죽습니다 - 커넥션 풀(Connection Pool)</title>
      <link>https://heebaek.tistory.com/110</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 시간까지는 JDBC를 이용해 데이터베이스에 연결하고, DAO와 Service, View 계층을 나누어 작은 도서 관리 프로그램을 만들어 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기능을 하나 더 추가하는 대신, 지금까지 너무 당연하게 사용했던 이것을 다시 살펴봤습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Connection connection = DriverManager.getConnection(...);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 데이터베이스 작업이 필요할 때마다 새 연결을 만들고, 사용이 끝나면 닫았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혼자 실행하는 작은 콘솔 프로그램에서는 별문제가 없어 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 프로그램을 여러 사람이 동시에 사용하는 서비스로 만든다면 이야기가 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수업에서는 이 문제를 해결하기 위한 &lt;b&gt;커넥션 풀(Connection Pool)&lt;/b&gt;을 배우고, 실제 프로젝트에 &lt;b&gt;HikariCP&lt;/b&gt;를 적용했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스 연결도 공짜가 아니었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC를 처음 배우면 보통 다음 흐름부터 익힙니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DB 연결
 &amp;darr;
SQL 실행
 &amp;darr;
결과 처리
 &amp;darr;
연결 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로는 익숙한 DriverManager.getConnection()을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Connection connection =
        DriverManager.getConnection(url, user, password);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 그냥 메서드 하나 호출하는 것이니 별것 아닌 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 데이터베이스와 새로운 연결을 만드는 과정에는 여러 작업이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 연결을 만들고, 데이터베이스와 통신하고, 인증을 수행하고, 세션을 준비해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 한 번 들어올 때마다 이것을 반복한다면 비용이 쌓입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 동시에 많은 사용자가 도서 관리 시스템에 접근한다고 생각해 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;사용자 A &amp;rarr; DB 연결 생성 &amp;rarr; 작업 &amp;rarr; 연결 종료
사용자 B &amp;rarr; DB 연결 생성 &amp;rarr; 작업 &amp;rarr; 연결 종료
사용자 C &amp;rarr; DB 연결 생성 &amp;rarr; 작업 &amp;rarr; 연결 종료
사용자 D &amp;rarr; DB 연결 생성 &amp;rarr; 작업 &amp;rarr; 연결 종료
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 늘어날수록 데이터베이스 연결을 만들고 끊는 작업도 계속 반복됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실제 서버 프로그램에서는 매번 새로운 연결을 만드는 대신 &lt;b&gt;미리 여러 개의 Connection을 만들어 두고 재사용하는 방식&lt;/b&gt;을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 커넥션 풀입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connection을 매번 만들지 말고 빌려 쓰자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀의 개념 자체는 상당히 직관적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 시작 시 데이터베이스 연결을 일정 개수 만들어 둡니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Connection Pool

[Connection 1]
[Connection 2]
[Connection 3]
[Connection 4]
[Connection 5]
      ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어오면 새 Connection을 생성하는 대신 풀에 준비되어 있는 Connection을 하나 빌립니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;요청
 &amp;darr;
Connection Pool
 &amp;darr;
Connection 하나 대여
 &amp;darr;
SQL 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업이 끝난 뒤에는 Connection을 다시 풀로 돌려보냅니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;SQL 실행 완료
 &amp;darr;
Connection 반환
 &amp;darr;
Connection Pool&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 기존 JDBC 코드와 비교했을 때 재미있는 부분이 하나 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 다음과 같은 코드를 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;try (Connection connection = DatabaseUtil.getConnection()) {

    // SQL 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드만 보면 close()가 호출되면서 연결이 종료되는 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 커넥션 풀을 사용하는 경우에는 일반적으로 물리적인 DB 연결을 끊는 것이 아니라 &lt;b&gt;사용하던 Connection을 풀에 반납하는 동작&lt;/b&gt;으로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 요청이 들어오면 그 연결을 다시 사용할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러면 커넥션 풀 라이브러리는 아무거나 사용해도 될까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀을 구현한 라이브러리는 하나만 존재하는 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 라이브러리마다 Connection을 가져오는 방법이 전부 다르다면 문제가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 라이브러리를 사용하다가 다른 커넥션 풀 라이브러리로 교체했는데 애플리케이션 전체의 데이터베이스 연결 코드를 전부 수정해야 한다면 꽤 골치 아픈 일이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Java에는 데이터베이스 연결을 제공하기 위한 공통 인터페이스가 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 DataSource입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DataSource&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 커넥션 풀 구현체가 이 공통 규약을 따르기 때문에 사용하는 쪽에서는 구현체의 세부사항에 지나치게 의존하지 않고 Connection을 받아 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 개념적으로 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;애플리케이션
     &amp;darr;
 DataSource
     &amp;darr;
커넥션 풀 구현체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현체가 바뀌더라도 사용하는 쪽에서는 최대한 동일한 방식으로 Connection을 요청할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Java에서 인터페이스와 다형성을 배웠을 때는 작은 예제로만 보았는데, 라이브러리의 교체 가능성을 설명하는 실제 사례를 만나니 그 이유가 훨씬 와닿았습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HikariCP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 커넥션 풀 구현체로 &lt;b&gt;HikariCP&lt;/b&gt;를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP는 Java에서 널리 사용되는 커넥션 풀 라이브러리이고, Spring Boot에서도 기본 커넥션 풀로 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Spring Boot 수업에 들어가지는 않았지만, 앞으로 다시 만나게 될 라이브러리를 JDBC 단계에서 먼저 직접 설정해 보는 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 build.gradle에 의존성을 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation group: 'com.zaxxer', name: 'HikariCP', version: '5.1.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP에서 출력하는 로그를 확인하기 위해 SLF4J 관련 라이브러리도 함께 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17'
implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.17'&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DatabaseUtil을 HikariCP 버전으로 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 DatabaseUtil에서는 Connection을 요청할 때마다 DriverManager를 직접 호출했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념적으로는 다음과 같은 구조였습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static Connection getConnection() throws SQLException {

    // 요청할 때마다 새로운 DB 연결을 생성합니다.
    return DriverManager.getConnection(
            URL,
            DB_USER,
            DB_PASSWORD
    );
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 이를 HikariCP를 이용하는 구조로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 HikariConfig를 이용해 커넥션 풀 설정을 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private static final HikariDataSource DATA_SOURCE;

static {

    HikariConfig config = new HikariConfig();

    // 데이터베이스 접속 정보를 설정합니다.
    config.setJdbcUrl(URL);
    config.setUsername(DB_USER);
    config.setPassword(DB_PASSWORD);

    // 커넥션 풀에서 유지할 연결 개수를 설정합니다.
    config.setMaximumPoolSize(10);
    config.setMinimumIdle(5);

    // 사용할 수 있는 Connection이 없을 때 기다릴 최대 시간을 설정합니다.
    config.setConnectionTimeout(3_000);

    // 설정을 이용해 실제 커넥션 풀을 생성합니다.
    DATA_SOURCE = new HikariDataSource(config);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 최대 10개의 Connection을 사용할 수 있도록 설정하고, 요청이 없어도 최소 5개를 준비 상태로 유지하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Connection이 사용 중이라면 새로운 요청은 반납될 Connection을 기다립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 설정한 시간 안에 Connection을 얻지 못하면 예외가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 그 대기 시간을 3초로 설정했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;getConnection()은 더 단순해졌습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀을 준비하고 나면 실제 Connection을 가져오는 코드는 오히려 단순합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static Connection getConnection() throws SQLException {

    // 새로운 물리 연결을 만드는 대신 풀에서 Connection을 가져옵니다.
    return DATA_SOURCE.getConnection();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드와 비교하면 핵심적인 변화는 한 줄입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DriverManager.getConnection(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에서&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DATA_SOURCE.getConnection()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;으로 바뀌었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 한 줄 뒤에서 일어나는 동작은 상당히 다릅니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;기존

요청
 &amp;darr;
새 DB Connection 생성
 &amp;darr;
사용
 &amp;darr;
종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀 적용 후에는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;요청
 &amp;darr;
Pool에서 Connection 대여
 &amp;darr;
사용
 &amp;darr;
Pool에 Connection 반납&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO 쪽에서는 여전히 DatabaseUtil.getConnection()만 호출하기 때문에 기존 코드 대부분은 수정할 필요가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분도 DatabaseUtil을 별도로 만들어 사용했던 이유가 드러나는 지점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 연결 방식이 바뀌었지만 DAO 전체를 고칠 필요 없이 연결을 담당하는 클래스만 교체할 수 있었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;close()의 의미도 달라졌습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소와 마찬가지로 DAO에서는 try-with-resources를 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;try (Connection connection = DatabaseUtil.getConnection()) {

    // SQL 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록을 빠져나오면 connection.close()가 자동 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DriverManager로 직접 만든 Connection이라면 데이터베이스 연결을 종료하는 의미가 강하지만, 커넥션 풀에서 받은 Connection은 풀에 다시 반환됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개별 Connection의 close()와 &lt;b&gt;커넥션 풀 자체의 종료&lt;/b&gt;는 구분해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 DatabaseUtil에 풀 전체를 종료하기 위한 메서드도 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public static void close() {

    // 애플리케이션 종료 시 커넥션 풀 자체를 정리합니다.
    if (!DATA_SOURCE.isClosed()) {
        DATA_SOURCE.close();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 도서 관리 프로그램이 끝날 때 이를 호출하도록 수정했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DatabaseUtil.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소의&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;connection.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 이제&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다 썼으니 이 Connection을 돌려드리겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 가깝고,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DATA_SOURCE.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램이 끝났으니 커넥션 풀 자체를 정리하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에 가깝다고 이해했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 연결하는 방식과도 비교해 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 커밋에는 커넥션 풀만 적용한 것이 아니라 기존 DriverManager 방식과 비교하기 위한 간단한 테스트 코드도 함께 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무 SQL도 실행하지 않고 Connection을 만들고 닫는 작업을 반복하면서 연결 자체에 드는 비용을 확인하는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 100; i++) {

    // 매번 실제 데이터베이스 연결을 생성하고 종료합니다.
    try (Connection connection =
            DriverManager.getConnection(url, user, password)) {

        // SQL은 실행하지 않습니다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 동일한 작업을 커넥션 풀에서도 반복해 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; 100; i++) {

    // 이미 만들어진 풀에서 Connection을 빌렸다가 반납합니다.
    try (Connection connection =
            DatabaseUtil.getConnection()) {

        // SQL은 실행하지 않습니다.
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이런 단순한 테스트만으로 실제 서버 환경의 성능을 정확하게 측정할 수 있는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Connection을 매번 새로 만드는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 만들어 놓은 Connection을 빌려 쓰는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조적으로 어떤 차이가 있는지 확인하기에는 충분한 실습이었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀은 개발자 면접 질문에서도 자주 등장하는 키워드라고 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 풀은 데이터베이스 Connection을 미리 생성해 두고 재사용하여 연결 생성 비용을 줄이는 기술입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 직접 DriverManager 방식으로 작성했던 프로그램을 HikariCP로 교체해 보면서 좀 더 구체적으로 이해할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;왜 필요한가?
    &amp;darr;
DB Connection 생성에는 비용이 있으니까

어떻게 해결하는가?
    &amp;darr;
Connection을 미리 만들어 풀에서 재사용

Java에서는 무엇으로 추상화하는가?
    &amp;darr;
DataSource

이번에 사용한 구현체는?
    &amp;darr;
HikariCP

close()하면?
    &amp;darr;
개별 Connection은 풀에 반납

프로그램이 끝나면?
    &amp;darr;
DataSource 자체를 종료&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단어 하나를 외우는 것보다 실제 코드에서 어디에 적용되는지를 보는 편이 훨씬 기억에 잘 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이번 실습에서는 DAO 코드는 그대로 두고 DatabaseUtil의 구현만 바꾸어 커넥션 풀을 적용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 배웠던 &lt;b&gt;인터페이스, 추상화, 역할 분리&lt;/b&gt; 같은 개념이 JDBC 실습 안에서 조금씩 다시 연결되는 느낌입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 앞으로 Spring Boot를 배우면 HikariCP를 직접 이렇게 설정하지 않고 프레임워크가 상당 부분 대신 처리해 줄 텐데, 그 전에 내부에서 대략 무엇이 일어나는지를 직접 만들어 본 것은 꽤 의미 있는 과정이라고 생각합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이번 수업 소스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Repository&lt;br /&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library&quot;&gt;https://github.com/heebaek200/class_library&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수업 Commit&lt;br /&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library/commit/4af8af45b1b753d660d754712d695a9495dfa261&quot;&gt;https://github.com/heebaek200/class_library/commit/4af8af45b1b753d660d754712d695a9495dfa261&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습/Java</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/110</guid>
      <comments>https://heebaek.tistory.com/110#entry110comment</comments>
      <pubDate>Thu, 10 Sep 2026 17:12:16 +0900</pubDate>
    </item>
    <item>
      <title>View &amp;rarr; Service &amp;rarr; DAO &amp;rarr; DB, 드디어 코드에 층이 생겼습니다</title>
      <link>https://heebaek.tistory.com/109</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 시간에는 JDBC를 이용해 Java에서 데이터베이스에 접속하고, DAO 클래스를 통해 기본적인 조회와 등록 기능을 구현해 보았습니다. 이번에는 여기서 한 단계 더 나아갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 SQL 하나를 실행하고 끝나는 것이 아니라 여러 SQL을 하나의 작업 단위로 묶는 트랜잭션을 적용했고, 프로그램도 점차 View &amp;rarr; Service &amp;rarr; DAO &amp;rarr; DB 형태로 역할을 나누기 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 DAO를 하나씩 만들어 테스트하던 작은 예제였는데, 이번 수업을 지나면서 슬슬 하나의 프로그램다운 구조가 보이기 시작했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JOIN 결과를 DTO에 담아보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 지난 시간에 이어 BorrowDAO의 현재 대출 도서 조회 기능을 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 정보를 저장하는 borrows 테이블만 조회해서는 실제 화면에 보여줄 정보가 부족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 기록에는 도서 ID와 학생 ID가 들어 있지만, 사용자가 보고 싶은 것은 보통 이런 정보입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 책을 빌렸는가&lt;/li&gt;
&lt;li&gt;누가 빌렸는가&lt;/li&gt;
&lt;li&gt;언제 빌렸는가&lt;/li&gt;
&lt;li&gt;반납했는가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 borrows, books, students 테이블을 JOIN하여 필요한 정보를 한 번에 조회했습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
      br.id
    , br.book_id
    , bk.title
    , br.student_id
    , st.name AS student_name
    , br.borrow_date
    , br.return_date
FROM borrows AS br
JOIN books AS bk
     ON br.book_id = bk.id
JOIN students AS st
     ON br.student_id = st.id
WHERE br.return_date IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 Borrow DTO에도 JOIN으로 가져온 도서와 학생 정보를 담을 수 있도록 필드를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 JDBC를 배울 때는 흔히 &quot;테이블 하나 &amp;rarr; DTO 하나&quot; 정도로 생각하기 쉬운데, 실제 조회 결과는 여러 테이블을 조합해서 만들어지는 경우가 훨씬 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서 그 모습을 직접 확인할 수 있었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이번 수업의 핵심, JDBC에서 수동 트랜잭션 처리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 가장 중요하게 배운 부분은 도서 대출 기능이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서 대출은 SQL 하나만 실행해서 끝나는 작업이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대략 다음 작업이 연속해서 수행되어야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해당 도서가 대출 가능한지 확인&lt;/li&gt;
&lt;li&gt;대출 기록 INSERT&lt;/li&gt;
&lt;li&gt;도서의 대출 가능 상태 UPDATE&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 문제가 하나 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 대출 기록까지 INSERT했는데, 그다음 도서 상태를 변경하는 UPDATE에서 오류가 발생했다고 가정해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 데이터베이스에는 '대출 기록은 있는데 책은 여전히 대출 가능 상태' &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;라는 모순된 데이터가 남게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 여러 SQL을 &lt;b&gt;하나의 작업 단위로 처리하는 트랜잭션&lt;/b&gt;이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서는 보통 다음과 같은 흐름으로 표현합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;START TRANSACTION;

-- 여러 SQL 실행

COMMIT;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도중에 문제가 발생하면 다음과 같이 되돌립니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;ROLLBACK;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC에서는 이를 Connection 객체를 통해 제어할 수 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;connection.setAutoCommit(false);

try {
    // SELECT
    // INSERT
    // UPDATE

    connection.commit();
} catch (Exception e) {
    connection.rollback();
    throw e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection.setAutoCommit(false)를 호출하면 SQL 하나가 실행될 때마다 자동으로 커밋되던 동작을 중지할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이후 필요한 SQL을 모두 실행한 뒤 문제가 없다면 commit(), 중간에 예외가 발생했다면 rollback()을 호출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서 배우던&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;START TRANSACTION &amp;rarr; COMMIT / ROLLBACK&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조가 JDBC 코드에서는 어떻게 표현되는지 연결해서 볼 수 있었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도서 대출도 하나의 작은 업무 흐름이었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 기능을 조금 더 자세히 보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 SELECT로 해당 책이 존재하는지, 현재 대출 가능한 상태인지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건을 통과하면 borrows 테이블에 대출 기록을 추가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 books 테이블의 대출 가능 여부를 변경합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;대출 가능 여부 SELECT
        &amp;darr;
대출 기록 INSERT
        &amp;darr;
도서 상태 UPDATE
        &amp;darr;
모두 성공 &amp;rarr; COMMIT
하나라도 실패 &amp;rarr; ROLLBACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 INSERT와 UPDATE 하나하나가 아니라, &lt;b&gt;이 전체 과정이 하나의 업무&lt;/b&gt;라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 입장에서는 서로 다른 SQL이지만 사용자의 입장에서는 그냥 &quot;책을 빌린다&quot;라는 하나의 행동입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션이 필요한 이유가 꽤 직관적으로 이해되는 예제였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반납 기능으로 트랜잭션 복습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서 반납 기능도 같은 방식으로 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 다음 순서입니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;현재 대출 기록 SELECT
        &amp;darr;
대출 기록의 반납일 UPDATE
        &amp;darr;
도서 상태를 대출 가능으로 UPDATE
        &amp;darr;
COMMIT 또는 ROLLBACK&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회할 때는 한 가지 조건도 신경 써야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 학생이 과거에 같은 책을 빌렸다가 반납했고, 이후 다시 빌렸을 수도 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단순히 '이 학생이 이 책을 빌린 기록이 있는가?' &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;만 확인해서는 안 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;현재 아직 반납되지 않은 대출 기록&lt;/b&gt;을 찾아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 조건을 하나씩 붙이다 보니 SQL도 단순한 데이터 조회가 아니라 실제 업무 규칙을 표현하기 시작했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 DAO만으로는 부족하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지는 데이터베이스 작업을 DAO가 담당했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 프로그램의 기능이 늘어나면서 DAO에 모든 판단을 넣는 것도 점점 어색해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 도서를 추가한다고 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도서 이름이 비어 있는지 확인하는 것은 SQL 실행 자체와는 별개의 문제입니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;도서 제목이 비어 있는가?
저자가 입력되어 있는가?
검색어가 정상적인가?
ID가 유효한 값인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 것은 데이터베이스에 접근하는 기술적인 로직이라기보다는 &lt;b&gt;프로그램의 업무 규칙&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이번에는 LibraryService 클래스를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조는 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;View
 &amp;darr;
Service
 &amp;darr;
DAO
 &amp;darr;
DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service는 BookDAO, StudentDAO, BorrowDAO를 가지고 있으며, 필요한 검증을 한 뒤 실제 DB 처리를 DAO에 맡깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 도서 추가라면 개념적으로 다음과 같습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void addBook(Book book) throws SQLException {

    // 입력값에 업무 규칙을 적용합니다.
    if (book.getTitle() == null || book.getTitle().trim().isEmpty()) {
        throw new SQLException(&quot;도서 제목은 필수 입력 항목입니다.&quot;);
    }

    // 실제 DB 처리는 DAO에 위임합니다.
    bookDAO.addBook(book);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LibraryService가 세 DAO를 합성 관계로 소유하고, 도서 추가&amp;middot;조회&amp;middot;학생 조회&amp;middot;대출&amp;middot;반납 등의 기능을 DAO에 위임하도록 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Spring을 배우기 전의 순수 Java 코드지만, 앞으로 프레임워크에서 자주 만나게 될 Service 계층의 역할을 미리 체험해 본 셈입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자의 입력과 출력은 View에서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 LibraryView 클래스를 작성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View의 역할은 비교적 명확합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자의 입력을 받고 결과를 출력하는 것&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 콘솔 프로그램이기 때문에 Scanner를 이용해 메뉴와 값을 입력받습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 View가 직접 SQL을 실행하는 것이 아니라 Service를 호출합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;사용자
 ↕
LibraryView
 ↕
LibraryService
 ↕
DAO
 ↕
Database&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LibraryView는 현재 로그인한 학생의 ID와 이름도 필드로 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 학생 정보가 null이라면 로그인하지 않은 상태이고, 값이 있다면 로그인한 상태로 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 대출처럼 로그인이 필요한 기능을 실행하려 할 때 로그인 여부를 먼저 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엄밀한 인증 시스템이라기보다는 학습용으로 단순화한 구조이지만, 사용자의 상태를 View에서 관리하고 필요한 기능을 Service에 요청하는 흐름을 이해하기에는 충분했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Main은 이제 프로그램을 시작하기만 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Main 클래스에는 DAO 객체를 직접 생성하고 각각의 메서드를 실행해 보는 테스트 코드가 들어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능 구현을 확인하기에는 좋았지만 프로그램의 최종 구조는 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 이 테스트 코드를 정리하고 LibraryView를 실행하는 역할만 남겼습니다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;public class Main {

    public static void main(String[] args) {
        new LibraryView().start();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로그램의 흐름은 각 계층으로 흩어졌습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;사용자 입력
   &amp;darr;
View
   &amp;darr;
Service
   &amp;darr;
DAO
   &amp;darr;
DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 조회 결과는 다시 반대 방향으로 올라옵니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DB
 &amp;uarr;
DAO
 &amp;uarr;
Service
 &amp;uarr;
View
 &amp;uarr;
사용자 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 클래스가 늘어나니 오히려 복잡해 보이지만, 각 클래스가 담당하는 일을 한 문장씩 설명할 수 있다는 것이 장점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;View는 입출력을 담당합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Service는 업무 규칙을 담당합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DAO는 데이터베이스 접근을 담당합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 책임을 나누면 기능이 커졌을 때도 어느 코드를 어디에 작성해야 할지 훨씬 명확해집니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC 문법에서 프로그램 구조로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 Java에서 실행하는 방법 자체보다,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 SQL을 하나의 트랜잭션으로 묶는 방법&lt;/li&gt;
&lt;li&gt;데이터베이스 접근과 업무 규칙을 분리하는 방법&lt;/li&gt;
&lt;li&gt;사용자의 입출력을 별도의 View로 분리하는 방법&lt;/li&gt;
&lt;li&gt;각 계층이 서로 어떤 순서로 호출되는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 &lt;b&gt;프로그램 전체의 구조&lt;/b&gt;가 더 중요하게 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 작은 콘솔 기반 도서 관리 프로그램이지만,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;View &amp;harr; Service &amp;harr; DAO &amp;harr; DB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 흐름이 만들어지고 나니 예전에 단순하게 DAO 메서드만 하나씩 호출하던 코드와는 확실히 다른 모습이 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 Spring Boot를 배우게 되면 지금 직접 new로 객체를 만들고 연결한 부분이나 Service 계층의 역할 등이 또 다른 형태로 바뀔 텐데, 그 전에 순수 Java로 이 흐름을 직접 만들어 본 경험이 꽤 도움이 될 것 같습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이번 수업 소스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Repository&lt;br /&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library&quot;&gt;https://github.com/heebaek200/class_library&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 수업 Commit&lt;br /&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library/commit/f369341d1332a46b8d7a6305d1bf8d975c77600c&quot;&gt;https://github.com/heebaek200/class_library/commit/f369341d1332a46b8d7a6305d1bf8d975c77600c&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>학습/Java</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/109</guid>
      <comments>https://heebaek.tistory.com/109#entry109comment</comments>
      <pubDate>Thu, 10 Sep 2026 16:51:09 +0900</pubDate>
    </item>
    <item>
      <title>팀이 바뀌어도 처음부터 다시 시작할 수 있도록: GitHub 협업 환경 구축 매뉴얼</title>
      <link>https://heebaek.tistory.com/108</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 팀 프로젝트에서 팀장을 맡아 GitHub 저장소와 Projects, Wiki를 함께 운영해 보니 생각보다 준비할 것이 많았습니다. 저장소만 만든다고 바로 협업이 시작되는 것이 아니었습니다. 문서를 옮기고, 팀원을 초대하고, 프로젝트 보드를 설정하고, 모두가 같은 절차를 이해하도록 설명하는 과정이 필요했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수강 과정에서는 프로젝트마다 조가 바뀔 수 있습니다. 다음 팀에서도 기억에 의존하지 않고 처음부터 같은 환경을 만들 수 있도록, 실제로 사용할 준비 순서를 매뉴얼로 남겨 둡니다. 협업 도구를 사용했다는 결과뿐 아니라 협업을 시작할 수 있는 환경과 절차까지 직접 설계했다는 기록이기도 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;전체 진행 순서&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;공개 저장소를 만들고 Wiki 문서를 준비합니다.&lt;/li&gt;
&lt;li&gt;팀원에게 Wiki 링크를 보내 읽게 하고 GitHub ID를 받습니다.&lt;/li&gt;
&lt;li&gt;팀원이 Wiki를 읽는 동안 저장소와 로컬을 연결하고 공통 구조를 Push합니다.&lt;/li&gt;
&lt;li&gt;저장소와 별도로 GitHub Project를 만들고 기본 설정을 마칩니다.&lt;/li&gt;
&lt;li&gt;저장소와 Project에 팀원을 각각 초대합니다.&lt;/li&gt;
&lt;li&gt;초대 수락과 접근 권한을 확인한 뒤 온보딩을 시작합니다.&lt;/li&gt;
&lt;li&gt;온보딩 중 첫 Issue를 함께 만들고 실제 작업을 시작합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 공개 저장소와 Wiki부터 준비하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 팀원의 GitHub ID를 받지 않은 상태에서 내 계정에 새 저장소를 만듭니다. 저장소는 &lt;code&gt;Public&lt;/code&gt;으로 설정하고, &lt;code&gt;README&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt;, 라이선스는 추가하지 않은 빈 저장소로 생성합니다. Wiki 기능이 켜져 있는지도 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.github.com/en/communities/documenting-your-project-with-wikis/about-wikis&quot;&gt;GitHub 공식 문서&lt;/a&gt;에 따르면 공개 저장소에 만든 Wiki는 저장소와 함께 공개되므로 Wiki의 공개 상태를 따로 바꿀 필요는 없습니다. 기본 설정에서는 저장소에 쓰기 권한이 있는 협업자만 Wiki를 수정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 문서를 처음부터 다시 쓰지는 않습니다. 이전 프로젝트의 Wiki에서 다음 문서를 가져와 그대로 붙여 넣습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Home&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GitHub Projects 도입&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;작업 순서&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Git 문제 해결&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가져온 뒤에는 문서의 내용이나 절차를 다시 손보지 않습니다. 이전 예시로 사용한 &lt;b&gt;저장소 링크와 GitHub Project 링크만 새 프로젝트 주소로 변경&lt;/b&gt;합니다. 이렇게 해 두면 팀이 바뀔 때마다 같은 설명을 반복해서 작성하지 않아도 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 팀원에게 가장 먼저 보낼 메시지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Wiki 준비가 끝나면 Discord로 링크를 전달하고 GitHub ID를 받습니다. 이때 내가 약 10분 동안 설정 작업에 집중해야 한다는 점도 분명하게 알립니다.&lt;/p&gt;
&lt;pre id=&quot;code_1789023301720&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  GitHub 협업 환경 설정 안내

지금부터 약 10분 동안 GitHub 협업 환경을 설정하겠습니다.

그동안 아래 두 가지를 먼저 진행해 주세요.

① 아래 Wiki 문서 읽어 보기
② 이 메시지에 각자의 GitHub ID 남기기

  Wiki
새 Wiki 링크

저는 그 사이에 저장소 초대, 로컬 저장소 연결, .gitignore 작성 및 첫 Push, GitHub Project 생성과 기초 설정을 연속해서 진행하겠습니다.

읽다가 궁금한 내용은 메모해 두셨다가 온보딩 때 한꺼번에 질문해 주시면 됩니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &amp;ldquo;잠시 기다려 주세요&amp;rdquo;라고만 말하는 것보다 어떤 작업을 하는지 함께 알려 주면, 팀원도 기다려야 하는 이유와 그동안 해야 할 일을 이해하기 쉽습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 팀원이 Wiki를 읽는 동안 할 일&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 저장소 초대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Discord로 받은 GitHub ID를 이용해 저장소의 Collaborator로 초대합니다. 초대가 전송됐다고 끝난 것이 아니라, 온보딩 전에 팀원들이 초대를 수락했는지도 확인합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 로컬 저장소 연결과 공통 구조 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 로컬 폴더를 Git 저장소로 만들고 원격 저장소와 연결합니다. 첫 Push 전에 만드는 것은 &lt;code&gt;.gitignore&lt;/code&gt;뿐입니다. 기능 코드, 샘플 화면, 첫 Issue는 미리 만들지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;는 &lt;a href=&quot;https://www.toptal.com/developers/gitignore&quot;&gt;gitignore.io&lt;/a&gt;에서 생성합니다. HTML&amp;middot;CSS&amp;middot;JavaScript로 진행하는 기본 프로젝트에서는 다음 태그를 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Windows, macOS, VisualStudioCode, Intellij, Eclipse&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제에서 생기는 불필요한 파일과 VS Code의 개인 설정이 저장소에 올라오는 것을 막기 위한 구성입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;.gitignore&lt;/code&gt;를 준비한 뒤 다음 순서로 로컬 저장소와 원격 저장소를 연결하고 첫 Push를 진행합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;git init
git branch -M main
git remote add origin 새_저장소_URL
git add .
git commit -m &quot;chore: 프로젝트 공통 구조 생성&quot;
git push -u origin main&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Push가 끝나면 GitHub 저장소 화면에서 폴더와 &lt;code&gt;.gitignore&lt;/code&gt;가 정상적으로 올라왔는지 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 프로젝트 생성과 라이브러리 의존성 설정은 팀원들과 함께 확인해야 할 첫 번째 작업으로 남겨 둡니다. 이를 통해 프로젝트가 어떤 설정으로 시작됐는지 모든 팀원이 알고, 각자의 로컬 환경에서도 같은 상태로 실행되는지 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. GitHub Project 생성과 초기 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Project는 저장소와 별개의 공간입니다. 새 Project를 만든 뒤 프로젝트 설정에서 공개 범위를 &lt;code&gt;Public&lt;/code&gt;으로 변경합니다. &lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects&quot;&gt;Project 공개 범위&lt;/a&gt;와 &lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-your-project/managing-access-to-your-projects&quot;&gt;Project 접근 권한&lt;/a&gt;은 별도로 관리됩니다. 따라서 공개 Project는 누구나 볼 수 있지만, 팀원이 항목을 수정하려면 각 GitHub ID를 Project에도 따로 초대하고 &lt;code&gt;Write&lt;/code&gt; 권한을 부여해야 합니다. 저장소 Collaborator 초대만으로 Project 편집 권한까지 생기는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 설정은 다음 순서로 진행합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 상태 단순화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Status&lt;/code&gt; 필드는 아래 세 단계만 남깁니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Todo / In Progress / Done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 프로젝트에서는 상태를 세분화하기보다 해야 할 일, 진행 중인 일, 끝난 일을 바로 구분하는 편이 좋았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 레이블 단순화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소의 &lt;code&gt;Issues &amp;rarr; Labels&lt;/code&gt;에서 이번 프로젝트에 사용할 레이블을 세 종류로 정리합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;설계 / 기능구현 / 테스트&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 날짜 필드 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Project에 다음 두 개의 Date 필드를 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;시작일
완료(예정)일&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;완료(예정)일&lt;/code&gt;은 계획 단계에서는 예정일로 쓰고, 작업이 끝난 뒤에는 실제 완료일로 갱신합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 보기 생성과 한글 제목 지정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 보기를 만들고 제목을 알아보기 쉬운 한글로 바꿉니다.&lt;/p&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;레이아웃&lt;/th&gt;
&lt;th&gt;보기 제목&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Board&lt;/td&gt;
&lt;td&gt;작업 보드&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Todo / In Progress / Done&lt;/code&gt; 상태 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;작업 목록&lt;/td&gt;
&lt;td&gt;담당자, 레이블, 날짜를 표로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roadmap&lt;/td&gt;
&lt;td&gt;프로젝트 일정&lt;/td&gt;
&lt;td&gt;시작일과 완료(예정)일을 시간 순서로 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Board의 열 기준은 &lt;code&gt;Status&lt;/code&gt;로 지정합니다. Roadmap의 시작 날짜에는 &lt;code&gt;시작일&lt;/code&gt;, 목표 날짜에는 &lt;code&gt;완료(예정)일&lt;/code&gt;을 연결합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5. Table과 Roadmap 필터 저장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;작업 목록&lt;/code&gt;에는 완료되지 않은 항목이 먼저 보이도록 다음 필터를 적용합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;-status:Done&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;프로젝트 일정&lt;/code&gt;에는 두 날짜가 입력된 항목만 표시되도록 다음 필터를 적용합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;has:&quot;시작일&quot; has:&quot;완료(예정)일&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터를 입력한 뒤에는 보기 메뉴에서 변경 내용을 반드시 저장합니다. 저장하지 않으면 다른 화면으로 이동했을 때 설정이 사라질 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-6. Project 공개 및 팀원 초대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Project 설정에서 다음 두 가지를 각각 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Visibility&lt;/code&gt;: &lt;code&gt;Public&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Manage access&lt;/code&gt;: 팀원의 GitHub ID를 추가하고 역할을 &lt;code&gt;Write&lt;/code&gt;로 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소 초대와 Project 초대는 별개이므로 둘 다 완료해야 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 온보딩 전 최종 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원에게 설명을 시작하기 전에 아래 항목을 먼저 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 팀원이 저장소 초대를 수락했는가?&lt;/li&gt;
&lt;li&gt;팀원에게 저장소 쓰기 권한이 있는가?&lt;/li&gt;
&lt;li&gt;모든 팀원이 GitHub Project를 열 수 있는가?&lt;/li&gt;
&lt;li&gt;Project에서 항목과 필드를 수정할 수 있는가?&lt;/li&gt;
&lt;li&gt;Wiki 링크가 로그인 여부와 관계없이 열리는가?&lt;/li&gt;
&lt;li&gt;Wiki 안의 저장소&amp;middot;Project 링크가 새 주소로 바뀌었는가?&lt;/li&gt;
&lt;li&gt;공통 폴더 구조와 &lt;code&gt;.gitignore&lt;/code&gt;가 &lt;code&gt;main&lt;/code&gt;에 Push되었는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 끝난 뒤 Discord에 설정 완료를 알리고 온보딩을 시작합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 팀원 온보딩 진행&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-1. 짧은 자기소개로 운영 기준 설명하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;절차를 따라 달라고 말하려면 왜 내가 이 방식을 제안하는지 먼저 설명하는 편이 낫습니다. 경력을 과장하기보다 이번 프로젝트에서 작업 분배와 통합을 담당할 근거로만 사용합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 일본 IT 기업에서 6년 반 동안 개발자로 근무하며 프로젝트 관리 도구를 이용한 협업을 경험했습니다. 여러 사람이 함께 개발할 때는 누가 무엇을 맡았고 어디까지 진행했는지를 공유하는 절차가 반드시 필요합니다. 이번 프로젝트에서는 GitHub Projects와 Wiki를 그 기준으로 사용하겠습니다. 어려운 부분은 함께 확인하되, 작업 충돌과 누락을 줄이기 위해 정리된 절차는 같이 따라주시기를 부탁드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-2. 프로젝트 관리 도구가 필요한 이유 설명하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인원이 적고 기간이 짧아도 말로만 작업을 나누면 누가 무엇을 맡았는지, 어디까지 끝났는지, 다음 작업이 무엇인지 금방 흐려집니다. GitHub Projects를 쓰는 목적은 일을 복잡하게 만드는 것이 아니라 다음 세 가지를 한 화면에서 확인하기 위해서라고 설명합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;누가 어떤 작업을 맡았는가?&lt;/li&gt;
&lt;li&gt;지금 어느 단계까지 진행됐는가?&lt;/li&gt;
&lt;li&gt;완료되지 않은 작업은 무엇인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-3. Wiki를 열어 실제 절차 설명하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원에게 미리 읽게 한 Wiki를 화면에 띄운 뒤 다음 흐름을 실제 메뉴와 함께 설명합니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;Issue 확인
&amp;rarr; Todo에서 In Progress로 이동
&amp;rarr; 최신 main 받기
&amp;rarr; Issue 번호로 브랜치 생성
&amp;rarr; 작업&amp;middot;Commit&amp;middot;Push
&amp;rarr; 테스트 후 Pull Request 작성
&amp;rarr; 리뷰와 수정
&amp;rarr; Merge
&amp;rarr; Issue 종료 및 Done 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령어는 Wiki의 &lt;code&gt;작업 순서&lt;/code&gt; 문서를 기준으로 보여 줍니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;git switch main
git pull origin main
git switch -c f/이슈번호&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6-4. 첫 Issue로 프로젝트 초기 설정 진행하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온보딩 중 첫 Issue를 생성하고, 프로젝트의 기본 환경을 구성하는 과정을 실제 협업 절차의 첫 사례로 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Issue 제목&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[공통] 프로젝트 초기 설정과 개발 환경 구성하기
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작업 목적&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 기본 구조와 라이브러리 의존성을 설정하여 저장소에 반영하고, 모든 팀원이 같은 프로젝트를 Clone하여 로컬에서 실행할 수 있도록 준비합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;작업 내용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트에서 사용할 언어와 프레임워크 버전을 확인합니다.&lt;/li&gt;
&lt;li&gt;프로젝트명, 패키지명, 빌드 도구 등 기본 설정을 결정합니다.&lt;/li&gt;
&lt;li&gt;백엔드 또는 프론트엔드 프로젝트를 생성합니다.&lt;/li&gt;
&lt;li&gt;프로젝트에서 공통으로 사용할 라이브러리 의존성을 추가합니다.&lt;/li&gt;
&lt;li&gt;생성된 프로젝트와 설정 파일을 작업 브랜치에 Push합니다.&lt;/li&gt;
&lt;li&gt;Pull Request에서 프로젝트 구조와 의존성 설정을 함께 확인합니다.&lt;/li&gt;
&lt;li&gt;Pull Request를 Merge한 뒤 각 팀원이 저장소를 Clone합니다.&lt;/li&gt;
&lt;li&gt;각자의 IDE에서 프로젝트를 불러오고 의존성 설치를 완료합니다.&lt;/li&gt;
&lt;li&gt;모든 팀원의 로컬 환경에서 빌드와 실행이 정상적으로 되는지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의 사항&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 비밀번호, API 키 등의 비밀 정보는 Commit하지 않습니다.&lt;/li&gt;
&lt;li&gt;개인 PC에서만 사용하는 IDE 설정 파일은 공유하지 않습니다.&lt;/li&gt;
&lt;li&gt;팀원마다 다르게 설정해야 하는 값은 예제 설정 파일이나 Wiki에 작성합니다.&lt;/li&gt;
&lt;li&gt;모든 팀원의 실행 확인이 끝나기 전에는 개별 기능 개발을 시작하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;완료 조건&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 기본 구조가 main 브랜치에 반영되어야 합니다.&lt;/li&gt;
&lt;li&gt;필요한 라이브러리 의존성이 설정되어야 합니다.&lt;/li&gt;
&lt;li&gt;모든 팀원이 저장소를 Clone해야 합니다.&lt;/li&gt;
&lt;li&gt;모든 팀원의 로컬 환경에서 의존성 설치가 완료되어야 합니다.&lt;/li&gt;
&lt;li&gt;모든 팀원이 프로젝트의 빌드와 실행에 성공해야 합니다.&lt;/li&gt;
&lt;li&gt;로컬 설정 중 발생한 공통 문제와 해결 방법이 Wiki에 반영되어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;담당자가 작업을 시작할 때 &lt;code&gt;In Progress&lt;/code&gt;로 옮기는 모습까지 보여 줍니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 프로젝트 진행 중 확인할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 시작된 뒤에는 절차를 계속 새로 설명하기보다 Wiki를 기준으로 확인합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작업 전에 최신 &lt;code&gt;main&lt;/code&gt;을 받았는가?&lt;/li&gt;
&lt;li&gt;Issue 번호에 맞는 브랜치에서 작업했는가?&lt;/li&gt;
&lt;li&gt;담당 범위를 벗어난 파일을 수정하지 않았는가?&lt;/li&gt;
&lt;li&gt;PR 전에 기능과 화면을 직접 테스트했는가?&lt;/li&gt;
&lt;li&gt;리뷰 수정은 같은 브랜치와 PR에서 이어서 진행했는가?&lt;/li&gt;
&lt;li&gt;Merge 후 Issue와 Project 상태를 정리했는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;충돌이 발생하면 그것도 협업 과정의 일부로 경험하게 둡니다. 실제로 문제가 생겼을 때 Wiki의 &lt;code&gt;Git 문제 해결&lt;/code&gt; 문서를 함께 보며 해결하고, 반복되는 문제만 문서에 보충합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 매뉴얼의 목적은 모든 팀을 똑같이 통제하는 것이 아닙니다. 팀이 바뀔 때마다 저장소를 만들고, Wiki를 복사하고, 권한과 Project를 설정하고, 협업 절차를 설명하는 일을 빠뜨리지 않기 위한 체크리스트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 프로젝트에서도 코드를 작성하기 전에 협업 환경을 준비하는 사람이 필요했습니다. 이번 경험을 통해 저는 기능 구현뿐 아니라 문서화, 작업 분배, 권한 설정, 일정 가시화와 팀 온보딩까지 프로젝트가 시작될 수 있는 기반을 직접 만들었습니다. 다음 팀에서는 이 글을 열어 두고 처음부터 같은 순서로 다시 시작할 생각입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 문서&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/communities/documenting-your-project-with-wikis/about-wikis&quot;&gt;GitHub Docs: About wikis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects&quot;&gt;GitHub Docs: Managing visibility of your projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects/managing-your-project/managing-access-to-your-projects&quot;&gt;GitHub Docs: Managing access to your projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects/customizing-views-in-your-project/filtering-projects&quot;&gt;GitHub Docs: Filtering projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.toptal.com/developers/gitignore&quot;&gt;gitignore.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>프로젝트/팀 프로젝트 관리</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/108</guid>
      <comments>https://heebaek.tistory.com/108#entry108comment</comments>
      <pubDate>Thu, 10 Sep 2026 13:24:00 +0900</pubDate>
    </item>
    <item>
      <title>Lv2 다음 큰 숫자 - 맞는 것 같은데 왜 효율성에서 죽었을까?</title>
      <link>https://heebaek.tistory.com/107</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 프로그래머스 Lv2 문제인 「다음 큰 숫자」를 풀어봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 자체는 단순합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 숫자 n보다 커야 하고&lt;/li&gt;
&lt;li&gt;2진수로 표현했을 때 1의 개수가 같아야 하며&lt;/li&gt;
&lt;li&gt;그 조건을 만족하는 수 중 가장 작은 값을 찾으면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;basic&quot;&gt;&lt;code&gt;78  = 1001110
83  = 1010011&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 1이 4개이고, 83이 그중 가장 작은 다음 숫자입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음에는 문자열로 직접 만들어봤습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 2진수 문자열을 직접 조작하는 방식으로 접근했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어는 이랬습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;숫자를 2진수 문자열로 변환합니다.&lt;/li&gt;
&lt;li&gt;오른쪽부터 1과 그 왼쪽의 0을 찾습니다.&lt;/li&gt;
&lt;li&gt;그 0을 1로 올리고 기존 1 하나를 내립니다.&lt;/li&gt;
&lt;li&gt;남은 1들을 가장 오른쪽으로 몰아넣습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1001110
&amp;rarr; 1010011&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 제출에서는 정확성 테스트 하나가 실패했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 1111처럼 앞에 바꿀 0이 없는 경우였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 간단하게:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;String bi = &quot;0&quot; + Integer.toString(n, 2);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처럼 앞에 0 하나를 붙였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러자 결과는:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;정확성 14 / 14
효율성 0 / 6&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 정답인데 효율성 테스트만 전멸했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그런데 아무리 봐도 느릴 이유가 없습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 아직도 미스터리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 제한은:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;n &amp;lt;= 1,000,000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2진수로 바꿔도 길이가 대략 20자리 정도밖에 되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 작성한 코드도 결국:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;2진수 변환
&amp;rarr; 길이 약 20인 배열 탐색
&amp;rarr; 길이 약 20인 배열 다시 탐색
&amp;rarr; 다시 정수 변환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡도로 보면 사실상:&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;O(log n)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 객체나 배열을 몇 번 생성한다고 해도, 20글자 남짓한 데이터를 다루는 코드가 효율성 테스트 6개에서 전부 시간초과가 나는 것은 쉽게 납득하기 어려웠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결국 비트 연산으로 다시 작성했습니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열을 완전히 버리고 비트 자체를 조작하는 방식으로 다시 풀었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 간단합니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;가장 오른쪽의 연속된 0을 건너뜁니다.
&amp;rarr; 그 다음 연속된 1의 개수를 셉니다.
&amp;rarr; 그 왼쪽 0을 1로 올립니다.
&amp;rarr; 아래쪽 비트를 모두 0으로 만듭니다.
&amp;rarr; 남은 1들을 가장 오른쪽부터 채웁니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1001110

오른쪽 0 건너뜀
연속된 111 확인
왼쪽 0을 1로 변경

1010000

남은 1 두 개를 오른쪽으로 이동

1010011&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;정확성 통과
효율성 통과&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전 통과였습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서 대체 무엇이 문제였을까요?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논리적인 반례가 있는지도 의심했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기존 문자열 풀이의 아이디어 자체는 맞았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 가능한 범위에서 결과를 비교해보더라도 정답 자체가 잘못되는 문제는 찾기 어려웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 현재로서는 몇 가지 가능성만 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 변환 과정에서 발생하는 객체 생성 비용이 효율성 테스트에서 예상보다 크게 작용했거나, 실제 제출 코드와 제가 다시 확인한 코드 사이에 아주 작은 차이가 있었을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹은 단순히 채점 환경에서 문자열 버전과 비트 연산 버전의 상수 시간 차이가 생각보다 크게 드러난 것일 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 n이 고작 100만 이하라는 조건을 생각하면 아직도 조금 이상합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제에서 가장 재미있었던 부분은 정답을 찾는 과정이 아니라 이 미스터리였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 버전도 알고리즘적으로는 충분히 빠른 것처럼 보였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데:&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;문자열 버전
&amp;rarr; 정확성 전부 통과
&amp;rarr; 효율성 전부 실패

비트 연산 버전
&amp;rarr; 완전 통과&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 결과가 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 시간초과의 정확한 원인은 아직도 잘 모르겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔은 코딩테스트에서도 정답보다 채점 결과가 더 어려운 문제가 나오는 모양입니다.&lt;/p&gt;</description>
      <category>학습/코딩테스트</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/107</guid>
      <comments>https://heebaek.tistory.com/107#entry107comment</comments>
      <pubDate>Wed, 9 Sep 2026 19:01:40 +0900</pubDate>
    </item>
    <item>
      <title>JDBC를 배우며 다시 떠올린 &amp;lsquo;반복 작업과의 싸움&amp;rsquo;</title>
      <link>https://heebaek.tistory.com/106</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 Java 수업에서 JDBC를 이용해 DAO 클래스를 직접 작성하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 실행하고 ResultSet에서 값을 꺼낸 뒤 DTO에 하나씩 담는 전형적인 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Student student = new Student(
        rs.getInt(&quot;id&quot;),
        rs.getString(&quot;name&quot;),
        rs.getString(&quot;email&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 배우는 내용 자체는 흥미롭지만, 코드를 작성하다 보면 자연스럽게 이런 생각이 듭니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼이 수십 개라면 이것을 전부 직접 작성해야 하나?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼 하나가 추가되면 SQL도 수정하고, DTO도 수정하고, ResultSet에서 값을 꺼내는 코드도 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 지금은 JDBC의 기본 동작을 배우는 과정이므로 직접 작성하는 것이 당연합니다. 오히려 이렇게 밑바닥부터 작성해 봐야 이후 프레임워크가 무엇을 대신 해주는지도 제대로 이해할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 반복 작업을 보고 있으니 약 10년 전 일본에서 개발자로 일하던 당시의 기억이 떠올랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;테이블이 수십 개였던 업무 시스템&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 회사는 개발자가 십수 명 정도인 비교적 작은 조직이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 만들던 업무 시스템의 규모는 결코 작지 않았습니다. 테이블이 수십 개에 달했고, 각각의 테이블에 대응하는 관리 화면과 입력 화면도 계속해서 만들어야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 사용하던 백엔드 프레임워크는 PHP의 FuelPHP였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규모의 시스템을 모든 개발자가 매번 처음부터 작성했다면 아마 개발 속도를 감당하기 어려웠을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 프로젝트에서는 각 필드에 대한 정보를 별도의 테이블에서 관리하는 구조를 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 하나의 필드에 대해 다음과 같은 정보를 정의했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 타입&lt;/li&gt;
&lt;li&gt;필수 입력 여부&lt;/li&gt;
&lt;li&gt;최대 길이&lt;/li&gt;
&lt;li&gt;입력값 검증 규칙&lt;/li&gt;
&lt;li&gt;프론트엔드에서 사용할 입력 태그&lt;/li&gt;
&lt;li&gt;화면 표시 방법&lt;/li&gt;
&lt;li&gt;각종 커스텀 타입 및 옵션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 메타데이터를 바탕으로 &lt;b&gt;테이블의 필드가 백엔드와 프론트엔드까지 이어지도록 만들어 놓았습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자가 매번 비슷한 검증 로직이나 HTML을 반복 작성하지 않아도 어느 정도 자동으로 적용되는 구조였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에는 그저 프로젝트를 굴리기 위해 필요한 시스템이라고 생각했는데, 지금 다시 떠올려보면 꽤 적극적으로 반복 작업을 자동화했던 셈입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동화한다고 모든 문제가 사라지는 것은 아니었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이렇게 만들어 놓았다고 해서 개발이 갑자기 평화로워진 것은 아니었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업무 시스템에는 언제나 예외가 등장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 깔끔하게 만든 공통 처리에 조금씩 조건문이 붙기 시작합니다.  &lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;if (이_고객사만_이상한_경우) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 얼마 지나지 않아 이런 코드가 추가됩니다. &lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (이_고객사인데_2017년_이전_데이터인_경우) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 어느 순간에는 그 예외를 처리하기 위한 예외가 다시 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동화를 열심히 해 놓고도 결국 그런 예외를 처리하느라 밤을 새웠던 기억이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프레임워크와 공통화는 반복 작업을 줄여줄 수 있지만, 현실의 요구사항 자체를 단순하게 만들어 주지는 않는다는 것도 당시 많이 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다시 Java를 배우면서 드는 궁금증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10년이 지난 지금은 Java로 JDBC를 직접 작성하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PreparedStatement를 만들고, SQL을 실행하고, ResultSet에서 타입에 맞게 값을 하나씩 꺼내고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에 이미 비슷한 반복 작업을 자동화했던 경험이 있다 보니 자연스럽게 궁금해집니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금의 Java 생태계에서는 이런 문제를 어떻게 해결하고 있을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 수업에서 Spring Boot를 배우게 될 예정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA나 MyBatis 같은 기술의 이름은 알고 있지만, 아직 본격적으로 사용해 본 적은 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAO의 반복적인 CRUD 코드를 얼마나 줄여주는지, 데이터베이스의 컬럼과 Java 객체를 어떤 방식으로 연결하는지, 입력값 검증은 어디까지 프레임워크에서 지원하는지도 직접 확인해 보고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 과거에 직접 구현했던 것처럼 데이터 타입과 입력 제약사항을 한 곳에서 정의하고 백엔드와 프론트엔드까지 일관되게 적용하는 문제를 현재의 프레임워크에서는 어떤 방식으로 해결하는지도 궁금합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 다소 번거롭게 느껴지는 JDBC 코드를 직접 작성하고 있지만, 오히려 이 과정을 경험하고 있기 때문에 이후 프레임워크가 제공하는 기능의 의미를 더 잘 이해할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10년 전에 필요에 의해 직접 만들었던 구조와, 지금의 Spring 생태계가 제시하는 해법이 얼마나 닮아 있을지 앞으로의 수업이 조금 기대됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 무엇보다도,&lt;/p&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;if (이_고객사만_이상한_경우)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 완전히 없애주는 프레임워크가 존재한다면 꼭 배우고 싶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 없겠지만요.&lt;/p&gt;</description>
      <category>개발 에세이</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/106</guid>
      <comments>https://heebaek.tistory.com/106#entry106comment</comments>
      <pubDate>Wed, 9 Sep 2026 17:59:44 +0900</pubDate>
    </item>
    <item>
      <title>JOIN 결과를 Java 객체 관계로 옮겨 보기</title>
      <link>https://heebaek.tistory.com/105</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업에서는 JDBC를 이용하여 여러 테이블을 JOIN하고, 조회 결과를 DAO에서 처리하는 방법을 실습했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 요구사항은 대출 정보를 조회하면서 도서명과 학생 이름을 함께 출력하는 정도였습니다. 하지만 단순히 필요한 문자열만 Borrow DTO에 추가하기보다, 조회된 데이터의 관계를 Java 객체에서도 표현해 보기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;먼저 데이터 구조부터 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제의 중심은 borrows 테이블입니다. 대출 기록에는 도서나 학생의 상세 정보를 직접 저장하지 않고, 각각의 기본 키를 가리키는 book_id와 student_id만 저장합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;borrows
└─ id: 1
   book_id: 3
   student_id: 1
   borrow_date: 2025-05-01
   return_date: NULL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기록만 보면 3번 도서를 1번 학생이 빌렸다는 사실만 알 수 있습니다. 실제 도서 제목과 학생 이름을 확인하려면 books, students 테이블을 JOIN해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;borrows.book_id    &amp;rarr; books.id       &amp;rarr; 알고리즘 문제 해결
borrows.student_id &amp;rarr; students.id    &amp;rarr; 홍길동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN 결과는 한 행에 모든 정보가 펼쳐진 형태로 반환됩니다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;대출 ID: 1
도서 ID: 3 / 제목: 알고리즘 문제 해결
학생 ID: 1 / 이름: 홍길동
대출일: 2025-05-01 / 반납일: NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 예제에서는 이 평평한 조회 결과를 그대로 Borrow의 개별 필드에 담지 않고, 다음과 같이 객체 간의 관계가 드러나는 구조로 변환했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Borrow
├─ id: 1
├─ borrowDate: 2025-05-01
├─ returnDate: null
├─ Book
│  ├─ id: 3
│  └─ title: 알고리즘 문제 해결
└─ Student
   ├─ id: 1
   └─ name: 홍길동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 데이터베이스에서는 외래 키로 표현된 관계를 JOIN으로 가져오고, Java에서는 Borrow 안에 Book과 Student 객체를 포함하는 관계로 다시 조립하는 것이 이번 실습의 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외래 키를 이름으로 바꾸는 데서 끝내지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;borrows 테이블에는 도서와 학생의 실제 정보가 아니라 book_id, student_id가 저장되어 있습니다. 따라서 화면에 도서 제목이나 학생 이름을 표시하려면 books, students 테이블을 연결해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
      br.id
    , br.book_id
    , bk.title
    , bk.author
    , bk.publisher
    , bk.publication_year
    , bk.isbn
    , bk.available
    , br.student_id
    , st.name AS student_name
    , st.student_id AS student_no
    , br.borrow_date
    , br.return_date
FROM
    borrows AS br
JOIN books AS bk
     ON br.book_id = bk.id
JOIN students AS st
     ON br.student_id = st.id
WHERE
    br.return_date IS NULL
ORDER BY
    br.id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제 요구사항대로라면 Borrow DTO에 bookTitle, studentName 같은 필드를 추가하는 것만으로도 충분했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇게 만들면 Borrow 객체에 도서와 학생의 일부 정보가 낱개의 필드로 흩어지게 됩니다. 또한 나중에 저자, 출판사, 학번처럼 필요한 정보가 늘어날 때마다 Borrow에 필드를 계속 추가해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 Borrow가 Book과 Student 객체를 포함하도록 구성했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class Borrow {

    private int id;
    private int bookId;
    private int studentId;
    private LocalDate borrowDate;
    private LocalDate returnDate;

    private Book book;
    private Student student;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 하나의 대출 기록을 가져오면 다음과 같은 객체 관계가 만들어집니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Borrow
 ├─ Book
 └─ Student&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 borrow.getBook().getTitle()처럼 도서 정보를 가져오고, borrow.getStudent().getName()처럼 대출한 학생의 정보도 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 포함하는 집합 관계라고 표현할 수 있습니다. 핵심은 관계형 데이터베이스의 외래 키 관계를 Java에서도 객체 간의 관계로 표현해 보았다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터베이스의 NULL을 Java에서 처리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대출 중인 도서는 아직 반납일이 정해지지 않았으므로 return_date에 NULL이 저장됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC에서 날짜를 가져올 때 다음 코드를 바로 실행하면 문제가 생길 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;resultSet.getDate(&quot;return_date&quot;).toLocalDate()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return_date가 NULL이면 getDate()의 결과도 null이므로, 이어서 toLocalDate()를 호출하는 순간 NullPointerException이 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 먼저 값을 꺼낸 뒤 null 여부를 확인했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Date dateReturnDate = resultSet.getDate(&quot;return_date&quot;);

borrow.setReturnDate(
        dateReturnDate != null
                ? dateReturnDate.toLocalDate()
                : null
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 메서드는 return_date IS NULL인 기록만 조회하므로 결과의 반납일은 항상 null입니다. 그래도 이 처리를 넣어 두면 나중에 전체 대출 이력을 조회하는 메서드로 확장할 때 반납된 기록과 대출 중인 기록을 모두 안전하게 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스의 NULL과 Java의 null이 연결되는 지점을 직접 다뤄 보았다는 점에서 의미가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JOIN 결과에서 반복되는 객체 재사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JOIN 결과는 행 단위로 반환됩니다. 한 학생이 여러 권의 도서를 빌렸다면 동일한 학생 정보가 여러 행에 반복될 수 있고, 한 도서의 전체 대출 이력을 조회한다면 같은 도서 정보도 계속 나타날 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행을 읽을 때마다 무조건 새로운 Book과 Student를 만들면 기본 키가 같은 데이터를 가진 객체가 여러 개 생성됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 줄이기 위해 기본 키를 키로 사용하는 HashMap을 두었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Map&amp;lt;Integer, Book&amp;gt; bookMap = new HashMap&amp;lt;&amp;gt;();
Map&amp;lt;Integer, Student&amp;gt; studentMap = new HashMap&amp;lt;&amp;gt;();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 만들어진 객체가 있다면 이를 다시 사용하고, 없을 때만 새 객체를 만드는 방식입니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Book book = bookMap.get(bookId);

if (book == null) {
    book = new Book(
            bookId,
            resultSet.getString(&quot;title&quot;),
            resultSet.getString(&quot;author&quot;),
            resultSet.getString(&quot;publisher&quot;),
            resultSet.getInt(&quot;publication_year&quot;),
            resultSet.getString(&quot;isbn&quot;),
            resultSet.getBoolean(&quot;available&quot;)
    );

    bookMap.put(bookId, book);
}

borrow.setBook(book);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학생 객체도 같은 방식으로 처리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Student student = studentMap.get(studentId);

if (student == null) {
    student = new Student(
            studentId,
            resultSet.getString(&quot;student_name&quot;),
            resultSet.getString(&quot;student_no&quot;)
    );

    studentMap.put(studentId, student);
}

borrow.setStudent(student);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 메서드 안에서만 사용하는 작은 객체 캐시와 비슷합니다. 단순한 예제에서는 성능 차이가 크지 않겠지만, JOIN 결과의 중복 데이터를 객체로 어떻게 표현할 것인지 생각해 볼 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단순 조회를 넘어 객체 구조까지 생각해 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 JOIN 문법을 작성하는 것에서 한 단계 더 나아가 다음 내용을 함께 적용했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Borrow가 Book과 Student 객체를 포함하도록 구성&lt;/li&gt;
&lt;li&gt;데이터베이스의 NULL 값을 Java에서 안전하게 변환&lt;/li&gt;
&lt;li&gt;JOIN 결과에 반복되는 객체를 HashMap으로 재사용&lt;/li&gt;
&lt;li&gt;여러 번 조회하지 않고 한 번의 JOIN으로 필요한 정보 조회&lt;/li&gt;
&lt;li&gt;현재 대출 중인 기록만 return_date IS NULL로 선별&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직은 작은 예제이므로 Borrow에 도서명과 학생 이름을 직접 추가하는 편이 더 간단할 수도 있습니다. 그러나 프로그램이 커지고 필요한 정보가 늘어나면 객체 간의 관계를 어떻게 구성할 것인지가 중요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 소스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서 사용한 전체 코드는 GitHub 저장소에 정리해 두었습니다. 이 글에서 다룬 BorrowDAO의 JOIN 조회와 DTO 객체 관계 구성은 아래 커밋에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library&quot;&gt;class_library 전체 저장소&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/heebaek200/class_library/commit/e019d043ff309b37386042bf623fffc9dbe6cf15&quot;&gt;이번 실습의 변경 내용&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>학습/Java</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/105</guid>
      <comments>https://heebaek.tistory.com/105#entry105comment</comments>
      <pubDate>Wed, 9 Sep 2026 17:04:00 +0900</pubDate>
    </item>
    <item>
      <title>Java JDBC 첫걸음 - 처음인데 왜 이렇게 익숙하지?</title>
      <link>https://heebaek.tistory.com/104</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 플레인 Java에서 JDBC를 이용해 MySQL에 연결하고, SQL을 실행한 뒤 결과를 받아오는 기본적인 흐름을 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 JDBC를 제대로 다뤄보는 것은 사실상 처음인데, 막상 코드를 따라가다 보니 생각보다 낯설지가 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 코드를 보면서 계속 이런 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&amp;ldquo;어? 이거 예전에 PHP에서 하던 거랑 거의 똑같은데?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어와 라이브러리는 달라졌지만, 데이터베이스를 사용하는 프로그램의 기본적인 흐름은 크게 다르지 않았습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC의 기본 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 대략 다음과 같은 순서로 DB에 접근했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DB 접속 정보 준비
    &amp;darr;
Connection 생성
    &amp;darr;
SQL 작성
    &amp;darr;
PreparedStatement 생성
    &amp;darr;
파라미터 바인딩
    &amp;darr;
SQL 실행
    &amp;darr;
ResultSet 처리
    &amp;darr;
예외 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 코드에서도 DriverManager.getConnection()으로 Connection을 얻고,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;statement = connection.prepareStatement(&quot;&quot;&quot;
    SELECT
        ...
    WHERE
        salary &amp;gt;= ?
    ...
&quot;&quot;&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PreparedStatement에 SQL을 전달한 뒤,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;statement.setInt(1, 45_000_000);
statement.setInt(2, 3);
statement.setInt(3, 50_000_000);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값을 별도로 바인딩하여 실행하는 구조였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 ResultSet으로 받아서 한 행씩 순회합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;while (resultSet.next()) {
    // 조회 결과 처리
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java 문법만 놓고 보면 처음 보는 클래스들이 많지만, 전체적인 구조는 PHP에서 PDO나 다른 DB API를 사용하던 방식과 크게 다르지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러고 보니 신입 시절에는 SQL을 그냥 이어붙였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PreparedStatement를 보니 자연스럽게 예전 일이 하나 떠올랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 신입 개발자였을 때는 SQL Injection에 대해서 제대로 이해하지 못하고 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 사용자 입력값을 받아 SQL을 만들 때도 별 생각 없이 문자열을 이어붙였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 지금 생각하면 이런 느낌이었습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String sql =
        &quot;SELECT * FROM users WHERE user_id = '&quot; + userId + &quot;'&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때는 그저&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&amp;ldquo;변수 값을 SQL 안에 넣어야 하니까 문자열을 붙이면 되겠지.&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 생각했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 실제로 쿼리도 잘 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 그게 문제였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 값만 들어오는 동안에는 아무 문제가 없어 보이기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 사용자 입력값 자체가 SQL 문법으로 해석될 수 있다면 이야기가 완전히 달라집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 지금은 다음처럼 SQL 구조와 값을 분리하는 방식이 너무나 당연하게 느껴집니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;PreparedStatement statement =
        connection.prepareStatement(&quot;&quot;&quot;
            SELECT
                *
            FROM
                users
            WHERE
                user_id = ?
        &quot;&quot;&quot;);

statement.setString(1, userId);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 문자열에 값을 직접 이어붙이는 것이 아니라 ? 자리를 만들어 놓고, DB 드라이버를 통해 별도로 값을 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 개발자가 직접 따옴표를 붙이고 이스케이프 처리를 고민하는 방식보다 훨씬 안전하게 쿼리를 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저도 다행히 신입 시절 큰 사고가 나기 전에 문제를 알게 되어 수정했지만, 지금 생각하면 제법 아찔한 기억입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때는 정말 &lt;b&gt;&amp;ldquo;실행되니까 맞는 코드겠지&amp;rdquo;&lt;/b&gt;라고 생각했던 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL 에러가 나면 실행된 SQL부터 보고 싶어진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 익숙하게 느껴진 부분은 예외 처리였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습 코드에서는 SQLException이 발생했을 때 다음 정보를 출력하도록 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;} catch (SQLException e) {
    System.err.println(&quot;연결 실패 : &quot; + e.getMessage());
    System.err.println(&quot;에러 코드 : &quot; + e.getErrorCode());
    System.err.println(&quot;SQL 상태 : &quot; + e.getSQLState());

    if (statement != null) {
        System.out.println(statement);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 메시지만 출력하는 것이 아니라, 가능하면 실행하려던 PreparedStatement도 함께 확인할 수 있도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분 역시 예전 실무 경험 때문에 굉장히 자연스럽게 느껴졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB 관련 오류가 발생했을 때 가장 먼저 궁금한 것은 보통 이것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: start;&quot;&gt;&amp;ldquo;그래서 실제로 무슨 SQL을 날렸는데?&amp;rdquo;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법 오류일 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼명을 잘못 작성했을 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바인딩된 값이 예상과 다를 수도 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 만든 조건에 문제가 있을 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 SQL 오류를 디버깅할 때는 예외 메시지와 함께 어떤 쿼리를 실행하려고 했는지 확인할 수 있는 정보가 굉장히 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실무에서 로그로 남길 때는 한 가지 주의할 점도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 무조건 통째로 로그에 남기면 비밀번호나 개인정보처럼 민감한 값까지 기록될 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 운영 환경에서는 SQL을 볼 수 있게 만드는 것과, 민감정보를 로그에 남기지 않는 것을 함께 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 결국 경험하면서 하나씩 배우게 되는 부분인 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JDBC가 처음이지, DB 프로그래밍이 처음은 아니었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 클래스 이름만 놓고 보면 전부 새롭습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DriverManager
Connection
PreparedStatement
ResultSet
SQLException&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 각각이 하는 일을 하나씩 대응해 보면 의외로 단순합니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Connection
&amp;rarr; DB 연결

PreparedStatement
&amp;rarr; 실행할 SQL과 파라미터 준비

executeQuery()
&amp;rarr; SELECT 실행

ResultSet
&amp;rarr; 조회 결과

SQLException
&amp;rarr; DB 작업 중 발생한 예외&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 언어가 PHP에서 Java로 바뀌었을 뿐,&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;연결하고
&amp;rarr; SQL 준비하고
&amp;rarr; 값을 넣고
&amp;rarr; 실행하고
&amp;rarr; 결과 받고
&amp;rarr; 실패하면 로그를 확인한다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 본질은 그대로였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이래서 한 언어에서 제대로 경험해 본 개념은 다른 언어를 배울 때도 자산이 되는 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 아직 JDBC의 가장 기본적인 단계인 Connection 생성과 SELECT 실행 정도를 배웠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 문법 자체보다 더 반가웠던 것은, 예전에 PHP 개발자로 일하면서 익혔던 DB 프로그래밍의 감각이 생각보다 많이 남아 있다는 점이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서는 Connection, PreparedStatement, ResultSet이라는 이름을 사용하지만, 결국 데이터베이스를 안전하게 다루기 위해 고민해야 할 것은 비슷합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자 입력값을 SQL에 직접 이어붙이지 않을 것&lt;/li&gt;
&lt;li&gt;파라미터 바인딩을 사용할 것&lt;/li&gt;
&lt;li&gt;예외가 발생했을 때 원인을 추적할 수 있는 정보를 남길 것&lt;/li&gt;
&lt;li&gt;단, 운영 로그에는 민감정보가 남지 않도록 주의할 것&lt;/li&gt;
&lt;li&gt;DB 자원은 사용 후 확실하게 정리할 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 배우는 JDBC인데도 어딘가 오래전에 사용해 본 도구처럼 친근하게 느껴졌습니다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;</description>
      <category>학습/Java</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/104</guid>
      <comments>https://heebaek.tistory.com/104#entry104comment</comments>
      <pubDate>Tue, 8 Sep 2026 15:12:55 +0900</pubDate>
    </item>
    <item>
      <title>프로그래머스 Lv.2 숫자의 표현 - 규칙 찾다가 등차수열로 방향을 틀었습니다</title>
      <link>https://heebaek.tistory.com/103</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 풀어본 문제는 프로그래머스 Lv.2 「숫자의 표현」입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연수 n을 연속된 자연수의 합으로 표현할 수 있는 방법의 수를 구하는 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 15는 다음과 같이 표현할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;1 + 2 + 3 + 4 + 5
4 + 5 + 6
7 + 8
15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 숫자 자체에서 규칙을 찾으려고 했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짝수일 때는 어떻고, 홀수일 때는 어떻고...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 접근하려 했는데 금방 복잡해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 방향을 바꿨습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;연속된 자연수의 합으로 생각하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x부터 y까지의 연속된 자연수 합은 등차수열의 합 공식으로 구할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;(x + y) &amp;times; 항의 개수 / 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 길이를 하나씩 늘려가면서, 그 길이로 만들 수 있는 시작값과 끝값을 계산한 뒤 실제 합이 n이 되는지를 확인했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;int length = y - x + 1;

if ((x + y) * length == n * 2) {
    answer++;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나눗셈 대신 양변에 2를 곱해서 비교하면 정수 나눗셈 때문에 생길 수 있는 오차도 피할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현하면서 사용한 방식&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이를 i라고 두고, 중앙값을 기준으로 시작값과 끝값을 계산했습니다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;x = n / i - (i - 1) / 2;
y = n / i + i / 2;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 x가 1 이하가 될 때까지 길이를 늘려가며 가능한 경우를 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 13시 28분에 시작해서 13시 44분에 통과했으니, 약 &lt;b&gt;16분&lt;/b&gt; 정도 걸렸습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;더 단순한 정석 풀이도 있었습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀고 나서 다시 정리해보니, 길이를 len이라고 했을 때:&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;x + (x + 1) + ... + (x + len - 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;은:&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;len &amp;times; x + len &amp;times; (len - 1) / 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로 표현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;n - len &amp;times; (len - 1) / 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 len으로 나누어떨어지면 시작값 x가 정수가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 문제는 사실:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이를 하나 정한다&lt;br /&gt;&amp;rarr; 시작값이 자연수이면서 정수인지 확인한다&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 형태로 더 깔끔하게 풀 수도 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제에서는 처음에 억지로 규칙을 찾으려다가, 익숙한 수학식으로 문제를 다시 표현하면서 해결할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기억해둘 포인트는 간단합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연속된 자연수의 합이 나오면 등차수열의 합 공식을 먼저 떠올린다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가능하다면:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이를 고정한 뒤 시작값이 정수인지 나머지 연산으로 판정한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정도로 확장해서 기억해두면 될 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 접근이 잘 안 풀릴 때 숫자 자체의 규칙을 계속 찾기보다, 이미 알고 있는 수식으로 문제를 다시 표현해보는 것도 좋은 방법이라는 걸 다시 확인한 문제였습니다.&lt;/p&gt;</description>
      <category>학습/코딩테스트</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/103</guid>
      <comments>https://heebaek.tistory.com/103#entry103comment</comments>
      <pubDate>Tue, 8 Sep 2026 13:52:27 +0900</pubDate>
    </item>
    <item>
      <title>작업 순서 - JDBC 실습을 위한 Java 프로젝트 만들기 (Gradle, Lombok, MySQL Connector/J 설정)</title>
      <link>https://heebaek.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 JDBC 실습에 들어가기 전, IntelliJ IDEA에서 &lt;b&gt;Gradle 기반 Java 프로젝트를 생성하고 필요한 라이브러리를 추가하는 과정&lt;/b&gt;을 정리해 보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 새로운 Java 프로젝트를 만들거나 팀 프로젝트를 클론했을 때 다시 참고할 수 있도록 환경 설정 과정을 기록해 두는 목적입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Gradle 기반 Java 프로젝트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ IDEA에서 새 프로젝트를 선택하고 다음과 같이 설정했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언어: Java&lt;/li&gt;
&lt;li&gt;빌드 시스템: Gradle&lt;/li&gt;
&lt;li&gt;Gradle DSL: Groovy&lt;/li&gt;
&lt;li&gt;JDK: Java 21&lt;/li&gt;
&lt;li&gt;Artifact ID: class_jdbc&lt;/li&gt;
&lt;li&gt;Git 저장소 생성: 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_1.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;717&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBlw8/dJMcadJ47vS/sKXRKk2th5kM2Q9ADaHEl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBlw8/dJMcadJ47vS/sKXRKk2th5kM2Q9ADaHEl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBlw8/dJMcadJ47vS/sKXRKk2th5kM2Q9ADaHEl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBlw8%2FdJMcadJ47vS%2FsKXRKk2th5kM2Q9ADaHEl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;786&quot; height=&quot;717&quot; data-filename=&quot;Screenshot_1.png&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;717&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서 처음 눈에 들어온 부분은 &lt;b&gt;빌드 시스템으로 Gradle을 사용한다는 점&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 Java 프로젝트라면 IDE에 JAR 파일을 직접 추가해서 라이브러리를 관리하는 경우도 많았지만, 프로젝트 규모가 커지고 외부 라이브러리가 많아질수록 이런 방식은 관리가 어려워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle을 사용하면 필요한 라이브러리를 build.gradle에 선언하는 것으로 관리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 MySQL JDBC 드라이버가 필요하다면 JAR 파일을 직접 다운로드해서 프로젝트 폴더에 넣는 대신 다음과 같이 선언할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;dependencies {
    implementation group: 'com.mysql', name: 'mysql-connector-j', version: '8.4.0'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해 두면 Gradle이 저장소에서 필요한 라이브러리를 받아서 프로젝트에 연결해 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 팀 프로젝트에서는 이 장점이 더 크게 느껴집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 저장소에는 라이브러리 JAR 파일을 일일이 올릴 필요가 없고, 다른 팀원이 프로젝트를 clone한 뒤 Gradle 동기화만 수행하면 동일한 의존성 환경을 구성할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. .gitignore 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 Git으로 관리할 예정이므로 .gitignore도 함께 준비했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 다음 사이트를 활용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.toptal.com/developers/gitignore&quot;&gt;https://www.toptal.com/developers/gitignore&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 항목을 선택하여 생성했습니다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Windows
macOS
IntelliJ
Eclipse&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제에서 생성되는 불필요한 파일이나 IDE별 개인 설정 파일이 Git 저장소에 올라가는 것을 방지하기 위한 설정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 여러 IDE를 사용하는 팀원이 있을 가능성을 고려해 IntelliJ뿐만 아니라 Eclipse 관련 항목도 함께 추가했습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. IntelliJ 기본 설정 확인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 생성한 뒤 몇 가지 기본 설정도 확인했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인코딩 UTF-8 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소스 파일과 프로젝트 인코딩이 UTF-8인지 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글 데이터를 다루거나 이후 JDBC를 통해 DB의 문자열 데이터를 처리하다 보면 인코딩 설정 하나 때문에 괜한 시간을 잡아먹는 경우가 있으므로 프로젝트 초기에 확인해 두는 편이 좋습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Annotation Processing 활성화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 경로에서 Annotation Processing을 활성화했습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;설정
&amp;rarr; 빌드, 실행, 배포
&amp;rarr; 컴파일러
&amp;rarr; 어노테이션 프로세서&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;b&gt;어노테이션 처리 활성화&lt;/b&gt;를 체크했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_2.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz7CHG/dJMcaamgdAm/KgKd3PBagk9BEp4MZ7qLmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz7CHG/dJMcaamgdAm/KgKd3PBagk9BEp4MZ7qLmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz7CHG/dJMcaamgdAm/KgKd3PBagk9BEp4MZ7qLmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz7CHG%2FdJMcaamgdAm%2FKgKd3PBagk9BEp4MZ7qLmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;885&quot; height=&quot;691&quot; data-filename=&quot;Screenshot_2.png&quot; data-origin-width=&quot;885&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 이후 Lombok을 사용할 예정이기 때문에 확인한 설정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lombok은 @Getter, @Setter, @Builder 등 어노테이션을 이용하여 컴파일 과정에서 코드를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 어노테이션 기반 코드 생성이 정상적으로 동작할 수 있도록 관련 설정을 확인해 두었습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Gradle 프로젝트지만 실행은 IntelliJ로 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 Gradle 관련 설정을 확인했습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;설정
&amp;rarr; 빌드, 실행, 배포
&amp;rarr; 빌드 도구
&amp;rarr; Gradle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값에서는 다음 항목이 Gradle로 되어 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;다음을 사용하여 빌드 및 실행: Gradle
다음을 사용하여 테스트 실행: Gradle&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 두 항목 모두 &lt;b&gt;IntelliJ IDEA&lt;/b&gt;로 변경했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_3.png&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2UUwo/dJMcag7Tqd7/LM6egQar0v7EvBQiQAZUxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2UUwo/dJMcag7Tqd7/LM6egQar0v7EvBQiQAZUxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2UUwo/dJMcag7Tqd7/LM6egQar0v7EvBQiQAZUxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2UUwo%2FdJMcag7Tqd7%2FLM6egQar0v7EvBQiQAZUxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;884&quot; height=&quot;692&quot; data-filename=&quot;Screenshot_3.png&quot; data-origin-width=&quot;884&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gradle을 사용하는 것과 IntelliJ가 실제 컴파일 및 실행을 담당하는 것은 별개의 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 &lt;b&gt;의존성과 프로젝트 구조는 Gradle로 관리하면서&lt;/b&gt;, 개발 중 일반적인 실행과 테스트는 IntelliJ의 빌드 시스템을 이용하도록 설정한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순 Java 프로젝트에서는 IntelliJ 빌드가 상대적으로 빠르고 가볍게 동작하기 때문에 개발 중 반복 실행하기 편리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 실제 CI/CD 환경이나 팀 프로젝트에서는 Gradle 명령으로 빌드가 정상적으로 수행되는지도 별도로 확인할 필요가 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. Lombok 추가&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프로젝트에서 사용할 라이브러리를 추가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Maven Repository에서 Lombok을 검색했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mvnrepository.com/&quot;&gt;https://mvnrepository.com/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색어:&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Lombok&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용할 버전을 선택한 뒤 Gradle 탭으로 이동하고 Format을 &lt;b&gt;Groovy Long&lt;/b&gt;으로 선택했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_4.png&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;943&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxk2Kp/dJMcadDdkS0/3UlNG2LGmfGU94v6C7lYNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxk2Kp/dJMcadDdkS0/3UlNG2LGmfGU94v6C7lYNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxk2Kp/dJMcadDdkS0/3UlNG2LGmfGU94v6C7lYNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcxk2Kp%2FdJMcadDdkS0%2F3UlNG2LGmfGU94v6C7lYNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;633&quot; height=&quot;943&quot; data-filename=&quot;Screenshot_4.png&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;943&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음과 같은 형태의 코드를 얻을 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.46'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 build.gradle의 dependencies 블록에 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;dependencies {
    // Lombok
    implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.46'

    testImplementation platform('org.junit:junit-bom:6.0.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. MySQL Connector/J 추가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JDBC로 Java와 MySQL을 연결하려면 JDBC Driver가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 &lt;b&gt;MySQL Connector/J&lt;/b&gt;가 이 역할을 담당합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Maven Repository에서 다음을 검색했습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;MySQL Connector/J&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_5.png&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;945&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7aXXD/dJMcafH4SOZ/KAVCoo48VvyXLywzRVss81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7aXXD/dJMcafH4SOZ/KAVCoo48VvyXLywzRVss81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7aXXD/dJMcafH4SOZ/KAVCoo48VvyXLywzRVss81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7aXXD%2FdJMcafH4SOZ%2FKAVCoo48VvyXLywzRVss81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;633&quot; height=&quot;945&quot; data-filename=&quot;Screenshot_5.png&quot; data-origin-width=&quot;633&quot; data-origin-height=&quot;945&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 Gradle의 Groovy Long 형식을 선택하여 코드를 복사합니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;implementation group: 'com.mysql', name: 'mysql-connector-j', version: '8.4.0'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에 Lombok과 함께 추가했습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;dependencies {
    // Lombok
    implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.46'

    // MySQL JDBC Driver
    implementation group: 'com.mysql', name: 'mysql-connector-j', version: '8.4.0'

    testImplementation platform('org.junit:junit-bom:6.0.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 버전은 무조건 위 숫자를 사용할 필요는 없고, &lt;b&gt;수업 또는 프로젝트에서 사용하는 Java/MySQL 환경과 호환되는 버전을 선택하는 것이 중요합니다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Gradle 동기화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle을 수정했다고 해서 곧바로 라이브러리가 프로젝트에 적용되는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntelliJ 오른쪽의 &lt;b&gt;Gradle 탭&lt;/b&gt;을 열고 새로고침 버튼을 눌러 프로젝트를 다시 동기화했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot_6.png&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1mjkJ/dJMcadDdkVF/2K7LJdlYmh2qR9MzcTHDq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1mjkJ/dJMcadDdkVF/2K7LJdlYmh2qR9MzcTHDq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1mjkJ/dJMcadDdkVF/2K7LJdlYmh2qR9MzcTHDq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1mjkJ%2FdJMcadDdkVF%2F2K7LJdlYmh2qR9MzcTHDq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1397&quot; height=&quot;577&quot; data-filename=&quot;Screenshot_6.png&quot; data-origin-width=&quot;1397&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화가 끝난 뒤 Dependencies를 살펴보면 다음과 같이 추가된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;com.mysql:mysql-connector-j
org.projectlombok:lombok&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태가 되면 프로젝트 코드에서 해당 라이브러리를 사용할 준비가 끝난 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 과정은 &lt;b&gt;팀 프로젝트를 clone했을 때도 반드시 확인해야 할 부분&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git에서 프로젝트를 받아왔다고 해서 외부 라이브러리까지 Git 저장소에서 받아오는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle에는 어떤 라이브러리가 필요한지가 적혀 있을 뿐이므로, Gradle 동기화를 통해 실제 라이브러리를 다운로드해야 합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Gradle을 사용하면서 느낀 핵심&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 설정 과정에서 Gradle의 역할을 다시 정리하면 크게 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트가 사용하는 외부 라이브러리를 선언할 수 있습니다.&lt;/li&gt;
&lt;li&gt;필요한 라이브러리와 하위 의존성을 자동으로 다운로드합니다.&lt;/li&gt;
&lt;li&gt;개발자마다 직접 JAR 파일을 관리할 필요가 없습니다.&lt;/li&gt;
&lt;li&gt;Git으로 build.gradle만 공유해도 동일한 프로젝트 환경을 재현하기 쉬워집니다.&lt;/li&gt;
&lt;li&gt;빌드와 테스트 과정을 일정한 규칙으로 관리할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 네 번째가 협업에서는 꽤 중요해 보입니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;dependencies {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 프로젝트의 일부로 함께 관리할 수 있기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;최종 프로젝트 구성&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 JDBC 실습을 시작하기 위한 기본 구성은 다음과 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;Java 21
  &amp;darr;
Gradle
  ├─ Lombok
  └─ MySQL Connector/J
       &amp;darr;
     JDBC
       &amp;darr;
     MySQL&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 생성부터 라이브러리 등록까지 마쳤으니 이제 Java 코드에서 JDBC를 이용하여 실제 MySQL Connection을 생성하고, SQL을 실행하는 단계로 넘어가면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글은 특별한 기능을 구현한 내용이라기보다는 &lt;b&gt;나중에 JDBC 프로젝트를 다시 만들 때 그대로 따라가기 위한 환경설정 체크리스트&lt;/b&gt;에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경설정은 한 번 해놓고 나면 당연하게 느껴지지만, 몇 달 뒤 새 프로젝트에서 다시 하려고 하면 꼭 하나씩 기억이 안 납니다. 그래서 이런 내용일수록 작업 순서를 기록해 두는 것이 오히려 쓸모가 있을 것 같습니다.&lt;/p&gt;</description>
      <category>학습/Java</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/102</guid>
      <comments>https://heebaek.tistory.com/102#entry102comment</comments>
      <pubDate>Tue, 8 Sep 2026 12:56:11 +0900</pubDate>
    </item>
    <item>
      <title>MySQL - SELECT 쿼리는 작성한 순서대로 실행되지 않는다</title>
      <link>https://heebaek.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 &lt;code&gt;GROUP BY&lt;/code&gt;와 &lt;code&gt;HAVING&lt;/code&gt;을 배우며 다음과 같은 SQL을 작성했습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
      department
    , COUNT(*)              AS `직원 수`
    , ROUND(AVG(salary), 0) AS `평균 연봉`
    , MAX(salary)           AS `최고 연봉`
    , MIN(salary)           AS `최저 연봉`
FROM
    tb_employees
WHERE
    salary &amp;gt;= 45000000
GROUP BY
    department
HAVING
    `직원 수` &amp;gt;= 3
    AND `평균 연봉` &amp;gt;= 50000000
ORDER BY
    `평균 연봉` DESC;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL을 작성할 때는 &lt;code&gt;SELECT&lt;/code&gt;부터 시작하지만, 데이터베이스가 쿼리를 이해하는 논리적인 순서는 이와 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SELECT 쿼리의 논리적 실행 순서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 SELECT 쿼리는 다음 순서로 생각할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;FROM&lt;/code&gt;과 &lt;code&gt;JOIN&lt;/code&gt;: 조회할 테이블을 정하고 연결합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WHERE&lt;/code&gt;: 그룹으로 묶기 전에 필요한 행만 남깁니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GROUP BY&lt;/code&gt;: 남은 행을 기준에 따라 그룹으로 묶습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HAVING&lt;/code&gt;: 만들어진 그룹의 집계 결과를 검사합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt;: 출력할 컬럼과 계산식을 결정합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DISTINCT&lt;/code&gt;: 중복된 결과를 제거합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt;: 결과의 순서를 정합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LIMIT&lt;/code&gt;: 반환할 행의 개수를 제한합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제에서는 먼저 연봉이 4,500만 원 이상인 직원만 남깁니다. 그다음 부서별로 묶어 직원 수와 평균 연봉을 계산하고, 마지막으로 직원이 3명 이상이면서 평균 연봉이 5,000만 원 이상인 부서만 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름을 알고 나니 &lt;code&gt;WHERE&lt;/code&gt;와 &lt;code&gt;HAVING&lt;/code&gt;의 차이도 명확해졌습니다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;WHERE  : 그룹으로 묶기 전, 개별 행을 검사
HAVING : 그룹으로 묶은 후, 집계 결과를 검사&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가능한 조건은 WHERE에서 먼저 거르기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;집계 함수와 관계없는 조건이라면 가능한 한 &lt;code&gt;WHERE&lt;/code&gt;에 작성하는 편이 자연스럽습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;WHERE
    salary &amp;gt;= 45000000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건을 먼저 적용하면 연봉이 기준에 미치지 못하는 직원은 그룹 계산에 참여하지 않습니다. 처리해야 할 행이 일찍 줄어들기 때문에 이후의 그룹화와 집계가 가벼워질 가능성이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음 조건들은 그룹이 만들어져야만 계산할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;HAVING
    COUNT(*) &amp;gt;= 3
    AND AVG(salary) &amp;gt;= 50000000&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직원 수와 평균 연봉은 개별 행에는 존재하지 않는 값이므로 &lt;code&gt;WHERE&lt;/code&gt;에서 검사할 수 없습니다. &lt;code&gt;GROUP BY&lt;/code&gt; 이후의 결과를 검사하는 &lt;code&gt;HAVING&lt;/code&gt;이 필요한 이유입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;별칭은 어디에서 사용할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에서는 &lt;code&gt;SELECT&lt;/code&gt;에서 만든 별칭을 &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, &lt;code&gt;ORDER BY&lt;/code&gt;에서 사용할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;HAVING
    `평균 연봉` &amp;gt;= 50000000
ORDER BY
    `평균 연봉` DESC&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;code&gt;WHERE&lt;/code&gt;에서는 &lt;code&gt;SELECT&lt;/code&gt; 별칭을 사용할 수 없습니다. &lt;code&gt;WHERE&lt;/code&gt;가 행을 고르는 시점에는 아직 출력값이 결정되지 않았기 때문입니다. MySQL이 &lt;code&gt;HAVING&lt;/code&gt;에서 별칭 사용을 허용한다고 해서 &lt;code&gt;SELECT&lt;/code&gt;가 물리적으로 먼저 실행된다고 이해해서는 안 됩니다. 이는 MySQL이 제공하는 이름 해석상의 편의에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;논리적 순서와 실제 실행 계획은 다르다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 실행 순서는 SQL을 이해하기 위한 &lt;b&gt;논리적 실행 순서&lt;/b&gt;입니다. MySQL이 내부에서 모든 작업을 반드시 이 순서 그대로 수행한다는 뜻은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 옵티마이저는 테이블의 통계와 인덱스를 참고하여 JOIN 순서를 바꾸거나, 조건을 더 이른 단계에 적용하거나, 비용이 적다고 판단한 접근 방법을 선택할 수 있습니다. 따라서 SQL의 성능은 문장만 보고 단정하기보다 실행 계획으로 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;EXPLAIN ANALYZE
SELECT
    ...;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;EXPLAIN&lt;/code&gt;은 MySQL이 선택한 실행 계획을 보여 주고, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;는 쿼리를 실제로 실행하여 예상 행 수와 실제 행 수, 실행 시간 등을 함께 보여 줍니다. 인덱스를 만들었다고 무조건 빨라졌다고 판단하지 말고 실행 계획과 실제 측정 결과를 확인하는 습관이 필요합니다.&lt;/p&gt;</description>
      <category>학습/DB・MySQL</category>
      <author>heebaek</author>
      <guid isPermaLink="true">https://heebaek.tistory.com/101</guid>
      <comments>https://heebaek.tistory.com/101#entry101comment</comments>
      <pubDate>Tue, 8 Sep 2026 10:37:04 +0900</pubDate>
    </item>
  </channel>
</rss>