2014년 2월 11일 화요일

Proxy 를 사용하는 방법. (Proxy Pattern)


Pure MVC라는 프레임워크에선 프록시가 모든 데이터의 운반 도구로 사용된다. 
프록시는 마치 튼튼한 캡슐안에 단단히 보호된 채, 다양한 경로에서 안전하게 데이터를 지키며 이동하는 보호막이자 케이블 같기도 하다.

보통 실무에서 프록시는 원격 데이터 접속 경로를 클라이언트에게 제공할 때 편의성을 보장하는 전략으로 채택되는 경우가 많다. 사용자는 복잡한 원격 데이터 접속 방법과 구현 로직을 세세하게 알 필요 없이 간단하게 프록시에서 제공하는 기능만 사용하여 데이터에 엑세스하고 제어한다.

퍼사드나 어댑터와 비슷하다고 볼 수도 있지만, 프록시는 다양한 타입을 동시에 (멀티스레딩으로) 제어하는 퍼사드보다는 서로 다른 유형을 연결하는데 주안점을 두는 어댑터와 더 가깝다고 할 수 있다.

그럼 어댑터와 프록시의 차이는? 

이것은 프로그래머가 설계하는 그림에 따라 다르다. 일반적인 경우로는 다른 계층의 레이어가 깔릴 경우(주로 데이터베이스나 소켓같은 원격 레이어)에는 프록시를 사용한다. 그외의 경우는 어댑터를 채택한다.

블로깅할 코드에서 프록시는 데이터베이스 접근 객체(DAO)에 클라이언트가 접근할 수 있게 해주는 역할을 부여했다.




