반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 알고리즘
  • NEXT JS
  • 자바스크립트 면접
  • 자바 디자인패턴
  • bean Validation
  • 토비의 스프링
  • try-with-resources
  • 싱글톤
  • 자바스크립트 인터뷰
  • 메이븐 페이즈
  • 이펙티브자바
  • 도커공식문서
  • 객체복사
  • 자바 검증
  • item8
  • next js app
  • 메이븐 라이프사이클
  • rust
  • 참조 해제
  • 추상 팩터리 패턴
  • 이펙티브 자바 item9
  • pnpm
  • 싱글톤 패턴
  • serverless computing
  • 디자인패턴
  • 작업기억공간
  • 스프링 검증
  • 메이븐 골
  • 팩터리 메서드 패턴
  • 싱글턴
  • Javadoc 자바독 자바주석 주석 Comment
  • item9
  • 자바스크립트
  • 자료구조
  • 이펙티브 자바
  • 러스트
  • 서버리스 컴퓨팅
  • 느린 쿼리
  • 자바
  • Pre-rendering
  • 슬로우 쿼리
  • 프로그래머의 뇌
  • 플라이웨이트패턴
  • prerendering
  • 빈 검증
  • 외래키 제약조건
  • Next.js
  • item7
  • Java
  • MSSQL

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

토비의 스프링 3장 요약 정리 - 템플릿
프레임워크/토비의 스프링

토비의 스프링 3장 요약 정리 - 템플릿

2022. 6. 20. 08:27

템플릿이란?

코드 블럭이 갖는 특성에 따라 코드를 분리하는 것이다. 다음과 같은 특징을 갖는다고 가정한다.

  • 변경을 통해 그 기능이 다양해지고 확장되는 성질을 가진 코드가 있다.
  • 고정되어 있고 변하지 않으려는 성질을 가진 코드가 있다.

일정한 패턴으로 유지되는 특성을 가지는 부분을 독립시킨 것이 템플릿(틀)이다.

책에서는 UserDAO에서 비즈니스 로직을 담당하는 부분(변화)과 DB를 연결하는 부분(고정)을 나누었다.

deleteAll() 개선하기

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        ps = c.prepareStatement("delete from users"); // 변할 수 있는 부분

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if(ps != null) { try { ps.close(); } catch (SQLException e) { } } // ps 리소스 반환
        if(c != null) { try { c.close(); } catch (SQLException e) { } } // c 리소스 반환
    }
}
  • 현재는 DB에 연결하는 부분과 커넥션을 닫는 부분 모두의 코드를 포함하고 있다.
  • 사실 위의 코드 중 DB에 커넥션을 맺고 끊는 부분은 매번 동일할 것이고, delete from users 만 유일하게 변하는 부분일 것이다.

개선1: 메서드 추출해보기

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        ps = makeStatement(c);

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
        if(c != null) { try { c.close(); } catch (SQLException e) { } }
    }
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
    return c.prepareStatement("delete from users");
}
  • 보통 공통 로직을 메서드로 추출하여 공통 메서드를 여러 로직에서 쓰는 방식으로 리팩토링하는 경우가 흔하다.
  • 그러나 위 경우에는 메서드로 뽑았을 때 별다른 이득이 없다.
    • 우리의 첫 목적은 변하지 않는 부분을 템플릿으로 만들어 계속 재활용하고 싶어했다.
    • 위의 형태대로라면, selectAll() 등과 같은 메서드를 또 만들어야 하고 그 안에는 DB 커넥션 템플릿을 입력해줘야 할 것이다.
    • 변하지 않는 부분을 다시 작성할 필요가 없게 만들도록 고민해보자.

개선2: 템플릿 메서드 패턴 이용해보기

public abstract class UserDao {
...
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;

...
}

public class UserDaoDeleteAll extends UserDao{
    @Override
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}
  • 변화하는 부분을 @Override 할 수 있어 템플릿 코드의 재사용이 가능하다.
  • 개방 폐쇄 원칙(OCP)도 어느정도 지켰다.
    • @Override 를 통해 변화에는 열려있고, 변경에는 닫혀있다.
  • 그러나 매번 클래스를 만들어서 상속받는 것은 아무래도 좀 무리가 있다.
  • 컴파일 시점에 관계가 이미 형성되어 있어 관계에 대한 유연성도 떨어진다.

개선3: 전략 패턴 이용해보기

public interface StatementStrategy {
    PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}

public class DeleteAllStatement implements StatementStrategy{
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}

