본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[스프링] 스프링의 기술

기술과 비즈니스 로직을 분리하고 POJO 방식의 애플리케이션 개발을 가능하게 한다는 스프링의 목적을 쉽게 이루려면 스프링과 같은 POJO 프레임워크가 필요합니다. 스프링에는 POJO 프로그래밍을 손쉽게 할 수 있도록 지원하는 세가지 가능 기술을 제공합니다. 앞서 살펴봤던 스프링 삼각형이라는 그림에 나와있듯이, 엔터프라이즈 개발에서 POJO 개발이 가능하려면 삼각형의 각 변을 이루고 있는 기술들이 뒷받침돼야 합니다. 그 세 가지 기술은 바로 IoC/DI, AOP, PSA입니다.


이 세가지 모두 스프링이 있기 이전에도 여러 가지 형태로 시도됐고 발전하고 있던 기술이었습니다. 사실 객체지향의 설계와 개발원리를 잘 적용하다 보면 자연스럽게 만들어지는 것이기도 합니다. 다만 스프링은 그것을 통일성 있게, 더 세련된 방법으로, 자바 엔터프라이즈 개발의 저니 영역에 걸쳐 효과적으로 적용될 수 있도록 프레임워크 형태로 제공하고 있습니다.


어떤 개발자는 스프링을 단지 이런 기술을 제공하는 기술 프레임워크로 이해하기도 합니다. 스프링은 IoC/DI 컨테이너라거나, AOP 툴이라거나, 엔터프라이즈 서비스 추상화를 제공해주는 프레임워크라는 식으로 이해한다는 뜻입니다. 물론 틀린 얘기는 아니지만 그렇게 스프링을 특정 기술을 지원해주는 단순한 프레임워크로 이해하면 스프링의 목적과 가치를 놓치기 쉽습니다.


스프링의 기술들은 스프링 프레임워크가 만들어진 진정한 목표인 POJO 기반의 엔터프라이즈 개발을 편리하게 해주는 도구일 뿐입니다. 또 다른 관점에서 보자면 IoC/DI, AOP, PSA라는 것 자체가 이미 스프링이 중요한 가치를 두는 객체지향의 원리를 충실히 적용해서 나온 결과이기도 합니다. 스프링은 엔터프라이즈 개발에 등장하는 다양한 기술에 대해 이미 잘 만들어진 서비스 추상화 기능을 제공하고 있지만, 그렇다고 스프링이 제공하는 PSA만 달랑 사용하고 말라는 뜻은 아닙니다. 스프링 사용자라면 스프링이 직접 제공하지 앟는 기술에 대해서도 PSA를 적용할 줄 알아야 합니다. 그것이 스프링의 목적과 개발 철학에 부합하는 스프링의 사용법입니다. 그래서 스프링의 기술들은 스프링의 목적과 핵심 가치를 기준으로 살펴보고 이해하는 것이 중요합니다.




제어의 역전(IoC) / 의존관계 주입(DI)


IoC/DI는 스프링의 가장 기본이 되는 기술이자 스프링의 핵심 개발 원칙이기도 합니다. 나머지 두가지 기술인 AOP와 PSA도 IoC/DI에 바탕을 두고 있습니다. 3대 기술은 아니지만 자주 등장하는 템플릿/콜백 패턴이 적용된 부분도 IoC/DI가 그 핵심 원리입니다. IoC/DI의 활용방법을 생각해보겠습니다.


이런 질문을 해볼 수 있습니다.


왜 두 개의 오브젝트를 분리해서 만들고, 인터페이스를 두고 느슨하게 연결한 뒤, 실제 사용할 대상은 DI를 통해 외부에서 지정하는 것일까요?


이렇게 DI 방식으로 하는 것이 그렇지 않은 경우, 즉 직접 자신이 사용할 오브젝트를 new 키워드로 생성해서 사용하는 강한 결합을 쓰는 방법보다 나은 점은 무엇일까요?


가장 간단한 답변은 '유연한 확장이 가능하게 하기 위해서'라고 할 수 있습니다. DI는 개방 폐쇄 원칙(OCP)이라는 객체지향 설계 원칙으로 잘 설명될 수 있습니다. 유연한 확장이라는 장점은 OCP의 '확장에는 열려있다(개방)'에 해당합니다. DI는 역시 OCP의 '변경에는 닫혀 있다(폐쇄)'라는 말로도 설명이 가능합니다. 폐쇄 관점에서 볼 때 장점은 '재사용이 가능하다'라고 볼 수 있습니다.


A→B라는 의존관계를 갖는 오브젝트 구조라고 생각해보면, 여기서 확장은 B가 자유롭게 변경될 수 있음을 의미합니다. 이는 B가 변경돼도 A는 아무런 영향을 받지 않고 그대로 유지 가능하다는 뜻이기도 합니다. B 관점에서는 유연한 확장이고 A 관점으로 보자면 변경 없이 재사용이 가능하다고 볼 수 있는 것입니다. B가 B1, B2, B3로 구현 방법이 바뀌어도 된다고 볼 수도 있고, B1, B2, B3처럼 의존 대상이 바뀌어도 A는 그대로 재사용이 가능하다고 볼 수도 있습니다.




