토비의 스프링 3의 springusergroup 예제에 대한 이야기 세 번째.
도메인 오브젝트를 DAO와 DAO 클라이언트 사이의 결합을 낮춰주고 구현 기술과 방법에 독립적으로 만들기 위한 DTO로 삼는다면, 더 나아가서 전 계층에서 일관되게 유지되고 접근할 수 있는 데이터 홀더이자 도메인 로직을 가진 오브젝트로 사용하려면 DAO는 그에 맞는 형태의 구조의 오브젝트를 돌려줘야 한다. 관건은 관계를 가진 엔티티/오브젝트에는 원한다면 언제든지 접근해야 하고, 그 오브젝트가 DAO에서 미리 읽어온 정보로 초기화 되어있지 않다면 어느 시점에라도(심지어 뷰에 이르러서라도) 다이내믹하게 DB를 호출해서 정보를 가져와야 한다. 물론 이를 사용하는 쪽에서는 전혀 그런 사실을 눈치채지 못하게 투명하게 그 과정이 백그라운드에서 자동으로 일어나야 한다. 일명 lazy-loading이다.
Lazy-loading하면 왠지 부정적인 느낌으로 들리지만 사실은 Smart-loading이라고 해줄 수 있을만큼 영리한 방식으로 동작한다. ORM은 오브젝트와 관계형(relational) DB 사이의 미묘한 차이를 효과적으로 줄여주는 기술이다. 하지만 오브젝트를 사용하더라도 RDB의 특징을 잘 이해할 필요가 있다. User테이블은 Group테이블의 PK인 id 컬럼의 값을 FK로 가지고 있다. 예를 들면 group_id 같은 필드이다. 그래서, 단지 관계를 가지고 있는 Group 테이블의 id만 필요하다면 굳이 Group테이블을 조인해서 읽을 필요가 없다. 하지만 User – Group 오브젝트 관계에서는 User 오브젝트가 groupid라는 정수형 필드 값을 가지지 않는다. Group의 id가 필요하더라도 Group오브젝트에 가서 id를 읽어와야 한다. 이것이 RDB 정보를 오브젝트로 전환했을 때 발생하는 부담이다.
그런데 이를 부담이 아닌 것처럼 만들 수 있다. 사실은 영리한 lazy-loading 덕분이다. Lazy-loading은 User 오브젝트에 직접 연결된 Group오브젝트를 필요한 시점에서 초기화 해준다. 그렇다고 초기화용 유틸 메소드를 따로 실행하지는 않는다. User 오브젝트를 사용하는 입장에서는 이미 처음부터 Group 오브젝트의 정보도 모두 존재하는 것처럼 사용하면 된다. 단지 Group의 내부 정보를 사용할 때 뒤에서 알아서 Group 테이블의 레코드를 읽어서 다이나믹하게 그 값을 채워넣어주는 것이다.
그런데 만약 어떤 User의 소속 Group에 대한 group_id만 필요한 경우가 있다고 해보자. 전통적인 SQL기반의 DAO라면 간단하다. FK는 User 테이블에 있기 때문에 Group 테이블에 접근할 것도 없이 select * from User 해도 그만이다. User 안에 group_id가 있기 때문이다.
반면에 오브젝트로 전환되면 Group의 id가 필요할 때 user.getGroupid()라고 할 수 없다. 이 때는 user.getGroup().getId()라고 해야 한다. 결국 User 클래스의 group 필드에 Group타입의 오브젝트가 들어있어야 한다는 말이다. 그렇다면 단지 group_id가 필요한 경우에도, User테이블에서 다 읽어올 수 있는 값인데도 Group 테이블의 정보를 모두 읽어오는 lazy-loading이 필요할까?
그건 아니다. Lazy-loading은 게으를 지는 몰라도 멍청하지는 않다. Lazy-loading에 의해서 Group정보를 실제로 가져오는 시점은 user.getGroup().getName()과 같이 FK/PK를 제외한, 즉 User테이블에서는 가져올 수 없는 값을 읽어올 때 뿐이다. 따라서 user.getGroup().getId()는 lazy-loading을 발생시키지 않는다. 영리하지 않는가?
그렇다면 lazy-loading 구현은 어떻게 할 수 있을까?
Lazy-loading은 투명해야 한다. User 오브젝트 입장에서 Group이 이미 모든 값을 가진, DB정보를 읽어와 초기화된 오브젝트라고 생각하게 해야 한다. 실제 초기화는 Group의 id가 아닌 접근자(getter)를 사용하는 시점이다. 디자인 패턴을 제대로 공부한 사람이라면 Lazy-loading하면 딱 떠오르는 패턴이 있어야 한다. 바로 프록시 패턴이다. 프록시 패턴은 런타임 시점에 다이내믹하게 오브젝트의 접근 방법을 제어(access control)해주는 패턴이다.
프록시 패턴은 구조적으로는 데코레이터랑 비슷하다. 하지만 그 의도(intent)와 사용 목적이 다르다. 사실 객체지향 디자인패턴의 구현 방법은 따져보면 다 비슷하다. 구조는 두가지로 분류할 수 있다. 하나는 블랙박스 패턴, GoF책의 용어로 하자면 Scope가 클래스인 것이고, 다른 하나는 화이트박스 패턴, Scope가 오브젝트인 것이다. 전자는 상속을 후자는 합성(위임, 프로토콜, 인터페이스, 메시지.. 뭐라고 해도 좋다)을 확장을 위해서 사용한다. 결국 두 가지 구현 방법, 구조면 되는 것을 굳이 십여 개의 패턴으로 세분화 한 것은 각각의 사용 목적과 해결하고자 하는 문제가 다르기 때문이다. 얘기가 샜다. –_-;
아무튼 프록시 패턴은 오브젝트 사이의 접근 방식을 다이내믹하게 제어해주는 것이 목적이다. A->B 사용 의존관계가 있을 때 A입장에서 B처럼 보이는 B2를 낑겨 넣는다. A->B2->B 그리고 B2가 B인 것처럼 A한테 행세 하면서 A의 B에 대한 접근을 제어한다. 접근 제어 방법은 다양한 데 그 중에서 가장 대표적이라고 볼 수 있는 것이 바로 lazy-loading이다. 프록시 패턴 = lazy loading이라고 생각하는 사람이 있을만큼 대표적이다. 그래서 프록시 패턴을 한마디로 ‘필요할 때 만든다’라고 설명하기도 한다. 즉, 초기에는 A->B2 형태로 존재하다가 A가 B의 기능을 사용하려고 하는 시점이 되면 B2가 B를 슬쩍 만들고는 모든 A의 요청을 B에게 위임해서 처리해서 마치 A가 B를 바로 사용하는 것처럼 사기치는 것이다. A->(사기)->B2->(떠넘기기)->B 구조라고 볼 수 있다.
여기서 관건은 실제 B가 아닌 B2가 어떻게 A한테 B인것처럼 행세하느냐 이다. 디자인 패턴의 정답은 두 가지 뿐이니 둘 중의 하나이다. 상속이거나 오브젝트 합성이다. 기본적으로 후자인 오브젝트 합성을 사용하는 것이 일반적이다. 즉, B2가 구현한 인터페이스를 두고, 이를 A가 사용하게 한다. 물론 인터페이스라는 것은 자바의 interface 키워드를 말하는게 아니다. 이걸 잘못 이해해서 인터페이스 얘기가 나오면 자바 언어의 단점을 들먹이며 그 때문에 디자인 패턴이 필요하다고 말하는 인간들도 있는데, 이런 사람들은 인터페이스라는 말이 무슨 뜻인지 이해를 못해서 그런 거다. 인터페이스란 A가 B를 어떻게 사용할지 미리 정해둔 약속이자, B를 바라보는 A의 창이다. 이런 정해진 접근방법이 있다면 interface 키워드로 정의된 인터페이스를 사용하든, 추상 슈퍼 클래스를 사용하든, 단지 미리 정해둔 다이나믹한 프로토콜을 사용하든, 전문으로 된 테스트 메시지를 사용하든 모두 인터페이스다. GoF가 말한 programming in interface라는 것은 그런 뜻이다. 그것이 지켜지면 B2는 A에게 B처럼 행세할 수 있다.
결국 어떻게든 A->B2 관계를 다이내믹하게 만들 수 있다면 된다. 다이내믹하다는 것은 A가 직접 B나 B2를 만들지 않는다는 뜻이다. A가 사용할 B, B2를 정해주는 것은 제3의 오브젝트 책임이고, 이렇게 해서 B대신 B2를 A가 사용하게 만드는 방식을 DI라고 하는 거다. DI는 이런 기법을 말하는 것이다.
얘기가 또 샜는데. 다시 돌아와서 정리해보자. A->B는 여기서 User->Group이다. 프록시 패턴을 사용해서 lazy-loading을 적용할 것이니 Group2가 필요하다. Group2는 Group가 인터페이스를 공유해서 A(User)가 입장에서 B(Group) 처럼 보여야 한다. 그렇다면 Group에 대한 인터페이스(여기선 자바의 interface)를 만들어야 하는가? 도메인 오브젝트인 Group에 굳이 그럴 이유는 없다. 다시 말하지만 인터페이스는 A가 B에 접근할 때 사용하는 정해진 약속일 뿐이다. User는 Group의 필드 정보에 접근하면 된다. 즉, Group의 수정자, 접근자에 접근하면 된다. 결국 Group이 자바 빈 규약을 따라서 만들어진 오브젝트라면 Group의 모든 프로퍼티 메소드(getter/setter)가 User가 알아야 할 Group의 인터페이스이다. 그래서 이 때는 lazy-loading을 위한 B2를 만들 때 인터페이스-구현 클래스 방식보다는 슈퍼 클래스-서브 클래스 방식이 낫다. Group의 인터페이스인 모든 public 메소드를 동일하게 가지고 있는 서브 클래스를 만들면 된다. 여기서 혼동하지 말아야 할 것은, 비록 상속을 사용했지만 이건 상속에 의한 확장(클래스 스코프, 블랙박스)이 아니다. 클래스 상속을 사용하지만 이 것은 인터페이스를 이용한 오브젝트 합성의 한 방법일 뿐이고, 여전히 programming to interface를 지키는 것이다.
그래서, 이런 lazy-loading에는 클래스를 이용한 프록시 오브젝트를 만드는 기법을 사용하는 것이 편리하다. 물론 직접 Group을 상속한 프록시를 만드는 것은 번거롭다. 자동으로 Group의 모든 기능을 가진 위임 프록시를 만들고, 그 기능을 중앙에서 제어하게 하는 방식을 쓰는 것이 자연스럽다. 나중에 User-Group이 아니라, Article->User나 Group->Category와 같은 의존 관계에도 동일한 lazy-loading을 적용하려면 다이내믹하게 프록시를 만들고 기능을 확장할 수 있도록 재활용 가능한 프록시를 만드는 것이 좋다.
이 때 주의 할 점은 프록시인 Group2의 메소드를 사용한다고 바로 Group을 DB에서 가져오면 안된다는 것이다. 앞에서 설명한 것처럼 원래 User테이블에서 읽어올 수 있는 FK인 group_id는 lazy-loading용 프록시인 Group2에 미리 넣어주고, Group2에게 getId()를 요청하면 그것만 돌려주면 그만이다. 이를 통해서 불필요한 DB접근을 최소화 할 수 있다. 사실 FK 값만 넘겨서 새로운 관계를 만드는 작업은 RDB에서 자주 일어난다. 따라서 id는 프록시가 제공하는 것을 원칙으로 하고, id외의 다른 필드, 즉 User 테이블에서 가져올 수 없는 정보에 접근하려고 할 때만 DB를 읽어서 Group을 가져와 User->Group2(프록시)->Group 구조로 만들어주면 된다.
구현 방법은 클래스 프록시를 쓴다고 했는데, 자바 API에는 인터페이스를 이용한 다이내믹 프록시만 제공된다. 그에 대한 클래스 버전의 프록시를 만드는 것은 CGLib을 사용해야 한다. CGLib은 사용방법이 다양하지만, 단순한 다이내믹 프록시 구현이라면 JDK의 다이나믹 프록시(Proxy 클래스)를 사용하는 것과 거의 비슷하다.
CGLib 써서 구현이 어려울까? 아니다. 쉽다. 살짝 제약이 있긴 하지만 어떤 엔티티에도 적용 가능한 lazy-loading 프록시 팩토리를 구현했더니 20줄 정도면 충분했다. 괄호가 한 줄씩 먹는 걸 빼면 10여줄이면 충분하다. TDD로 만든답시고 테스트 꼬박 다 만들고, DI적용하려고 인터페이스 따로 정의하고, 빈 설정 등록하고, 각종 예외상황에 대한 테스트까지 다 만들어서 돌려봐도 30분 정도면 충분했다.
사용방법은 이렇다. Lazy-loading 프록시 오브젝트 다이내믹하게 만들어진다. JDBC를 사용하는 UserDao에서 User를 읽어올 때 lazy-loading을 적용하기로 했으면 group 필드에 대해서는 다음과 같은 코드로 프록시를 만들어 넣으면 된다.
user.setGroup(UserDaoJdbc.this.entityProxyFactory.createProxy(Group.class, groupDao, rs.getInt("groupid")));
여기서 entityProxyFactory는 UserDao에서 DI 받은 EntityProxyFactory 타입 빈 오브젝트다. 실제 사용한 빈 클래스는 EntityProxyFactory를 구현한 CglibEntityProxyFactory이다. 프록시를 만들 타입과 lazy-loading에 사용할 DAO 그리고 id(FK) 값만 넣어주면 끝.
이후로 user.getGroup()이나 user.getGroup().getId() 또는 ${user.group.id}을 사용하면 프록시에서 id 값을 가져오거나 프록시 자체를 리턴한다. 하지만 ${user.group.name}과 갈이 원래 User테이블에서는 가져올 수 없는 정보에 접근하면 DAO의 get(id)를 실행해서 Group 오브젝트를 읽어와 이를 사용하게 해준다. 물론 한번 읽어오면 그 뒤로는 DB에 다시 접근할 것 없이 읽어둔 Group 정보를 계속 활용한다.
그렇다면 항상 이렇게 lazy-loading으로 접근해야 할까? 물론 그건 아니다. 만약 해당 DAO메소드에서 User를 가져가서 사용하는데, 대부분 Group 정보를 같이 사용한다면 이 때는 아예 JOIN을 해서 User-Group을 한번에 읽어와서 User-Group 오브젝트로 만들어주는 것이 낫다. 또는 Group이 자주 변경되지 않는, 조회만 자주 되는 정보라면 메모리 캐시에 두어도 좋다. 프록시 생성기를 조금 지능적으로 만들면 캐시에서 가져올 수 있는 정보라면 lazy-loading 프록시 대신 캐시 정보를 읽어서 넣어주게 만들 수도 있다.
아무튼 이렇게 해서 JDBC를 사용하는 경우에도 어렵지 않게 도메인 오브젝트 구조를 유지하고, lazy-loading을 적용할 수 있다는 것을 Springusergroup 예제에서 보여주고 있다. 물론 아주 간단한 구현이다. 단지 아이디어를 제공해주는 것이 목적이니 이를 적절히 확장하고 실전에서 활용하는 것은 독자의 몫이다.
상세한 구현 내용과 테스트 코드, 적용 코드는 토비의 스프링 3의 부록 CD에서 찾을 수 있다. 이제 일주일쯤 지나면 나오겠구나. 인쇄소 휴가기간도 걸리고 해서 책이 나오는 데 시간이 조금 걸린다고 한다.
책에 미처 넣지 못한 예제 설명 중에서 두 가지는 대충 끝냈고, 이제 한 가지만 남았다. 휴~
토비의 스프링 3에 포함된 예제 애플리케이션인 springusergroup에 대한, 책에는 없는 이야기 두 번째.
Springusergroup은 의도적으로 JPA와 SpringJDBC를 함께 사용하도록 만들었다. User와 Group이라는 many-to-one 관계에 있는 엔티티에 대해서 UserDao는 SpringJDBC를 사용해서 만들고 GroupDao는 JPA를 사용하도록 만들었다. 스프링에서 엔티티별로 다른 DA기술을 적용하는 것은 간단한 일이다. 트랜잭션 통합 기능을 활용하면 JDBC-iBatis-하이버네이트/JPA/JDO로 만든 DAO를 하나의 트랜잭션으로 묶어서 스프링의 선언적 트랜잭션의 효과를 그대로 유지할 수 있다. 뭐, JTA를 이용해도 되고.
문제는 JDBC와 JPA로 만든 DAO의 작업을 하나의 트랜잭션으로 묶어주는 것이 아니다. 그보다는 DAO패턴의 의도대로 DA(data access)기술에 투명한 방식으로 DAO를 사용할 수 있도록 만들어주는 것이다. DAO의 내부 구현 기술이나 방식에 상관없이 동일한 DAO 인터페이스를 통해서 가져온 오브젝트(도메인 오브젝트/엔티티)는 동일한 방식으로 사용할 수 있어야 한다. 그래야지만 DAO 내부 구현과 DAO를 사용하는 서비스 계층의 코드의 결합도를 낮출 수 있고, 계층형 아키텍처를 도입한 효과를 얻을 수 있다.
단지 기능의 종류에 따라서 빈을 분리하는 것으로 계층형 아키텍처를 적용했다고 할 수 없다. 계층 사이에 인터페이스를 두었다고 해도 마찬가지다. 제대로 된 계층 구조라면 계층 사이의 인터페이스의 규약이 동일하다면 구현 기술/방법을 변경해도 이를 사용하는 클라이언트에 영향을 주지 않아야 한다. 하지만 실제로는 그렇지 않은 경우가 많다.
예를 들어보자.
User와 Group은 n:1 관계에 있다고 하자. 즉 User는 하나의 Group에 소속된다. DB에서는 FK/PK를 써서 User테이블에 group_id 같은 필드를 두어서 간접적인 관계를 만들어둔다. 반면에 직접 레퍼런스를 가질 수 있는 자바에서는 User에 직접 Group 오브젝트를 참조할 수 있도록 다음과 같이 만드는 것이 좋다.
class User {
Group group;
…
문제는 이렇게 도메인 모델을 반영한 오브젝트 구조를 만들었을 때 DAO의 동작 방식이다. UserDao에 다음과 같은 메소드가 있다고 해보자.
public User getUser(int id);
id를 가지고 User 오브젝트를 가져오는 DAO 메소드다. 그런데 여기서 가져온 User 오브젝트의 group 프로퍼티에는 과연 Group 오브젝트가 들어있을까, 없을까? Group 오브젝트를 항상 넣어줘야 할까, 아닐까?
전통적인 DB 개발스타일을 DAO에 적용하는 개발자라면 사실 DAO 메소드에 따라서 어떤 정보를 가져오는지를 명확하게 만들려고 할 것이다. 예를 들어 getUser()는 User만 달랑 가져온다거나, getUserWithGroup()은 Group까지 가져온다거나 하는 식이다. User의 기본 정보만 있으면 충분한 로직에서 Group까지 가져오는 것은 낭비기 때문이다. 생성되는 SQL 입장에서 보더라도 전자는 User테이블 정보만 읽어오고, 후자는 User와 Group테이블을 조인해서 읽어오도록 만들게 된다.
문제는 이렇게 DAO를 사용하는 코드에서 각 DAO 메소드가 돌려주는 데이터의 범위를 정확히 알고 있어야 한다는 것은 DAO와 이를 사용하는 코드 사이의 결합을 강하게 만든다는 것이다. 또, 가져오는 정보의 범위에 따라서 비슷한 SQL을 가진 메소드가 중복되서 만들어진다. 결국 전통적인 트랜잭션 스크립트 스타일의, 업무로직 하나에만 종속되는 코드 그룹을 만들기 쉽상이고, 그에 따라서 재사용성은 떨어지고 중복은 늘어난다. 그럴 바엔 아예 DAO를 따로 두지 말고 DA코드와 로직을 한데 섞는 것이 나을지도 모르겠다.
DAO의 에소드가 어떤 정보를 돌려주는지, 그 범위는 어디까지인지를 아는 것은 비즈니스 로직을 담고 있는 서비스 코드만의 문제는 아니다. 나중에 이 정보를 렌더링할 뷰도 이를 알고 있어야 한다. 따라서 웹 프레젠테이션 계층 – 서비스 계층 – DAO 계층이 강하게 결합되고 DAO의 구현에 대해서 구체적인 정보를 알고 있어야 하는 구조의 코드가 만들어지기 쉽상이다. 흔히 보게 되는 화면의 기능 하나당 계층별로 1:1:1의 코드가 만들어지게 될 수도 있다.
결국 도메인 오브젝트 그래프 안에 정보를 담는 것에 대한 회의가 들게 되고, 어짜피 DAO가 어떤 정보를 돌려줄지를 이를 사용하는 나머지 계층이 알고 있어야 한다면 차라리 맵에 담든지 그때마다 DTO를 하나씩 따로 만들어서 사용하는게 낫다고 생각할지 모르겠다. 굳이 User-Group 도메인 오브젝트 구조를 사용할 필요가 없게된다는 말이다. 그 결과 비즈니스 로직이 도메인 오브젝트에 들어갈 기회는 차단될 것이고, 로직은 SQL이나 거대한 서비스 계층의 코드에 분산되고 중복되서 나타나게 될 것이다.
뭐, 이런 개발 방법은 잘못됐고 나쁘다고 말하고 싶은 것은 아니다. 이런 아키텍처와 스타일을 선택하고 그 장점(주로 초기에 개발자별로 독립적으로 빠르게 개발이 가능하는 것 등)을 살려서 만들 수도 있다. 간단한 앱이거나 단순한 조회가 전부이거나 대부분 리포트 쿼리로 되어있는 경우라면 나쁘지 않다.
반면에 도메인 모델을 반영한 오브젝트 그래프를 계층 사이의 데이터 전송 매체로 삼고 주요 도메인 로직을 도메인 오브젝트 안에 두고 싶거나, 아예 도메인 계층을 따로 두는 아키텍처를 지향한다면 DAO가 돌려주는 데이터의 범위에 강하게 결합되는 코드를 만드는 것은 피해야 한다. 그래서 JPA나 하이버네이트와 같은 ORM이 이런 경우에 더 적합하다. JPA나 하이버네이트는 도메인 오브젝트 그래프를 유지한 채로 정보를 읽어올 뿐만 아니라, 지능적인 방식으로 사용할 데이터의 범위를 다이내믹하게 조정할 수 있게 해준다.
예를 들어 getUser()에서는 User오브젝트만 읽어오지만, 이를 사용하는 로직 어디에선가 Group의 이름을 알고 싶다면 user.getGroup().getName()으로 오브젝트를 따라서 네비게이션해서 정보를 읽어올 수 있게 만들어준다. 중요한 것은 이 때 Group 정보를 어떻게 가져오는지는 이를 사용하는 코드 내에서는 신경쓰지 않아도 된다는 것이다. DAO 내부에서 미리 User와 Group을 함께 조인해서 읽어왔을 수도 있고, 아니면 lazy-loading을 이용해서 일단 User만 읽어두고 Group 내부의 정보를 사용하려고 시도할 때 즉석에서 Group을 읽어오게 할 수도 있다. 또는 Group을 2nd-level 캐시 같은 캐시에 저장해 두었다가 필요할 때 캐시에서 꺼내오게 할 수도 있다. 핵심은 Group 정보를 어떻게 가져오는지는 DAO 내부의 구현이고 이는 DAO를 사용하는 코드에서는 전혀 신경 쓰지 않아도 되며 나중에라도 언제든지 변경할 수 있고 그 변경이 DAO를 사용하는 코드에는 영향을 주지 않는다는 것이다. 이게 ORM 기술의 매력이다.
많은 사람들이 ORM은 최적화된 SQL을 각각 따로 만들어서 사용하는 방식에 비해서 성능이 떨어진다고 생각한다. 그 이유 중의 하나는 오브젝트 단위로 정보를 읽어오고, 이를 위해서 최적화되지 않은 쿼리가 자주 만들어진다는 것이다. 흔히 말하는 n+1 문제이다. 하지만 ORM은 일단 도메인 오브젝트를 이용해서 애플리케이션을 모두 개발한 뒤에, 코드에는 영향을 주지 않은 채로 ORM이 생성하는 SQL을 효과적으로 튜닝할 수 있게 해준다. SQL을 직접 작성하는 경우에는 두 개의 테이블의 정보를 따로 읽어오는 것을 한방에 읽도록 만들려면 DAO 코드를 수정해야 한다. 또, 이를 사용하는 코드도 수정해야 한다. User만 읽었다가, User 내의 groupid 값을 가지고 다시 Group을 읽었던 것을 User와 Group을 한방에 읽어주는 메소드를 사용하도록 수정하는 등의 작업 말이다. 하지만 지능적인 ORM에서는 그런 수고가 필요없다.
그래서 JPA와 같은 ORM은 DAO계층이 제대로 분리된 독립적인 계층으로 존재할 수 있도록 만들어준다. 구현 기술은 물론이고 가져오는 데이터의 범위도 신경쓰지 않아도 된다. User를 가져와서 User 테이블 내의 정보만 사용하든지 Group 내의 정보까지 사용하든지 상관없다. 마치 DB가 아니라 메모리 내에 존재하는 거대한 오브젝트 정보를 사용하듯이 사용하면 된다. 개발은 편해지고, 중복은 제거되고, 분석과 설계 모델과 일치하고 연동하는 코드를 유지할 수 있으며, 테스트 편의성이 증대된다. 결국 개발생산성은 올라가고, 적절한 튜닝을 적용하면 SQL을 직접 짜는 것과 성능면에서 거의 차이 없는 코드를 만들 수 있다. 리포트성 쿼리도 얼마든지 QL을 이용하거나 네이티브 쿼리를 사용해서 혼합해서 사용하면 된다.
이 때 백그라운드에서 중요한 역할을 해주는 기술이 바로 lazy-loading이다. DAO 입장에서는 User를 읽을 때 Group까지 필요한지 아닌지 모른다. 물론 애플리케이션을 개발완료하고 사용 패턴을 분석해보니 90% 이상이 User를 쓸 때는 Group도 쓴다고 하면 최적화를 해서 항상 조인을 이용해서 두 엔티티를 eager fetching 해오는 것이 좋다. 반면에 개발 중에는 일단 각각 독립적으로 가져오는 구조로 만들어두는 것이 낫다. 즉, getUser()를 실행하면 일단 User만 가져오고 Group은 DB에서 읽지 않는다. 하지만 이 User 오브젝트를 가져간 코드에서 Group 내부의 정보까지 접근하면 그때 Group을 DB에서 읽는 기능이 백그라운드에서 동작하고 그 결과를 즉석에서 넣어주면 된다. 만약 끝까지 Group의 정보에는 접근하지 않는다면 그대로 두어도 좋다. 중요한 것은 이 과정이 투명해야 한다는 것이다. 즉 서비스 계층 코드에서 user.getGroup().getName() 하거나, 뷰에서 ${user.group.name}과 같이 해서 오브젝트 관계를 따라서 접근하려고 하면 자동으로 lazy-loading이 일어나야지, 명시적으로 lazyLoad(user.group); 과 같은 코드를 사용하도록 해서는 안된다는 것이다. JPA나 하이버네이트라면 이런 건 신경쓰지 않아도 알아서 되니 편하다.
문제는 SQL을 명시적으로 사용해서 DAO를 작성하는 JDBC나 iBatis 방식의 DAO이다. JDBC를 그대로 쓰는 DAO를 만들 땐 어쩔 수 없이 강한 결합을 가지고, 구현 내용을 외부에서 알고 있어야 하는 DAO를 만들 수 밖에 없을까?
물론 그건 아니다. 이 때도 원한다면 lazy-loading 사용하게 할 수 있다.
사실 토비의 스프링 3에서는 이런 얘기를 하면서 ‘도메인 오브젝트 중심의 코드를 만들려면 가능한 JPA/하이버네이트를 사용하면 좋겠지만, 굳이 SpringJDBC를 사용해야 한다면 lazy-loading을 직접 구현하셈’이라고만 하고 넘어가버렸다. 나중에 생각해보니 조금 무책임한 설명인 듯 해서, springusergroup예제의 스프링JDBC를 이용한 DAO에서 lazy-loading을 적용한 예를 직접 만들어서 넣었다. Lazy-loading 구현은 사실 어렵지 않다. 나는 TDD로 해서 한 30분 만에 만들었다.
그 덕분에 JPA와 JDBC를 사용한 두 개의 DAO를 함께 사용하지만 이를 사용하는 코드에서는 어떤 기술을 사용했는지에 대해서 신경쓰지 않고 도메인 오브젝트의 구조를 따라서 코드를 작성할 수 있게 만들 수 있었다. 어떤 식으로 만들었고, 어떻게 사용하는 지에 대해서는 다음 번으로 미뤄야 겠다. 오늘은 얘기가 너무 길었네.
토비의 스프링 3의 부록 CD에는 책의 1부와 2부에 나온 예제코드와 함께 springusergroup이라는 웹 애플리케이션 샘플이 들어있다. 이 예제는 2부에서 설명한 주요 스프링 기술, 특별히 @MVC를 적용해서 만든 웹 앱 예제이다. 스프링 처럼 다양한 기술 옵션을 가진 프레임워크를 단일 예제를 만들어가면서 설명하는 방법은 적절치 않다고 생각했기 때문에 책에서는 각 기술 옵션을 적용한 코드가 담겨있는 학습 테스트 코드가 주로 등장한다. 각 기술의 사용방법을 꼼꼼히 설명하기에는 이 방법이 적절하다. 그렇다고 동작하는 하나의 완성된 예제 애플리케이션이 없다는 것은 아쉬운 일이다.
그래서 springusergroup이라는 간단한 웹 애플리케이션 예제를 만들어 넣어 두었다. 애플리케이션 하나에서 선택할 수 있는 기술 옵션에는 한계가 있으니 모든 기술의 사용방법을 보일 수는 없겠지만, 그래도 가능한 다양한 방식의 접근방법을 보일 수 있도록 만들었다. 또, 책에선 살짝 언급만 하고 넘어갔더 몇 가지 기술과 접근방법에 대한 실제 구현 코드를 넣어두기도 했다. 예를 들면 스프링 JDBC를 사용해서 만든 DAO에 하이버네이트/JPA와 같은 lazy-loading을 적용해두었다. 이를 이용해서 Many-to-One관계의 엔티티를 일단 Many만 먼저 읽어두고 다른 계층에서 필요에 따라 One쪽에 접근하면 다이내믹하게 DB를 읽어서 실제 오브젝트를 가져오는 JDBC 기반 DAO 코드를 만들 수 있다.
이렇게 예제에 적용한 기술 중에서 미처 책에서는 설명하지 못했던 것들을 몇 번에 나눠서 설명할 생각이다.
오늘은 그 첫 번째인 테스트용 필드 주입 유틸이다.
컨테이너나 프레임워크에 의한 필드 주입은 Java5에 애노테이션이 등장하고, JEE5에 @Resource 등의 필드 레벨 애노테이션을 활용한 기술이 소개되면서 자연스럽게 보편화되기 시작한 기법이다. 스프링도 JEE5/EJB3에 적용된 애노테이션을 이용한 주입/설정 기술을 스프링 2.5에서 차용하고 발전시키는 과정에서 자연스럽게 필드 주입을 사용하기 시작했다. 필드 주입은 애플리케이션 코드에서는 사용되지 않지만 컨테이너의 DI작업에 필요하기 때문에 추가했던, 그래서 클래스를 지저분하게 만드는 수정자 메소드를 제거할 수 있게 해준다. 필드 주입을 사용하는 것이 반드시 수정자 메소드를 삭제하도록 강제하는 것은 아니다. 그래도 굳이 애플리케이션 코드에선 필요하지 않은 단순 주입용 수정자 메소드를 그대로 둘 개발자는 거의 없을 것이다. 설정 코드와 메소드가 존재하지 않는 private으로 정의된 필드가 처음엔 어색하게 보일 수는 있겠지만, 어짜피 초기화 코드 없이도 컨테이너가 알아서 오브젝트 초기화 작업을 수행해준다는 IoC개념으로 보자면 자연스러운 코드일 수도 있다. 게다가 @Autowired 같은 DI 애노테이션은 기본적으로 설정 필수 옵션이 적용되어 있어서, DI를 통해서 주입이 일어나지 않으면 초기화 에러가 발생한다. 애노테이션을 단순한 메타 정보가 아니라 하나의 기능을 가진 코드로 보자면 필드 주입을 통한 DI는 자연스럽다.
문제는 테스트다. 수정자 메소드를 개발자들이 번거롭게 생각하는 이유는 애플리케이션 코드에서는 직접 사용할 일이 없기 때문이다. 하지만 테스트를 생각하면 얘기가 달라진다. 자바는 언어가 후져서 DI를 하려면 프레임워크가 필요하다고 주장하는 일부 다이내믹 언어 개발자들의 궤변과는 달리 DI는 자바 언어의 객체지향적 특징 때문에 자연스럽게 만들어지는, 자바 언어만 있어도 간단히 적용할 수 있는 프로그래밍 모델일 뿐이다. DI 프레임워크는 보다 복잡한 엔터프라이즈 앱에서 편리하게 DI를 적용하기 위해서 선택적으로 필요한 도구다. 이를 증명할 수 있는 가장 좋은 방법은 스프링으로 만들어진 코드에 대한 단위 테스트를 살펴보는 것이다. 스프링 스타일의 코드는 단위 테스트를 작성하기에 편리하다. 스프링 없이도 자바 언어 만으로 DI를 이용해서 간단히 고립된 테스트를 만들 수 있기 때문이다.
스프링을 적용했을 때 애플리케이션 코드에서는 사용되지 않는 것처럼 보이는 수정자 메소드나 주입용 생성자 등이 꼭 필요한 이유는 바로 테스트 때문이다. 테스트를 애플리케이션 개발의 필수 프로세스로 생각한다면, 더 나아가서 테스트 코드를 애플리케이션의 빠질 수 없는 한 부분으로 생각한다면 수정자 등이 애플리케이션에서 사용되지 않는 다는 말은 틀렸다. 굳이 프로퍼티별로 개별 수정자를 만들지 않고 한 두 개의 초기화 메소드를 사용해도 좋다. 어쨌든 테스트 코드에서 컨테이너의 도움 없이도 직접 오브젝트의 의존 관계를 지정할 수 있는 DI 작업이 필요하고, 이를 위해서는 public 접근자를 가진 주입용 메소드가 필요하다.
여기서 필드 주입의 부작용이 등장한다. 필드 주입을 사용하면 이해하기 쉽고 간결하면서 컨테이너가 IoC/DI를 수행하기 위해서는 아무런 문제가 없는 코드를 만들 수 있다. 반면에 단위 테스트에서 (테스트용 컨텍스트를 만드는 스프링 테스트라면 상관없지만) 직접 DI를 하려면 문제가 된다. 그래서 필드 주입을 적용하고 수정자 메소드 생략까지 해버리면 단위 테스트를 할 때 난처해진다. 물론 테스트를 안만들거나 만들어도 통합 테스트만 한다면야 상관없겠지만.
필드 주입을 적용하면서 단위 테스트를 간단히 만들 수 있는 방법을 생각해보자.
첫 번째 방법은 수정자, 생성자 내지는 초기화 용 주입 메소드를 만드는 방법이다. @Autowired 등은 필드에 적용하지만 수정자를 따로 만들어 두는 것은 아무 문제가 없다. 기존 수정자/생성자 주입만을 지원하던 시절과 비슷하게 테스트 코드를 작성할 수 있다. 이 방법을 사용할 때는 주입을 위한 수정자를 만들지 않아도 되므로 수정자보다는 관련된 몇 개의 필드를 한번에 주입하게 해주는 초기화 메소드가 낫다. 수정자 메소드가 길게 늘어지자 않아서 코드를 보기도 좋고, 테스트 코드도 작성하기 편하다. 그렇다고 생성자 주입처럼 매번 한방 주입을 해야하는 부담도 없으므로 테스트 코드도 유연하게 만들 수 있다.
이 방법은 로드 존슨이 권장하는 방법이다. 2007년 샌프란시스코 QCon에서 로드 존슨의 스프링 2.5 소개 세션에 참석한 적이 있는데, 그때 로드 존슨이 필드 주입을 소개하면서 당부했던 것이 단위 테스트를 만들 생각이라면 수정자를 잊지 말라는 것이었다. 그렇다면 단위 테스트를 만들지 않아도 되는 것도 있을까? 물론 있다. DAO라면 그렇다. DAO는 단위 테스트 대상으로 적절하지 않고, DAO 단위 테스트는 대부분 가치가 없다. 따라서 DAO는 대부분 DB(Fake DB든 뭐든)가 연동되는 통합 테스트를 작성하는 것이 원칙이다. 따라서 DAO에 적용되는 DI는 @Resource DataSource dataSource; 처럼 필드 주입만을 사용해도 좋다. 반면에 테스트용 의존 오브젝트(주로 DAO)를 코드 내에서 직접 주입해서 고립된 단위 테스트를 만드는 것이 필요한 서비스 계층 코드라면 수정자를 만들어서 단위 테스트를 쉽게 만들 수 있게 해야 한다는 것이 로드 존슨의 당부.
두 번째 방법은 내가 예전부터 선호하던 방법이다. 필드를 private이 아니라 접근자가 아예 없는 디폴트 접근자(패키지 프로텍티드라고도 하는)로 만드는 것이다. 디폴트 접근자를 가진 필드는 같은 패키지 내의 클래스에서는 접근이 가능하다. 방법은 단위 테스트를 클래스와 같은 패키지에 만드는 것이다. 물론 테스트 클래스는 소스 위치가 아예 다르기 때문에 패키지가 같아도 문제는 없다. 이렇게 해두면 번거로운 방법을 사용하지 않고도 테스트에서 간단히 클래스의 필드에 접근할 수 있기 때문에 단위 테스트를 작성하기 편리하다. 디폴트 접근자에 대해서 알레르기가 있고, 죽어도 private으로 필드를 만들어야겠다고 한다면 사용하기 힘들겠지만, 조금 타협할 수만 있다면 제법 간단하고 유용한 방법이다.
세 번째 방법은 springusergroup 예제에 적용한 방법이다. 바로 필드 주입용 유틸 메소드를 사용하는 것이다. 스프링은 어떻게 수정자 없이 private 필드의 값을 수정할 수 있을까? 뭐, 자바에서 되니까 했을 뿐이다. 리플렉션을 이용하면 private으로 제한되어있는 필드, 메소드에 얼마든지 접근하고 실행할 수 있다. 그렇다면 컨테이너가 썼던 방법을 테스트 코드에서도 이용할 수 있지 않을까? 어짜피 컨테이너에서 일어나는 DI를 테스트 코드에서 테스트 용으로 재구성하는 것이 목적이니 못쓸 것도 없다. 자바의 리플렉션은 대체로 타입에 안전한 편이며 강력한 기능을 가졌고 사용하기도 간편하다. 새로운 코드(클래스)를 다이내믹하게 추가하는 것은 바이트코드 생성 등의 작업이 필요하므로 라이브러리의 도움이 필요하지만, 이미 존재하는 클래스와 코드에 접근하는 것은 한 두 줄의 코드면 충분하다.
아무튼 이를 이용해서 간단히 필드 인젝션을 지원하는 유틸 메소드를 만들어 두면 테스트에서 유용하게 사용할 수 있다. Springusergroup 예제에는 FieldInjectionUtils라는 이름의 클래스에 스태틱 메소드로 필드 주입용 메소드가 만들어져있다.
물론 잘 알려진 테스트 지원 라이브러리에도 리플렉션을 이용해서 필드의 값을 읽고 쓰는 기능을 찾을 수 있다. 하지만 필드 이름을 지정해야 한다는 것이 맘에 안들었다. @Autowired와 가장 유사한 주입을 하려면 타입에 의한 필드 선택과 주입이 가능해야 한다. 매번 이름을 지정한다는 것은 타입 안전성이 떨어지고 오타로 인해 실수하기 쉽다.
예를 들어보자. 다음과 같은 B타입의 오브젝트를 DI하는 클래스가 있다고 하자.
class A {
@Autowired B b;
…
}
이를 FiedInjectionUtils를 이용해서 테스트 코드에서 Test용 B를 주입해주려면 다음과 같이 만들면 된다. 스태틱 메소드는 미리 import되었다고 하자.
A a = new A();
inject(a, B.class, new B());
inject 메소드는 a 오브젝트의 필드 중에서 B 클래스 타입을 찾아서 new B() 오브젝트를 넣어준다.
물론 같은 타입이 두 개 이상일 경우에는 qualifier나 이름이 필요하다. Qualifier는 좀 복잡하고, 일단 이름을 지정하는 두 번째 메소드를 만들었다. A에 B타입의 b1, b2 두 개의 필드가 있다고 하고 그 중에서 b1에 목을 넣어야 한다면 다음과 같이 해주면 된다. mock()는 Mockito의 mock().
A a = new A();
inject(a, B.class, “b1”, mock(B.class));
Springusergroup 예제에는 세 가지 방법을 이용한 테스트가 등장한다. 스프링 컨테이너를 띄우는 통합 테스트와 수정자를 사용하는 단위 테스트, 그리고 필드 주입 유틸을 사용하는 단위 테스트이다.
유틸 메소드를 좀 더 다듬어서 한정자(qualifier)도 사용할 수 있게 해주고 본격적인 필드 주입 애노테이션 인식도 하게 해준다면 매우 매력적인 DI테스트용 코드를 만들 수도 있을 것이다. Inject()에 넣는 클래스 정보도 사실 생략 가능하다. 테스트를 위해서 의도적으로 주입하지 않은 필드에는 자동으로 Mokito 목을 넣어주는 기능도 좋을 것 같다. 목이 안전한 디폴트 리턴 값을 돌려주게 할 수 있다면 DI할 오브젝트가 많은 테스트에서는 매우 유용할 것 같다. 시간 날 때 조금씩 더 다듬어봐야겠다.
필드 주입 유틸의 구현 코드와 적용 예제는 책의 부록 CD에서 찾을 수 있다.
방금 토비의 스프링 3 CD에 들어갈 파일작업을 마무리했다. 이제 정말 끝이다.
책에 매달려 보낸 지난 2년의 시간이 여기서 마감되는구나. 끝이 보이지 않는 캄캄한 동굴을 겨우 빠져나온 느낌이다.
이제 내 손을 떠났으니 자유다.
앞으로는 내가 원하는 다른 기술을 마음 것 공부하고, 비즈니스도 본격적으로 다시 시작하고, 블로그도 쓰며 지낼 수 있겠다. 평화랑 맘 것 놀아줄 수도 있고, 잡초가 무성한 정원을 제대로 손 볼 시간도 있을 것 같다. 무엇보다도 그런 일을 마음의 부담 없이 할 수 있게 됐다는 것이 가장 감격스러울 것 같다.
책에 대해서 이런 저런 할 말이 많기는 하지만 별로 하고 싶지 않다. 앞으로 며칠간 책을 써온 그 간의 이야기나 좀 정리해놓고 다 잊어야 겠다. 얼마전 들은 황석영씨의 이야기가 생각이 난다. 일단 출간된 책은 더 이상 저자의 책이 아니라 독자의 책이라는. 책에 관해선 독자들이 알아서 지지고 볶아 주겠지.
나는 이제 새롭게 내 열정을 부을 다른 일을 찾아서 떠나야지.
그만 안녕.

마우스 제스처는 복잡한 단축키나 여러 번의 마우스 클릭이 필요한 반복적인 작업을 간단한 마우스 움직임 만으로 실행할 수 있게 해주는 기능이다. 상대적으로 마우스를 많이 사용하게 되는 브라우저에서는 필수 기능이다. 내가 가장 많이 사용하는 제스처는 탭 모두 닫기 기능이다. 탭 지원 브라우저를 사용하면 종종 필요 이상의 페이지를 여러 탭에 나눠서 띄우게 되고 그러다 보면 필요한 페이지만 놔두고 다 닫아버려야 할 때가 자주 있다. 그럴 때마다 x버튼을 눌러서 일일이 탭을 닫는 것은 불편한 일이다. Ctrl-W나 Ctrl-F4 같은 키를 누르는 것도 귀찮고, 탭으로 마우스를 끌어올려서 오른쪽 버튼을 누르고 close other tabs 메뉴를 선택하는 것도 번거롭게 느껴지기도 한다. 아마 제스처를 처음 사용하기 시작한 이유가 바로 이런 탭 한방에 닫기를 자주 사용해야 하기 때문이었던 것 같다. 마우스가 어느 위치에 있든 간단한 동작만으로 원하는 기능을 빠르게 실행할 수 있으니 이젠 없으면 불편해서 웹 서핑도 못할 것 같다.
그런데 이클립스를 사용하다보면 비슷한 상황이 종종 발생한다. F3나 Ctrl-클릭, F4, Ctrl-T, Ctrl-Alt-H 등으로 클래스, 인터페이스를 네비게이션 하면서 작업하다보면 소스 탭이 수십개로 늘어나는 것은 금방이다. 역시 그 때마다 탭에 마우스를 대고 Close others 메뉴를 클릭하는 것은 번거롭다. Close others 커맨드에 단축키를 할당하는 방법이 있긴하다. 하지만 대부분의 이클립스 단축키는 손가락이 꼬이는 키 세 개쯤은 결합해서 사용해야 하니 그다지 편하지도 않다.
그래서 이클립스에도 마우스 제스처 기능이 있으면 좋겠다 싶었다. 그래서 혹시나 하고 찾아보니 있다!
이름은 egest. 플러그인 홈피는 https://egest.dev.java.net/
설치를 하고 Mouse Gestures Config 뷰를 열어서 원하는 키와 커맨드를 바인딩 시킬 수 있다. 나는 기본 바인딩 항목에 Close others와 JUnit 테스트 실행 기능 두 가지를 추가해놨다. 마우스 움직임 한번으로 탭을 모두 닫거나 테스트를 실행할 수 있다. 완전 편해.
제스처 기능을 사용 하려면 먼저 툴바의 gesture switch 버튼을 눌러서 recognizer가 동작하게 해줘야 한다.