템플릿과 디자인 패턴

정훈희·2022년 10월 21일
0

Spring

목록 보기
10/24
post-thumbnail

템플릿과 디자인 패턴

참조

  • 토비의 스프링 3.1 vol.1 209p~240p

템플릿

성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

스프링에 적용된 템플릿 기법을 살펴보고 적용해보자.

public void deleteAll() throws SQLException {
	Connection c =dataSource.getConnection();

	PreparedStatement ps =c.prepareStatement("delete from users");
	ps.executeUpdate();

	ps .close();
	c.close();
}

만약

PreparedStatement ps =c.prepareStatement("delete from users");

ps.executeUpdate();

이 부분에서 예외가 발생한다면, 메소드 실행이 중된된다

→ 위에서 생성된 connection이 종료되지 않는다.

→ 오류가 여러번 발생해서 커넥션이 쌓이면 치명적인 에러 발생가능

예외 발생시에도 connection을 close하도록 수정한 코드

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) {
			}
		}
		if (c != null) {
			try {
				c.close();
			} catch (SQLException e) {
			}
		}
	}
}

이런 경우에는 에러가 발생할 수 있는 부분을 try로 묶어주고, finally문으로 예외가 발생했던 안했던 커넥션이 null이 아니라면 커넥션을 종료한다.

또한 이런식으로 DB 연결이 필요한 다른 메소드에도 이런식의 예외처리가 필요하다.

→ 이런식으로 복잡한 패턴을 여러군데서 사용하다보면 실수하기 쉬워짐


분리와 재사용을 위한 디자인 패턴 적용

위와 같은 문제를 해결하기 위해서는 일단 변하는 성격이 다른 것을 찾아내야한다.

살펴보면 ps =c.prepareStatement("delete from users"); 이 부분을 제외하면 나머지는 DB 연결 및 예외 시 남은 연결들을 close해주는 부분이다.

→ 템플릿 메소드 패턴을 적용해보자.

템플릿 메소드 패턴

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.

일단 자주바뀌는 부분을 메소드로 추출해보자.

public void deleteAll() throws SQLException {
	// ...
	try (
		c =dataSource.getConnection();
		ps = makeStatement(c);
		ps.executeUpdate();
	} catch (SQLException e)
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
	PreparedStatement ps;
	ps = c.prepareStatement("delete from users'’);
	return ps;
}

변하는 부분인 makeStatement 메소드를 추상메소드로 변경해보자.

public class UserDaoDeleteAll extends UserDao (
	protected PreparedStatement makeStatement(Connection c) throws SQLException {
		PreparedStatement ps = c.prepareStatement("delete from users");
		return ps;
	}
}

위과 같이 UserDao를 상속받아 makeStatement 메소드를 DeleteAll에 맞게 구현하였다.

image

하지만 위와 같이 메소드 하나마다 서브클래스를 만들어야한다…

→ 전략 패턴을 적용해보자.

전략패턴

image

위의 사진과 같이 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서 의존하도록 만드는 것을 전략 패턴이다.

우리가 만든 deleteAll 메소드에서는 context는 변하지 않는 DB연결 및 예외처리 부분이고, 변하는 부분이 strategy이다. 각 변화마다 contreteStrategy A, B 같은 식으로 strategy인터페이스를 구현한다.

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

위와 같이 변하는 부분을 interface로 만들고,

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

StatementStrategy interface를 deleteAll의 기능에 맞게 DeleteAllStatement로 구현하였다.

public void deleteAll() throws SQLException (
	// ...
	try (
		c =dataSource.getConnection();
		StatementStrategy strategy = new DeleteAllStatement(); ps =strategy.makePreparedStatement(c);
		ps.executeUpdate();
	} catch (SQLException e) {
	// ...
}

이렇게 전략패턴을 적용한 deleteAll 메소드가 완성되었다.

하지만 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어있다. → 클라이언트와 컨텍스트 분리 필요


DI 적용을 위한 클라이언트/컨텍스트 분리

전략패턴에서는 context가 사용할 전략은 context를 사용하는 client가 결정하는게 일반적이다.

image

잘 살펴보면 1장에서 UserDao의 문제점을 해결했을때 적용한 방법이다.

위의 방법을 적용시켜보면

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) {} }
	}
}

메소드로 분리한 DB연결 및 예외처리 기능을 가진 context 코드를 작성하였고,

public void deleteAll() throws SQLException {
	StatementStrategy st = new DeleteAllStatement(); //
	jdbcContextWithStatementStrategy(st);
}

클라이언트 책임을 담당할 deleteAll 메소드는 어떤 전략 클래스를 사용할 것인지 정하고, 컨텍스트에 전략 클래스를 전달해준다.

이로써 비슷한 기능의 DAO메소드가 필요할 때마다 만들어둔 컨텍스트와 전략을 활용할 수 있으니 코드도 간결해지고, 실수할 염려가 없어졌다.

하지만, 아직 DAO메소드 마다 새로운 StatementStrategy 구현 클래스를 만들어야한다.