DI의 활용 방법


개념적인 설명 말고 좀 더 구체적으로 DI의 활용 방식을 살펴보면서 그 장점을 생각해 보겠습니다.


1) 핵심기능의 변경

DI의 가장 대표적인 적용 방법은 바로 의존 대상의 구현을 바꾸는 것입니다. 디자인 패턴의 전략 패턴이 대표적인 예입니다. A→B 구조에서 A의 기능 일부를 B에게 위임한다고 했을 때 B의 구현 방식을 필요에 따라 통째로 B1, B2, B3 로 바꾸는 것입니다. 


예를 들어보면 서비스 오브젝트가 사용하는 DAO가 있다고 할 때, DAO의 구현을 JDBC로 했다가, 그것을 JPA, 하이버네이트, JDO, iBatis 등으로 변경하는 것을 생각할 수 있습니다. 구현 방식을 통째로 바꾸는 것입니다. 사용자 관리 서비스라고 보자면 사용자의 등급을 결정하는 정책을 담은 코드를 DI로 분리할 수 있습니다. 만약 비즈니스로직이 변경돼서 새로운 등급결정 정책을 적용해야 한다면, DI를 이용해 새로운 정책을 담은 클래스를 통째로 변경해주면 됩니다.


이렇게 실제 의존하는 대상이 가진 핵심기능을 DI 설정을 통해 변경하는 것이 대표적인 DI의 활용 방법입니다. 하지만 이게 다는 아닙니다.


전략패턴이란 실행시점(at runtime)에 알고리즘을 선택할 수 있는 방법입니다. 


스프링에서의 전략패턴을 잘 설명한 PPT 링크입니다.



2) 핵심기능의 동적인 변경

두번째 활용 방법은 첫 번째랑 비슷하게 의존 오브젝트의 핵심기능 자체를 바꾸는 것입니다. 하지만 일반적인 DI를 이용한 변경 방법과는 달리, 동적으로 매번 다르게 변경할 수 있습니다. DI도 기본적으로는 런타임 시에 동적으로 의존 오브젝트를 연결해주는 것이긴 하지만, 일단 DI되고 나면 그 후로는 바뀌지 않습니다. 즉 동적인 방식으로 연결되지만 한번 DI되면 바뀌지 않는 정적인 관계를 맺어주는 것입니다.


하지만 DI를 잘 활용하면 애플리케이션이 동작하는 중간에 그 의존 대상을 다이내믹하게 변경할 수 있습니다.


예를 들면, 사용자의 등급에 따라서 DataSource를 사용하게 만들 수도 있습니다. DAO는 DataSource에 의존합니다. DAO→DataSource 관계가 만들어집니다. 그런데 이를 DAO 하나가 여러개의 DataSource에 의존하게 만들 수도 있습니다. 그리고 현재 접속한 사용자의 등급에 따라서 그때그때 다른 DataSource를 DAO가 사용하게 할 수도 있습니다. VIP 사용자는 좀 더 속도가 빠른 DB를 이용하게 해서 빠른 처리 속도를 보장해주려고 할 때 적용할 수 있는 기법입니다. 물론 DAO를 따로 만들 필요는 없습니다. 대신 매우 지능적인 방식으로 동작하는 DI 덕분에 선택적으로 사용할 DataSource를 바꿔주는 기법이 가능합니다.


또 다른 예를 생각해보면 사용자별로 모두 독립적인 의존 오브젝트를 두게 만들 수도 있습니다. 한번 로그인한 사용자는 로그아웃하거나 다른 브라우저로 다시 들어오기 전에는 계속 자신만의 오브젝트를 유지하게 하게 서비스 오브젝트가 이를 DI 받아서 사용하게 할 수 있습니다. 이때는 핵심 기능이 바뀐다기보다는 기능은 같지만 독립적인 상태정보를 저장할 수 있는 자신만의 오브젝트를 가질 수 있다는 뜻입니다. 매번 요청이 있을 때마다 새로운 오브젝트가 필요하다면 new를 이용해 새로 만들어도 그만이겠지만, 한번 로그인한 사용자에게는 계속 같은 오브젝트가 적용되려면 DI를 이용하는 이 방식이 가장 편리합니다.


동적인 방식으로 핵심기능을 변경하는 건, 기술적으로 보자면 다이내믹 라우팅 프록시나 프록시 오브젝트 기법을 활용하는 것입니다. 그런 기법을 적용할 수 있었던 이유는 역시 DI가 있기 때문입니다. DI의 원칙은 여전히 지켜지므로 확장과 재사용이라는 장점은 손상되지 않고 오히려 더 가치를 드러냅니다.



