예시 시나리오: 로그인 시도 로깅 및 인증 처리(Spring Boot @AspectJ 애노테이션 기반 AOP 방식)
- 이 방식은 런타임 위빙과 프록시 기반 동작(JDK 동적 프록시 또는 CGLIB)을 사용하며, 횡단 관심사(로그인 로깅 및 인증 처리)를 효과적으로 분리합니다.
상황 설명
온라인 쇼핑몰 애플리케이션을 개발한다고 가정해보겠습니다. 사용자 인증은 애플리케이션의 핵심 기능 중 하나이며, 모든 로그인 시도에 대해 다음과 같은 작업을 수행하고자 합니다:
1. 로그인 시도 기록: 사용자가 로그인할 때마다 시도 시간을 로그에 남깁니다.
2. 권한 검사: 로그인 시도 시 사용자의 계정이 잠겨 있거나 비활성화되어 있는지 검사합니다.
3. 실패한 로그인 시도 제한: 일정 횟수 이상의 로그인 실패 시 계정을 잠급니다.
이러한 기능들은 여러 곳에서 공통적으로 필요하며, 핵심 비즈니스 로직과는 별도로 관리하는 것이 좋습니다. 따라서 AOP를 사용하여 횡단 관심사로 분리합니다.
1. 프로젝트 구조 설정
우선, 프로젝트의 기본 패키지 구조를 설정합니다.
com.example
├── aspect
│ └── AuthenticationAspect.java
├── service
│ ├── UserService.java
│ └── UserServiceImpl.java
├── controller
│ └── UserController.java
├── model
│ └── User.java
└── Application.java
2. 핵심 비즈니스 로직 구현
2.1 User 모델 클래스
package com.example.model;
public class User {
private String username;
private String password;
private boolean locked;
private int failedAttempts;
// 생성자, getter, setter 생략
}
2.2 UserService 인터페이스
package com.example.service;
import com.example.model.User;
public interface UserService {
User findByUsername(String username);
void login(String username, String password);
void lockUser(User user);
// 기타 메서드 생략
}
2.3 UserServiceImpl 클래스
package com.example.service;
import com.example.model.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
// 사용자 데이터를 저장하는 임시 저장소
private Map<String, User> userRepository = new HashMap<>();
@Override
public User findByUsername(String username) {
return userRepository.get(username);
}
@Override
public void login(String username, String password) {
User user = findByUsername(username);
if (user == null) {
throw new RuntimeException("사용자를 찾을 수 없습니다.");
}
if (user.isLocked()) {
throw new RuntimeException("계정이 잠겼습니다.");
}
if (!user.getPassword().equals(password)) {
user.setFailedAttempts(user.getFailedAttempts() + 1);
if (user.getFailedAttempts() >= 3) {
lockUser(user);
throw new RuntimeException("비밀번호를 3회 이상 틀렸습니다. 계정이 잠겼습니다.");
}
throw new RuntimeException("비밀번호가 틀렸습니다.");
}
// 로그인 성공 시 실패 횟수 초기화
user.setFailedAttempts(0);
System.out.println("로그인 성공: " + username);
}
@Override
public void lockUser(User user) {
user.setLocked(true);
System.out.println("계정 잠김: " + user.getUsername());
}
// 기타 메서드 생략
}
3. 횡단 관심사 정의: AuthenticationAspect 구현
로그인 시도 시 로그를 남기고, 추가적인 인증 처리를 Aspect로 구현합니다.
3.1 AuthenticationAspect 클래스
package com.example.aspect;
import com.example.model.User;
import com.example.service.UserService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuthenticationAspect {
private final UserService userService;
public AuthenticationAspect(UserService userService) {
this.userService = userService;
}
// Pointcut: UserService의 login 메서드에만 적용
@Pointcut("execution(* com.example.service.UserService.login(..))")
public void loginMethod() {}
// Advice: 로그인 시도 전에 실행
@Before("loginMethod() && args(username, password)")
public void beforeLogin(JoinPoint joinPoint, String username, String password) {
System.out.println("로그인 시도: 사용자명 = " + username);
User user = userService.findByUsername(username);
if (user != null && user.isLocked()) {
throw new RuntimeException("계정이 잠겨 있습니다.");
}
}
// Advice: 로그인 시도 후에 실행 (성공 시)
@AfterReturning("loginMethod() && args(username, password)")
public void afterLoginSuccess(JoinPoint joinPoint, String username, String password) {
System.out.println("로그인 성공: 사용자명 = " + username);
}
// Advice: 로그인 시도 중 예외 발생 시 실행
@AfterThrowing(pointcut = "loginMethod()", throwing = "error")
public void afterLoginFailure(JoinPoint joinPoint, Throwable error) {
System.out.println("로그인 실패: 에러 메시지 = " + error.getMessage());
}
}
3.2 설명
• Pointcut: loginMethod()는 UserService의 login 메서드를 대상으로 합니다.
• Before Advice: 로그인 시도 전에 실행되어 계정이 잠겨 있는지 확인합니다.
• AfterReturning Advice: 로그인 성공 후에 실행되어 성공 로그를 출력합니다.
• AfterThrowing Advice: 로그인 시도 중 예외가 발생하면 실행되어 실패 로그를 출력합니다.
+빈 메서드를 사용하는 이유(loginMethod())
스프링 AOP에서 Pointcut을 정의할 때, 빈 메서드를 사용하여 Pointcut 표현식에 이름을 부여합니다. 이 빈 메서드는 실제로 호출되지는 않으며, Pointcut을 재사용 가능하고 가독성 있게 관리하기 위한 목적으로 사용됩니다.
4. 스프링 설정
4.1 Application 클래스
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4.2 AOP 활성화
@SpringBootApplication에는 이미 @EnableAspectJAutoProxy가 포함되어 있어 별도의 설정이 필요 없습니다. 하지만 명시적으로 설정하고자 하면 다음과 같이 작성할 수 있습니다.
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {
// ...
}
5. 테스트를 통한 동작 확인
5.1 UserController 추가
package com.example.controller;
import com.example.service.UserService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// 생성자 주입
public UserController(UserService userService) {
this.userService = userService;
}
// 사용자 로그인 엔드포인트
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password) {
userService.login(username, password);
return "로그인 성공";
}
}
5.2 애플리케이션 실행 및 시나리오 테스트
1. 사용자 생성
UserServiceImpl에 사용자 데이터를 미리 설정합니다.
public UserServiceImpl() {
// 사용자 데이터 추가
User user = new User();
user.setUsername("john");
user.setPassword("password");
user.setLocked(false);
user.setFailedAttempts(0);
userRepository.put("john", user);
}
2. 성공적인 로그인 시도
• 요청: POST /users/login?username=john&password=password
• 예상 결과:
• Before Advice 실행: “로그인 시도: 사용자명 = john”
• 로그인 로직 실행: 비밀번호 확인 및 실패 횟수 초기화
• AfterReturning Advice 실행: “로그인 성공: 사용자명 = john”
• 응답: “로그인 성공”
3. 잘못된 비밀번호로 로그인 시도
• 요청: POST /users/login?username=john&password=wrongpassword
• 예상 결과:
• Before Advice 실행: “로그인 시도: 사용자명 = john”
• 로그인 로직 실행: 비밀번호 불일치, 실패 횟수 증가
• AfterThrowing Advice 실행: “로그인 실패: 에러 메시지 = 비밀번호가 틀렸습니다.”
• 응답: 에러 메시지 반환
4. 로그인 실패 3회 후 계정 잠김 확인
• 3회 이상 잘못된 비밀번호로 로그인 시도하여 계정이 잠기게 합니다.
• 그 후 다시 로그인 시도:
• Before Advice에서 계정 잠김 확인 후 예외 발생: “계정이 잠겨 있습니다.”
• AfterThrowing Advice 실행: “로그인 실패: 에러 메시지 = 계정이 잠겨 있습니다.”
• 응답: 에러 메시지 반환
6. 전체 동작 과정 상세 설명
6.1 로그인 시도 시 동작 흐름
1. 클라이언트 요청: /users/login?username=john&password=password로 로그인 시도.
2. Controller에서 Service 호출: UserController는 UserService.login 메서드를 호출.
3. AOP 적용 - Before Advice 실행:
• AuthenticationAspect.beforeLogin 메서드가 실행.
• 사용자 계정이 잠겨 있는지 확인.
• 로그인 시도 로그 출력.
4. Service의 로그인 로직 실행:
• 비밀번호 확인.
• 실패 횟수 초기화 또는 증가.
5. AOP 적용 - AfterReturning 또는 AfterThrowing Advice 실행:
• 로그인 성공 시 afterLoginSuccess 실행하여 성공 로그 출력.
• 로그인 실패 시 afterLoginFailure 실행하여 실패 로그 출력.
6. 응답 반환:
• 로그인 성공 또는 실패 메시지를 클라이언트에게 반환.
6.2 AOP를 사용한 이점
• 코드 중복 제거: 모든 로그인 로직에 로그 출력이나 계정 잠김 확인 코드를 반복해서 작성할 필요가 없습니다.
• 유지보수성 향상: 로그 형식이나 인증 처리 로직 변경 시 Aspect만 수정하면 됩니다.
• 비즈니스 로직과 횡단 관심사의 분리: 핵심 로그인 로직에만 집중할 수 있습니다.
7. 추가 기능 구현
7.1 트랜잭션 관리 Aspect 추가
데이터베이스 작업에 트랜잭션 관리를 추가하고자 할 때도 AOP를 활용할 수 있습니다.
@Aspect
@Component
public class TransactionAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Around("serviceMethods()")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("트랜잭션 시작");
try {
Object result = joinPoint.proceed();
System.out.println("트랜잭션 커밋");
return result;
} catch (Exception e) {
System.out.println("트랜잭션 롤백");
throw e;
}
}
}
7.2 설명
• Pointcut: com.example.service 패키지의 모든 메서드에 적용.
• Around Advice: 메서드 실행 전후로 트랜잭션 시작 및 종료를 관리.
• 이점:
• 모든 서비스 메서드에 트랜잭션 관리를 일괄적으로 적용.
• 트랜잭션 로직을 비즈니스 로직과 분리하여 코드의 명확성 향상.
8. 핵심 개념 간의 관계 재확인
• Aspect: AuthenticationAspect, TransactionAspect 등 횡단 관심사를 모듈화한 클래스.
• Join Point: UserService.login 메서드 실행 지점 등 Aspect가 적용될 수 있는 지점.
• Pointcut: loginMethod(), serviceMethods() 등 Join Point 중에서 실제로 Advice를 적용할 지점을 선정하는 표현식.
• Advice: beforeLogin, afterLoginSuccess, manageTransaction 등 Pointcut에서 실행될 구체적인 동작.
• Weaving: 스프링 AOP가 런타임에 프록시 객체를 생성하여 Aspect를 타겟 객체에 적용하는 과정.
9. 결론
이러한 예시를 통해 AOP가 실제 애플리케이션에서 어떻게 적용되는지 구체적으로 살펴보았습니다. AOP를 사용함으로써 다음과 같은 이점을 얻을 수 있습니다.
• 코드의 모듈화: 횡단 관심사를 별도의 Aspect로 분리하여 코드의 응집도를 높입니다.
• 유지보수성 향상: 공통 기능의 변경이 필요한 경우 Aspect만 수정하면 됩니다.
• 비즈니스 로직 집중: 핵심 비즈니스 로직에만 집중하여 개발 생산성을 높입니다.
• 코드 중복 제거: 반복되는 코드를 제거하여 코드의 간결성과 가독성을 향상시킵니다.
'spring' 카테고리의 다른 글
[Spring] JPA (0) | 2024.12.03 |
---|---|
[Spring] 프록시(proxy) (0) | 2024.11.30 |
[Spring] AOP(Aspect-Oriented Programming) (0) | 2024.11.29 |
[Spring] 물리 트랜잭션, 논리 트랜잭션 (0) | 2024.11.28 |
[Spring] @Transaction (1) | 2024.11.28 |