해당 내용은 

그림으로 공부하는 IT 인프라 구조를 기반으로 정리하였습니다.

 

1. 배열과 연결 리스트

 

배열과 연결리스트는 모두 순차적으로 데이터를 처리하는 구조이지만, 구조가 다르기에 성능 측면의 특징도 많이 다르다.

배열과 연결리스트는 다양한 데이터구조를 이해하기 위한 기본 구조이다.

 

배열은 같은형태의 상자를 빈틈없이 순서대로 나열한 데이터 구조다. "같은크기" 의 상자를 "빈틈없이 순서대로" 나열하기 때문에 몇 번째 상자인지만 알면 해당 상자에 한번에 액세스 할 수 있다.

 

 연결 리스트는 상자를 선으로 연결한 형태의 데이터 구조다. 다음 상자의 위치 정보를 가지고 있다. 빈틈 없이 나열되지 않아도 되는 대신, 상자를 찾으려면 끝에서부터 순서대로 하나씩 상자 내부를 확인해야 한다.

 

즉 배열이 탐색은 빠르다. 하지만 배열은 도중에 상자를 추가하려면 그 이후 상자를 전부 하나씩 뒤로 옮겨야 한다. 상자를 빼는 경우엔 모두 하나씩 앞으로 옮겨야 한다. 이런 이유로 배열은 데이터 추가 및 삭제가 느린 데이터 구조라고 할 수 있다.

 

반면 연결 리스트는 도중에 상자를 추가하거나 삭제하면 선만 바꿔서 연결 해주면 된다. 따라서 연결 리스트는 데이터 추가, 삭제가 빠른 데이터 구조라 할 수 있다.

 

 특징을 간단히 정리하면 다음과 같다.

 

  •  배열은 데이터를 빈틈없이 순서대로 나열한 데이터 구조
  • 연결 리스트는 데이터를 선으로 연결한 데이터 구조
  • 탐색이 빠른 것은 배열이고, 느린 것은 연결 리스트
  • 데이터 추가, 삭제가 빠른 것은 연결 리스트, 느린 것은 배열.

 

참고로 위의 탐색은 처음부터 연속으로 찾는 것을 의미하는 것이 아니라, N 번째 요소 탐색을 의미한다. 

 

위의 삭제가 빠른 연결 리스트와 탐색이 빠른 배열을 조합한 하이브리드형 데이터 구조가"해시 테이블" 이다.

 

 2. 해시 테이블 / 트리, 인덱스

 

간단히 얘기하자면, modular 연산을 통해, 배열의 어느 인덱스에 해당하는 놈인지 찾고.

이 배열에는 각각 연결 리스트가 있어서, 배열에 연결된 연결 리스트 안의 데이터를 순차적으로 탐색해서 똑같은 놈이 있는지 찾아낸다.

 

해시 테이블은 키와 값 조합으로 표를 구성한 데이터 구성이다. 키는 해시 함수를 통해 해시 값으로 변환된다. 해시 값은 고정길이 데이터이기 때문에 조합 표의 데이터 구조가 간단해서 검색이 빠르다는 장점이 있다. 해시 테이블에서는 아무리 데이터 양이 많아진다고 해도 기본적인 등호 검색의 속도는 변하지 않는다. 

 검색 속도는 1이나 다름 없으나, 범위 검색이 약하다는 문제가 있다.

 

데이터베이스에서 인덱스를 사용하면 왜 검색이 빨라지는 걸까?

인덱스를 사용한다고 해서 항상 빨라지는 것이 아닌 이유는 왜일까?