public class UserDAO {

@Autowired
private DataSource dataSource;
private StatementStrategy strategy;
public void setStatementStrategy(StatementStrategy strategy){
this.strategy = strategy;
}
public void add() throws SQLException{
Connection connection = null;
PreparedStatement pstmt = null;
try {
pstmt = strategy.makeStatement(dataSource.getConnection());
if(pstmt.executeUpdate() > 0)
System.out.println("> ORA : 1개의 행이 삽입되었습니다");
catch (Exception e) {
throw e;
}finally{
if(pstmt != null){
try {
pstmt.close();
catch (Exception e2) {
// TODO: handle exception
}
}
if(connection != null){
try {
connection.close();
catch (Exception e2) {
// TODO: handle exception
}
}
}
}
public UserVO get(String id) throws SQLException{
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
UserVO user = new UserVO();
try {
pstmt = strategy.makeStatement(dataSource.getConnection());
pstmt.setString(1, id);
rs = pstmt.executeQuery();
while(rs.next()){
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
catch (Exception e) {
throw e;
}finally{

if(pstmt!=null){
try {
pstmt.close();
catch (Exception e2) {
}
}
if(rs!=null){
try {
pstmt.close();
catch (Exception e2) {
}
}
if(connection != null){
try {
connection.close();
catch (Exception e2) {
}
}
}
return user;
}

}


자바에서는 데이터베이스에 접근할 때 JDBC 라는 라이브러리를 사용한다. 
jdbc에서는 응용프로그램에서 DBMS에 접근할 수 있게 여러가지 드라이버를 제공하기 때문이다.

그러나 jdbc는 커넥션 관리에서 불안정하다. 
그러므로 언제나 예외 계층에서 코딩해야 하므로, 코드가 길어지고 지저분해지기 쉽다.

위와 같이 jdbc는 항상 try 로직에서 돌아야한다. 그러므로 실무에서는 jdbc를 바로 사용하지 않고(특히 웹계층은 멀티스레드 환경이기 때문에 jdbc는 너무나 위험하다) 느슨하게 퍼시스턴스 계층을 삽입하거나(MyBatis), 혹은 ORM을 쓴다(하이버네이트). 하지만 퍼시스턴스나 ORM 프레임웍을 쓴다고 해도 기본적인 jdbc 동작 원리는 알고 있어야 한다.

dataSource는 SimpleDriverDataSource라는 스프링이 제공하는 jdbc기능을 사용했다.
스프링 프레임웍에 대해서는 설명 생략. 방긋!

스프링에 applicationContext가 로드되면서 dataSource가 빈으로 등록된 오브젝트(UserDAO)에 와이어링 된다. 이후로 UserDAO는 오라클에 접속할 때 dataSource가 제공하는 커넥션으로 DB를 열고 쿼리를 쓰고 다시 쿼리를 닫고, 그후 커넥션을 닫는 흐름을 갖추게 된다.

즉 클라이언트는 원격 데이터에 왔다갔다 해야하는 로직이 형성된 것이다. 여기서 사용자는 DAO 객체를 바로 사용하지 않고 프록시를 통해 간접적으로 접근한다. 이렇게 하면 내부적으로 로직이 어떻게 변경되든 관계 없이, 클라이언트는 쉽사리 변하지 않고 항상 익숙한 환경을 제공해주는 프록시에게만 의지하면 된다. 

프록시가 내부적으로 DAO를 어떻게 다루는지, 얼마나 많은 일을 수행하는지에 관계없이, 사용자는 프록시만 있으면 모든 일을 할 수 있게 된다. 프록시가 오브젝트를 다루는 방식이 lazy-loading이나 lazy-init이라고 해도 사용자는 알아채지도 못하고 알 필요도 없게 된다. 

클라이언트가 서버에 접근할 때 항상 안정적이고 편의성이 보장되는 방식. 
바로 Proxy Pattern의 기본적인 개념이다.

public class UserProxy {
private UserDAO dao;
public void setUserDAO(UserDAO dao){
this.dao = dao;
}
public UserVO get(String id) throws Exception{
dao.setStatementStrategy(new GetUserStatement());
return dao.get(id);
}
public void add(UserVO user) throws Exception{
dao.setStatementStrategy(new AddUserStatement(user));
dao.add();
}

}



예제에서 프록시는  DB에 데이터를 등록하는 add와 등록한 데이터의 id로 해당하는 데이터를 읽어오는 get 이라는 기능을 갖고 있다. 사용자는 바로 프록시의 이 두 가지 기능을 사용하지만, 프록시는 내부적으로 dao의 add와 get을 사용한다.

여기서 프록시는 어댑터의 역할만이 아니라, 오브젝트 팩토리의 기능도 수행하며,  Strategy Pattern의 개념까지 도입하여 context와 전략을 연결해주는 매핑 기능도 갖추고 있다.

좀 더 자세히 설명하면, context가 전략을 사용하는 방법은 context의 자체 관심사로 두기 보다는 팩토리나 클라이언트층의 제 3자 관심사로 분리하여 context에 전략이 의존하지 않게 만들어주는 로직이 합리적이다.

그러므로 context에 도입될 전략은 context가 알지 못하며, 이것은 context와 전략을 매핑시켜줄 또 다른 오브젝트의 책임이 된다. 여기선 바로 그 오브젝트가 프록시라고 할 수 있다.

add와 get은 역할이 다르기 때문에 쿼리가 다르다. 그러므로 서로 다른 쿼리가 적용되어야 하는데 필자는 이것을 스트레티지 패턴의 전략으로 채용했다. 느슨한 매핑을 위해서 인터페이스의 사용은 필수라고 할 수 있다.

public interface StatementStrategy {
public PreparedStatement makeStatement(Connection connection) throws SQLException;

}


jdbc에서 쿼리를 쓰는 객체중에 하나인 PreparedStatement는 커넥션에서 생성되기 때문에 인자로 커넥션을 주입받게 설계한다. 그리고 이 인터페이스를 구현하는 두 개의 클래스를 만든다.

public class GetUserStatement implements StatementStrategy{
@Override
public PreparedStatement makeStatement(Connection connection) throws SQLException {
return connection.prepareStatement("select * from USERS where id = ?");
}

}


public class AddUserStatement implements StatementStrategy{
private UserVO vo;
public AddUserStatement(UserVO vo){
this.vo = vo;
}

@Override
public PreparedStatement makeStatement(Connection connection) throws SQLException {
PreparedStatement pstmt = connection.prepareStatement("insert into USERS (id, name, password) values (?,?,?)");
pstmt.setString(1, vo.getId());
pstmt.setString(2, vo.getName());
pstmt.setString(3, vo.getPassword());
return pstmt;
}

}


UserDAO라는 아이는 쿼리객체를 리턴하는 클래스가 무엇인지는 알 필요가 없다. 

그저 그 클래스가 특정 인터페이스를 구현했다는 최소한의 정보만 알고 있으면, 나머지는 프록시가 알아서 선정해주게 된다. 

실무에선 이렇게 간단한 코드는 드물지만, 어떤 복잡한 코드라도 처음에는 이렇게 시작을 한다.

오늘날 호랑이, 사자, 사람 같은 복잡한 생물도 처음 시작은 아주 단순한 세포로부터 진화했다는 것을.

아이폰과 같은 완성체도 int형 변수 하나를 선언하는 것부터 시작했다는 것을.








댓글 없음:

댓글 쓰기