youngseo's TECH blog

[Java/Spring] 템플릿 콜백 패턴으로 JDBC 템플릿 구현해보기 본문

BackEnd/JAVA\SPRING

[Java/Spring] 템플릿 콜백 패턴으로 JDBC 템플릿 구현해보기

jeonyoungseo 2024. 4. 2. 00:30

Template 메소드 패턴과 Callback 패턴에 대해 우선 따로따로 이해해보는 게 좋겠다 !
(이론 -> 구현 내용 순으로 구성하였습니다.)


이론

Template 메소드 패턴

(문제 상황) 핵심 기능 & 부가 기능이 모조리 섞여 있는 문제 
(해결 방법) 변하는 것과 변하지 않는 것(반복되는 코드)을 분리하겠다 !
-> 상속으로 푼다.

구조 그림

이론보다 , 코드 샘플로 이해해보자!
AbstractTemplate (변하지 않는 부분, 반복되는 부분)
이 부분이 바로 Template, 우리가 아는 파워포인트 템플릿과 비슷하게 변하지 않는, 반복되는 부분에 해당한다.

package hello.advanced.trace.template.code;
import lombok.extern.slf4j.Slf4j;
 
@Slf4j
public abstract class AbstractTemplate {
	public void execute() {
		long startTime = System.currentTimeMillis(); //비즈니스 로직 실행
		call(); //상속으로 구현됨
		//비즈니스 로직 종료
		long endTime = System.currentTimeMillis(); 
		long resultTime = endTime - startTime; 
		log.info("resultTime={}", resultTime);
	}

  protected abstract void call();
}

변하는 부분1

 package hello.advanced.trace.template.code;
 import lombok.extern.slf4j.Slf4j;
 @Slf4j
 public class SubClassLogic1 extends AbstractTemplate {
     @Override
     protected void call() {
	     log.info("비즈니스 로직1 실행"); 
     }
}
 

변하는 부분2

 package hello.advanced.trace.template.code;
 import lombok.extern.slf4j.Slf4j;
 @Slf4j
 public class SubClassLogic2 extends AbstractTemplate {
     @Override
     protected void call() {
	     log.info("비즈니스 로직2 실행"); 
     }
}
 

적용하는 부분 

@Test
 void templateMethodV1() {
     AbstractTemplate template1 = new SubClassLogic1();
     template1.execute();
     AbstractTemplate template2 = new SubClassLogic2();
     template2.execute();
 }

위를 확인해보면 일일이 SubClassLogic1, SubClassLogic2 ..를 생성하는 문제가 있다. 
직접 클래스를 만들지 않고 그냥 익명 내부 클래스를 사용해 문제를 해결하자.

@Test
 void templateMethodV2() {
    AbstractTemplate template1 = new AbstractTemplate() {
	     @Override
	     protected void call() { 
		     log.info("비즈니스 로직1 실행");
		   } 
		};
		
		log.info("클래스 이름1={}", template1.getClass()); 
		template1.execute();
    
    AbstractTemplate template2 = new AbstractTemplate() {
			@Override
			protected void call() { 
				log.info("비즈니스 로직1 실행");
			} 
		};
		
		log.info("클래스 이름2={}", template2.getClass());
    template2.execute();
 }

마무리

  • 상속의 경우 T(제네릭)을 사용해서 추가로 구현할 수 있다. (제네릭에서 void만 Void로 return 해줘야 하는 것만 주의!)
  • 이를 통해 SRP (단일 책임 원칙) 을 지킬 수 있다 ! 
  • 한계가 있다면 상속!
    자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는데, 부모 클래스를 알아야 한다. 그리고 익명 내부 클래스도 만들어야 한다.ㅠㅠ

전략 패턴

상속보다는 구성(구현, implementation)을 사용한 방법이다 ! 템플릿 메소드 패턴의 상속이라는 한계를 극복할 수 있다.
변하지 않는 부분을 Context에, 변하는 부분을 Strategy에 넣는다.

구조 그림

긴 말 말고 코드로 확인해보자 !
변하는 부분 - Strategy

package hello.advanced.trace.strategy.code.strategy;
 public interface Strategy {
     void call();
}

이 Strategy 인터페이스를 구현한 것

package hello.advanced.trace.strategy.code.strategy;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class StrategyLogic1 implements Strategy {
     @Override
     public void call() {
			log.info("비즈니스 로직1 실행");
		}
}

ContextV1 필드에 전략 보관 - 변하지 않는 로직
Context는 Strategy 인터페이스에만 의존한다.

