AI 글
템플릿 패턴의 본질은 실수 방지를 넘어서 자원의 생명주기를 프레임워크가 관리하는 데 있다.
// ❌ 위험한 코드
public void transferMoney(Account from, Account to, BigDecimal amount) {
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
accountRepository.withdraw(from, amount, conn);
accountRepository.deposit(to, amount, conn);
conn.commit();
} catch (Exception e) {
conn.rollback(); // 예외 발생 시 롤백을 잊으면?
throw e;
}
conn.close(); // finally 없으면 리소스 누수!
}문제점:
- 리소스 해제를 잊을 수 있음
- 중첩 호출 시 Connection을 파라미터로 계속 전달해야 함
- 트랜잭션 전파(propagation) 로직을 매번 작성해야 함
// ✅ 안전한 코드 - 템플릿/콜백 패턴
transactionTemplate.execute(status -> {
accountRepository.withdraw(from, amount); // Connection 자동 공유
accountRepository.deposit(to, amount); // 프레임워크가 관리
return null;
});// 실제 코드를 단순화한 개념적 구조
public class Template {
public <T> T execute(Callback<T> action) {
Resource resource = acquireResource(); // 1. 자원 획득
try {
return action.doInResource(resource); // 2. 사용자 로직 실행
} catch (Exception ex) {
handleException(resource, ex); // 3-a. 예외 처리
throw ex;
} finally {
releaseResource(resource); // 3-b. 자원 해제 (항상)
}
}
}핵심 흐름:
execute()호출 시 자원 획득- 콜백 실행 중 예외 발생 → 예외 처리 후 재throw
- 정상 종료 → 최종 처리
- 항상 자원 해제 (finally)
프레임워크가 "열고 닫는 시점"을 결정하고, 사용자는 비즈니스 로직만 작성한다.
template.execute(status -> {
userRepository.save(user); // Resource 필요
orderService.createOrder(order); // 이 안에서도 같은 Resource 필요 (아직 해제 안됨)
return result;
});파라미터로 Resource를 계속 넘기는 건 비현실적이다.
// 핵심 개념을 보여주는 단순화된 구조
public class ResourceManager {
private static final ThreadLocal<Resource> currentResource = new ThreadLocal<>();
public static Resource getResource() {
return currentResource.get();
}
public static void bindResource(Resource resource) {
currentResource.set(resource);
}
public static void unbindResource() {
currentResource.remove();
}
}생명주기:
1. 요청 시작 → 스레드 할당
2. Template → bindResource(resource)
3. 비즈니스 로직 실행 → getResource()로 획득
4. Template 종료 → unbindResource()
5. 요청 종료 → 스레드 반환
핵심: ThreadLocal은 스레드당 격리된 저장소를 제공하므로, 같은 스레드 내에서는 자동으로 컨텍스트 전파됨.
// 핵심 개념을 보여주는 단순화된 구조
public class ReactiveTemplate {
public <T> Mono<T> execute(Function<Resource, Mono<T>> action) {
return Mono.usingWhen(
// 자원 획득
acquireResource(),
// 정상 실행
action,
// 정상 종료 시 해제
this::releaseResource,
// 에러 발생 시 해제
(resource, ex) -> releaseResource(resource),
// 취소 시 해제
this::releaseResource
).contextWrite(ctx -> ctx.put(RESOURCE_KEY, new ResourceContext()));
}
}| 특성 | ThreadLocal | Reactor Context |
|---|---|---|
| 환경 | 동기 (Thread-per-request) | 비동기 (Event Loop) |
| 전파 | 자동 (같은 스레드) | 명시적 (contextWrite) |
| 저장 단위 | 스레드당 | Subscriber당 |
| 주의사항 | 스레드 풀 재사용 시 정리 필수 | 체인 중간에 컨텍스트 유실 가능 |
template.execute(outerCallback -> {
// 이미 자원을 획득한 상태
template.execute(innerCallback -> {
// 새 자원? 기존 자원 재사용? → 프레임워크가 판단
return innerResult;
});
return outerResult;
});// 핵심 개념을 보여주는 단순화된 구조
public class Template {
public <T> T execute(Callback<T> action) {
Resource resource = ResourceManager.getResource();
// 이미 자원이 있는지 확인
if (resource != null) {
return handleExisting(resource, action); // 기존 자원 활용
}
// 새 자원 시작
resource = acquireResource();
ResourceManager.bindResource(resource);
try {
return action.doInResource(resource);
} finally {
ResourceManager.unbindResource();
releaseResource(resource);
}
}
private <T> T handleExisting(Resource resource, Callback<T> action) {
// 전파 정책에 따라 다르게 처리
// - REQUIRED: 기존 자원 재사용
// - REQUIRES_NEW: 기존 중단 후 새 자원 생성
// - NESTED: 중첩 자원 생성 (Savepoint 등)
}
}핵심: 컨텍스트가 있어야 "이미 자원을 획득한 상태인지" 판단 가능하다.
Template (인터페이스)
↓ execute(Callback)
ConcreteTemplate (구현)
↓ 내부에서 사용
ResourceManager
↓ 자원 바인딩
Context (ThreadLocal or Reactor Context)
결국 "콜백의 바깥에서 안까지 관통하는 암묵적 상태 공유"가 필요하고, 실행 모델에 따라 적절한 컨텍스트 매체를 선택하는 것이다.
템플릿이 획득(acquire)과 해제(release) 를 책임지고, 사용자는 비즈니스 로직만 작성.
ThreadLocal(동기), Reactor Context(비동기) 등 실행 모델에 맞는 매체를 통해 암묵적으로 자원 공유.
프레임워크가 "이미 자원이 있는가?"를 판단하여, 전파 정책에 따라 적절히 처리.