기존 DBMS와 인메모리 DB에 적합한 인덱스가 다른 이유는 무엇일까?

 

 해시나 트리는 탐색 알고리즘이 아닌 데이터 구조이지만, 효율적인 탐색을 위해 사용된다. 필요한 때에 필요한 데이터를 신속하게 찾기 위해서는 데이터를 정리해둘 필요가 있다.

 

  • 필요한 때에 필요한 데이터를 빠르게 찾기 위해서 데이터를 정리해 둘 필요가 있다.
  • 데이터를 찾을 때의 데이터 구조와 데이터 저장 방식 특성에 따라 적합한 데이터 "정리 방법" 이 달라진다.
  • 데이터의 정리 방법을 데이터 구조, 찾는 방법을 탐색 알고리즘이라고 한다.
  • 처리 상대에 맞추어 데이터 구조를 정리할 필요가 있기 때문에 알고리즘과 데이터 구조는 자주 함깨 다루어진다.

 

 

 인덱스 스캔을 한다고 반드시 빨라지는 것은 아니다. 디스크에서 원하는 데이터를 얼마나 적은 노력으로 추출하느냐가 관건으로, 인덱스는 이를 위한 하나의 수단에 불과하다. 

 

 인덱스가 없는 경우에는, SQL을 발행해서 한 건의 데이터를 취득하는 경우라도 디스크에서 테이블 데이터를 모두 읽어서 조사해야 한다. 테이블의 모든 블록을 처음부터 순서대로 읽어나가는 것을 풀 스캔(full scan) 이라고 한다. 색인도 없는 사전에서 단어를 찾는 것과 같다.

 

 DBMS 인덱스로 일반적으로 사용되는 것은 B트리 인덱스다.

 

인덱스가 있으면 최소한의 필요 블록만 읽으면 된다. '인덱스'는 우리말로 '색인'이다. 사전을 찾을 때 색인을 이용하는 것과 마찬가지다.

 

 하지만 인덱스가 있다고 무조건 좋은 것만은 아니다. 검색이 빨라지는 대신에 데이터 추가, 갱식, 삭제 시에 테이블 뿐마 아니라 인덱스 데이터도 갱신해야 한다. 인덱스 갱신 때문에 불필요한 오버헤드가 발생할 수 있다. 

 

인덱스는 읽을 블록수를 줄이기 위한 수단이지만, 인덱스를 사용하면 오히려 읽을 블록수가 늘어날 수도 있다. 예를들어 테이블 데이터를 모두 취득해야 하는 경우 등이다. 이때는 테이블의 모든 블록뿐만 아니라 인덱스 블록까지 읽어야 하기 때문에 디스크 I/O 가 증가한다. 

 일반적으로 DBMS는 풀스캔을 하는 경우, 1회 디스크 I/O로 가능한 한 큰 크기의 데이터를 일겅서 I/O 횟수를 줄이려 한다. 하지만 인덱스 스캔에서는 인덱스 블록을 읽으면서 테이블 블록을 하나씩 읽기 때문에 액세스 블록 수가 늘어남과 동시에 I/O 횟수가 늘어난다.

 

 B트리가 자주 사용되는 것은, 트리 구조 계층이 깊어지지 않도록 디스크 I/O를 최소한으로 제어하기 때문이다. 반대로 인메모리 DB에서는 디스크 I/O를 신경 쓸 필요가 없기 때문에 T 트리 인덱스라는, 이진 트리의 일종을 사용하는 경우가 있다. 이진 트리는 가지가 두개 밖에 없어서 계층이 깊어지지만, 키 값 비교 횟수가 적다는 이점이 있다

 

더보기

 B트리로 이루어진 인덱스는, 루트 블록, 브랜치 블록, 리프 블록으로 분류된다.

잎 부분이 원하는 데이터 자장위치가 기록된 곳이다.

 

 

 

 데이터 구조는 데이터를 찾는 방식이나 데이터 저장 위치의 특성을 고려해서 선택할 필요가 있다. 또한, DBMS에서 인덱스를 만들면 검색은 빨라지지만 갱신 시에 오버헤드가 걸린다는 단점도 있기 때문에 함께 고려해야 한다.

블로그 이미지

맛간망고소바

,

엄청난 요약본이다.

토비의 스프링이 너무 정리가 잘 돼있어서.... 음...

결국 받아쓰기가 된 것 같은 느낌이 ㅠㅠ

 

AOP

 

 이제 AOP 에 대해서 알아보자. AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반기술의 하나다. AOP를 바르게 하려면 AOP의 필연적인 등장 배경과 스프링이 도입한 이유, 장점을 제대로 이해해야 한다. 스프링에 적용된 가장 인기 있는 AOP의 적용 대상은 바로 "선언적 트랜잭션 기능" 이다. 

 

