# Intellij) JDBC 프로젝트 (8) DAO 인터페이스와 클래스

> (글 이동 목차 삽입 예정)

# Insert 구현

우선 DAO 클래스를 만들고, INSERT문을 수행하는 메서드를 작성해 봅시다.

이때 우리는 User 테이블의 CRUD를 담당할 DAO 인터페이스와 DAO 클래스 한 쌍을 만들 겁니다.

인터페이스를 두는 이유는, 나중에 확장에 유리하기 때문입니다.

## UserDao 인터페이스

* **package**: `com.example.app.auth.dao`
    

```java
package com.example.app.auth.dao;

import com.example.app.auth.domain.User;

public interface UserDao {
    int insert(User user);
}
```

지금은 insert만 구현할 것이기 때문에 메서드도 하나만 만들었습니다. 인터페이스는 메서드가 모두 `public` 접근 지정자를 기본값으로 갖기 때문에 접근 지정자 작성을 생략할 수 있습니다.

## PostgresqlUserDao 클래스

보통 인터페이스-클래스가 일대일 관계로 예상되거나, 인터페이스가 하나의 클래스와만 밀접할 것으로 예상할 때 선택하는 네이밍 컨벤션은 다음과 같습니다.

* (suffix) `UserDaoImpl`: 오랫동안 선택되어 온 네이밍 관례입니다. 아직 채택율이 가장 높습니다.
    
* (prefix) `DefaultUserDao`: 마치 헝가리안표기처럼 '프로그래밍 신택스'와 관련한 표현을 네이밍 관례로 사용하는 것에 반발하는 사람들은 `-Impl` 접미사보다 `Default-` 접두사가 더욱 직관성에 맞는다고 주장합니다. 저 또한 이 방식을 더 권합니다.
    

이것은 알아만 둡시다.

우리는 조금 더 명확하게 이 DAO가 Postgresql용 SQL 신택스를 사용하고 있음을 명시하기 위해서 `PostgersqlUserDao`라고 명명할 수 있습니다. (나중에는 SQL을 자바 소스 코드가 아닌 다른 파일에서 관리할 수도 있습니다. 이것을 자바 프로그램이 읽어서 실행할 수 있죠. 이번에는 코드에 바로 씁니다.)

### INSERT SQL 작성

JDK 15 이상에서는 여러 줄 문자열을 작성할 수 있는 '텍스트 블록'을 제공합니다. `insert` 메서드 안에 다음처럼 SQL 문자열을 작성해 둡시다.

* SQL 문자열의 끝에 세미콜론은 생략해야 합니다.
    
* 보통 콤마(,)를 빠뜨리거나 마지막에 더 넣는 등 실수를 많이 합니다. 오타 점검 시 참고하세요.
    
* 이 SQL 문자열 중 우리가 나중에 채울 데이터를 물음표(?) 기호로 사용합니다.
    

```sql
// 첫 줄은 """로 시작하고 우측을 비웁니다.
String sql = """
        INSERT INTO "user" (
            username,
            password,
            nickname,
            status,
            created_at,
            updated_at
        ) VALUES (
            ?, -- username
            ?, -- password
            ?, -- nickname
            ?, -- status
            ?, -- created_at
            ?  -- updated_at
        )"""; // 마지막 줄은 """로 마칩니다.

// 전체 라인 중 가장 왼쪽에서 시작하는 것이 좌측 패딩(padding)의 기준이 됩니다.
// 의도적으로 띄어쓰기를 더 추가하는 경우 \s 기호로 추가할 수 있습니다.
```

### DB 접속 및 INSERT 수행

이제 Connection을 만들고, 이 커넥션을 사용할 PreparedStatement라는 것을 만들 겁니다.

* connection: DB에 접속하는 데 필요합니다. 이 커넥션을 통해 DB와 소통하게 됩니다.
    
* prepared statement: SQL 구문을 다룹니다. 앞서 작성한 물음표를 채우는 등 작업도 이 객체로 수행합니다.
    

connection을 생성하는 코드는 매번 바로 떠오를 만큼 직관적이지는 않기 때문에, private 메서드를 만들어서 활용하겠습니다. `getConnection()` 메서드입니다.