public void deleteAll() throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();

        StatementStrategy strategy = new DeleteAllStatement();
        ps = strategy.makePreparedStatement(c);

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
        if(c != null) { try { c.close(); } catch (SQLException e) { } }
    }
}
  • 인터페이스를 통해 의존하게 만드는 방식이다.
    • 인터페이스를 이용함으로써 클래스를 이용할 때보다 구현에 훨씬 자유로워졌다.
      • 인터페이스를 상속함으로써 부모 클래스의 특성을 이어받을 필요 없이 단순히 메서드 1개만 구현하면 된다.
      • 어떻게든 PreparedStatement 타입을 반환하는 makePreparedStatement() 메서드만 구현하면 구현에 제약이 없다.
    • 런타임에 관계설정이 되어 훨씬 유연해졌다.
  • 여전히 템플릿 코드가 deleteAll()에 남았다는 점은 고쳐야 한다.
    • 들어오는 쿼리를 delete 로 한정할 것도 아니기 때문에 메서드 이름도 deleteAll() 이 될 필요가 전혀 없다.

개선4: DI 적용을 위해 클라이언트/컨텍스트 분리하기

public class DeleteAllStatement implements StatementStrategy{
    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        return c.prepareStatement("delete from users");
    }
}

public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
    Connection c = null;
    PreparedStatement ps = null;

    try {
        c = dataSource.getConnection();
        ps = stmt.makePreparedStatement(c);

        ps.executeUpdate();
    } catch (SQLException e) {
        throw e;
    } finally {
        if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
        if(c != null) { try { c.close(); } catch (SQLException e) { } }
    }
}

public void deleteAll() throws SQLException {
    StatementStrategy strategy = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
    jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
  • 컨텍스트를 메서드 형태(jdbcContextWithStatementStrategy)로 분리시켰다.
    • 마침내 메서드 이름을 훨씬 범용성 있고 의도도 잘 표현하는 이름으로 변경하였다.
  • 이제서야 올바르게 메서드에 변하지 않는 템플릿 부분이 들어갔다.
  • 관심사 분리와 유연한 확장에 대해 매우 유리해졌다.
  • 이제 위 메소드로 업데이트 등 다른 쿼리도 만들어낼 수 있다.
    • 전략 클래스의 구현만 변경해주면 된다.
    • 한번 쓰고 말 클래스라면 중첩 익명 클래스로 구현해도 무방하다.
  • 매번 귀찮게 try/catch/finally를 안해줘도 돼서 코드의 양도 줄고 실수를 할 이유도 줄었다.

스프링에서의 DI

  • 스프링은 프레임워크가 작성한 인터페이스에 구현체를 주입해주는 방식으로 DI를 해준다.
    • 클라이언트에서 직접 주입하는 것이 아닌 프레임워크에서 지원하는 것이라 IoC의 개념이 적용되었다고 볼 수 있다.
  • DI는 사실 꼭 클래스 단위로 일어나는 것이 아니라, 매우 작은 단위의 코드와 메서드 사이에서도 일어날 수 있다.

전략패턴 구조 다시 살펴보기

  • 전략: DeleteAllStatement 클래스
    • 변하는 부분에 대한 구현
  • 컨텍스트: UserDao.jdbcContextWithStatementStrategy()
    • 변하지 않는 부분
  • 클라이언트: UserDao.deleteAll()
    • 전략과 컨텍스트가 합쳐져 최종적으로 클라이언트가 사용하는 메서드

위와 같이 3가지 부분으로 나뉘어져있다.

추가 개선1: 클래스 나누기

public class JdbcContext {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
        Connection c = null;
        PreparedStatement ps = null;

        try {
            c = dataSource.getConnection();
            ps = stmt.makePreparedStatement(c);

            ps.executeUpdate();
        } catch (SQLException e) {
            throw e;
        } finally {
            if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
            if(c != null) { try { c.close(); } catch (SQLException e) { } }
        }
    }
}
  • 위의 코드처럼 전략패턴의 3가지 구성요소 중 컨텍스트 부분만 따로 빼내서 클래스로 만들면, UserDao는 더이상 DataSource를 사용할 필요가 없어진다.
  • 클래스의 책임과 분리가 좀 더 명확해지고 있다.
public class UserDao {
    ...
    JdbcContext jdbcContext;

    public void setJdbcContext(JdbcContext jdbcContext) {
        this.jdbcContext = jdbcContext;
    }
    ...

    public void add(User user) throws SQLException {
      StatementStrategy stmt = c -> {
          PreparedStatement ps = c.prepareStatement(
                  "insert into users(id, name, password) values (?, ?, ?)"
          );

          ps.setString(1, user.getId());
          ps.setString(2, user.getName());
          ps.setString(3, user.getPassword());

          return ps;
      };

      jdbcContext.workWithStatementStrategy(stmt);
    }

    public void deleteAll() throws SQLException {
        StatementStrategy strategy = c -> c.prepareStatement("delete from users");
        jdbcContext.workWithStatementStrategy(strategy);
    }
    ...
}
  • DataSource 의존성에서 해방되어서, 조금 더 깔끔한 코드가 되었다.

추가 개선2: 의존성 주입 xml 적용하기

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="username" value="postgres" />
        <property name="password" value="iwaz123!@#" />
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost/toby_spring" />
    </bean>

    <bean id="jdbcContext" class="toby_spring.chapter1.user.jdbc_context.JdbcContext">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
        <property name="dataSource" ref="dataSource" />
        <property name="jdbcContext" ref="jdbcContext" />
    </bean>