→ StatementStrategy 전략클래스를 내부클래스로 정의

public void add(User user) throws SQLException {
	class AddStatement implements StatementStrategy{
		public PreparedStatement makePreparedStatement(Connection c)
				throws SQLException (
			PreparedStatement ps =
				c.prepareStatement("insert into users(id, name, password)
					values(?,?,?)");
			ps.setString(l, user.getld());
			ps.setString(2, user.getName());
			ps.setString(3, user.getPassword());
			return ps;
		}
	}
	StatementStrategy st =new AddStatement();
	jdbcContextWithStatementStrategy(st);
}

위와 같이 특정 메소드에서만 사용되는 경우는 로컬 클래스로 만들 수 있다.

로컬 클래스를 사용했을 때 장점

  1. 클래스 파일이 줄어든다
  2. 해당 메소드 내에서 관련 클래스의 로직을 바로 볼 수 있음
  3. 로컬 클래스는 내부 클래스이므로 자신이 선언된 곳의 정보에 접근할 수 있음

참고로, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야한다.

  • add 메소드의 파라미터로 받는 user는 메소드 내부에서 변경될 일이 없으므로 final로 선언해도 무방하다.

여기서 내부클래스를 아래와 같이 이름이 없는 익명 내부 클래스로 만들수도 있다

jdbcContextWithStatementStrategy(
	new StatementStrategy() {
		// 클래스 내용
	}
);
  • 익명 클래스 선언 방법: new 인터페이스_이름() {클래스 내용}

컨텍스트 분리

이번엔 jdbcContextWithStatementStrategy를 다른 DAO에서도 사용할 수 있도록 UserDao클래스 밖으로 독립시켜보자.

public class JdbcContext (
	// DataSource 타입 Bean을 DI받게 준비해놓음
	private DataSource dataSource;
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	// 전략 클래스에 맞게 DB 연결 및 에러처리를 해주는 메소드
	public void workWithStatementStrategy(StatementStrategy stmt)
			throws SQLException {
		Connection c =null;
		PreparedStatement ps =null;
		try (
			c =this.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) {} }
		}
	}
}

또한 UserDao를 JdbcContext를 DI받게 수정하자.

public class UserDao (
	// ...
	// 아까 만든 JdbcContext를 DI받도록 만든다.
	private JdbcContext jdbcContext;
	public void setJdbcContextOdbcContext jdbcContext) {
		this.jdbcContext = jdbcContext;
	}

	public void add(final User user) throws SQLException {
		// DI받은 jdbcContext의 메소드를 사용한다.
		this.jdbcContext.workWithStatementStrategy(
			new StatementStrategy() { ... }
		};
	}

	public void deleteAll() throws SQLException {
		this.jdbcContext.workWithStatementStrategy (
			new StatementStrategy() { ... }
		);
	}
}

이제 새롭게 작성된 오브젝트 간의 의존관계를 살펴보자.

스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적이다.(단, 위 코드에서 JdbcContext는 인터페이스가 아니지만 구현 방법이 바뀔 가능성이 없으므로 괜찮다.)

image

위 그림은 JdbcContext를 적용한 의존관계를 나타내주는 클래스 다이어그램이다.

스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의된다.

image

기존에는 userDao 빈이 dataSource빈을 직접 의존했지만, 이제는 위 그림과 같이 jdbcContext빈이 그 사이에 끼게 된다.


수동 DI

JdbcContext를 스프링의 빈으로 등록해서 UserDao에 DI하는 대신 UserDao 내부에서 직접 DI를 적용하는 방법이 있다.

이 방법을 쓰려면 JdbcContext를 싱글톤으로 만드는것을 포기해야한다. 대신 DAO마다 하나의 JdbcContext를 가지고 있으면 된다.

JdbcContext를 스프링 빈으로 등록하지 않았으므로 다른 누군가가 JdbcContext의 생성과 초기화를 책임져야 한다. 이 제어권은 각 Dao에게 주는 것이 적당하다.

JdbcContext는 의존 오브젝트를 DI를 통해 제공받기 위해 빈으로 등록되어야 한다. 하지만 UserDao에서 JdbcContext를 생성해서 사용하려면 JdbcContext 자신은 스프링의 빈이 아니니 DI 컨테이너를 통해 DI 받을 수 없다.

이러한 경우에는 UserDao에게 DI까지 맡길 수 있다. 그러면 아래와 같은 구조가 된다.

image

이제 위 사진의 구조에 맞게 설정파일을 변경해주고, 아래처럼 JdbcContext 생성과 DI 작업을 수행하도록 변경하였다.

public class UserDao {
	// ...
	private JdbcContext jdbcContext;
	public void setDataSource(DataSource dataSource) {
		this.jdbcContext = new JdbcContext();
		this.jdbcContext.setDataSource(dataSource);
		this.dataSource = dataSource;
	}
}

setDataSource 메서드를 통해 JdbcContext를 생성 및 DI 작업을 진행했다.

profile
DB를 사랑하는 백엔드 개발자입니다. 열심히 공부하고 열심히 기록합니다.

0개의 댓글