제대로 들어가기전에 다이내픽 프록시와 팩토리 빈에 대해서 알고 넘어가자.

 

 단순히 확장성을 고려해서 한 가지 기능을 분리한다면 전형적인 전략 패턴으르 사용하면 된다. 하지만 우리가 원하는건 트랜잭션을 적용한다는 사실 자체가, 우리가 짠 코드에는 보이지 않는 것이다. (메소드 안에 안 드러나게)

 

 트랜잭션이라는 기능은 사용자 관리 비즈니스 로직과는 성격이 다르기 때문에 아예 그 적용 사실 자체를 밖으로 분리할 수 있다.

 

 핵심기능은 부가기능을 가진 클래스의 존재 자체를 모른다. 그래서 부가기능은 마치 자신이 핵심기능을 가진 클래스인것처럼 꾸며서, 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다. 

 

 이렇게 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 대리자, 대리인과 같은 역할을 한다고 해서 프록시(proxy) 라고 부른다. 

 

클라이언트 -> 프록시 -> 타깃

 

프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있는 위치에 있다는 것이다.

 

데코레이터 패턴.

 

 간단히 설명하자면, 프록시 개수를 확정하지 않는, 프록시 개념을 이용하는 프록시 비스무레한 패턴이다. (물론 둘은 다르다.)

InputStream 이라는 인터페이스를 구현한 타깃인 FileInputStream에 버퍼 읽기 기능을 제공해주는 BufferedInputStream 이라는 데코레이터를 사용하는 예시를 보자.

 

InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));

 

프록시로 동작하는 각 데코레이터는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 아니면 다음 단계의 데코레이터 프록시로 위임하는지 알지 못한다.

 

프록시 패턴.

 

일반적으로 사용하는 프록시라는 용어와 디자인 패턴에서 말하는 프록시 패턴은 구분할 필요가 있다. 전자는 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트를 두는 방법을 총칭한다면,

 

 후자는 프록시를 사용하는 방법 중에서 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우를 가리킨다.

 

프록시 패턴의 활용 예 중 하나는, 타깃 오브젝트를 미리 만들지 않고, 사용하는 시점에 만들고 싶다. 하지만 타깃 오브젝트에 대한 레퍼런스가 미리 필요하다. 요런 상황에, 프록시 패턴을 적용해서 실제 타깃 오브젝트 대신 프록시를 넘겨준다. 프록시의 메소드를 통해 타깃을 사용하려고 하면, 그 때 프록시가 타깃 오브젝ㅌ르르 생성하고 요청을 위임해준다.

 

 또는 원격 프로젝트를 이용하는 경우... 다른 서버에 존재하는 오브젝트를 사용해야 한다면, 원격 오브젝트에 대한 프록시를 만들어주고 클라이언트는 마치 로컬에 존재하는 오브젝트를 쓰는 것처럼 프록시를 사용하게 할 수 있다.

 

 또튼 타깃에 대한 접근권한을 제어하기 위해 프록시 패턴을 사용할 수 있다. 프록시의 특정 메소드를 사용하려고 하면 접근이 불가능 하다고 예외를 던진다.

 Collections의 unmodifiableCollection() 을 통해 만들어진 오브젝트가 전형적인 접근권한 제어용 프록시라 볼 수 있다.

 

 프록시와 데코레이터는 상당히 유사하지만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고있는 경우가 많다. 

 

 

 

 다이내믹 프록시

 

프록시를 일일이 만드는 것은 귀찬핟. 인터페으스를 구현해서 클래스를 새로 정의하지 않고 프록시를 편리하게 사용할 방법은 없을까? 있다. 자바에서는 java.lang.reflect 패키지 않에 프록시를 손쉽게 만들 수 있도록 지원해주는 클래스들이 있다. 일일이 프록시 클래스를 정의하지 않고도 몇 가지 API를 이용해 프록시처럼 동작하는 오브젝트를 다이내믹하게 생성하는 것이다.

 

 프록시는 다음의 두 가지 기능으로 구성된다.

 - 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출도면 타깃 오브젝트로 위임한다.

 - 지정된 요청에 대해서는 부가기능을 수행한다.

 

 

 리플렉션

 다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다. 

 자바의 모든 클래슨느 그 클래스 자체의 구성정보를 담은 Class 타입의 오브젝트를 하나씩 갖고 있다. 클래스이름.class 라고 하거나 오브젝트의 getClass() 메소드를 호출하면 클래스 정보를 담은 Class 타입의 오브젝트를 가져올 수 있다.

 

 