```java
public class PostgresqlUserDao implements UserDao {

    private final String url;
    private final String username;
    private final String password;

    public PostgresqlUserDao(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    // (intellij) Ctrl + I 키를 눌러서 메서드 목록을 자동완성 할 수 있습니다.

    @Override
    public int insert(User user) {
        // 주의: SQL 끝에 세미콜론을 생략해야 합니다.
        String sql = """
                INSERT INTO "user" (
                    username,
                    password,
                    nickname,
                    status,
                    created_at,
                    updated_at
                ) VALUES (
                    ?, -- username
                    ?, -- password
                    ?, -- nickname
                    ?, -- status
                    ?, -- created_at
                    ?  -- updated_at
                )""";

        // try~catch~resources 구문으로 close를 보장합니다.
        try (
                Connection connection = getConnection();
                PreparedStatement statement = connection.prepareStatement(sql)
        ) {
            // N 번째 물음표임을 나타냅니다. 1-based 인덱싱을 하기 때문에 nth(N 번째)로 명명했습니다.
            int nth = 1;
            statement.setString(nth++, user.username);
            statement.setString(nth++, user.password);
            statement.setString(nth++, user.nickname);
            statement.setString(nth++, user.status.name());
            Timestamp createdAt = Timestamp.from(user.createdAt);
            statement.setTimestamp(nth++, createdAt);
            // 지금은 그냥 사용하지만 이런 코드에서 오류가 발생할 예정입니다. (뒷부분 설명에서 고칠게요.)
            Timestamp updatedAt = Timestamp.from(user.updatedAt);
            statement.setTimestamp(nth, updatedAt);

            // insert, update 등
            // DB 데이터에 변경을 주는 작업은 executeUpdate로 사용합니다.
            return statement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private Connection getConnection() {
        return DriverManager.getConnection(url, username, password);
    }
}
```

중요한 것은, 원래 conneciton, statement 등을 사용 후 `.close()`를 수행해 주는 것입니다. 이것을 원래는 `finally` 블록에서 작성하곤 했죠. 지금은 try catch resources 구문으로 `close()`를 보장해 줍니다.

```java
// 우리는 직접 작성하지 않아도 되도록 try~catch~resources를 사용했습니다.
connection.close();
statement.close();
```

## Null 처리 대행자

앞서 주석 중에는 오류가 발생할 예정이라는 내용이 있었습니다. 원래는 실행해 본 다음 오류를 접하고, 그 메시지를 보면서 공부해 가는 것도 좋을 것입니다.

그래도 빠르게 따라해 보는 글의 취지상 스포를 해 보자면, 저때 우리가 접할 예외는 통상 `NPE`라고 하는 `NullPointerException`입니다. `Timestamp.from(...)` 함수는 파라미터로 `null`을 받으면 `NPE`를 띄웁니다.

### **참고**: NullPointerException이 발생하는 경우들

NullPointerException은 `null`인 객체를 통해 하위 속성에 접근하려고 할 때, 이미 `null` 포인터에서 접근을 시도하고 있다는 것을 알려 주는 예외입니다. 말이 어렵죠?

우리가 작성한 코드에서 NullPointerException이 발생한다면 다음 중 하나입니다.

* (우리가 직접 작성한 코드에서 발생한다면) 점 앞에 온 객체가 `null`인 경우입니다.
    
    ```java
    // given:
    someNullObject = null;
    
    someNullObject.field; // NPE
    someNullObject.doSomething(); // NPE
    ```
    
* (우리가 이용한 라이브러리의 함수를 이용할 때 발생한다면) 매개변수로 넘겨 준 것이 `null`입니다.
    
    ```java
    user.updatedAt = null;
    Timestamp updatedAt = Timestamp.from(user.updatedAt); // NPE
    ```
    

### Null 처리 대행자 클래스

우리는 앞서 `statement.setString(nth++, user.username);` 같은 코드를 작성했습니다. 그런데 이 모든 과정에서 `null` 처리를 해 줄 필요가 있을 수 있습니다.

그래서 `null` 처리를 기본적으로 담당해 주는 클래스로 대체하려고 합니다.

* setString, setTimestamp, setLong 등 우리가 자주 사용하는 것들만 구현해 주어도 됩니다.
    
* 저는 참고용으로 몇 가지 메서드를 더 추가했습니다. 메서드 하나하나는 비슷하게 생겼을 거예요.
    
