핵심 로직에만 집중하자
로그 출력, 트랜잭션 경계 설정, 성능 측정 등은 비즈니스 로직과는 무관하지만 반드시 필요한 코드다. 이런 부가 기능을 핵심 코드에 직접 넣으면 유지보수가 어려워진다. 스프링의 AOP(Aspect-Oriented Programming)는 이 문제를 해결해 준다. 핵심 코드를 건드리지 않고도 메서드 실행 전·후·예외 상황에 원하는 동작을 삽입할 수 있다.
핵심 용어 정리
- Pointcut: 부가 기능을 적용할 조인 포인트(주로 메서드)를 선별하는 표현식
- Advice: 실제로 실행할 부가 로직(로그, 트랜잭션 등)
- Aspect: Pointcut + Advice를 하나로 묶은 모듈
- Join Point: Advice가 적용될 수 있는 지점(메서드 호출, 예외 발생 등)
JDK Dynamic Proxy 실습
스프링 AOP는 기본적으로 프록시 기반이다. JDK가 제공하는 Proxy 클래스로 인터페이스 기반 프록시를 만들어보자.
public interface PaymentService {
void pay(BigDecimal amount);
}
@Service
public class SimplePaymentService implements PaymentService {
@Override
public void pay(BigDecimal amount) {
System.out.println("결제 금액: " + amount);
}
}
public class PaymentProxyFactory {
public static PaymentService createProxy(PaymentService target) {
return (PaymentService) Proxy.newProxyInstance(
PaymentProxyFactory.class.getClassLoader(),
new Class[]{PaymentService.class},
(proxy, method, args) -> {
System.out.println("[시작] " + method.getName());
Object result = method.invoke(target, args);
System.out.println("[종료] " + method.getName());
return result;
}
);
}
}
스프링 @Aspect 활용하기
1. 의존성 추가
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. Aspect 정의
@Aspect
@Component
public class AuditAspect {
@Pointcut("within(@org.springframework.stereotype.Service *)")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logStart(JoinPoint jp) {
System.out.printf("[START] %s.%s%n",
jp.getSignature().getDeclaringTypeName(),
jp.getSignature().getName());
}
@AfterReturning(pointcut = "serviceLayer()", returning = "ret")
public void logReturn(JoinPoint jp, Object ret) {
System.out.printf("[END] %s.%s → %s%n",
jp.getSignature().getDeclaringTypeName(),
jp.getSignature().getName(),
ret);
}
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logError(JoinPoint jp, Throwable ex) {
System.err.printf("[ERROR] %s.%s : %s%n",
jp.getSignature().getDeclaringTypeName(),
jp.getSignature().getName(),
ex.getMessage());
}
@Around("serviceLayer()")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long end = System.nanoTime();
System.out.printf("[TIME] %s.%s took %d ms%n",
pjp.getSignature().getDeclaringTypeName(),
pjp.getSignature().getName(),
(end - start) / 1_000_000);
}
}
}
3. 활성화
@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
JdbcTemplate으로 DB 접근 간소화
순수 JDBC는 Connection, PreparedStatement, ResultSet 등 반복 코드가 많다. 스프링이 제공하는 JdbcTemplate은 이런 보일러플레이트를 제거하고 SQL만 작성하면 된다.
설정
@Configuration
public class DataConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/demo")
.username("demo")
.password("demo")
.driverClassName("com.mysql.cj.jdbc.Driver")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) {
return new JdbcTemplate(ds);
}
}
사용 예
@Repository
public class AccountRepository {
private final JdbcTemplate jdbc;
public AccountRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public void save(Account account) {
jdbc.update("INSERT INTO account(name, balance) VALUES (?, ?)",
account.getName(), account.getBalance());
}
public Optional<Account> findById(long id) {
return jdbc.query(
"SELECT id, name, balance FROM account WHERE id = ?",
(rs, rowNum) -> new Account(
rs.getLong("id"),
rs.getString("name"),
rs.getBigDecimal("balance")),
id
).stream().findFirst();
}
public List<Account> findAll() {
return jdbc.query(
"SELECT id, name, balance FROM account",
new BeanPropertyRowMapper<>(Account.class));
}
}
선언적 트랜잭션 관리
트랜잭션 시작·커밋·롤백 코드를 매번 작성할 필요 없이 @Transactional 하나면 충분하다.
Java Config
@Configuration
@EnableTransactionManagement
public class TxConfig {
@Bean
public PlatformTransactionManager txManager(DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}
서비스 계층
@Service
public class TransferService {
private final AccountRepository repo;
public TransferService(AccountRepository repo) {
this.repo = repo;
}
@Transactional
public void transfer(long fromId, long toId, BigDecimal amount) {
repo.deduct(fromId, amount);
repo.add(toId, amount);
// 예외 발생 시 자동 롤백
}
}
XML 방식
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="transfer*" propagation="REQUIRED"/>
<tx:method name="get*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* com.example.service..*Service.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOperation"/>
</aop:config>
스프링 AOP는 단순히 로그 출력용이 아니라 트랜잭션, 보안, 캐싱 등 다양한 크로스 컷팅 컨선(cross-cutting concern)을 깔끔하게 분리할 수 있게 해준다. 핵심 로직에만 집중하고 나머지는 Aspect로 위임하자.