리플렉션 API 메소드 중에서 메소드에 대한 정의를 담은 Method라는 인터페이스를 이용해 메소드 호출을 해보자.

 

Method 인터페이스에 정의된 invoke() 메소드를 사용하자.

 

 public Object invoke(Object obj, Object... args);

 

Method lengthMethod = String.class.getMethod("length");

int length = lengthMethod.invoke(name);

 

 

 

 다이내믹 프록시는 "프록시 팩토리" 에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트다. 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다.

 

 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다. 이 덕분에 프록시를 만들 때 인터페이스를 모두 구현해가면서 클래스를 정의하는 수고를 덜 수 있다. 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어주기 때문이다.

 

 다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 프록시로서 필요한 부가긴으 제공 코드는 직접 작성해야 한다. 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다. InvocationHandler 인터페이스는 다음과 같은 메소드 한 개만 가진 간단한 인터페이스다.

 

에라이...

 

 

간단히 InvocationHandler 의 구현 클래스를 정리하고 넘어가자...

 

public class UppercaseHandler implements InvocationHandler {

 Hello target;

 

 public UppercaseHandler(Hello target) {

  this.target=target;

 }

 public Object invoke(Object proxy, Method method, Object[] args) {

  String ret = (String) method.invoke(target,args);

 return ret.toUpperCase();

 }

}

 

다이내믹 프록시의 생성은 Proxy클래스의 newProxyInstance() 스태틱 팩토리 메소드를 이용하면 된다.

 

Proxy.newProxyInstance( getClass().getClassLoader(), newClass[] {Hello.calss}, new UppercaseHandler(new HelloTarget()));

 

....

첫 번째 파라미터는 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더,

두번 째는 구현할 인터페이스,

세 번재는 부가기능과 위임 코드를 담은 Invocationhandler이다.

 

 

Class의 이름을 알고 있다면 Class.forName("java.util.Date").newInstance() 로 기본 생성자를 호출해서 값일 받아올 수 있다.

리플렉션 API이다.

 

 

 

팩토리 빈

 

 팩토리빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.

 

 

다이내믹 프록시를 만들어주는 팩토리 빈

 

 Proxy의 newProxyInstance() 메소드를 통해서만 생성이 가능한 다이내믹 프록시 오브젝트는 일반적인 방법으로는 스프링의 빈으로 등록할 수 없다. 대신 팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링의 빈으로 만들어줄 수가 있다.

 

 

 다이내믹 프록시를 생성해주는 팩토리 빈을 사용하는 방법은 여러 가지 장점이 있다. 한 번 부가기능을 가진 프록시를 생성하는 팩토리 빈을 만들어두면 타깃의 타입에 상관없이 재사용 할 수 있기 때문이다.

 

 이렇게 프록시 팩토리 빈을 사용하면 1. 인터페이스를 구현하는 프록시 클래스를 일일이 만들 필요가 없어짐. 2. 부가적인 기능이 여러 메소드에 반복적으로 나타나게 되는 코드 중복이 없어짐.

 

 하지만 단점이 있다.

 1. 비슷한 프록시 팩토리 빈의 설정이 중복되는건 막을 수 없음. (트랜잭션과 같이 비즈니스 로직을 담은 클래스의 메소드에 적용한다면...)

2. 하나의 타갯에 여러개의 부가기능을 넣는것도 애매함.

3. 프록시 팩토리 빈 개수만큼 InvocationHandler 오브젝트가 생김.

 

 

 

스프링의 프록시 팩토리 빈

 

이제 스프링의 프록시 팩토리 빈을 살펴보자.

 

스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.

 

생성된 프록시는 스프링의 빈으로 등록돼야 한다. 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 "팩토리 빈"을 제공해준다.

 

 여기는 InvoacationHandler가 아니라, MethodInterceptor 인퍼에스를 구현해서 부가기능을 만든다.

InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않는다, 따라서 타깃은 InvocationHandler를 구현한 클래스가 직접 알고 있어야 한다. 반면 MethodInterceoptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브ㅔㄱ트에 대한 정보도 함께 제공 받는다. 그 차이 덕분에 MethodInterceptor는 타깃 오브젝트에 상관 없이 독립적으로 만들어질 수 있다. 따라서 여러 프록시에서 함께 사용할 수 있고, 싱글톤 핀으로 등록 가능하다.

 

 

 어드바이스: 타깃이 필요없는 순수한 부가기능.

 

 MethodInterceptor 처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스(advice) 라고 부른다. 어드바이스는 타깃 오브젝트에 "종속 되지 않는" 순수한 부가기능을 담은 오브젝트다.

 

 포인트컷: 부가기능이 적용될 메소드 선정 알고리즘을 담은 오브젝트. 

 

 MethodInterceptor 오브젝트는 여러 프록시가 공유해서 사용할 수 있다. 따라서 타깃 정보를 갖고 있지 않다. 그 덕분에 MethodInterceptor 는 스프링의 싱글톤 빈으로 등록할 수 있다. 

 그렇다면 타깃에 대한 벙보가 없는 MethodInterceptor는 어떻게 부가기능을 적용할 메소드를 "선별" 할 수 있을까?

스프링은 메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.

 

 

 

 

 

스프링의 AOP

 

위와 뭐가 다르냐? 방금전 위는 스프링의 프록시 팩토리 빈이었다.

 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 스프링의 ProxyFactoryBean의 어드바이스를 통해 해결됐다.

남은 것은 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정 정보를 추가해주는 부분이다.

 

 반복적인 프록시의 메소드 구현을, 코드 자동생성 기법을 이용해 해결했다. -> 반복적인 ProxyFactoryBean 설정 문제는 설정 자동방법 기법으로 해결하면 안되나? -> 또는 실제 빈 오브젝트가 되는 것은 ProxyFactoryBean을 통해 생성되는 프록시 그 자체이니까 프록시가 자동으로 빈으로 생성되게 할 수는 없을까?

 

빈 후처리기를 이용한 자동 프록시 생성기

 

  스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다.

 

 그중에서 관심을 가질 만한 확장 포인트는 BeanPostProcessor 인터페이스를 구현해서 만드는 "빈 후 처리기" 이다.

 빈 후처리기는 말 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해준다.

 

 여기서는 스프링이 제공하는 빈 후처리기 중의 하나인 DefaultAdvisorAutoProxyCreator 를 살펴보겠다. 이름을 보면 알 수 있듯이 DefaultAdvisorAutoProxyCreator는 어드바이저를 이용한 자동 프록시 생성기다.

 

 스프링은 빈 후처리기가 빈에 등록돼 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다.

빈 오브젝트의 프로퍼티를 강제로 수정할 수도 있고, 별도의 초기화 작업을 수행할 수도 있다. 심지어는 만들어진 빈 오브젝트를 바꿔칠수도 있다. 따라서 스프링으 설정을 참고해서 만든 오브젝트가 아닌 다른 오브젝트를 빈으로 등록시키는 것이 가능하다..!

 

 -> 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수 있다!

 

확장된 포인트컷

 사실 포인트컷은 메소드만 선별할 수 있는게 아니라, 클래스도 선별 가능하다. DefaultAdivisorAutoProxyCreator는 클래스와 메소드 선정 아록리즘을 모두 갖고 있는 포인트컷이 필요하다. 정화깋는 그런 포인트컷과 어드바이스가 결합되어 있는 "어드바이저" 가 등록돼 있어야 한다.

 

 

 자동 프록시 생성기인 DefaultAdvisorAutoProxyCreator는 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾는다. 그리고 생성되는 모든 빈에 대해 어드바이저의 포인트컷을 적용해보면서 프록시 적용 대상을 선정한다. 빈 클래스가 프록시의 선정 대상이라면 프록시를 만들어 원래 빈 오브젝트와 바꿔치기한다.

 

 

 

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

 