3) 부가기능의 추가

DI의 세번째 활용 방법은 핵심기능은 그대로 둔 채로 부가기능을 추가하는 것입니다. 데코레이터 패턴을 생각해보면 됩니다. 인터페이스를 두고 사용하게 하고, 실제 사용할 오브젝트는 외부에서 주입하는 DI를 적용해두면 데코레이터 패턴을 쉽게 적용할 수 있습니다. 그래서 핵심기능과 클라이언트 코드에는 전혀 영향을 주지 않으면서 부가적인 기능을 얼마든지 추가할 수 있습니다.


트랜잭션 기능을 부여했던 것이 그 대표적인 예입니다. 때로는 핵심기능은 그대로 둔 채로 결과나 전달 파라미터를 조작할 수도 있고, 파라미터나 리턴 결과를 활용해 로깅이나 보안 처리 같은 부가적인 작업을 수행할 수도 있습니다. 부가기능이라기보다는 부가 작업이라고 본다면 이벤트 발생 작업의 추가 같은 것을 생각해볼 수 있습니다. 부가 기능의 추가 방식을 특정 오브젝트가 아니라 좀 더 많은 대상으로 일반화해서 적용하면 AOP가 됩니다.


부가기능을 추가할 수 있는 것도 바로 DI 덕분입니다. DI 구조로 만들어놨기 때문에 가능한 것이며 DI의 핵심 원칙인 OCP에도 충실하게 잘 들어맞습니다. OCP가 말하는 확장에 열려있다는 것은 전략 패턴에서처럼 핵심기능을 변경해서 쓰는 수준만을 말하는 게 아님을 기억해야 합니다.



4) 인터페이스의 변경

때로는 사용하려고 하는 오브젝트가 가진 인터페이스가 클라이언트와 호환되지 않는 경우가 있습니다. 또는 여러 종류의 인터페이스를 가졌지만 사실은 비슷한 기능을 담당하는 오브젝트를 바꿔가면서 사용하고 싶은 때도 있습니다. 이렇게 클라이언트가 사용하는 인터페이스와 실제 오브젝트 사이에 인터페이스가 일치하지 않는 경우에도 DI가 유용합니다.


A가 C 오브젝트를 사용하려 한다고 해보겠습니다. 하지만 A는 원래 B 인터페이스를 사용하도록 만들어져 있고 C는 B 인터페이스를 구현하지는 않았습니다. 이때 A가 DI를 통해 B의 구현 오브젝트를 받도록 만들어져 있다면 B 인터페이스를 구현했으면서 내부에서 C를 호출해주는 기능을 가진 어댑터 오브젝트를 만들어 A에 DI 해주면 됩니다. 220V 전기를 원하는 9V, 6V, 12V 식으로 바꿔주는 어댑터처럼, 인터페이스가 다른 오브젝트를 클라이언트가 사용하는 인터페이스로 바꿔주는 기능을 이용하면 되는 것입니다. A→B(C로 위임)→C처럼 구성됩니다. 여전히 A는 DI 덕분에 자신의 코드를 수정하지 않아도 됩니다. 이처럼 인터페이스가 일치하지 않는 호출이 필요한 경우에도 DI는 유용합니다. 디자인 패턴에서 말하는 오브젝트 방식의 어댑터 패턴의 응용이라고 볼 수 있습니다.


이를 좀 더 일반화해서 아예 인터페이스가 다른 다양한 구현을 같은 방식으로 사용하도록, 중간에 인터페이스 어댑터 역할을 해주는 레이어를 하나 추가하는 방법도 있습니다. DI의 응용 방법 중 하나이자 스프링의 대표적인 기술로도 분류되는 일관성 있는 서비스 추상화가 그런 방법입니다. PSA는 클라이언트가 일관성 있게 사용할 수 있는 인터페이스를 정의해주고 DI를 통해 어댑터 역할을 하는 오브젝트를 이용하게 해줍니다. 이를 통해서 다른 인터페이스를 가진 로우레벨의 기술을 변경하거나 확장해가면서 사용할 수 있는 것입니다.



5) 프록시

프록시 패턴의 전형적인 응용 방법도 있습니다. 필요한 시점에서 실제 사용할 오브젝트를 초기화하고 리소스를 준비하게 해주는 지연된 로딩(lazy loading)을 적용하려면 프록시가 필요합니다. 원격 오브젝트를 호출할 때 마치 로컬에 존재하는 오브젝트처럼 사용할 수 있게 해주는 원격 프록시를 적용하려고 할 때도 프록시가 필요합니다. 두 가지 방법 모두 DI를 필요로 합니다. 스프링은 EJB 원격 호출을 포함해서 웹 서비스, REST 호출, HTTP 방식의 호출 등 다양한 리모팅 기술을 지원합니다. 당연히 모두 DI를 통해 이뤄집니다.



6) 템플릿/콜백

