프로그래밍 일기 — Aspect

배우는 자(Learner Of Life)
27 min readOct 1, 2023

--

관점(Aspect)에 따른 프로그래밍

프로그래밍은 관점(Aspect)의 예술이다(1).

이번 프로젝트에서 사용했던 개념 중 하나가 Aspect Oriented Programming(AOP)라는 것이 었다. Aspect란 영어로 “관점", “측면"을 이야기한다. 예를 들어 건물을 짓는 방식은 건설사와 고객사가 원하는 것, 즉 그들이 바라보는 관점에 따라 차이가 있을 수 있다. 고객사는 더 저렴한 건설방식 저비용 고효율을 원하지만, 건설사는 책임 소재 때문에 비용보다는 안전성을 최우선으로 고려해야할 수 있다. 이 두 집단이 서로의 요구사항을 만족하면서 타협하며 건설이 이루어진다.

프로그래밍도 마찬가지다. 내가 무엇을 가장 중요하게 여기는지, 그 관점에 따라서 프로그래밍의 설계 방식이 달라진다. 프로그램의 안정성과 유지 보수성을 최우선으로 삼을 것인지, 아니면 프로그램이 서버에 최소한의 부하가 가도록 설계를 할 것인지, 등등 여러가지 관점들이 있을 수 있다. 그런 관점에 따라서 프로그래밍을 설계하는 방법론 중 가장 유명한 것이 AOP(Aspect Oriented Programming)이라 할 수 있다.

이번 글에서는 이 AOP 개념에 대해 좀 더 자세하게 파헤쳐 보기로한다.

Aspect Oriented Programming(AOP) 기초(2)

Spring을 활용한 프로그래밍에서 아주 강력한 도구로써 매우 유용하다. 최근에는 AspectJ Annotation(3)을 활용해 Spring AOP로 개발을 할 수 있게 되었는데, 이 글에서는 XML 설정에 기반한 Spring AOP를 위주로 다루어 보기로 하겠다.

AOP는 여러가지가 복잡하게 얽힌 문제들을 하나씩 나누어 모듈화(module)시키는 방식으로 해결하려는 프로그래밍 패러다임이다. 기존 코드에 추가적인 행위(메서드)를 더하는 방식으로, 기존 코드의 수정은 피하는 전략이라고 할 수 있다. 혹은 새로운 코드에 새로운 행위를 정의하는 방식으로도 이루어진다.

0. Dependency 추가

모든 프레임워크가 그렇듯이 Build.gradle 상에서 dependency를 추가해 주어야한다. 아래와 같이 AOP 프레임워크를 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-aop' 

1. AOP 개념 및 용어 이해

다음으로 AOP의 개념 및 용어를 이해할 필요가 있다. 프로그램이 실행되는 과정에서 여러가지 모듈들을 각각 Pointcut으로 나눈다. 이 것들을 각각 프로그램의 기능적 관점(Aspect)에 따라 묶어 Join Points를 만든다. Advice는 특정 Joinpint에서 Aspect에 의한 행동이라고 볼 수 있다(즉, 행동을 정의하는 부분). 구체적으로 시각화 하자면 아래와 같다.

AOP의 개념 및 용어의 의미를 한눈에 보여주는 그림(4)

2. Business 객체 이해

Business 객체란 일반적인 비즈니스 로직을 담은 클래스를 말한다. 예를 들어 아래처럼 두 가지 숫자를 더하는 비즈니스 로직을 구현한 Business 객체가 있다고 가정하자. 이 클래스는 특별한 Spring Annotation이 없다.

// Business 객체 예제
public class SampleAdder {
public int add(int a, int b) {
// 비즈니스 로직은 두 숫자를 더하는 것이다.
return a + b;
}
}

3. Aspect 이해

Aspect(관점)이란 여러 클래스를 한 면으로 잘라 문제를 세분화(모듈화)하는 개념이다. 단일화된 로깅(unified-logging)이 이러한 Aspect의 좋은 예라 볼 수 있다.

