2014년 2월 11일 화요일

Adapter + Iterator



public class BookDTO {
private String name;
public BookDTO(String $name){
name = $name;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}




import java.util.ArrayList;

public class Books {

private ArrayList<BookDTO> books = new ArrayList<BookDTO>();
public Books(){}
public void addBook(BookDTO book){
for(int i=0, l=books.size(); i<l; ++i){
if(books.get(i).equals(book)){
return;
}
}
books.add(book);
}
public void removeBook(BookDTO book){
for(int i=0, l=books.size(); i<l; ++i){
if(books.get(i).equals(book)){
books.remove(book);
break;
}
}
}
public int getLength(){
return books.size();
}
public BookDTO getAt(int index){
if(index > books.size() - 1) return null;
return books.get(index);
}

}





public class BookManager {
private static BookManager instance = null;
private Books books = new Books();
private BookManager(){}
public static BookManager sharedObject(){
if(instance == null){
instance = new BookManager();
}
return instance;
}
public Books getBooks(){
return books;
}
public Iterator iterator(){
//return new BookIterator(books);
return new NewIterator(books);
}

}





public interface Iterator {
public boolean hasNext();
public Object next();

}




public class BookIterator implements Iterator{
protected Books books;
protected int index;
public BookIterator(Books $books){
books = $books;
}
@Override
public boolean hasNext() {
return (index < books.getLength());
}

@Override
public Object next() {
BookDTO dto = books.getAt(index++);
return dto;
}
}



public interface Adapter {
public boolean adaptedHasNext();
public BookDTO adaptedNext();

}



public class NewIterator extends BookIterator implements Adapter{

public NewIterator(Books $books) {
super($books);
}

@Override
public boolean adaptedHasNext() {
return hasNext();
}

@Override
public BookDTO adaptedNext() {
return (BookDTO) next();
}

}




public class Tester {
public static void main(String args[]){
BookManager manager = BookManager.sharedObject();
manager.getBooks().addBook(new BookDTO("Extended Phonotype"));
manager.getBooks().addBook(new BookDTO("Why Evolution Is True"));
manager.getBooks().addBook(new BookDTO("The Magic Of Reality"));
NewIterator iterator = (NewIterator) manager.iterator();
while(iterator.adaptedHasNext()){
BookDTO book = iterator.adaptedNext();
System.out.println(book.getName());
}
}

}










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형 변수 하나를 선언하는 것부터 시작했다는 것을.








함수포인터를 대체하는 전략. Template & Command



자바에서는 특정 라이브러리를 쓰지 않는한 SE 자체에서 함수 포인터를 지원하지 않는다. 
왜냐하면 C++보다 한 수위의 캡슐화를 지향하는 자바는 커맨드를 함수 단위보다는 오브젝트 단위로 설계하는 것을 지지하기 때문이다. 그래서 절차지향에 익숙한 프로그래머는 함수포인터 조차도 오브젝트단으로 그림을 그려야하는 객체지향을 불편해하기도 한다. 

그러나 아주 작은 단위조차도 오브젝트로 구성되는 객체지향은 절차지향이 따라갈 수 없을 만큼 온 세계를 프로그래밍하기에 적합한 패러다임이다.

필자는 OOP와 패턴을 공부한 이후부터는 절차지향적 코드는 아예 쳐다보지도 않을 만큼(유지보수시에도 그러한 코드를 발견하면 모두 객체지향적으로 뜯어고친다) 이러한 패러다임에 매료되었다.

기본적인 구조가 잡혀있는 템플릿에서 핵심로직을 콜백 방식으로 호출하고 싶을 경우, 자바에서는 함수포인터대신, 오브젝트 포인터를 커맨드 패턴 개념을 도입하여 사용하면 된다. 오히려 펑션단으로 호출하는 명령보다 오브젝트단으로 호출하는 명령이기 때문에 그 확장성은 더욱 높다고 할 수 있겠다.

2
4
6
8
10

이런 숫자들이 적혀있는 test.txt가 있다고 하고, 이 숫자들을 합산해서 콘솔에 찍어주는 예제를 살펴보면

먼저 템플릿 구조는 이렇게 구성할 수 있다.

abstract public class CalulatorSupport {

public Integer fileReadTemplate(String fileLocation, BufferedReaderCallback callback) throws Exception{
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(fileLocation));
return callback.doSomethingWithReader(br);
catch (Exception e) {
throw e;
}finally{
if(br != null){
try {
br.close();
catch (Exception e2) {
// TODO: handle exception
}
}
}
}
public abstract Integer sum(String fileLocation) throws Exception;
}


이 템플릿 로직에서 사용할 커맨드 로직은 클래싱(슈퍼클래스에서 자식클래스로 오버라이딩해서 구현하는 방법등을 표현하는 IT 은어)해서 클로저 방식으로 만들어보면,

public class Calulator extends CalulatorSupport{

@Override
public Integer sum(String fileLocation) throws Exception {
BufferedReaderCallback callback = new BufferedReaderCallback() {
@Override
public Integer doSomethingWithReader(BufferedReader br) throws IOException{
Integer sum = 0;
String result = null;
while( (result=br.readLine()) != null ){
sum += Integer.valueOf(result);
}
return sum;
}
};
return fileReadTemplate(fileLocation, callback);
}

}

코드의 완벽한 해석을 위해선 파일을 읽고 쓰는 자바 스트림에 대해선 따로 학습이 필요하다.

일단 패턴에 관해선 CalulatorSupport를 클래싱한 Calulator에서 클로저 방식으로 BufferedReaderCallback형의 커맨드 객체를 뽑아내고 그 커맨드 객체를 다시 슈퍼클래스로 올리는 형태로써 Template Method Pattern의 전형적인 유형이다. 함수포인터 대신 커맨드의 개념으로 오브젝트 포인터를 만들어 올리기 때문에 Command Pattern도 흉내낸셈이 된다.


public interface BufferedReaderCallback {

public Integer doSomethingWithReader(BufferedReader br) throws IOException;

}



BufferedReaderCallback는 위와 같은 유형만 갖추고 있는 인터페이스이다.
사실 Template Method는 기본적으로 객체의 관계가 상속적 구조를 갖추게 됨으로 인해 상당한 문제점을 가지고 있다.

그래서 필자는 구조 자체가 상속이 아니라 합성을 통해서 관계를 맺는 Strategy Pattern을 더욱 즐겨 사용하는 편이다.

결국 프로그래밍은 구조와 논리의 문제다.