Home > Today I Learned > 2026_04_15~2026_04_19 > 2026-04-17 정리본

2026-04-17 정리본
java database

제네릭 구현


타입을 나중에 확정시키기 위해서 사용한다. 만약 Object의 기능을 대신해주고 그 기능을 수행할 수있도록 선언해주는 것이다.

public class 클래스명 <문자> {

public 문자 field1

public 문자 field2

}

또한 geneiric은 여러개의 문자를 사용하는 것이 가능하다. 우리가 클래스를 사용할 때는 하나의 타입이 아닌 여러 타입을 사용하게 되고 클래스의 타입을 명시를 해야한다. 그 중 컨테이너 객체는 객체를 감싸는 기능을 수행한다. 이 경우 generic을 구현해주면 타입을 체크해주고 꺼낼 때 타입을 보장해줘서 유용하게 사용가능하다.

제네릭 제한


public class 클래스명 <K extends Number, V, T> {

public 문자 field1

public 문자 field2

}

제네릭을 사용하면 유용하게 대처할 수 있는 경우도 많지만 오히려 그 범용성 때문에 쓸 수 있는 메서드 수가 줄어들 수 있다. 그리고 단일 책임 원칙(Single Responsiblility Principle)을 지킬 수 있어서 유지보수성을 높이는 쪽에 도움이 된다. 이를 위해 타입에 대한 제한을 두어 해당 타입이 제공하는 기능을 안전하게 보장할 수가 있다.

함수형 인터페이스


프로그램의 진행에 대한 추적과 새로운 값을 불변성을 위해 도입하기 위해서 함수가 필요한데 java는 메서드만 있지 함수는 없다. 이를 위해 함수형 인터페이스의 도입으로 함수형 프로그래밍을 구현한다.

//함수형 인터페이스 구현
void main(){
    ABC a = () -> {
        IO.println("A");
    };
    
    a.a();
    a.b();
}

public interface ABC{
    void a();
    default void b(){
        IO.println("B");
    }
}

제어의 역전(Inversion Of Control, IoC)과 라이브러리, 프레임워크의 관계


라이브러리는 주체가 사용자가 되어 내가 필요할 때 호출하거나 전체 흐름을 설계하는 것이 가능하다. 프레임워크는 IoC를 기반으로 동작하여 주체가 프로그램이 된다.
이 경우 결합도가 감소되어 인터페이스 중심의 설계가 가능해지고 코드 변경 영향을 최소화 하는 것이 가능하다.

DAO, 아키텍쳐 개념


아키텍처는 객체들을 어떻게 나누고 어떻게 협력하게 만들지에 대한 설계구조이다. 대표적인 객체지향 아키텍쳐에는 3계층이 존재한다.

  1. Controller : 요청/응답 처리
  2. Service : 비지니스 로직
  3. DAO/Repository : DB 접근

이렇게 책임을 계층별로 분리하는 것이 아키텍쳐를 사용하는 핵심이라고 볼 수가 있다.

그 중 DAO가 지금 배우고 있는 jdbc와 큰 관계가 있는데 DAO는 데이터베이스의 데이터에 접근하고 조작하는 로직을 전담하는 객체이다.

jdbc 코드 만들기


앞에서 배운 내용을 토대로 기본적인 Jdbc 예제를 만들어보자

public class JdbcTemplate {

    // 공통된 부분은 여기서 처리
    // 다른 부분만 받기
    private final DataSource dataSource;

    public JdbcTemplate(DataSource dataSource){
        this.dataSource = dataSource;
    }

Datasource 객체를 만들고 선언하여 HikariCP와 같은 커넥션 풀을 연결을 한번만 선안하기 위해서 사용한다. Datasource를 외부에서 주입하여 IoC를 구현하였다. 그리고 이 템플릿은 JDBC를 통한 DB 작업흐름만 다루고 있기 때문에 단일 책임 원칙을 만족한다.

    //단건조회
    public <T> Optional<T> queryForOptional(
            String sql,
            StatementPlaceHolderBinder pss,
            RowMapper<T> mapper
    ){
        List<T> results = queryForList(sql, pss, mapper);

        if(results.isEmpty()){
            return Optional.empty();
        }

        if(results.size() > 1){
            throw new RuntimeException("Too many results");
        }

        return Optional.of(results.getFirst());
    }

Optional으로 결과를 감싸주어서 nullPointException을 방지했고 결과를 0건, 1건, 2건이상을 다루었다. 0건인 경우 null값 방지를 위해 Optional의 empty() 메서드를 다루었고 2건이상인 경우 오류메시지를 반환하도록 하였다. getFirst()를 통해 1건만 반환하도록 했다.

    public <T> T queryForObject(
            String sql,
            StatementPlaceHolderBinder pss,
            RowMapper<T> mapper
    ){
        List<T> results = queryForList(sql, pss, mapper);

        if(results.isEmpty()){
            throw new RuntimeException("No Result!");
        }

        if(results.size() > 1){
            throw new RuntimeException("Too many results");
        }

        return results.getFirst();
    }

이 메서드는 앞의 queryForOptional과 다르게 무조건 1건만 반환하고 아니면 예외를 던지는 메서드이다. 이를 사용해주는 것은 Primary key와 같이 반드시 존재해야만 하는 값을 조회힐 때 사용가능하다. 로그인이나 PK조회와 같은 기능에서 Optional은 null값에 대해 흐름을 차단하지 않아 데이터 무결성을 깨트릴 우려가 있기에 이렇게 분류해서 사용하는 것이 필요하다.

    // 다건조회를 처리하는 메서드
    public <T> List<T> queryForList(
            String sql,
            StatementPlaceHolderBinder pss,
            RowMapper<T> mapper
    ){
        try(
            Connection connection = dataSource.getConnection();
            PreparedStatement stmt = connection.prepareStatement(sql);
        ){
            //select * from product where name = ?
            if(pss != null){
                pss.set(stmt);
            }

            try (ResultSet rs = stmt.executeQuery()) {
                List<T> result = new ArrayList<>();
                while(rs.next()){
                    result.add(mapper.map(rs));
                }

                return result;
            }
        }catch (SQLException e){
            throw new RuntimeException(e);
        }
    }

try에 connection, PreparedStatement 객체를 넣어 자동 close를 하도록 코드를 구성했다. preparedStatement를 활용하여 sql의 ? 부분에 값을 넣어 진행하였다. rs의 커서가 빈 곳을 가리킬 때까지 반복하여 result값을 구현했다.


    public Long insertAndReturnKey(
            String sql,
            StatementPlaceHolderBinder pss
    ) {
        try(
            Connection connection = dataSource.getConnection();
            PreparedStatement stmt = connection.prepareStatement(sql);
        ){

            if(pss != null){
                pss.set(stmt);
            }

            stmt.executeUpdate();

            try(ResultSet rs = stmt.getGeneratedKeys()){
                if (rs.next())
                    return rs.getLong(1);
                return null;
            }

        }catch (SQLException e){
            throw new RuntimeException(e);
        }
    }


}

데이터를 DB에 Insert를 하고 DB가 생성해준 key값을 반환하는 기능을 수행한다. stmt.executeUpdate로 pss에 대해 업데이트를 사용해준다.