전통적인 객체지향 기술의 설계 방법으로는 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가기능을 어떻게 모듈화 할 것인가 연구해온 사람들은, 이 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 새로운 특성이 있다고 생각했다.

 

 그래서 이런 부가기능 모듈을 "애스펙트" 라고 부르기 시작했다.

애스펙트란 그 자체로 애플리케이션의 핵심기능을 담고 있지는 않지만, 애플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심기능에 부가되어 의미를 갖는 특별한 모듈을 가리킨다.

 

애스펙트는 그 단어의 의미대로 애플리케이션을 구성하는 한 가지의 측면이라고 생각할 수 있다.

 

 이렇게 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 독특한 모듈로 만들어서 설계하고 개발하는 방법을 애스펙트 지향 프로그래밍 또는 약자로 AOP 라고 부른다. AOP는 OOP를 돕는 보조적인 기술이지 OOP를 완전히 대체하는 새로운 개념은 아니다.

 

 AOP는 결국 애플리케이션을 다양한 측면에서 독립적으로 모델링하고, 설계하고, 개발할 수 있도록 만들어주는 것이다. 애플리케이션을 핵심 로직 대신 어느 부가기능의 관점에서 바라보고, 그 부분에 집중해서 설계하고 개발할 수 있게 해주는 것. 이렇게 특정한 관점을 기준으로 바라볼 수 있게 해준다는 의미에서  AOP를 관점지향 프로그래밍이라고 하는 것이다.

 

블로그 이미지

맛간망고소바

,

일단 기본적인 JDBC, DataSource, Connection Pool 등에 대한 정리는

 

https://minwan1.github.io/2017/04/08/2017-04-08-Datasource,JdbcTemplate/

 

Wan Blog

WanBlog | 개발블로그

minwan1.github.io

이곳에 무지 잘 설명돼 있으니, 여기를 살펴보자.

 

 

 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다. 전체가 다 성공하든지 아니면 전체가 다 실패해야 한다. 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다. 이것이 트랜잭션이다.

 DB는 그 자체로 트랜잭션을 지원한다. 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다고 믿을 수 있다. 하지만 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션으로 취급해야 하는 경우가 있다. 중간에 실패가 발생해서 그 전에 처리한 SQL 작업까지 모두 취소하는 작업을 트랜잭션 롤백이라고 한다.

 반대로 SQL이 모두 성공적으로 마무리 됐으면 DB에 알려줘서 작업을 확정시켜야 한다. 이것을 트랜잭션 커밋이라고 한다.

 

 JDBC 트랜잭션의 트랜잭션 경계설정.

 

모든 트랜잭션은 시작하는 지점과 끝나는 지점이 있다. 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부른다. JDBC 의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다. 트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄진다. 자동 커밋을 꺼주면, 트랜잭션이 한 번 시작되면 commit() 혹은 rollback() 메소드가 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다. 이렇게 setAutoCommit(false) 로 트랜잭션의 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다.

 트랜잭션의 경계난 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점도 기억해두자. 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬트랜잭션이라고 한다.

 

 

 JdbCTemplate은 하나의 템플릿 메소드 안에서 Datasource의 getConnection() 메소드를 호출해서 Connection 오브젝트를 가져오고, 작업을 마치면 Connection을 확실하게 닫아주고 템플릿 메소드를 빠져나온다. 결국 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫히는 일까지 일어난다. 따라서 템플릿 메소드가 호출될 때마다 트랜잭션이 새로 만들어지고 메소드를 빠져나오기 전에 종료된다.

 이러면, 각 메소드들이 JdbcTemplate을 사용한다고 가정하면, 여러 메소드들을 하나의 트랜잭션으로 묶을 수 없다.

 

따라서 하나의 트랜잭션으로 묶기위해 직접 DB Connection을 생성하고, 트랜잭션을 시작하고, Connection 오브젝트를 각 메소드들에게 전달하는 방식을 생각할 수 있다. (마지막에는 당연히 Connection을 종료해준다.)

 하지만 이 또한 여러 문제점이 있다. 

 1. JdbcTemplate을 사용할 수 없으며 try catch finally 가 반복된다.

 2. Connection 파라미터가 계속 전달되게 생겼다. 지저분하다.

 3. Connection 파라미터가 추가되면서 더이상, 파라미터를 사용하는 객체는 데이터 액세스 기술에 "독립적일 수 없다."

 4. 테스트에도 영향을 끼친다.