아래 에제에서는 Object 라는 한 타입의 인자나 로그(logs)만을 받는 afterReturn 이라는 이름의 메서드를 가진 간단한 Java클래스를 정의했다. AdderAfterReturnAspect 역시 표준화된 Java 클래스이며, 그 어떤 Spring Annotation도 사용하지 않는다.

// Aspect의 예제 (단일화된 로거, unified-logger)
public class AdderAfterReturnAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public void afterReturn(Object returnValue) throws Throwable {
logger.info("value return was {}", returnValue);
}
}

4. Joinpoint 이해

Joinpoint는 이러한 Aspect를 비즈니스 로직과 연결(wire)할 수 있는 방법이라고 볼 수 있다. 예를 들어 메서드의 실행이나 에러에 대한 처리같은 것이다. Spring AOP에서 Joinpoint는 항상 메서드의 실행을 의미한다.

5. Pointcut 이해

Advice와 매칭되는 지를 알려주는 입언(predicate, 논리적으로 긍정인지 부정인지를 결정하는 것)으로써, 특정 Joinpoint에 Aspect로 적용된다. Pointcut 표현은 보통 Advice와 함께 관련을 짓는 경우가 많은데, Advice는 특정 Pointcut과 매칭되는 그 어떤 Joinpoint에서도 동작한다.

6. Advice 이해

Advice란 특정 Joinpoint에서 Aspect에 의해 행해지는 행위를 말한다. Advice에는 around , before , after 등의 타입이 존재한다. Spring에서 Advice는 인터셉터(interceptor)로써 모델이 되는데, 특정 Joinpoint의 주변에서 인터셉터의 사슬(chain)을 유지한다.

7. Business 객체와 Aspect의 연결

아래 예제에서는 Business 객체를 Aspect와 After-Returning advice를 통해 연결하는 것을 보여준다. 아래 예제는 <beans> 태그 내에서 표준 Spring 설정을 한 부분을 보여준다. simpleAdder 라는 단순한 형태의 bean을 생성했으며, 이는 Business 객체의 인스턴스를 표현한다. 또한 AdderAfterReturnAspect라는 Aspect의 인스턴스를 생성했다. 물론 이렇게 XML 형태의 설정이 유일한 방법은 아니다. AspectJ Annotation 역시 활용될 수 있다.

aop:config 태그는 AOP관련 설정을 정의하는데 활용될 수 있다. 이 태그내에서 Aspect를 표현하는 클래슬르 정의할 수 있다. 이후에는 이전에 생성한 doAfterReturningAspect 라는 Aspect bean을 참조하도록 했다. 다음으로 pointcut 태그를 활용해 Pointcut을 정의했다. 아래 예제에서 사용된 Pointcut은 execution(* org.example.logger.SampleAdder+.*(..)) 로써, “그 어떤 수의 인자(argument)들을 받고 그 어떤 타입의 값을 리턴하는, SampleAdder 클래스의 메서드에 Advice를 적용하라”는 의미다. 즉, 입출력 값을 구분하지 않고 SampleAdder 클래스에 속하는 모든 메서드에 대해 Advice를 적용한다는 의미다.

이후에는 어떤 advice를 적용할지를 정의한다. 아래 예제에서는 after-returning advice를 적용하였다. 이는 AdderAfterReturnAspect 라는 Aspect에서, 속성 메서드(attribute method)를 통해 정의한 afterReturn 이라는 메서드를 실행하여 정의하였다.

이 Advice는 Aspect내에서 Object 타입의 매게변수 한 개 만을 받는다. 이 매게변수는 타깃 메서드의 호출 이전/이후에 우리가 무엇을 할지 결정할 수 있게 한다. 아래 예제에서는 메서드 실행 후 리턴 값을 로깅하도록 되어있다. Spring AOP는 Annotation 기반 설정을 통해 여러 타입의 Advice를 지원한다. 더 많은 예제는 참조 (5) , (6)에서 읽어볼 수 있다.

<bean id="sampleAdder" class="org.example.logger.SampleAdder" />
<bean id="doAfterReturningAspect"
class="org.example.logger.AdderAfterReturnAspect" />
<aop:config>
<aop:aspect id="aspects" ref="doAfterReturningAspect">
<aop:pointcut id="pointCutAfterReturning" expression=
"execution(* org.example.logger.SampleAdder+.*(..))"/>
<aop:after-returning method="afterReturn"
returning="returnValue" pointcut-ref="pointCutAfterReturning"/>
</aop:aspect>
</aop:config>