템플릿/콜백 패턴은 DI의 특별한 적용 방법입니다. 반복적으로 등장하지만 항상 고정적인 작업 흐름과 그 사이에서 자주 바뀌는 부분을 분리해서 템플릿과 콜백으로 만들고 이를 DI 원리를 응용해 적용하면 지저분하게 매번 만들어야 하는 코드를 간결하게 만들 수 있습니다. 스프링이 제공하는 20여 가지의 템플릿/콜백이 적용된 기능을 가져다 활용하는 것뿐 아니라 필요에 따라서는 DI 원리를 따라 직접 응용할 수 있어야 합니다. 콜백을 템플릿에 주입하는 방식으로 동작하게 하는 것은 DI의 원리에 가장 충실한 응용 방법입니다. 콜백을 얼마든지 만들어서 사용할 수 있다는 건 개방을 통한 유연한 확장성을 보여주는 것이며, 템플릿은 한 번 만들어두면 계속 재사용할 수 있다는 건 기능의 확장에도 변하지 않는다는 OCP의 폐쇄 원칙에 가장 잘 들어맞는 것입니다. 



7) 싱글톤과 오브젝트 스코프

DI가 필요한 중요한 이유 중 한 가지는 DI 할 오브젝트의 생명주기를 제어할 수 있다는 것입니다. DI를 프레임워크로 이용한다는 건 DI 대상 오브젝트를 컨테이너가 관리한다는 의미입니다. 오브젝트의 생성부터 관계설정, 이용, 소멸에 이르기까지의 모든 과정을 DI 컨테이너가 주관하기 때문에 그 오브젝트의 스코프를 자유롭게 제어할 수 있습니다.


가장 기본이 되는 스코프는 역시 싱글톤입니다. 하나 또는 소수의 오브젝트가 수많은 클라이언트를 상대로 고성능 서비스를 제공하는 방식은 엔터프라이즈 개발에서 매우 중요합니다. 상태를 갖지 않도록 만든 오브젝트가 동시에 여러 스레드의 요청을 처리하는 이런 방식을 적용하려면, 만들어지는 오브젝트의 개수를 제어하는 일이 매우 중요합니다. 전통적인 싱글톤 패턴은 오브젝트에 많은 제약을 가해서 만들어지기 때문에 그다지 권장되지 않습니다. 그보다는 컨테이너가 오브젝트를 관리하는 IoC 방식이 유용합니다. 스프링의 DI는 기본적으로 싱글톤으로 오브젝트를 만들어서 사용하게 합니다. 컨테이너가 알아서 싱글톤을 만들고 관리하기 때문에 클래스 자체는 싱글톤을 고려하지 않고 자유롭게 설계해도 된다는 장점이 있습니다.


때론 단일 싱글톤이 아니라 임의의 생명주기를 갖는 오브젝트가 필요할 때도 있습니다. 스프링에서는 싱글톤 외에도 다양한 스코프를 갖는 오브젝트를 만들어 DI에 사용할 수도 있습니다. HTTP 요청당 하나의 오브젝트가 만들어지거나, HTTP 세션당 하나씩 오브젝트가 만들어지게 할 수 있습니다. 개발자 스스로 일정한 스코프를 갖는 오브젝트를 만들고 이를 DI에 적용하는 것도 가능합니다. 


이렇게 오브젝트 스코프를 제어하는 방법 또한 DI를 적용했기 때문에 가능한 활용 방법입니다.



8) 테스트

마지막으로 살펴볼 DI의 중요한 용도는 바로 테스트입니다. 여타 오브젝트와 협력해서 동작하는 오브젝트를 효과적으로 테스트하는 방법은 가능한 한 고립시키는 것입니다. 즉 다른 오브젝트와의 사이에서 일어나는 일을 테스트를 위해 조작할 수 있도록 만듭니다. 그래야만 테스트 대상인 오브젝트의 기능에 충실하게 테스트가 가능합니다. 자칫 다른 오브젝트와의 협력을 통해 동작하는 기능을 다 허용하고 테스트하다가는 한번에 수십 개의 오브젝트와 DB, 환경까지 모두 테스트해야 하는 부담을 안을 수 있습니다. 그래서 테스트할 대상이 의존하는 오브젝트를, 테스트를 목적으로 만들어진 목오브젝트로 대체하면 유용합니다. 복잡한 테스트 데이터가 준비되어 있어야 원하는 결과를 가져올 수 있는 DAO를 사용하는 오브젝트가 있다고 해보겠습니다. DAO를 이용하는 서비스 오브젝트를 테스트하기 위해서 DAO도 완벽하게 작성되어 있음을 먼저 확인해야 하고, DAO가 테스트를 위해 적합한 결과를 돌려주도록, 필요한 테스트 데이터까지 모두 준비해야 한다면 배보다 배꼽이 더 큰일 되기 십상입니다. 정작 원하는 것은 십여 줄밖에 안 되는 조건에 따라 다르게 동작하는 서비스 오브젝트 코드인데, 테스트를 준비하면서 DAO와 테스트 데이터 때문에 너무 많은 시간을 쏟아야 한다면 곤란합니다. 그래서 테스트 만드는게 짐이 되고, 테스트를 사용하기가 점점 꺼려질 것입니다.