package hello.advanced.trace.strategy.code.strategy;
import lombok.extern.slf4j.Slf4j;
/**
* 필드에 전략을 보관하는 방식 */
@Slf4j
public class ContextV1 {
	  private Strategy strategy;
	  public ContextV1(Strategy strategy) {
      		this.strategy = strategy;
		}
		public void execute() {
			long startTime = System.currentTimeMillis(); //비즈니스 로직 실행
			strategy.call(); //위임
			//비즈니스 로직 종료
			long endTime = System.currentTimeMillis();
      long resultTime = endTime - startTime;
      log.info("resultTime={}", resultTime);
		} 
}

ContextV1에 나에게 맞는 Strategy 를 넣는다.

@Test
 void strategyV1() {
     Strategy strategyLogic1 = new StrategyLogic1();
     ContextV1 context1 = new ContextV1(strategyLogic1);
     context1.execute();
     Strategy strategyLogic2 = new StrategyLogic2();
     ContextV1 context2 = new ContextV1(strategyLogic2);
     context2.execute();
}

실행과정
Context에 원하는 Strategy가 주입된다. -> 클라이언트는 Context를 실행한다. -> Context는 로직을 시작한다. -> Context 로직 중간에 strategy.call()이 호출되어 주입받은 strategy 로직을 실행한다. -> context는 나머지 로직을 실행한다.
마무리

  • 람다와 함께 쓰는 경우가 많다.(Java8) - 인터페이스 메소드가 하나만 있을 때 사용 가능
  • 선조립 후 실행
    실행 전에 원하는 모양으로 조립해둔 후 context1.execute();
  • Strategy(전략)을 Context 필드에 넣는다.

템플릿 콜백 패턴

템플릿 콜백 패턴은 사실상 전략패턴과 유사하다. 전략 패턴 + 익명 내부 클래스 로 이해해도 좋다. 콜백이란, 쉽게 호출(call)한 코드는 코드를 넘겨준 곳의 뒤(back)에서 실행된다는 의미이다.
이름만 Context -> Template , Strategy -> Callback으로 바뀐다.

@Test
void callbackV1() {
	TimeLogTemplate template = new TimeLogTemplate();
    
    template.execute(new Callback() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    });
    
    template.execute(new Callback() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    });
    
    //람다 사용
    TimeLogTemplate template = new TimeLogTemplate();
    template.execute(() -> log.info("비즈니스 로직1 실행"));
    template.execute(() -> log.info("비즈니스 로직2 실행"));
}

execute 로직을 수행할 때 굳이 새로운 Context를 만들어 따로 contextv1.execute() 하지 않아도 된다.
반복되는, 변하지 않는 부분들은 뒤에서 알아서 (Callback하여) 해결해준다.


JDBC Template

JDBC를 이용하는 작업의 일반적인 순서는 아래와 같다.

그러면 여기에서 어떤 게 변하는 것이고 변하지 않는 것인지 파악하고, 템플릿과 콜백을 어떤 곳에 둘지를 생각해야 한다.
아마 변하는 부분은 연결하는 DB 종류, SQL문 정도 일 것 이다.
여기<=에서 템플릿 패턴을 적용해보았다.
처음에는 우선 Java Spring만을 이용해 온전히 DB에 connection을 맺고, JSONObject를 이용해 데이터를 가져오는 과정을 생으로 구현해본 후
다음으로는 변하는 부분과 변하지 않는 부분을 나누어주었다. 이 때 SQL(변하는 부분)은 template에서 parameter로 넘겨서 구현했기에 사실상 뒷단에서 실행되는 '콜백' 형태를 만족시키지는 않는다.
따라서 콜백형태와 구현(impl)을 적절히 활용해 Connection을 parameter로 받고, SQL은 구현과정에서 주입받아 구현하였다.
이 후 Override 가 하나만 있었기에 Lambda와 익명 내부 클래스를 활용해서 콜백 패턴을 마무리하였다!

아래에서는 마무리한 코드에 대해 설명해보겠다.

JDBC Template 구현 내용

로직

Template을 실행하면서 JdbcCallback 을 전달한다. 아래의 UserCallback의 경우에는 람다나 익명 내부 클래스를 사용하지 않고 순수하게 JdbcCallback을 implements 하는 부분을 추가적으로 구현했다.

JdbcTemplate (변하지 않는 부분)

//** 변하는 부분 ** 에서 콜백을 수행한다.

package com.example.jdbctemplate.pattern;

import java.sql.*;

//변하지 않는 부분
public class JdbcTemplate {