AspectJ (7)

AOP에 대한 개념을 리뷰했으니, AspectJ Annotation에 대해서 알아볼 시간이다. AspectJ는 Java언어의 Extensions을 활용하여 프로그램을 만들면서 생기는 염려사항(concerns)들의 서로 복잡하게 얽힌 관계를 풀어주면서, 각각 독립적으로 분리해 줄 수 있다.

AspectJ 프로그램을 실행하려면, 클래스 경로(classpath)가 사용하려는 클래스 및 Aspect, 그리고 AspectJ의 runtime 라이브러리인 aspectjrt.jar를 포함해야한다.

또한 AspectJ의 런타임 라이브러리와 함께, aspectjweaver.jar 파일을 포함해야 advice를 Java 클래스에 로딩 시간동안 불러올 수 있다.

dependency들은 build.gradle 상에서 아래와 같이 정의한다.

  // aspectj runtime
implementation "org.aspectj:aspectjrt:1.9.6"
// aspectj weaver
runtimeOnly 'org.aspectj:aspectjweaver:1.9.20.1'

1. Aspect 생성

AspectJ는 AOP의 구현체(implementation)을 제공하며, 아래와 같은 3가지 코어 개념이 존재한다. 이는 위 AOP의 설명과도 일치하는 부분이다.

  • Join Point
  • Pointcut
  • Advice

예제를 통해 위 개념들이 어떻게 구현되어 있는지 확인해보자. 예를 들어서 유저의 계좌 잔액을 확인하고 싶은 프로그램을 만든다고 하자. 먼저 Account 클래스를 생성하고 특정 잔약을 지정한다, 그리고 금액을 인출 하는 행위를 수행하는 메서드를 선언한다.

// 계좌 잔액을 확인하고, 금액을 인출할 수 있는 메서드를 포함한 클래스
public class Account {
// 계좌 잔액을 나타내는 필드
int balance = 20;

// 계좌에서 현금 인출을 할 수 있는 메서드
public boolean withdraw(int amount) {
if (balance < amount) {
return false;
}
balance = balance - amount;
return true;
}
}