그래서 의존 오브젝트를 대신해서 스텁 또는 목 오브젝트 같은 테스트 대역을 활용해야 합니다.  이때도 DI는 중요한 역할을 합니다. DI를 위해 만든 수정자 메소드를 사용하면 테스트 코드 안에서 수동으로 목 오브젝트를 주입할 수 있습니다. 또는 테스트용으로 설정을 별도로 만드는 방법도 있습니다. DI 없이는 이런 테스트 기법을 적용하기란 불가능 합니다. 갈수록 테스트이 중요성이 커져가고 있으니 DI의 활용 방법에서 테스트가 차지하는 비중도 커질 것입니다.



그 외에도 DI의 활용 방법은 다양합니다. 이 정도만 살펴봐도 런타임 시 유연하게 구현을 바꿀 수 있다는 DI라는 개념이 실전에서 얼마나 활용도가 다양한지 알 수 있을 것입니다. 잘 살펴보면 DI의 용도는 디자인 패턴 중에서 오브젝트 합성 방식을 따르는 패턴과 관련이 있음을 알 수 있습니다. GoF의 디자인 패턴 중에서 인터페이스를 두고 오브젝트를 분리하는 구조를 가진 오브젝트 스코프의 패턴은 DI의 구조에 대부분 잘 들어맞습니다. 그런 패턴의 장점들을 애플리케이션 전 영역에서 간단한 설정만으로 자연스럽게 적용할 수 있게 만들어주는 것이 바로 DI입니다.


이런 활용 방법은 한 번에 한가지만 선택적으로 사용해야 하는 건 아닙니다. 여러 가지 활용 방법을 한 번에 적용할 수도 있습니다. 예를 들면 하나의 DI 대상에 대해 핵심기능도 업무 변화에 따라 바꾸면서, 부가기능도 여러 개 추가해넣고, 테스트에서도 활용하는 식으로 사용해도 됩니다.




애스펙트 지향 프로그래밍(AOP)


애스펙스 지향 프로그래밍이라고 많이 알려진 AOP도 스프링의 3대 가능기술의 하나입니다. 계속해서 스프링은 객체지향 기술과 프로그래밍을 위해 존재하는 프레임워크라고 설명했는데, 난데없이 애스펙트 지향 프로그래밍이라는 새로운 프로그래밍 패러다임이 왜 필요할까요? 사실 애스펙트 지향 프로그래밍은 객체지향 프로그래밍(OOP)처럼 독립적인 프로그래밍 패러다임이 아닙니다. AOP와 OOP는 서로 배타적이 아니라는 말입니다. 


객체지향 기술은 매우 성공적인 프로그래밍 방식임에는 분명합니다. 하지만 한편으로는 점점 복잡해져 가는 애플리케이션의 요구조건과 기술적인 난해함을 모두 해결하는데 한계가 있기도 합니다. AOP는 바로 이러한 객체지향 기술의 한계와 단점을 극복하도록 도와주는 보조적인 프로그래밍 기술입니다. AOP를 사용하면 그 결과로 OOP를 더욱 OOP답게 만들 수 있습니다. AOP는 90년대부터 연구됐고, 여러 가지 제품으로도 나왔지만 이를 가장 성공적으로 엔터프라이즈 개발에 보급한 것이 바로 스프링입니다.


스프링의 목적인, POJO만으로 엔터프라이즈 애플리케이션을 개발하면서도 엔터프라이즈 서비스를 선언적으로 제공하는 데 반드시 필요한 것이 바로 이 AOP 기술입니다. IoC/DI를 이용해서 POJO에 선언적인 엔터프라이즈 서비스를 제공할 수 있지만 일부 서비스는 순수한 객체지향 기법만으로는 POJO의 조건을 유지한 채로 적용하기 힘듭니다. 바로 이런 문제를 해결하기 위해 AOP가 필요합니다. 스프링의 AOP는 스프링이 POJO 프로그래밍을 지원하려는 그 핵심 목적을 위해 중요한 역할을 하고 있습니다.



AOP의 적용 기법

AOP를 자바 언어에 적용하는 기법은 크게 두 가지로 분류할 수 있습니다.


1) 첫 번째는 스프링과 같이 다이내믹 프록시를 사용하는 방법입니다.