...등등의 문제가 있다.

 

트랜잭션 동기화

 

그러면 Connection을 파라미터로 직접 전달하는 문제를 해결해보자. 트랜잭션 경계설정은 무슨 수를 쓰던 하긴 해야 한다. 하지만 Connection을 메소드의 파라미터로 전달하는건 피하고 싶다. 이를 위해 스프링이 제안하는 방법은 독립적인 트랜잭션 동기화 방식이다. 트랜잭션 동기화란, 트랜잭션을 시작하기 위해 만든 Connection Object를 특별한 저장장소에 보관해두고 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다. 

 [트랜잭션 동기화 저장소의 활용]

 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.

 

 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 TransactionSynchronizationManager다. 이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청한다.

 

 

트랜잭션 서비스 추상화

 

 그런데 만약 DB를 여러개 사용하고, 여러개의 DB 사이에서 이뤄지는 작업을 하나의 트랜잭션으로 묶으려면 어떻게 해야 할까? JDBC의 Connection을 이용한 트랜잭션 방식인 로컬트랜잭션으로는 불가능하다. 왜냐하면 로컬 트랜잭션은 하나의 DB Conenction 에 종속되기 때문이다. 따라서 각 DB와 독립적ㅇ로 만들어지는 Connection 을 통해서가아니라, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 "글로벌 트랜잭션(global tranaction)" 방식을 사용해야 한다. 글로벌 트랜잭션을 적용해야 트랜잭션 매니저를 통해 여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다. 자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는, 트랜잭션 매니저를 지원하기 위한 API인 JTA(Java Transaction API) 를 제공하고 있다.

 

 

애플리케이션은 기존의 방식대로 DB는 JDBC를, 메시징 서버라면 JMS 같은 API를 사용해서 필요한 작업을 수행한다. 단, 트랜잭션은 JDBC나 JMS API를 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임한다.

 

 그런데 이러면 하나의 DB만 쓰는 고객을 위해서는 JDBC를 이용한 트랜잭션 관리 코드를, 다중 DB를 위한 글로벌 트랜잭션을 필요로 하는 곳에서는 JTA를 이용한 트랜잭션 관리 코드를 적용해야 하는 상황이 된다. 또한.. 하이버네이트 같은 경우 Connection 을 직접 사용하지 않고, Session이란 것을 사용하며 독자적인 트랜잭션 관리 API를 사용한다.

 

 결국은 Connection, UserTransaction, Session/Transaction API 등에 종속되지 않게 해야한다.

 다행히도 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조다. 공통점을 뽑아서 분리시킬 수 있다.(추상화) 이렇게 하면 하위시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방식으로 접근할 수 있다.

 

 트랜잭션의 경계설정을, 어떤 상황에 오던 일관적으로 하기 위해서, 트랜잭션의 경계설정을 담당하는 코드를 추상화하자. 이렇게 하면 하위 시스템이 어떤 것인지 알지 못해도, 또는 하위 시스템이 바뀌더라도 일관된 방법으로 접근할 수 있다.

 

 

스프링의 트랜잭션 서비스 추상화

 

스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다. 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.

 

스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager다. 

 

 JDBC 를 이용하는 경우에은 먼저 Connection 을 생성하고 나서 트랜잭션을 시작했다. 하지만PlatformTransactionmanager 에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 호출만 하면 된다. 필요에 따라서 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문이다.

 

 ** PlatformTransactionManager 로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다. PlatformTransactionManager를 구현한 DatasSourceTransactionManager 오브젝트는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리한다. 따라서 PlatformTransactionManager를 통해 시작한 트랜잭션은 JdbcTemplate안에서 사용된다.

 

 

 -> 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.

 

 

 

코드에 만족하지 말고 계속 개선하려고 노력하자.

DI는 모든 스프링 기술의 기반이 되는 핵심 엔진이자 원리이다. 

스프링을 DI 프레임워크라 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다. 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다.

 

 

 

 

블로그 이미지

맛간망고소바

,