위 간단한 클래스에 정의되어 있는 계좌 정보를 로깅할 수 있고, 계좌의 잔액을 검증할 수 있는 AccountAspect.aj 파일을 생성한다고하자(AspectJ 파일은 .aj 확장자를 갖는다.

금액을 인출할 수 있는 메서드( callWithDraw )에서는 pointcut을 활용했는데, 여기서 정의된 pointcut을 참조하는 3개의 advice를 만들었다. 아래 예제를 이해하기 위해서 조금 더 개념적으로 이해해야할 것들이 있다.

  • Aspect: 문제의 모듈화된 것으로써, 여러 객체를 단면으로 자르는 행위와 같다. 각 Aspect는 특정 단면 기능에 집중한다.
  • Join point: 스크립트의 실행 동안 존재하는 지점(point)들, 예를 들어 메서드의 실행이나 속성(property)에 대한 접근
  • Advice: 특정 join point에서 aspect에 의해 행해지는 행위
  • Pointcut: join point와 매칭되는 정규표현식(regular expression)이며, Advice는 pointcut 표현과 관계되어있다. pointcut과 매칭되는 그 어떤 join point에서 실행가능하다.

위 개념에 대한 더 많은 정보는 참조 (8)에서 확인가능하다.

// 계좌 잔액을 검증하고, 계좌 정보를 로깅할 수 있는 Aspect
public aspect AccountAspect {

final int MIN_BALANCE = 10;

// 금액인출 pointcut 메서드 활용
pointcut callWithDraw(int amount, Account acc) :
call(boolean Account.withdraw(int)) && args(amount) && target(acc);

before(int amount, Account acc) : callWithDraw(amount, acc) {
}

boolean around(int amount, Account acc) :
callWithDraw(amount, acc) {
if (acc.balance < amount) {
return false;
}
return proceed(amount, acc);
}

after(int amount, Account balance) : callWithDraw(amount, balance) {
}
}

다음으로 aspectjweaver 를 활용해 Aspect를 코드에 적용해야한다. 대표적으로 AspectJ가 지원하는 3가지 방법이 있는데, compile-time weaving, post-compile weaving, 그리고 load-time weaving이 있다.

3. Weaving 수행

Compile-Time Weaving

가장 쉬운 방법이라 할 수 있다. Aspect에 대한 소스 코드와 Aspect를 사용할 코드를 동시에 가지고 있는 상황이라면, AspectJ 컴파일러는 소스로부터 컴파일을 할 것이며, 연결된 클래스 파일을 출력할 것이다. 이후, 코드를 실행하면, 연결 프로세스 출력 클래스는 JVM에 일반적인 Java클래스로 로딩될 것이다.

AspectJ 개발 툴(9)은 AspectJ 컴파일러를 함께 제공하기 때문에, 많은 사람들이 다운로드 받는다. 이 것의 가장 중요한 기능 중 하나는 단면의 염려(crosscutting concerns)에 대해 시각화를 가능하게 해주어, pointcut의 세부 사항을 디버깅하는 것을 용이하게 해 준다는데 있다. 코드가 배포되기 전에 발생할 수 있는 것을 미리 시각화 해 볼 수 있다는 장점이 있다.

아래와 같이 Mojo의 AspectJ Maven Plugin을 활용하여 AspectJ의 Aspect를 AspectJ 컴파일러를 사용해 클래스와 연결할 수 있다. AspectJ 컴파일러에 대한 더 많은 정보는 참조(10)에서 확인 가능하다.

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.14.0</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8 </encoding>
</configuration>
<executions>
<execution>
<goals>
<!-- use this goal to weave all your main classes -->
<goal>compile</goal>
<!-- use this goal to weave all your test classes -->
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>

이제 준비가 되었으니 AspectJ의 Compile-Time Weaver 방법으로 Account 클래스에 대해 테스트 케이스를 작성해 볼 수 있다.

// Compile-Time Weaver를 활용한 테스트 코드 (Account 클래스 대상)
public class AccountTest {
private Account account;

// 테스트 전 수행할 것 정의
@Before
public void before() {
account = new Account();
}

// 테스트 성공시 수행할 행위 정의
@Test
public void given20AndMin10_whenWithdraw5_thenSuccess() {
assertTrue(account.withdraw(5));
}

// 테스트 실패시 수행할 행위 정의
@Test
public void given20AndMin10_whenWithdraw100_thenFail() {
assertFalse(account.withdraw(100));
}
}

위 테스트 케이스를 실행할 경우, weaving 성공시 아래와 같은 결과를 볼 수 있다.

[INFO] Join point 'method-call
(boolean com.example.aspectj.Account.withdraw(int))' in Type
'com.example.aspectj.test.AccountTest' (AccountTest.java:20)
advised by around advice from 'com.example.aspectj.AccountAspect'
(AccountAspect.class:18(from AccountAspect.aj))

[INFO] Join point 'method-call
(boolean com.example.aspectj.Account.withdraw(int))' in Type
'com.example.aspectj.test.AccountTest' (AccountTest.java:20)
advised by before advice from 'com.example.aspectj.AccountAspect'
(AccountAspect.class:13(from AccountAspect.aj))

[INFO] Join point 'method-call
(boolean com.example.aspectj.Account.withdraw(int))' in Type
'com.example.aspectj.test.AccountTest' (AccountTest.java:20)
advised by after advice from 'com.example.aspectj.AccountAspect'
(AccountAspect.class:26(from AccountAspect.aj))

2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Balance before withdrawal: 20
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Withdraw ammout: 5
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Balance after withdrawal : 15
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Balance before withdrawal: 20
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Withdraw ammout: 100
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Withdrawal Rejected!
2016-11-15 22:53:51 [main] INFO com.example.aspectj.AccountAspect
- Balance after withdrawal : 20

Post-Compile Weaving

Binary Weaving이라고도 하며, 기 존재하는 클래스 및 JAR 파일을 weaving하기 위해 사용한다. Compile-Time Weaving처럼 weaving에 활용된 Aspect는 소스 코드 혹은 Binary형태로 존재하며, 이러한 형태는 Aspect에 의해 weaving된 형태일 수 있다.

Mojo의 AspectJ Maven Plugin으로 이를 수행하려면, plugin 설정에 weave하고자하는 모든 JAR 파일을 설정해 주어야한다.

<configuration>
<weaveDependencies>
<weaveDependency>
<groupId>org.agroup</groupId>
<artifactId>to-weave</artifactId>
</weaveDependency>
<weaveDependency>
<groupId>org.anothergroup</groupId>
<artifactId>gen</artifactId>
</weaveDependency>
</weaveDependencies>
</configuration>

Weaving하기 위해 필요한 클래스들을 담은 JAR 파일들은 반드시 Maven Project에서<dependencies/> 내에 정의 되어야하며, AspectJ Maven Plugin에서 <configuration><weaveDependencies/> 로 리스트되어야한다.

Load-Time Weaving

Load-time weaving은 단순히 binary weaving을 클래스 로더가 클래스 파일을 불러오고 JVM에 클래스를 정의할 때까지 미룬 후에 실행하는 것을 말한다. 이를 지원하기 위해서는 한 개 이상의 “weaving class loader”라는 것이 필요한데, 이는 runtime 환경에서 직접 제공하는 것을 사용하거나 weaving agent에서 활성화하는 방법을 사용할 수 있다.

Load-time weaving을 활성화하기위해 클래스 로딩 과정에서 AspectJ agent를 활용할 수 있다. JVM에 로딩되기 전에 그 어떤 타입도 weaving할 수 있다.-javaagent:pathto/aspectjweaver.jar 명령어를 통해 javaagent 옵션을 JVM에 명기하거나 Maven plugin을 활용해 javaagent 를 설정하는 방식으로 할 수 있다.

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<argLine>
-javaagent:"${settings.localRepository}"/org/aspectj/
aspectjweaver/${aspectj.version}/
aspectjweaver-${aspectj.version}.jar
</argLine>
<useSystemClassLoader>true</useSystemClassLoader>
<forkMode>always</forkMode>
</configuration>
</plugin>

Configuration Weaver

AspectJ의 load-time weaving agent는 aop.xml 파일을 사용하여 설정할 수 있다. 이는 META_INF 경로내 하나 이상의 aop.xml 파일을 classpath에서 찾는데, 모든 파일의 컨텐츠를 고려하여 weaver 설정을 결정한다. aop.xml 파일은 아래와 같은 2가지 키 섹션을 가진다.

  • Aspect: weaver에 대한 한 개 이상의 Aspect를 정의하고, weaving 과정에서 사용되는 Aspects들을 제어한다. Aspect 요소는 옵션으로 한 개 이상의 include 혹은 exclude 요소를 가질 수 있다(기본적으로 모든 정의된 Aspect들은 weaving에 사용된다.).
  • Weaver: weaver에 대한 weaving 옵션을 정의하며 weaving되어야할 유형들의 모음을 지정한다. 만약 include 요소가 지정되지 않는다면, weaver에 대한 모든 보이는 타입들이 weave될 것이다.

아래는 Aspect를 Weaver에 설정하는 예제이다. AccountAspect 를 가리키는 Aspect가 설정되는데, AspectJ는 오직 com.example.aspectj 의 소스코드만을 weaving한다.

<aspectj>
<aspects>
<aspect name="com.example.aspectj.AccountAspect"/>
<weaver options="-verbose -showWeaveInfo">
<include within="com.example.aspectj.*"/>
</weaver>
</aspects>
</aspectj>

4. Aspect에 대한 Annotation

이미 익숙해진 AspectJ의 코드 기반 선언에 추가적으로 AspectJ5는 Annotation 기반의 Aspect 선언도 지원한다. 이는 비공식적으로 @AspectJ Annotation이라는 개발 스타일로 일컬어 지기도 한다.

예를 들어 아래와 같이 Annotation을 활용할 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Secured {
public boolean isLocked() default false;
}

@Secure Annotation을 사용하여 메서드를 활성화 및 비활성화 할 수 있다.

public class SecuredMethod {

@Secured(isLocked = true)
public void lockedMethod() {
}

@Secured(isLocked = false)
public void unlockedMethod() {
}
}

다음으로 AspectJ Annotation 스타일을 사용해 Aspect를 추가하고, @Secured Annotation의 속성에 기반한 허가(permission)을 체크할 수 있다.

@Aspect
public class SecuredMethodAspect {
@Pointcut("@annotation(secured)")
public void callAt(Secured secured) {
}

@Around("callAt(secured)")
public Object around(ProceedingJoinPoint pjp,
Secured secured) throws Throwable {
return secured.isLocked() ? null : pjp.proceed();
}
}

AspectJ Annotation 스타일에 대한 더 많은 정보는 참조(11)에서 확인할 수 있다. 이후 클래스 및 Aspect를 load-time weaver를 활용하여 weaving할 수 있으며, aop.xmlMETA-INF 폴더 내에 넣을 수 있다.

<aspectj>
<aspects>
<aspect name="com.example.aspectj.SecuredMethodAspect"/>
<weaver options="-verbose -showWeaveInfo">
<include within="com.example.aspectj.*"/>
</weaver>
</aspects>
</aspectj>

마지막으로 단위 테스트를 추가하여 결과를 체크한다.

@Test
public void testMethod() throws Exception {
SecuredMethod service = new SecuredMethod();
service.unlockedMethod();
service.lockedMethod();
}

위 테스트를 실행시키면 콘솔 상에서 출력값을 확인해 Aspect와 클래스가 소스 코드에서 성공적으로 weaving 되었는지를 확인할 수 있다.

[INFO] Join point 'method-call
(void com.example.aspectj.SecuredMethod.unlockedMethod())'
in Type 'com.example.aspectj.test.SecuredMethodTest'
(SecuredMethodTest.java:11)
advised by around advice from 'com.example.aspectj.SecuredMethodAspect'
(SecuredMethodAspect.class(from SecuredMethodAspect.java))

2016-11-15 22:53:51 [main] INFO com.example.aspectj.SecuredMethod
- unlockedMethod
2016-11-15 22:53:51 [main] INFO c.b.aspectj.SecuredMethodAspect -
public void com.example.aspectj.SecuredMethod.lockedMethod() is locked

AspectJ에 대한 더 많은 정보는 AspectJ 공식 홈페이지(참조(12))에서 확인 가능하며, 이 글에서 사용된 소스코드는 참조(13)에서 확인 가능하다.

참조:

(1) https://pixabay.com/photos/modern-office-architecture-1044807/

(2) https://www.baeldung.com/spring-aop

(3) https://www.baeldung.com/aspectj

(4) https://www.baeldung.com/wp-content/uploads/2017/11/Program_Execution.jpg

(5) https://www.baeldung.com/spring-aop-advice-tutorial

(6) https://www.baeldung.com/spring-aop-pointcut-tutorial

(7) https://www.baeldung.com/aspectj

(8) https://eclipse.org/aspectj/doc/next/progguide/semantics-pointcuts.html

(9) https://eclipse.org/aspectj/downloads.php#ides

(10) http://www.mojohaus.org/aspectj-maven-plugin/ajc_reference/standard_opts.html

(11) https://eclipse.org/aspectj/doc/released/adk15notebook/annotations-aspectmembers.html

(12) https://eclipse.dev/aspectj/

(13) https://github.com/eugenp/tutorials/tree/master/spring-aop

--

--

배우는 자(Learner Of Life)
배우는 자(Learner Of Life)

Written by 배우는 자(Learner Of Life)

배움은 죽을 때까지 끝이 없다. 어쩌면 그게 우리가 살아있다는 증거일지도 모른다. 배움을 멈추는 순간, 혹은 배움의 기회가 더 이상 존재하지 않는 순간, 우리의 삶은 어쩌면 거기서 끝나는 것은 아닐까? 나는 배운다 그러므로 나는 존재한다. 배울 수 있음에, 그래서 살아 있음에 감사한다.

No responses yet