이 방법은 기존 코드에 영향을 주지 않고 부가기능을 적용하게 해주는 데코레이터 패턴을 응용한 것입니다. 자바의 객체지향 패턴을 활용한 방법이기 때문에 만들기 쉽고 적용하기 간편합니다. 대신 부가기능을 부여할 수 있는 곳은 메소드의 호출이 일어나는 지점뿐이라는 제약이 있습니다. 인터페이스와 DI를 활용하는 데코레이터 패턴이 기반원리이기 때문입니다. 부가기능을 구현한 코드나 기능을 적용할 대상을 찾는 방법 모두 평범한 자바 클래스로 만들면 됩니다. 스프링의 기본적인 AOP 구현 방법은 다이내믹 프록시를 이용하는 프록시 AOP 방식입니다. 엔터프라이즈 개발에서 필요로 하는 AOP는 대부분이 이 프록시 방식의 AOP이면 됩니다.


2) 두 번째는 자바 언어의 한계를 넘어서는 언어의 확장을 이용하는 방법입니다.

AspectJ라는 유명한 오픈소스 AOP 툴이 있습니다. 이 AspectJ는 강력한 고급 기능을 가진 AOP를 제공합니다. AspectJ는 프록시 방식의 AOP에서는 불가능한 다양한 조인포인트를 제공합니다. 메소드 호출뿐 아니라 인스턴스 생성, 필드 액세스, 특정 호출 경로를 가진 메소드 호출 등에도 부가 기능을 제공할 수 있습니다. 이런 고급 AOP 기능을 적용하려면 자바 언어와 JDK의 지원만으로는 불가능합니다. 그 대신 별도의 AOP 컴파일러를 이용한 빌드 과정을 거치거나, 클래스가 메모리로 로딩될 때 그 바이트코드를 조작하는 위빙과 같은 별도의 방법을 이용해야 합니다. 그만큼 사용하기 까다롭고 번잡하지만 경우에 따라서는 프록시 방식의 AOP로는 할 수 없는 작업을 위해 AspectJ를 사용해야 합니다.


스프링은 프록시 방식의 AOP를 기본으로 하고 있지만, 원한다면 AspectJ를 이용한 AOP로 바꿔서 사용할 수 있습니다. 스프링의 특별한 기능 중에는 AspectJ를 꼭 사용해야 하는 것도 있습니다.



AOP의 적용 단계

AOP가 객체지향 개발 방법에서 기본 아이디어를 가져왔다고는 하지만 본격적으로 적용하기에는 그 성격이 자바의 일반적인 개발 방법과는 상당히 다르기 때문에 제대로 적용하려면 충분한 시간과 노력이 필요합니다. 또 AOP의 장점이 많다고 해서 무작정 사용하면 심각한 문제가 발생할 위험이 있습니다. 개발자 개개인이 아무렇게나 AOP를 남발해서 사용하다 보면 다른 개발자가 만든 코드가 예상하지 않은 방식으로 돌아가는 등의 혼란을 초래할 수 있기 때문입니다. AOP는 하나의 모듈이 수많은 오브젝트에 보이지 않게 적용되기 때문에 매우 주의해서 사용해야 합니다.


AOP에 익숙하지 않은 상태라면 차근차근 단계를 밟아 AOP를 도입하는 접근방법이 좋습니다.


AOP 적용 1단계: 미리 준비된 AOP 이용

일단 처음에는 스프링이 미리 만들어서 제공하는 AOP 기능을 그대로 가져다 적용하는 것으로 시작합니다. 스프링이 직접 제공하는 대표적인 AOP는 바로 트랜잭션입니다. DB를 사용하는 애플리케이션이라면 트랜잭션이 필요한 테니 이 트랜잭션 적용을 스프링 AOP 도입의 첫번째 단계로 이용합니다. AOP 설정을 통해서 트랜잭션이 어떻게 많은 오브젝트에 투명하게 적용되는지 관찰해보고, AOP의 특성과 동작원리를 이해해보겠습니다.


스프링에는 트랜잭션만큼 자주 사용되진 않지만 특정 아키텍처를 선택했을 때 사용할 수 있도록 준비된 AOP 기능이 하나 더 있습니다. @Configurable 애노테이션을 이용해서 도메인 오브젝트에 DI를 자동적용해주는 AOP 기능입니다. 도메인 오브젝트를 전용 계층에 두고 접근하는 아키텍처 방식을 따를 때 반드시 필요합니다. 프록시 AOP면 충분한 트랜잭션과 달리, @Configurable을 위해서는 AspectJ를 이용한 AOP가 반드시 필요합니다.


이 두 가지가 스프링이 미리 준비해서 제공하는 대표적인 AOP 기능입니다. 두 가지 모두 간단한 설정을 추가하는 것만으로도 쉽게 적용되기 때문에 AOP에 대한 지식이나 경험이 많지 않더라도 간단히 이용 가능하다는 장점이 있습니다.


AOP 적용 2단계: 전담팀을 통한 정책 AOP 적용

