| Advice형태 | 설명 |
|---|---|
| Before | Join Point 앞에서 실행할 Advice |
| After | Join Point 뒤에서 실행할 Advice |
| AfterReturning | Join Point가 완전히 정상 종료한다음 실행하는 Advice |
| Around | Join Point 앞과 뒤에서 실행되는 Advice |
| AfterThrowing | Join Point에서 예외가 발생했을 때 실행되는 Advice |

| Spring AOP | AspectJ | |
|---|---|---|
| 구현 | 순수 자바 | 자바 언어 확장 사용 |
| Goal | Simple Solution | Complete Solution |
| 특징 | 별도의 컴파일 과정 불필요 | AspectJ compiler(ajc)가 필요 |
| Weaving | Runtime weaving | compile-time, post-compile, load-time weaving 지원 |
| 대상 | Spring Container에 의해서 관리되는 Spring Bean | 모든 객체들 |
| JoinPoint | Method 실행시에만 가능 | Method 실행시, Constructor 실행시, field 참조시, field 할당시 등등 |
| 성능 | 비교적 느림 | 비교적 빠름 |
Spring AOP Proxies

먼저 Spring AOP는 Proxy의 메커니즘을 기반으로 AOP Proxy를 제공하고 있다.

다음 그림처럼 Spring AOP는 사용자의 특정 호출 시점에 IoC 컨테이너에 의해 AOP를 할 수 있는 Proxy Bean을 생성해준다. 동적으로 생성된 Proxy Bean은 타깃의 메서드가 호출되는 시점에 부가기능을 추가할 메서드를 자체적으로 판단하고 가로채어 부가기능을 주입해주는데 이처럼 호출 시점에 동적으로 위빙을 하여 런타임 위빙(Runtime Weaving)이라 한다.
따라서 Spring AOP는 런타임 위빙의 방식을 기반으로 하고 있으며, Spring에선 런타임 위빙을 할 수 있도록 상황에 따라 JDK Dynamic Proxy와 CGLIB 방식을 통해 Proxy Bean을 생성을 해준다.
Spring은 AOP Proxy를 생성하는 과정에서 자체 검증 로직을 통해 타깃의 인터페이스 유무를 판단한다.
Target이란 횡단기능(Advice)이 적용될 객체(Object)를 뜻한다.

이때 만약 타깃이 하나 이상의 인터페이스를 구현하고 있는 클래스라면 JDK Dynamic Proxy의 방식으로 생성되고 인터페이스를 구현하지 않은 클래스라면 CGLIB의 방식으로 AOP 프록시를 생성해준다.
우선 JDK Dynamic Proxy란 Java의 리플렉션 패키지에 존재하는 Proxy라는 클래스를 통해 생성된 Proxy 객체를 의미한다.
리플랙션의 Proxy 클래스가 동적으로 Proxy를 생성해준다하여 우리가 아는 JDK Dynamic Proxy라 불리는 것이다. 이 클래스를 사용하여 Proxy를 생성하기 위해선 몇 가지 조건이 있지만, 아무래도 그 중 핵심은 타깃의 인터페이스를 기준으로 Proxy를 생성해준다는 점이다.
무엇보다 Spring AOP는 JDK Dynamic Proxy를 기반으로 AOP 기술을 구현했을 만큼, JDK Dynamic Proxy가 어떻게 Proxy를 생성하는지에 대한 부분은 Spring AOP를 통해 Aspect를 구현한다면 꼭 짚고 넘어가야할 부분이다.

다음과 같이 Proxy를 생성하는 과정에서 핵심적인 부분은, 무엇보다 인터페이스를 기준으로 Proxy 객체를 생성해 준다는 점이다. 따라서 구현체는 인터페이스를 상속받아야하고, @Autowired를 통해 생성된 Proxy Bean을 사용하기 위해선 반드시 인터페이스의 타입으로 지정해줘야 된다.
다른 관점에서 보자면 JDK Dynamic Proxy는 Proxy 패턴의 관점을 구현한 구현체라 할 수 있다.
이 Proxy 패턴은 점근제어의 목적으로 Proxy를 구성한다는 점도 중요하지만, 무엇보다 사용자의 요청이 기존의 타깃을 그대로 바라볼 수 있도록 타깃에 대한 위임코드를 Proxy 객체에 작성해줘야 한다. 생성된 Proxy 객체의 타깃에 대한 위임코드는 바로 InvocationHandler에 작성해줘야 한다.

CGLIB는 Code Generator Library의 약자로, 클래스의 바이트코드를 조작하여 Proxy 객체를 생성해주는 라이브러리이다.
Spring은 CGLIB을 사용하여 인터페이스가 아닌 타깃의 클래스에 대해서도 Proxy를 생성해주고 있는데 CGLIB은 Enhancer라는 클래스를 통해 Proxy를 생성할 수 있다.
다음과 같이 CGLIB은 타깃 클래스를 상속받아 다음 그림과 같이 Proxy를 생성해준다.