</beans>
  • 의존성 주입 xml 은 클래스에 대한 청사진이므로 이를 보고 구조를 이해하면 조금 쉽다.
  • JdbcContext 클래스가 DataSource 의존성을 가져가므로, DataSource 의존성을 넣어주면 된다.
  • JdbcContext 클래스 역시 스프링 컨테이너의 빈으로 올라가 다른 곳에 주입될 수 있게 되었다.

UserDao의 경우 완전히 DataSource 의존성에서 해방된 것은 아니므로 여전히 의존성이 필요하긴 하다.

템플릿과 콜백

  • 전략 패턴에서 컨텍스트를 템플릿이라고도 부르고, 전략을 콜백이라고도 부른다.
    • 자바스크립트와 같이 변수 안에 함수를 할당할 수 있는 언어에서는 위와 같은 콜백을 적극 활용한다.

JdbcContext로 살펴보는 템플릿 콜백 동작 예

콜백 재활용해보기: 변경 전

public void deleteAll() throws SQLException {
    StatementStrategy strategy = c -> c.prepareStatement("delete from users"); // 선정한 전략 클래스의 오브젝트 생성
    jdbcContext.workWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
  • 사실 위 코드에서 "delete from users"를 제외한 나머지 부분들은 재사용이 가능하다.

콜백 재활용해보기: 변경 후

public void deleteAll() throws SQLException {
    executeSql("delete from users");
}

public void executeSql(String sql) throws SQLException {
    StatementStrategy strategy = c -> c.prepareStatement(sql); // 선정한 전략 클래스의 오브젝트 생성
    jdbcContext.workWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}

public void add(User user) throws SQLException {
    this.jdbcContext.executeSql("insert into users(id, name, password) values (?, ?, ?)"
            , user.getId()
            , user.getName()
            , user.getPassword()
    );
}

public void executeSql(String sql, Object ...parameters) throws SQLException {
    StatementStrategy stmt = c -> {
        PreparedStatement ps = c.prepareStatement(sql);

        for(int i=1; i<=parameters.length; i++) {
            ps.setObject(i, parameters[i-1]);
        }

        return ps;
    };

    this.workWithStatementStrategy(stmt);
}
  • 자주 쓰는 콜백을 메서드로 빼서 메서드만 공개하므로 더욱 캡슐화가 잘되어있고, 응집도도 높아졌다.

정리

  • 맨 처음 deleteAll() 메서드에는 변화하는 부분과 변화하지 않는 부분이 마구잡이로 섞여있었다.
    • 크게 나누자면 계속 반복되는 DB 연결 부분과 변화가 가능한 PreparedStatement 를 만드는 부분이 있었다.
  • DB 연결하는 부분의 코드는 고정적으로 보고 해당 부분을 템플릿 이라고 칭했다.
  • 개선1 에서 메서드로 추출해보았는데, 분리를 이용해 적절히 재사용할 수 있는 형태가 아니었다.
  • 개선2 에서 추상 메서드를 이용한 클래스 상속을 이용하여 드디어 분리를 이용해 적절히 재사용할 수는 있었지만, 클래스 상속이라는 불편함이 있었다.
  • 개선3 에서 인터페이스를 도입한 전략 패턴으로 이전보다 훨씬 유연하게 StatementStrategy 인터페이스만 구현하면 되는 방식으로 변했다.
  • 개선4 에서 실제 클라이언트가 호출하게 되는 클라이언트 부분과 실제 동작을 수행하는 컨텍스트 부분이 나뉘었다.
  • 이후 나머지 개선에서 Jdbc 의 DataSource 와 관련된 부분을 JdbcContext 클래스로 나누고, UserDao 에는 설정된 JdbcContext 를 이용해 데이터를 주고받는 부분만 남겨두었다.

추후에는 DB 와 소통하기 위한 JDBC API 를 사용하는 부분은 단계적으로 JdbcContext 로 모두 옮기고 UserDao 는 클라이언트와 DB 의 소통창구 정도의 역할을 해주는 것이 설계상 바람직할 것 같다.

반응형
저작자표시 비영리 (새창열림)

'프레임워크 > 토비의 스프링' 카테고리의 다른 글

토비의 스프링 5장 요약 정리 - 서비스 추상화  (0) 2022.09.06
토비의 스프링 4장 요약 정리 - 예외 처리  (0) 2022.06.21
토비의 스프링 2장 요약 정리 - 테스트  (2) 2021.12.26
토비의 스프링 1장 요약 정리 - 오브젝트와 의존관계  (3) 2021.12.26
토비의 스프링 0장 정리  (0) 2021.12.14
    '프레임워크/토비의 스프링' 카테고리의 다른 글
    • 토비의 스프링 5장 요약 정리 - 서비스 추상화
    • 토비의 스프링 4장 요약 정리 - 예외 처리
    • 토비의 스프링 2장 요약 정리 - 테스트
    • 토비의 스프링 1장 요약 정리 - 오브젝트와 의존관계
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바