다음 단계는 좀 더 적극적으로 AOP를 적용할 차례입니다. 아직까지는 개발자 개개인이 AOP 기능을 직접 이용하게 해서는 안됩니다. 대신 애플리케이션 전체적으로 이용 가능한 것을 소수의 AOP 담당자 관리하에 적용해볼 수 있습니다. 대표적으로 비즈니스 로직을 가진 오브젝트에 대한 보안, 특정 계층의 오브젝트 이용 전후의 작업 기록을 남기는 로깅, 데이터 추적을 위한 트레이싱, 특정 구간의 실시간 성능 모니터링과 같은 정책적으로 적용할 만한 기능에 AOP를 이용하는 것입니다.


이런 기능을 개발자가 직접 자신이 만드는 코드에 추가하려면, 개발 표준이나 가이드라인이 미리 완벽하게 준비되어 있어서 이를 따라 개발하게 해야합니다. 하지만 개발자가 실수로 빼먹을 수도 있고, 가이드라인을 제대로 따르지 못하고 엉뚱하게 적용할 수도 있습니다. 더 큰 문제는 개발 정책이나 기준이 바뀌면 모든 개발자가 지금까지 작업한 것을 모두 수정하고 일일이 검증해야 하는 큰 부담을 지게 됩니다. 하지만 이런 일을 AOP를 이용해 한 번에 적용한다면 일반 개발자의 작업에는 전혀 영향을 주지 않을 수 있습니다. AOP를 책임지는 소수의 팀만 수고하면 그만입니다. 


또한 AOP는 언제든지 기능을 추가하거나 제거할 수 있습니다. 기존 코드에는 당연히 아무런 영향을 주지 않으면서 말입니다. 이런 특징을 잘 이용하면 운영 중에 필요한 기능 외에 개발 가이드라인이나 표준을 따라서 코드가 작성되어 있는지를 검증하는 작업을 AOP를 이용해 할 수 있습니다. 예를 들면 레이어 간의 호출에 대한 제한이 있다고 생각해보겠습니다. JSP 뷰에서는 DAO나 서비스 계층의 오브젝트를 직접 호출하면 안된다는 정책이 있습니다. 하지만 개발자는 곧잘 이런 기준을 무시하고 제멋대로 짜기 일쑤입니다. 이런 것을 코드 리뷰를 통해 일일이 검증하기는 쉽지 않습니다. 이럴 때 AOP가 유용하게 쓰일 수 있습니다. 모든 DAO의 메소드 호출에 적용되는 AOP 모듈을 하나 만듭니다. 그리고 메소드 호출이 일어났을 때 어드바이스를 통해 호출 경로를 조사할 수 있습니다. 만약 서비스 계층 같은 허용된 계층으로부터의 호출이 아니라면 정책위반 예외를 던지게 할 수 있습니다. 또 서비스 계층에서 던질 수 있는 예외의 종류가 정해져 있다고 해보겠습니다. 그런데 개발자가 임의의 예외를 만들거나 선택해서 마구 사용할 수도 있습니다. 이런 경우라면 서비스 계층의 메소드에 대해 예외가 던져졌을 때만 동작하는 AOP 모듈을 만들 수 있습니다. 그리고 그 안에서 예외의 종류를 검사해서 허용된 게 아니면 역시 정책위반 예외를 만들어서 던지고 관리자에게 통보가 가도록 할 수 있습니다. 이렇게 AOP는 동적으로 동작하면서 개발 정책을 위반한 코드를 잡아내는 데도 유용합니다. 물론 개발이 끝나고 실전에 적용할 때는 정책 검증을 위한 AOP 설정을 간단히 제거해버리면 됩니다.


AOP 적용 3단계: AOP의 자유로운 이용

첫 번째와 두 번째 단계를 거쳐서 AOP에 어느 정도 친숙해지고, 그 장단점과 응용 전략, 윟럼성 등을 어느 정도 이해했다면 이제는 개발자 스스로가 AOP를 활용할 수 있는 단계로 넘어갈 수 있습니다. 이전 단계에서는 애플리케이션 전체적으로 적용되는 정책 AOP를 위주로 했다면 이제는 개발자가 구현하는 기능에 적용하면 유용한 세부적인 AOP를 이용할 수 있습니다. 큰 범위에 걸쳐서 적용되는 기능은 아니지만 한 모듈 또는 특정 기능 안에서도 AOP로 분리하면 유용한 것들이 있습니다. 물론 다른 팀이나 개발자가 만든 코드에 몰래 적용되는 AOP 기능은 만들어선 안됩니다. 그런 위험만 주의한다면 얼마든지 개발자가 자신이 다루는 코드에 AOP를 적극 활용할 수 있습니다.




포터블 서비스 추상화(PSA)


