loginMemberId
와 accessNumber
를 받아서 단순히 누가 어떤 데이터에 접근했다는 메시지를 남기는 반환하는 예제입니다.@Component
public class ParametersAccessService {
private static String READ_MESSAGE = "%d번 멤버가 %d번 data에 READ 권한이 필요한 접근을 했어요.";
private static String MAINTAIN_MESSAGE = "%d번 멤버가 %d번 data를 MAINTAIN 권한이 필요한 접근을 했어요.";
private static String HOST_MESSAGE = "%d번 멤버가 %d번 data를 HOST 권한이 필요한 접근을 했어요.";
@RequiredPermission(requiredLevel = MemberLevel.READ) // AOP 적용을 위한 어노테이션
public String readInfos(Long loginMemberId, Long accessNumber) {
return String.format(READ_MESSAGE, loginMemberId, accessNumber);
}
@RequiredPermission(requiredLevel = MemberLevel.MAINTAIN)
public String maintainInfos(Long loginMemberId, Long accessNumber) {
return String.format(MAINTAIN_MESSAGE, loginMemberId, accessNumber);
}
@RequiredPermission(requiredLevel = MemberLevel.HOST)
public String hostInfos(Long loginMemberId, Long accessNumber) {
return String.format(HOST_MESSAGE, loginMemberId, accessNumber);
}
}
public enum MemberLevel {
HOST, MAINTAIN, READ
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiredPermission {
MemberLevel requiredLevel() default MemberLevel.READ;
}
@Getter
@ToString
@RequiredArgsConstructor
public class MemberAccessInfo {
private final Long loginMemberId;
private final Long accessNumber;
}
@Aspect
@Component
@Slf4j
public class PermissionParametersAspect {
private static final String LOGIN_ID_PARAM = "loginMemberId"; // 찾을 파라미터의 이름
private static final String ACCESS_NUMBER_PARAM = "accessNumber"; // 찾을 파라미터의 이름
// 해당 어노테이션이 붙은 메서드의 실행전에 로직을 실행
@Before("@annotation(com.example.aopreflection.aspect.RequiredPermission)")
public void validateMemberLevel(JoinPoint joinPoint) { // 접근권한 검증 로직
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 메서드에 부착된 어노테이션의 정보를 가져옵니다.
RequiredPermission requiredPermission = signature.getMethod().getAnnotation(RequiredPermission.class);
Object[] args = joinPoint.getArgs(); // 메서드의 파라미터의 값 배열을 꺼내옵니다.
String[] parameterNames = signature.getParameterNames(); // 메서드의 파라미터들의 이름 배열을 꺼내옵니다.
Class<?>[] parameterTypes = signature.getParameterTypes(); // 메서드의 파라미터들의 타입 배열을 꺼내옵니다.
// 로그를 통한 확인(참고)
for (int i = 0; i < args.length; i++) {
log.info("args[{}] = {}", i, args[i].toString());
}
log.info("====================================");
for (int i = 0; i < parameterNames.length; i++) {
log.info("parametersNames[{}] = {}", i, parameterNames[i]);
}
log.info("====================================");
for (int i = 0; i < parameterTypes.length; i++) {
log.info("parameterTypes[{}] = {}", i, parameterTypes[i].toString());
}
// 파라미터 정보들의 배열을 통해서 MemberAccessInfo를 생성합니다.
MemberAccessInfo memberAccessInfo = getMemberAccessInfo(args, parameterNames, parameterTypes);
// TODO 권한 체크 로직
// 로그인 멤버가 특정 데이터에 접근할 권한이 있는지를 확인하는 로직
// level에 맞춰서 권한이 없다면 예외를 던지는 로직을 구현합니다... 해당 예제에서는 생략합니다.
MemberLevel memberLevel = requiredPermission.requiredLevel();
log.info("required level = {}", requiredPermission.requiredLevel());
// ...생략
}
private MemberAccessInfo getMemberAccessInfo(Object[] args, String[] parameterNames, Class[] parameterTypes) {
Object loginId = null; // 필요한 파라미터 정보
Object accessNumber = null;
// 파라미터들의 배열, 즉 3개의 정보들은 같은 인덱스를 가지게 됩니다.
// 파라미터의 이름, 타입을 통해서 객체의 값을 찾습니다.
for (int i = 0; i < parameterNames.length; i++) {
// loginMemberId를 찾는 로직
if (parameterNames[i].equals(LOGIN_ID_PARAM) && parameterTypes[i].equals(Long.class) && loginId == null) {
loginId = args[i];
}
// accessNumber를 찾는 로직
if (parameterNames[i].equals(ACCESS_NUMBER_PARAM) && parameterTypes[i].equals(Long.class) && accessNumber == null) {
accessNumber = args[i];
}
}
// 해당 메서드에 정보가 담겨있지 않을 수 있기 때문에 null을 체크합니다.
checkNull(loginId, accessNumber);
// 타입을 캐스팅해서 객체를 생성, 반환합니다.
return new MemberAccessInfo((Long) loginId, (Long) accessNumber);
}
private void checkNull(Object arg1, Object arg2) {
if(arg1 == null || arg2 == null) {
throw new NullPointerException("파라미터가 null 입니다");
}
}
}
@SpringBootTest
class AccessServiceTest {
@Autowired
private ParametersAccessService parametersAccessService;
private final Long loginMemberId = 1L;
private final Long accessNumber = 1L;
@Test
void ParametersAspectTest() {
assertThat(parametersAccessService.readInfos(loginMemberId, accessNumber))
.isEqualTo("1번 멤버가 1번 data에 READ 권한이 필요한 접근을 했어요.");
assertThat(parametersAccessService.maintainInfos(loginMemberId, accessNumber))
.isEqualTo("1번 멤버가 1번 data에 MAINTAIN 권한이 필요한 접근을 했어요.");
assertThat(parametersAccessService.hostInfos(loginMemberId, accessNumber)).
isEqualTo("1번 멤버가 1번 data에 HOST 권한이 필요한 접근을 했어요.");
}
2022-12-15 02:19:30.231 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[0] = 1
2022-12-15 02:19:30.232 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[1] = 1
2022-12-15 02:19:30.232 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.232 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[0] = loginMemberId
2022-12-15 02:19:30.232 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[1] = accessNumber
2022-12-15 02:19:30.233 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.233 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[0] = class java.lang.Long
2022-12-15 02:19:30.233 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[1] = class java.lang.Long
2022-12-15 02:19:30.244 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
2022-12-15 02:19:30.244 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : required level = READ
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[0] = 1
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[1] = 1
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[0] = loginMemberId
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[1] = accessNumber
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[0] = class java.lang.Long
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[1] = class java.lang.Long
2022-12-15 02:19:30.329 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
2022-12-15 02:19:30.330 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : required level = MAINTAIN
2022-12-15 02:19:30.330 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[0] = 1
2022-12-15 02:19:30.332 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : args[1] = 1
2022-12-15 02:19:30.332 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.332 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[0] = loginMemberId
2022-12-15 02:19:30.332 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parametersNames[1] = accessNumber
2022-12-15 02:19:30.332 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : ====================================
2022-12-15 02:19:30.333 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[0] = class java.lang.Long
2022-12-15 02:19:30.333 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : parameterTypes[1] = class java.lang.Long
2022-12-15 02:19:30.333 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
2022-12-15 02:19:30.333 INFO 62906 --- [ Test worker] c.e.a.aspect.PermissionParametersAspect : required level = HOST
MemberAccessInfo
객체가 정상적으로 생성된 것을 확인할 수 있습니다. @RequiredPermission
어노테이션이 붙은 메서드의 파라미터의 이름에 수정, 오타가 생긴다면 정상적으로 작동하지 않습니다.@Component
public class CustomObjectAccessService {
private static String READ_MESSAGE = "%d번 멤버가 %d번 data에 READ 권한이 필요한 접근을 했어요.";
private static String MAINTAIN_MESSAGE = "%d번 멤버가 %d번 data에 MAINTAIN 권한이 필요한 접근을 했어요.";
private static String HOST_MESSAGE = "%d번 멤버가 %d번 data에 HOST 권한이 필요한 접근을 했어요.";
@RequiredPermission(requiredLevel = MemberLevel.READ)
public String readInfos(MemberAccessInfo memberAccessInfo) { // 파라미터를 MemberAccessInfo로 수정
return String.format(READ_MESSAGE, memberAccessInfo.getLoginMemberId(), memberAccessInfo.getAccessNumber());
}
@RequiredPermission(requiredLevel = MemberLevel.MAINTAIN)
public String maintainInfos(MemberAccessInfo memberAccessInfo) {
return String.format(MAINTAIN_MESSAGE, memberAccessInfo.getLoginMemberId(), memberAccessInfo.getAccessNumber());
}
@RequiredPermission(requiredLevel = MemberLevel.HOST)
public String hostInfos(MemberAccessInfo memberAccessInfo) {
return String.format(HOST_MESSAGE, memberAccessInfo.getLoginMemberId(), memberAccessInfo.getAccessNumber());
}
}
@Component
@Aspect
@Slf4j
public class PermissionMemberAccessInfoAspect {
@Before("@annotation(com.example.aopreflection.aspect.RequiredPermission)")
public void validateMemberLevel(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
RequiredPermission requiredPermission = signature.getMethod().getAnnotation(RequiredPermission.class);
log.info("required level = {}", requiredPermission.requiredLevel());
// 파라미터의 값 배열만으로 필요한 객체를 찾아옵니다.
MemberAccessInfo memberAccessInfo = getMemberAccessInfo(joinPoint.getArgs());
log.info(memberAccessInfo.toString());
}
private MemberAccessInfo getMemberAccessInfo(Object[] args) {
return Arrays.stream(args)
.filter(a -> a instanceof MemberAccessInfo) // 타입 체크
.map(MemberAccessInfo.class::cast) // 캐스팅
.findFirst()
.orElseThrow(AccessException::new); // 찾는 파라미터가 없다면 예외반환
}
}
@SpringBootTest
class AccessServiceTest {
@Autowired
private CustomObjectAccessService customObjectAccessService;
private final Long loginMemberId = 1L;
private final Long accessNumber = 1L;
@Test
void MemberAccessInfoAspectTest() {
assertThat(customObjectAccessService.readInfos(new MemberAccessInfo(loginMemberId, accessNumber)))
.isEqualTo("1번 멤버가 1번 data에 READ 권한이 필요한 접근을 했어요.");
assertThat(customObjectAccessService.maintainInfos(new MemberAccessInfo(loginMemberId, accessNumber)))
.isEqualTo("1번 멤버가 1번 data에 MAINTAIN 권한이 필요한 접근을 했어요.");
assertThat(customObjectAccessService.hostInfos(new MemberAccessInfo(loginMemberId, accessNumber)))
.isEqualTo("1번 멤버가 1번 data에 HOST 권한이 필요한 접근을 했어요.");
}
}
2022-12-15 02:44:54.779 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : required level = READ
2022-12-15 02:44:54.791 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
2022-12-15 02:44:54.873 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : required level = MAINTAIN
2022-12-15 02:44:54.874 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
2022-12-15 02:44:54.875 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : required level = HOST
2022-12-15 02:44:54.875 INFO 63274 --- [ Test worker] c.e.a.a.PermissionMemberAccessInfoAspect : MemberAccessInfo(loginMemberId=1, accessNumber=1)
AOP 로직에서 joinpoint로 메서드의 파라미터들을 리플렉션을 통해서 사용할 수 있습니다.
import org.aspectj.lang.reflect.MethodSignature;
위에서 설명한 2가지 예시 모두 동작 방식은 동일합니다.
2번째의 커스텀 객체를 통한 활용도 같은 타입의 파라미터가 추가로 필요한 메서드가 생긴다면 파라미터의 이름을 확인해야합니다.
AOP의 적용을 통해서 메서드의 파라미터의 타입 또는 파라미터의 이름에 강제성이 생깁니다.
하지만 커스텀 어노테이션과 AOP 적용을 통해서 로직을 분리시키고 코드의 중복을 최소화할 수 있습니다.