* 각 함수가 하는 일은 결국 `statement.set머시기(nth, 값);` 형태입니다. 여기에 null safe 처리만 추가되어 있습니다.
    
* `AutoClosable` 인터페이스를 구현하였고, `close()`를 오버라이딩 하여 `statement.close()`를 병행합니다. `AutoClosable` 인터페이스를 구현하면 try catch resources에 선언할 수 있습니다.
    
* `setNullOrSkip` 메서드는 삽입할 매개변수가 `null`인 경우 `null`을 그대로 넣고, `null`이 아닌 경우는 그대로 종료되는 메서드입니다. `null`을 넣었다면 `true`를 반환합니다.
    

```java
package example.common.support.jdbc;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.UUID;

public class NullSafePreparedStatement implements AutoCloseable {

    // delegation (위임)
    private final PreparedStatement statement;

    public NullSafePreparedStatement(PreparedStatement statement) {
        this.statement = statement;
    }

    public void setLong(int parameterIndex, Long value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.BIGINT)) {
            return;
        }
        statement.setLong(parameterIndex, value);
    }

    public void setString(int parameterIndex, String value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.VARCHAR)) {
            return;
        }
        statement.setString(parameterIndex, value);
    }

    public void setInt(int parameterIndex, Integer value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.INTEGER)) {
            return;
        }
        statement.setInt(parameterIndex, value);
    }

    public void setTimestamp(int parameterIndex, Instant value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.TIMESTAMP)) {
            return;
        }
        Timestamp timestamp = Timestamp.from(value);
        statement.setTimestamp(parameterIndex, timestamp);
    }

    public void setTimestamp(int parameterIndex, Timestamp value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.TIMESTAMP)) {
            return;
        }
        statement.setTimestamp(parameterIndex, value);
    }

    public void setUuid(int parameterIndex, UUID value) throws SQLException {
        if (setNullOrSkip(parameterIndex, value, Types.BINARY)) {
            return;
        }
        statement.setObject(parameterIndex, value);
    }

    public ResultSet executeQuery() throws SQLException {
        return statement.executeQuery();
    }

    public int executeUpdate() throws SQLException {
        return statement.executeUpdate();
    }

    @Override
    public void close() throws SQLException {
        statement.close();
    }

    private boolean setNullOrSkip(int parameterIndex, Object value, int typesValue) throws SQLException {
        boolean isNull = value == null;
        if (isNull) {
            statement.setNull(parameterIndex, typesValue);
        }
        return isNull;
    }
}
```

어쩌다 보니 코드가 굉장히 길었지만, 이로써 안전하게 사용할 수 있게 되었습니다. 앞서 작성한 DAO의 코드를 다음처럼 안전하게 고쳐 봅시다.

```java
try (
        Connection connection = getConnection();
        // 변수명 중복을 피하기 위해 preparedStatement로 변수명 변경
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        NullSafePreparedStatement statement = new NullSafePreparedStatement(preparedStatement)
) {
    // 우리가 새로 만든 statement 사용
    int nth = 1;
    statement.setString(nth++, user.username);
    statement.setString(nth++, user.password);
    statement.setString(nth++, user.nickname);
    statement.setString(nth++, user.status.name());
    statement.setTimestamp(nth++, user.createdAt);
    statement.setTimestamp(nth, user.updatedAt);

    // ...
```

# 데이터 Insert 확인

```java
package com.example.app;

public class MainApplicaiton {
    public void run() {
        UserDao userDao = new PostgresqlUserDao();
        User user = User.builder()
                .username("abc123")
                .password("abc123!@")
                .nickname("gildong")
                .status(UserStatus.ACTIVE)
                .createdAt(Instant.now())
                .build();

        userDao.insert(user);
    }
}
```

이제 메인 함수를 한 번만 실행한 후 DBeaver 등 DB 접속 도구로 데이터가 삽입되었나 확인해 봅시다.

(데이터 확인은 DB 접속 정보를 기억한다면 금방 할 수 있습니다. 검색해 보세요! 나중에 이 글에 추가해 두겠습니다.)

---

**&lt; Prev**

[(7) Entity 클래스와 롬복(Lombok)](https://letsdev.hashnode.dev/intellij-jdbc-7-kor)