    private Connection getConnection(String url, String username, String password) throws SQLException{
        return DriverManager.getConnection(url, username, password);
    }

    // SQL 쿼리 생성 및 실행
    public void executeQuery(JdbcCallback callback, String url, String username, String password){
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = getConnection(url, username, password);

            // ** 변하는 부분 - SQL 로직 수행 **
            ps = callback.setPreparedStatement(conn);

            ResultSet result = ps.executeQuery();
            //단순한 출력 로직
            while (result.next()){
                System.out.println(result.getString(1));
            }
        } catch (SQLException e){
            e.printStackTrace();
        } finally {
            closeConnection(conn);
        }
    }

    // DB 연결 끊기
    private void closeConnection(Connection conn){
        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e){
                e.printStackTrace();
            }
        }
    }
}

JdbcCallback

conn을 파라미터로 받아 여기에서 아예 DB에 SQL문을 수행하여 결과값을 가져온다.

package com.example.jdbctemplate.pattern;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

//변하는 부분
public interface JdbcCallback {

    //SQL 문
    //PreparedStatement 생성하는 부분 콜백!
    public PreparedStatement setPreparedStatement(Connection conn) throws SQLException;

}

UserCallback

Lambda와 익명 내부 클래스로 구현할 수 있으나 한 번 추가적으로 전략패턴으로도 구현해보았다.

package com.example.jdbctemplate.pattern;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserCallback implements JdbcCallback {
    @Override
    public PreparedStatement setPreparedStatement(Connection conn) throws SQLException {
        return conn.prepareStatement("SELECT * FROM USER");
    }
}

테스트를 위한 실행 코드! 실행해보쟈-

package com.example.jdbctemplate.pattern;


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class PatternMain {

    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/project";
        String username = "root";
        String password = "root";

        JdbcTemplate jdbcTemplate = new JdbcTemplate();

        jdbcTemplate.executeQuery(new JdbcCallback(){
            @Override
            public PreparedStatement setPreparedStatement(Connection conn) throws SQLException {
                return conn.prepareStatement("SELECT * FROM USER");
            }
        }, url, username, password);
    }


}

결과

의도한 값이 아주 잘 출력되는 것을 볼 수 있다.! 쿼리를 바꾸어도 아주 잘 출력된다.


출처가 기억이 안나지만 어디에선가 디자인 패턴을 이론적으로만 공부한다면 자전거 타는 법을 책으로 배우는 것이다는 말을 보았다. 이 말에 자극받아서 스스로 과제하는 것처럼 미션수행(?)을 해봤는데 직접 이해한 내용을 통해 구현해보며 전략/템플릿 메소드 패턴이 손에 조금은 익은 것 같아 기쁘다✨. 정석적인 JDBC template은 아닐 수 있으나 '중복 코드 분리'에 집중해서 봐주면 될 것 같다. JDBC 템플릿을 통해 필요한 쿼리들을 일일히 객체로 구현하거나 파라미터로 전달하지 않아도 콜백할 수 있다는 점과, 반복되는 코드들이 한곳에 모여있다는 점들이 SPRING의 프레임워크적 장점으로 다가왔다. 앞으로 SPRING을 더 잘 쓰고 싶다 !! 😎
 
참고

더보기

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

 

스프링 핵심 원리 - 고급편 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com

https://m.yes24.com/Goods/Detail/7516721

 

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리 - 예스24

대한민국 전자정부 표준 프레임워크 스프링을 설명하는 책!단순한 예제를 스프링 3.0과 스프링 3.1의 기술을 적용하며 발전시켜 나가는 과정을 통해 스프링의 핵심 프로그래밍 모델인 IoC/DI, PSA, A

m.yes24.com

https://velog.io/@heoseungyeon/JdbcTemplatefeat.%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%BD%9C%EB%B0%B1-%ED%8C%A8%ED%84%B4

 

JdbcTemplate(feat.템플릿 콜백 패턴)

JDBC는 Java DataBase Connectivity 의 약자로서 Java에서 데이터 베이스에 접속할 수 있도록 해주는 Java API 인데요. Java 언어를 사용하여 데이터베이스에 접근할 때 일반적으로 사용하는 API입니다. JDBC를

velog.io

https://nauni.tistory.com/304

 

[jdbc 미션] jdbc 라이브러리: 공통된 부분을 추상화

공통된 부분과 아닌 부분을 분리 어떤 부분을 공통으로 사용하여 추상화 할 것인지 아닌지를 결정해야 한다. 아래와 같은 부분들은 공통으로 추상화할 수 있다. Connection 생성 Statement 준비 및 실

nauni.tistory.com