AOP: 관점 지향 프로그래
AOP:Aspect Orented Programming는 관점 지향 프로그래밍이라고 말하는데 여기서 관점이란 관심사라는 말로 쓰인다.
이러한 관심사의 예로는 프로그램의 실행 시간이 얼마인지 측정하거나 트랜잭션을 적용하는 것 등이 있다.
이러한 관심사들은 '핵심 로직'은 아니지만, 코드를 완전하게 만들기 위해서는 필요한 것들이다.
과거에는 핵심 로직(비즈니스 로직)을 구현하면서 내부에 필요한 관심사를 두어 처리하는 방식을 사용했다면 AOP는 과거 개발자가 작성했던 '관심사 + 핵심로직(비즈니스 로직)'을 분리해서 별도의 코드로 작성하도록 하고(관심사의 분리) 실행할 때 이를 결합하는 방식으로 접근하였다.
관심사와 핵심로직은 코드를 컴파일 혹은 실행 시점에 결합이 된다. 실제 실행은 결합된 상태의 코드가 실행되기 때문에 개발자들은 핵심 비즈니스 로직에만 근거해서 코드를 작성하고, 나머지는 어떤 관심사들과 결합할 것인지를 결정하는 것만으로 개발을 좀 더 편리하게 할 수 있게 된다.
아래는 트랜잭션을 적용하고자 하는 관심사
트랜잭션 경계 설정 코드 예시를 보면 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치함을 볼 수 있다. 이때 트랜잭션 경계설정 코드와 비즈니스 로직 코드 간에 주고받는 정보가 없기 때문에 이를 분리해볼 것이다. 하지만 트랜잭션 적용 코드는 기존에 써왔던 방법으로는 간단하게 분리해서 독립된 모듈로 만들 수 없었다. 트랜잭션 경계 설정 기능은 다른 모듈의 코드에 부가적으로 부여되는 기능이라는 특징이 있기 때문이다.
트랜잭션 경계 설정 기능과 같은 부가기능은 왜 핵심기능과 같은 방식으로 모듈화하기가 힘들까? (핵심기능 모듈화는 기존 방식으로 가능하다.) 그 이유는 부가기능 자체가 말 그대로 핵심 기능에 부가 기능을 부여하는 것이기 때문에 부가기능은 스스로 독립적으로 존재하기 어렵다. 핵심기능을 가진 모듈은 그 자체로 독립하여 존재할 수 있지만 부가기능은 부가기능이 적용되는 대상, 즉 타깃이 존재해야만 의미가 있다.
핵심기능을 담당하는 코드에 여기저기 흩어져 있던 부가기능을 독립적인 모듈로 만들기 위해 고민을 하였고 이에 특별한 기법을 생각하였다. 바로 DI, 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷과 같은 기법이다.
사람들은 부가기능 모듈화 작업은 기존의 객체지향 설계 패러다임과는 구분되는 특성이 있다고 생각하였다. 그래서 이 부가기능 모듈을 특별한 이름으로 명명하기로 했는데 그것이 바로 애스펙트:Aspect다. 애스펙트란 즉 그자체로 애플리케이션의 핵심 기능을 담고 있지는 않지만, 어플리케이션을 구성하는 한 가지 측면이자 중요 요소중 하나이고 핵심기능에 부가되어 의미를 갖는 특별한 모듈(부가기능 모듈)을 말한다.
애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트라는 특별한 모듈로 만들어서 설계하고 개발하는 방법을 관점 지향 프로그래밍:AOP (Aspect Oriented Programming)라고 부른다. AOP는 애스펙트를 분리함으로 핵심 기능을 설계하고 구현할 때 객체지향적인 가치를 지킬 수 있도록 도와주는 것이라고 볼 수 있다. 어플리케이션을 핵심 기능과 애스펙트로 분리하여 개발을 한다면 핵심 기능만 집중하여 개발할 수 있고, 또한 부가기능 관점에서 바라보고 해당 측면에서 집중해서 설계하고 개발할 수 있게 된다.
※횡단 관심사
부가 기능으로, 비즈니스 로직과는 다소 거리가 있으나 여러 모듈에 걸쳐 공통적이고 반복적으로 필요로 하는 처리 내용을 횡단 관심사라 부른다. 대표적인 횡단 관심사로는 다음과 같은 것들이 있다.
- 보안
- 로깅
- 트랜잭션 관리
- 모니터링
- 캐시 처리
- 예외 처리
💡AOP를 이용하는 기법: Proxy
스프링은 IoC/DI 컨테이너와 데코레이터 패턴, 다이내믹 프록시, 오브젝트 생성 후처리, 자동 프록시 생성, 포인트컷 등의 다양한 기술을 조합해 AOP를 지원하고 있다. 이중 중요한 것은 프록시(Proxy)를 사용했다는 것이다.
프록시를 만들었기 때문에 DI로 연결된 빈 사이에 적용해 타깃의 메소드 호출 과정에 참여해서 부가기능을 제공해주도록 만들수 있었다. 독립적으로 개발한 부가기능 모듈을 다양한 타깃 오브젝트의 메소드에 다이내믹하게 적용해주기 위해 가장 중요한 역할을 하고 있는 것이 바로 프록시 이다. 따라서 스프링 AOP는 프록시 방식의 AOP라 할 수 있다.
스프링 AOP는 자바의 기본 JDK와 스프링 컨테이너 외에 특별한 기술이나 환경, JVM 없이 구현된다.
※프록시 방법이 아닌 AOP: AspectJ
프록시 방식이 아닌 AOP도 있는데 바로 AOP 프레임워크인 AspectJ이다. 스프링이 프록시 방식의 AOP를 사용하지만 AspectJ의 포인트컷 표현식을 차용해서 사용할 만큼 AspectJ는 강력한 AOP 프레임워크이다. AspectJ는 스프링처럼 다이내믹 프록시 방식을 사용하지 않는다. 그럼 우리가 알고 있는 프록시 방식을 사용하지 않으면 어떤 방식으로 부가기능을 타깃 오브젝트에 적용하는 것일까? 바로 컴파일된 타깃읠 클래스 파일 자체를 수정하거나 클래스가 JVM에 로딩되는 시점을 가로채서 바이트코드를 조작하는 복잡한 방법을 사용한다. 바로 비즈니스 로직과 부가기능 로직을 함께 있을 때 처럼 만드는 것이다.
AspectJ 방식을 사용하면 스프링과 같은 컨테이너가 사용되지 않는 환경에서도 손쉽게 AOP의 적용이 가능하고, 프록시 방식보다 훨씬 강력하고 유연한 AOP가 가능하다. 물론 대부분의 부가기능은 프록시 방식을 사용해 메소드의 호출 시점에 부여하는 것으로도 충분하기 때문에 특별한 AOP 요구사항이 생기지 않는 이상 프록시 방식의 스프링 AOP만 사용하여도 된다. 스프링의 프록시 AOP 수준을 넘어서는 기능이 필요하다면, AspectJ를 사용하면된다. 스프링 AOP를 기본적으로 사용하면서 동시에 AspectJ를 이용할 수 있기에 이러한 방식으로 적용해도 된다.
AOP 용어
aspect: 부가기능 모듈
여러 객체에 공통으로 적용되는 기능(횡단 관심사)을 Aspect라 한다.
AOP의 예로 자주 언급되는 '로그를 출력한다', '예외를 처리한다', '트랜잭션을 관리한다'와 같은 관심사가 Aspect이다.
한 개 이상의 포인트컷과 어드바이스의 조합으로 만들어지며 보통 싱글톤 형태의 오브젝트로 존재한다.
advice
- JoinPoint에서 실행되는 코드로, 횡단 관심사를 실제로 구현해서 처리하는 부분이다.
- 즉 Aspect에서 실질적인 기능에 대한 구현체이다.
- 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의하고 있다.
예를 들어 '메소드를 호출하기 전'(언제)에 '트랜잭션 시작'(공통 기능) 기능을 적용한다는 것을 정의한다.
Advice는 오브젝트로 정의하기도 하지만 메소드 레벨에서 정의할 수도 있다.
※Advice 유형
종류 | 설명 |
Before | 대상 객체의 메소드 호출 전에 공통 기능을 실행 |
After Returning | 대상 객체의 메소드가 정상적으로 실행된 이후에 공통 기능을 실행 |
After Throwing | 대상 객체의 메소드를 실행하는 도중 예외 발생한 경우에 공통 기능을 실행한다. |
After | 예외 발생 여부에 상관없이 대상 객체의 메소드 실행 후 공통 기능을 실행한다. (try-catch-finally의 finally 블록 느낌) |
Around | 메소드의 실행 자체를 제어할 수 있는 가장 강력한 코드 직접 대상 메소드를 호출하고 결과나 예외를 처리할 수 있다. |
@Aspect
public class MyAspect{
@Before("execution( * runSomething())")
public void before(ProceedingJointPoint joinPoint){
...
Object result = joinPoint.proceed();
...
}
}
target
- 부가기능인 advice가 적용되는 클래스를 의미한다.
- 순수한 비즈니스 로직을 의미한다. 어떠한 관심사들과도 관계를 맺지 않는 순수한 코어이다.
- 이때 advice를 주기능에 적용하는 것을 weaving이라 한다.
Join Point: 연결 가능한 지점
- 어드바이스(Advice)가 적용될 수 있는 위치를 말한다.
- 횡단 관심사가 실행될 지점이나 시점(메소드 실행이나 예외 발생 등)을 말한다.
- Target 객체가 가진 메소드이다. 외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식으로 이해할 수 있다. 스프링은 Proxy를 이용해서 AOP를 구현하기 때문에 메소드 호출에 대한 Joinpoint만 지원한다. Target 오브젝트가 구현한 인터페이스의 모든 메소드는 JoinPoint가 된다.
Pointcut: Aspect 적용 위치 지정자
- Target에는 여러 메소드가 존재하기 때문에 어떤 메소드에 관심사를 결합할 것인지를 결정해야 하는데 이 결정을 Pointcut이라 한다.
- Join Point 중에서 실제로 Advice를 적용할 곳을 선별하기 위한 표현식(expression)을 말한다.
- 즉, 관심사와 비즈니스 로직이 결합되는 지점을 결정한다.
- 스프링 AOP의 JoinPoint는 메소드의 실행이고 스프링의 Pointcut은 메소드를 선정하는 기능이다.
그래서 포인트컷의 표현식은 메소드의 실행이라는 의미인 execution으로 시작하고, 메소드의 시그니처를 비교하는 방법을 주로 사용한다. 메소드는 클래스 안에 존재하는 것이기 때문에 메소드 선정이란 결국 클래스를 선정하고 그 안의 메소드를 선정하는 과정을 거치게 된다.
@Aspect
public class MyAspect{
@Before("execution( * runSomething())")
public void before(ProceedingJointPoint joinPoint){
...
Object result = joinPoint.proceed();
...
}
}
Pointcut은 * runSomething()이다. 즉 쉽게 설명하면 Pointcut은 [어디에(Where)]를 의미한다.
※@Before("execution( * runSomething())") 의 뜻은?
지금 선언하고 있는 메소드(public void before)를
지정된 메소드( * runSomething)가
실행되기 전(@Before)에 실행하라는 의미이다.
여기서 선언하고 있는 메소드(public void before)는 횡단 관심사를 실행하는 메소드이다.
즉, Pointcut은 횡단 관심사를 적용할 타깃 메소드를 선택하는 지시자이다.
※타깃 메소드 지시자에 넣을 수 있는 정규식, 표현식은?
[접근제한자패턴] 리턴타입패턴 [패키지&클래스패턴.]메소드이름패턴(파라미터패턴) [throws 예외패턴]
public void test.Boy.runSomething() |
접근제한자가 public이고 리턴타입은 void이고 test 패키지 밑의 Boy 클래스 안에 파라미터가 없으며 던져지는 에러가 있든 없든 이름이 runSomething인 메소드를(들을) Pointcut으로 지정하라 |
* runSomething() |
접근제한자는 무엇이라도 좋고(생략) 리턴타입도 무엇이라도 좋고(*) 모든 패키지 밑의(생략) 모든 클래스 안에(생략) 파라미터가 없으며 던져지는 에러가 있든 없든 이름이 runSomething인 메소드를(들을) Pointcut으로 지정하라 |
Proxy
프록시는 클라이언트와 타깃 사이에 투명하게 존재하면서 부가기능을 제공하는 오브젝트다. DI를 통해 타깃 대신 클라이언트에게 주입되며, 클라이언트의 메소드 호출을 대신 받아서 타깃에 위임해주면서, 그 과정에서 부가기능을 부여한다. 스프링은 프록시를 이용해 AOP를 지원한다.
Advisor: 언제, 어디서, 무엇을
Advisor = 한 개의 Advice + 한 개의 Pointcut
Advisor는 스프링 AOP에서만 사용되며 스프링에서도 이제는 잘 쓰이지 않는다. 스프링이 발전하여 다수의 Advice와 다수의 Pointcut을 다양하게 조합해서 사용할 수 있는 방법인 Aspect가 나왔기 때문에 하나의 Advice와 하나의 Pointcut만을 결합하는 Advisor를 사용할 필요가 없어졌기 때문이다.
또한 AOP에 대한 이해를 돕기 위해 AOP 어노테이션을 사용했지만, 어노테이션 없이 POJO와 XML 설정 기반으로 AOP를 사용할 수 있다.
AOP에서 Aspect는 여러 개의 Advice와 여러 개의 Pointcut의 결합체를 의미하는 용어다.
Aspect = Advice들 + Pointcut들
위에서 설명했듯이 Pointcut은 [어디에(Where)]를 의미한다. Advice는 [언제(When), 무엇을(What)]를 의미한다.
결국 Aspect는 [언제(When), 어디에(Where), 무엇을(What)]이 된다.
AOP 사용
pom.xml 설정
<properties>
<java-version>11</java-version>
<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
<org.aspectj-version>1.9.0</org.aspectj-version>
<org.slf4j-version>1.7.25</org.slf4j-version>
...
</properties>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
AOP 활성화
1) 자바 기반 설정 방식: @EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.example")
@EnableAspectJAutoProxy
@MapperScan(basePackages={"com.example.mapper"})
public class RootConfig{
//생략
}
2) XML 기반 설정 방식: <aop:aspectj-autoproxy>
root-context.xml 네임스페이스에 'aop'와 'context'를 추가해준다.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
smlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd">
<context:component-scan base-package="com.example.service" />
<context:component-scan base-package="com.examle.aop/>
<aop:aspectj-autoproxy />
<!-- 생략 -->
</beans>
Aspect 구현
@Aspect
@Component
public class MethodStartLoggingAspect{
@Before("execution(* *..*test.*(..))")
public void startLog(JoinPoint joinPoint) {
System.out.println("메소드 시작: " + joinPoint.getSignature());
}
}
- @Aspect
애스펙트로 식별되도록 클래스에 @Aspect 어노테이션을 붙여준다. - @Component
컴포넌트 스캔 대상이 되어 DI 컨테이너에서 관리되도록 @Component 어노테이션을 붙여준다. - @Before()
Before 어드바이스로 식별되도록 붙여준다. 지정식은 포인트컷 표현식으로 어드바이스가 적용될 대상을 정의한다.
위 코드에서는 이름이 test로 끝나는 클래스의 모든 public 메소드를 대상으로 지정되었다. - joinPoint.getSignature()
JoinPoint 객체를 통해 실행 중인 메소드의 정보를 확인할 수 있다.
ProceedingJoinPoint 메소드
Advice에서 사용할 공통 기능 메소드는 대부분 파라미터로 전달받은 ProceedingJoinPoint의 proceed() 메소드를 호출하면 된다.
※ProceedingJoinPoint 인터페이스가 제공하는 메소드
Signature getSignature() | 호출되는 메소드에 대한 정보를 구한다. |
Object getTarget() | 대상 객체를 구한다. |
Object[ ] getArgs() | 파라미터 목록을 구한다. |
※Signature 인터페이스가 제공하는 메소드
String getName() | 호출되는 메소드의 이름을 구한다. |
String toLongString() | 호출되는 메소드를 완전하게 표현한 문장을 구한다. (메소드의 리턴 타입, 파라미터 타입이 모두 표시된다.) |
String toShortString() | 호출되는 메소드를 축약해서 표현한 문장을 구한다. (기본 구현은 메소드의 이름만을 구한다.) |
Aspect 어노테이션 적용 순서
@Aspect
@Order(1)
public class stopTimeAspect{
...
}
@Aspect
@Order(2)
public class cacheAspect{
...
}
@Order 어노테이션의 값이 작으면 먼저 적용되고 크면 나중에 적용된다.
즉 위에서는 stopTimeAspect가 먼저 적용되고 cacheAspect가 나중에 적용됨을 알 수 있다.
XML기반 어드바이스 정의
//기존
@Aspect
public class MyAspect{
@Before("execution( * runSomething())")
public void before(JointPoint joinPoint){
//실행할 코드
}
}
//변경
//POJO & XML 기반 - 스프링 프레임워크에 종속되지 않음
public class MyAspect{
public void before(JointPoint joinPoint){
//실행할 코드
}
}
XML 기반으로 하였기 때문에 @Aspect 어노테이션과 @Before 어노테이션이 사라졌다.
따라서 MyAspect.java 파일은 스프링 프레임워크에 의존하지 않는 POJO가 되었다.
스프링 설정 파일(aop.xml)
<!--기존-->
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
smlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop.MyAspect" />
<bean id="boy" class="aop.Boy" />
<bean id="girl" class="aop.Girl" />
</beans>
<!--변경-->
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
smlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop.MyAspect" />
<bean id="boy" class="aop.Boy" />
<bean id="girl" class="aop.Girl" />
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut="execution(* runSomething())" />
</aop:aspect>
</aop:config>
</beans>
예제 코드: After 어드바이스
POJO & XML 파일
MyAspect.java
import org.aspectj.lang.JoinPoint;
public class MyAspect {
public void before(JoinPoint joinPoint) {
System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
public void lockDoor(JoinPoint joinPoint) {
System.out.println("열쇠로 문을 잠그고 집을 나간다.");
}
}
aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop.MyAspect" />
<bean id="boy" class="aop.Boy" />
<bean id="girl" class="aop.Girl" />
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut="execution(* runSomething())" />
<aop:after method="lockDoor" pointcut="execution(* runSomething())" />
</aop:aspect>
</aop:config>
</beans>
xml 기반의 설정 파일을 다음과 같이 리팩터링 해주는 것이 프로그래밍적으로 더 좋다.
... 생략 ...
<aop:config>
<aop:pointcut expression="execution(* runSomething())" id="pc" />
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut-ref="pc" />
<aop:after method="lockDoor" pointcut-ref="pc" />
</aop:aspect>
</aop:config>
어노테이션(@) 기반
MyAspect.java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyAspect {
@Before("execution(* runSomething())")
public void before(JoinPoint joinPoint) {
System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
@After("execution(* runSomething())")
public void lockDoor(JoinPoint joinPoint) {
System.out.println("열쇠로 문을 닫고 집에서 나간다");
}
}
aop.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop.MyAspect" />
<bean id="boy" class="aop.Boy" />
<bean id="girl" class="aop.Girl" />
</beans>
어노테이션(@) 기반에서도 중복되는 부분을 리팩터링 해주는 것이 프로그래밍적으로 더 좋다.
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MyAspect {
@Pointcut("execution(* runSomething())")
private void pc() {
// 이곳은 무시해도 무방하다. 여기에 무엇을 적어도 무시된다.
}
@Before("pc()")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
@After("pc()")
public void lockDoor(JoinPoint joinPoint) {
System.out.println("주인님 나갔다: 어이 문 잠가!!!");
}
}
'Spring Framework' 카테고리의 다른 글
Spring IoC 컨테이너 계층구조 및 구성(웹 어플리케이션) (0) | 2022.08.20 |
---|---|
Spring 스프링 컨테이너(IoC 컨테이너)와 빈(Bean) (0) | 2022.08.20 |
Spring 스프링의 핵심 이해: IoC/DI, AOP, PSA (0) | 2022.08.17 |
Spring 스프링이란? ( + POJO ) (0) | 2022.08.17 |
Spring 스프링(Spring) 설치하기 (feat. Eclipse) (0) | 2022.07.27 |