본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

반복되는 부분으로부터 반복되지 않는 부분을 분리하는 패턴

by 민휘 2023. 3. 13.

동일한 목적을 수행하는 세 가지 패턴

코드를 작성하다보면 반복되는 요구사항에 대해 중복된 코드가 만들어지기 마련이다.

중복 코드는 가시적인 코드 스멜의 주범이다. 이 문제를 해결하기 위한 디자인 패턴을 알아보자.

 

반복되는 부분과 반복되지 않는 부분을 분리하기 위한 패턴은 총 세 가지다.

(1) 전략 패턴 (2) 템플릿 메소드 패턴 (3) 템플릿 콜백 패턴.

 

목적이 동일하므로 필요로 하는 역할도 동일하다.

반복되는 부분을 담는 역할, 반복되지 않는 부분을 담는 역할, 이 둘의 의존성을 주입하는 역할,

그리고 이 연결관계가 만들어진 두 객체를 사용하는 역할이다.

 

세 가지 패턴을 구분하는 것은 구현 방법이다. 목적은 동일하지만 구현 방법이 약간 다르다.

전략 패턴은 인터페이스를 두어 전략 클래스를 구현하고,

템플릿 메소드 패턴은 추상클래스를 두어 전략 메소드를 구현하고,

템플릿 콜백 패턴은 익명 내부 클래스를 사용해 구현한다.

 

세 가지 패턴 중 하나를 선택할 때는 요구사항에 가장 적절한 구조를 가지는 구현을 선택해야 한다.

이 글에서는 전략 패턴과 템플릿 콜백 패턴의 구현을 비교한다.

 

 

 

전략 패턴의 구현

전략 패턴에서 반복되는 부분은 Context가, 반복되지 않는 부분은 Strategy가 담당한다.

그리고 Strategy는 인터페이스로 구성되어 개별 전략은 해당 인터페이스를 구현한 클래스가 수행한다.

외부에서 이 둘의 의존성을 해결할 것이다.

이 책임은 클라이언트가 수행해도 되고, 외부 설정 파일에서 지정해도 된다.

 

 

전략 패턴 구현의 단점

전략 패턴은 모든 개별 전략에 대해 클래스가 필요하다.

클래스가 여러 곳에서 재사용될 수 있다면 의미 있는 구현이었겠지만,

개별 전략이 딱 한 곳에서 사용되고 만다면 클래스 파일을 하나씩 만드는 것이 번잡스러울 것이다.

 

 

이럴 때는 익명 내부 클래스!

개별 전략이 일회용으로 사용된다면 클라이언트에서 익명 내부 클래스로 개별 전략을 만드는 것이 낫다. 클래스 파일의 개수가 적어질 것이다.

또 클라이언트 코드에 응집도 높은 코드가 남을 것이므로 클라이언트 객체의 응집도가 높아질 것이고, 코드의 이해도도 높아질 것이다.

 

코드로 확인해보자. UserDao 클래스는 JDBC를 사용해 데이터베이스로부터 값을 읽고 쓰는 역할을 한다.

UserDao의 메소드는 하나의 sql문을 실행한다.

이때 모든 메소드에 리소스를 가져와 PreparedStatement를 생성하고 예외 처리하고 리소스를 반환하는 부분이 반복된다.

이 부분은 UserDao의 jdbcContextWithStatementStrategy 메소드로 추출한다.(컨텍스트 메소드)

달라지는 부분은 개별 전략으로 구현한다.

 

전략 패턴을 적용한 코드를 보자. User 하나를 삽입하는 메소드에 적용해보았다.

삽입하는 SQL을 실행할 수 있는 PreparedStatement를 만드는 AddStatement 개별 전략 클래스를 구현했고,

add()에서 구현 전략을 생성자로 만들어 컨텍스트 메소드에 넘겨주었다.

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

public class AddStatement implements StatementStrategy {

    User user;

    public AddStatement(User user) {
        this.user = user;
    }

    @Override
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
        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;
    }
}

public void add(User user) throws SQLException {
	StatementStrategy addStatementStrategy = new AddStatement(user);
	jdbcContextWithStatementStrategy(addStatementStrategy);
}

 

 

문제는 이렇게 만든 AddStatement는 add()에서 일회용으로 사용하는 개별 전략이라는 것이다. UserDao에서 사용되는 모든 메소드마다 개별 전략 클래스를 구현해 생성하는 방식은 지나치게 많은 개별 전략 클래스 파일을 만들 것이고, 그렇게 되면 시스템이 복잡해질 것이다.

 

그렇다면 일회용으로 생성되는 클래스를 클라이언트가 내부에서 구현하는 방식을 사용하자. 템플릿 콜백 패턴을 적용해보는 것이다. 클라이언트인 add() 안에서 익명 내부 클래스인 addStatementStrategy를 정의했다. 마침 StatementStrategy는 추상 메소드가 하나인 함수형 인터페이스이므로 람다 표현식으로 더 깔끔하게 표현할 수 있다. 이렇게 되면 모든 개별 전략마다 클래스 파일이 만들어지지 않을 것이다. 그리고 이 코드를 읽는 다른 개발자들도 어떤 sql문을 만들어서 실행하는 것인지 파일 이동 없이 바로 이해할 수 있다.

public void add(User user) throws SQLException {
	StatementStrategy addStatementStrategy = 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(addStatementStrategy);
}