이 과정에서 CGLIB은 final 메서드 또는 클래스에 대해 재정의를 할 수 없으므로 Proxy를 생성할 수 없다는 단점이 있지만, CGLIB은 바이트 코드로 조작하여 Proxy를 생성해주기 때문에 성능에 대한 부분이 JDK Dynamic Proxy보다 좋다.
성능의 차이의 근본적인 이유는 CGLIB은 타깃에 대한 정보를 제공받기 때문이다.
CGLIB은 성능이 좋긴 하지만, Spring은 JDK Dynamic Proxy를 기반으로 Proxy를 생성해주고 있다.
이러한 이유엔, 기존의 CGLIB은 3가지의 한계가 존재했기 때문이다.
Spring boot로 넘어가면서 다음의 3가지 한계가 개선이 되었고 CGLIB방식을 사용하게 되었다.
Exception Handling 이라고 하며, 잘못된 하나로 인해 전체 시스템이 무너지는 결과를 방지하기 위한 기술적인 처리이다. java에서는 예외와 에러도 객체로 처리한다.
예외가 주로 발생하는 원인
Throwable 클래스는 예외처리를 할 수 있는 최상위 클래스이다. Exception과 Error는 Throwable의 상속을 받는다.
개발자는 error가 발생했다는 사실을 알 수는 있다. 유명한 에러로는 다음과 같다.
Thread pool과 DB Connection pool은 공유되는 자원이다.
thread safe 하게 만드는 기본적인 방법은 공유자원을 만들지 않는 것이다.
@Bean과 StereoType Annotation은 서로 같은 bean이고 설정은 로직에 의존하면 안되고 로직은 설정에 의존해야 한다.
Aspect도 Bean으로 등록을 해야 ApplicationContext가 Weaving을 해줄 수 있다.
bean 내부에서 생성자 주입방식을 쓰고 주입되는 필드가 final일 경우 @InjectMocks가 동작하지 않는다.
테스타 데이터를 테스트 드라이버에 입력으로 넣고, 모듈을 호출하고 모듈 아래의 테스트 스텁들을 호출하여 연산을 하고, 모듈은 테스트 드라이버에 실행결과를 반환
테스트 드라이버
- 테스트 대상이 되는 모듈을 호출하여 준비한 테스트 데이터를 제공하고 모듈의 실행결과를 받는 모듈
- 일반적으로 상향식 테스트에서 아직 통합되지 않은 상위 컴포넌트의 동작을 시뮬레이션 하기 위헤 사용
테스트 스텁
- 호출되는 모듈의 개발이 완료되지 않은 경우, 호출하는 모듈을 시험하기 위해 생성한 더미 모듈
- 함수와 헤더 등의 코드 루틴만 정의하고 내부 코드는 제한적으로 구현하거나 구현하지 않는 경우가 많음
public interface Resource extends InputStreamSource {
boolean exists();
boolean isReadable();
boolean isOpen();
boolean isFile();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
ReadableByteChannel readableChannel() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String relativePath) throws IOException;
String getFilename();
String getDescription();
}
public interface InputStreamSource {
InputStream getInputStream() throws IOException;
}
UrlResource
class ConsoleLogProcessorTest {
@Test
void testUrlResource() throws Exception {
try (InputStream inputStream = new UrlResource("https://www.manty.co.kr").getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
ClassPathResource
@Test
void testClassPathResource() throws Exception {
try (InputStream inputStream = new ClassPathResource("data/test.txt").getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
FileSystemResource
@Test
void testFileSystemResource() throws Exception {
try (InputStream inputStream = new FileSystemResource("/etc/hosts").getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
PathResource
@Test
void testPathResource() throws Exception {
try (InputStream inputStream = new PathResource(Paths.get("/etc/hosts")).getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
) {
String line;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
ServletContextResource
InputStreamResource
ByteArrayResource
Class.getResource()는 결국 ClassLoader.getResource()에 위임(delegates)하게 된다. 때문에 두 개의 메서드는 실제로 매우 비슷하다. 하지만 첫 번째 메서드가 좀 더 좋다.
첫 번째 메서드가 더 좋은 이유는 ClassLoader.getResource() 메서드는 절대경로로 전부 적어줘야 해서 경로가 바뀌었을 때 전부 변경해줘야 하기 때문이다. 하지만 getClass()를 사용해서 getResource() 메서드를 사용하게 되면 현재 실행 클래스를 기준으로 찾기 때문에 해당 클래스 경로에 데이터를 넣어주거나 현재 클래스 이름.class.getResource()를 사용하여 오류를 피할 수 있다.
@Nullable
@NonNull
@NonNullApi
@NonNullFields
<configuration>
<property name="USER_HOME" value="/home/nhn" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${USER_HOME}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration>
java -DUSER_HOME="/home/nhn" MyApp
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
boolean acceptsProfiles(Profiles profiles);
}
public interface PropertyResolver {
boolean containsProperty(String key);
String getProperty(String key);
String getProperty(String key, String defaultValue);
// ..
}
Environment env = context.getBean(Environment.class);
System.out.println(env.getProperty("from"));
java command -d는 vm 옵션을 의미한다.
-Dspring.profiles.active=dev
@Configuration
public class MainConfig {
@Bean("envGreeter")
@Profile({"dev", "default"})
public Greeter devGreeter(){
return new Greeter() {
@Override
public boolean sayHello() {
System.out.println("Dev Hi!");
return true;
}
};
}
@Bean("envGreeter")
@Profile("real")
public Greeter realGreeter(){
return new Greeter() {
@Override
public boolean sayHello() {
System.out.println("Real Hi!");
return true;
}
};
}
}
출력하려는 값을 OutputStream 객체에 저장하기 위해 기본적으로 설정되어 있는 System.out의 값을 내가 사용하고 싶은 타입의 객체로 변경하는 방법
private static ByteArrayOutputStream outputMessage = new ByteArrayOutputStream();
System.setOut(new PrintStream(outputMessage));
System의 out은 PrintStream 타입이기 때문에 PrintStream 객체를 생성한 값으로 지정한다.
테스트 종료 후 정상적인 콘솔창 출력을 위해서 다시 되돌려놓는 과정이 필요하다.
System.setOut(System.out);