세번째 가능기술은 환경과 세부 기술의 변화에 관계없이 일관된 방식으로 기술에 접근할 수 있게 해주는 PSA(Portable Service Abstraction)입니다. POJO로 개발된 코드는 특정 환경이나 구현 방식에 종속적이지 않아야 합니다. 스프링은 JavaEE를 기본 플랫폼으로 하는 자바 엔터프라이즈 개발에 주로 사용됩니다. 따라서 다양한 JavaEE 기술에 의존적일 수밖에 없습니다. 특정 환경과 기술에 종속적이지 않다는 게 그런 기술을 사용하지 않는다는 뜻은 아닙니다. 다만 POJO 코드가 그런 기술에 직접 노출되어 만들어지지 않는다는 말입니다. 이를 위해 스프링이 제공하는 대표적인 기술이 바로 일관성 있는 서비스 추상화 기술입니다.


스프링은 엔터프라이즈 개발에 사용되는 다양한 기술에 대한 서비스 추상화 기능을 제공합니다. 어떤 것은 AOP나 템플릿/콜백 패턴과 결합돼서 사용되기 때문에 직접적으로 서비스를 이용할 필요가 없습니다. 대신 설정을 통해 어떤 종류의 기술을 사용할지 지정해줘야 합니다.


트랜잭션 서비스 추상화는 코드를 이용해 트랜잭션을 제어하지 않는다면 직접 이용할 이유가 없습니다. 트랜잭션은 대부분 AOP를 이용해 적용하기 때문에 직접 코드를 만들지 않기 때문입니다. 대신 설정에서는 스프링의 트랜잭션 추상화 인터페이스인 PlatformTransactionManager를 구현한 구체적인 서비스 클래스를 빈으로 등록해줘야 합니다. JTA를 이용해 트랜잭션을 적용하고 싶다면 JtaTransactionManager를 빈으로 등록하고 JTA 환경에 대한 설정을 프로퍼티로 넣어주면 됩니다.


직접 스프링이 제공하는 API를 사용해서 만드는 경우도 있습니다. OXM이나 JavaMail을 이용한다면 스프링이 정의한 추상 API를 이용해 코드를 작성합니다. 그리고 구체적인 기술과 설정은 XML 파일 안에서 지정합니다.


스프링의 서비스 추상화의 개념과 장점을 잘 이해한다면 때에 따라 직접 서비스 추상화 기법을 적용할 필요도 있습니다. 엔터프라이즈 개발에 사용되는 기술은 끊임없이 쏟아져나옵니다. 표준 기술뿐 아니라 오픈소스 라이브러리, 상용 프레임워크 형태로도 하루가 멀다하고 새로운 기술이 등장합니다. 보편적으로 사용되는 기술이라면 아마도 다음 버전의 스프링에서 서비스 추상화 대상으로 포함시킬 가능성이 있습니다. 하지만 그것을 굳이 기다려야 할 이유는 없습니다. 필요하면 스프링이 그랬던 것처럼 직접 추상 레이어를 도입하고 일관성 있는 API를 정의해서 사용하면 됩니다.


서비스 추상화를 위해 필요한 기술은 DI뿐입니다. 결국 DI 응용 방법의 한 가지이므로 DI를 적극 활용해서 개발한다면 서비스 추상화는 자연스럽게 만들어 쓸수 있습니다. 서비스 추상화는 단지 구체적인 기술에 종속되지 않게 하기 위해서만 사용되는 것은 아닙니다. 테스트가 어렵게 만들어진 API나 설정을 통해 주요 기능을 외부에서 제어하게 만들고 싶을 때도 이용할 수 있습니다.




스프링의 상세한 기술을 공부하기 전에 먼저 이해하고 기억해야할 사항은 다음과 같습니다.


스프링은 그 개발철학과 목표를 분명히 이해하고 사용해야 합니다.

스프링은 오픈소스 소프트웨어이며, 애플리케이션 개발의 모든 기술과 영역을 종합적으로 다루는 애플리케이션 프레임워크입니다.

엔터프라이즈 애플리케이션 개발의 복잡함은 비즈니스 로직과 엔터프라이즈 시스템의 기술적인 요구에 의해 발생합니다. 기존의 접근 방법은 이 복잡도를 낮추지 못하며 자바의 객체지향적인 장점을 포기해야 한다는 문제점이 있습니다.

자바의 근본인 객체지향적인 원리에 충실하게 개발할 수 있으며, 환경과 규약에 의존적이지 않은 POJO를 이용한 애플리케이션 개발은 엔터프라이즈 시스템 개발의 복잡함이 주는 많은 문제를 해결할 수 있습니다.

스프링의 목적은 이런 POJO를 이용해 엔터프라이즈 애플리케이션을 쉽고 효과적으로 개발할 수 있도록 지원해주는 데 있습니다.

POJO 방식의 개발을 돕기 위해 스프링은 IoC/DI, AOP, PSA와 같은 가능기술을 프레임워크와 컨테이너라는 방식을 통해 제공합니다.


스프링이 어떻게 해서 엔터프라이즈 개발이 주는 복잡함을 제거하고, POJO 프로그래밍이라는 효과적인 방법을 사용할 수 있게 하는지에 관심을 갖는 것이 스프링을 가장 빠르게 이해하고 적용할 수 있는 지름길입니다.