AOP 란 ❓
Spring의 핵심 개념 중 하나인 DI(의존성 주입)가 애플리케이션 모듈들 간의 결합도를 낮춘다면, AOP(Aspect-Oriented Programming)는 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는 부가 기능을 모듈화(어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것)하여 재사용할 수 있도록 지원하는 것을 말한다.
Aspect-Oriented Programming이란 단어를 번역하면 관점(관심) 지향 프로그래밍 이 되는데, 이것은 곧 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미이다.
부가기능 관점에서 바라보면 각각의 Service의 get 메서드를 호출하는 전후에 before과 after라는 메서드가 공통되는 것을 확인할 수 있다.
즉, AOP는 기존에 OOP에서 바라보던 관점을 다르게 하여 부가 기능적인 측면에서 바라봤을 때 공통된 요소를 추출하자는 것이다. 이때 가로(횡단) 영역의 공통된 부분을 잘라냈다고 하여, AOP를 크로스 컷팅(Cross-Cutting) 이라고 부르기도 한다.
OOP
: 비즈니스 로직의 모듈화
➡ 모듈화의 핵심 단위는 비즈니스 로직
AOP
: 인프라 혹은 부가기능의 모듈화
➡ 모니터링 및 로깅, 동기화, 오류 검사 및 처리, 성능 최적화(캐싱) 등
➡ 각각의 모듈들의 주 목적 외에 필요한 부가적인 기능들
OOP에선 공통된 기능을 재사용하는 방법으로 상속이나 위임을 사용하는데, 전체 애플리케이션에서 여기저기 사용되는 부가기능들을 상속이나 위임으로 처리하기에는 모듈화가 깔끔하게 이루어지기가 어렵다.
그래서 등장한 것이 AOP이다. 간단하게 한줄로 AOP를 정리해보자면, AOP는 공통된
기능을 재사용하는 기법이다. AOP의 장점으로는 애플리케이션 전체에 흩어진 공통 기능이 하나의 장소에서 관리되어 유지보수가 좋고, 핵심 로직과 부가 기능의 명확한 분리로, 핵심 로직은 자신의 목적 외에 사항들에는 신경쓰지 않는다는 점이 있다.
AOP 관련 용어
1) Join point
➡ 추상적인 개념 으로 advice가 적용될 수 있는 모든 위치를 말한다.
➡ 메서드 실행 시점, 생성자 호출 시점, 필드 값 접근 시점 등
➡ 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점
2) Pointcut
➡ 조인 포인트 중에서 advice가 적용될 위치를 선별하는 기능
➡ 스프링 AOP는 프록시 기반이기 때문에 조인 포인트가 메서드 실행 시점 뿐이 없고
포인트컷도 메서드 실행 시점만 가능
3) Target
➡ advice의 대상이 되는 객체 / Pointcut으로 결정
4) advice
➡ 실질적인 부가 기능 로직을 정의하는 곳
➡ 특정 조인 포인트에서 Aspect에 의해 취해지는 조치
5) Aspect
➡ advice + pointcut을 모듈화 한 것
➡ @Aspect와 같은 의미
6) Advisor
➡ 스프링 AOP에서만 사용되는 용어로 advice + pointcut 한 쌍
AOP를 코드로 구현한 결과는 아래와 같다.
@Aspect
@Component
public class SimpleAop {
// 어느 경로의 패키지에 적용시킬 것인지 지정
@Pointcut("execution(* com.example.aop.member..*.*(..))")
private void cut() {
System.out.println("컷");
}
// 조인 포인트, 어느 시점 즉 메서드가 실행되기 전을 지정
@Before("cut()")
public void before(JoinPoint joinPoint) {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
System.out.println(method.getName() + " 메소드 실행 전");
}
// 조인 포인트, 어느 시점 즉 메서드가 실행된 후를 지정
@AfterReturning(value = "cut()", returning = "returnObject")
public void afterReturning(JoinPoint joinPoint, Object returnObject) {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
System.out.println(method.getName() + " 메소드 실행 후");
if (returnObject != null) {
System.out.println(returnObject.getClass().getName() + " 반환");
} else {
System.out.println("반환값이 null입니다.");
}
}
}
🧐 스프링에서의 예외 처리
만약 Controller 에서 비지니스 로직을 처리하던 중 에러가 발생하면 어떻게 될까?
BasicErrorController 란 스프링 부트의 기본 예외처리 Controller 이다. 따라서, 별도의 설정을 하지 않았다면 예외가 발생했을 때 BasicErrorController로 예외처리 요청이 전달된다.
기본적으로 예외 발생 시 요청 전달 흐름은 아래와 같다.
WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> 컨트롤러(예외발생)
-> 인터셉터 -> 서블릿 -> 필터 -> WAS -> 필터 -> 서블릿 -> 인터셉터
-> 컨트롤러(BasicErrorController)
즉, 컨트롤러와 필터 그리고 인터셉터가 두 번씩 호출되는 것을 알 수 있고, 기본 에러는 클라이언트에게 status code 500
에 Internal server error
로만 응답하기 때문에, 클라이언트는 무슨 에러인지 알 수가 없다.
따라서, Spring은 에러 처리라는 공통 관심사를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다.
대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다. 그래서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.
스프링의 예외 처리 흐름
1) ExceptionHandlerExceptionResolver가 동작
예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사
컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면
ControllerAdvice로 넘어감
ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음
처리기로 넘어감
2) ResponseStatusExceptionResolver가 동작
@ResponseStatus가 있는지 또는 ResponseStatusException인지 검사
맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달되고, 서블릿이
BasicErrorController로 요청을 전달함
3) DefaultHandlerExceptionResolver가 동작
Spring의 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어감
4) 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달되고, 서블릿은
SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달함
💻 예외처리 구현하기
@ExceptionHandler
➡ 매우 유연하게 에러처리를 할 수 있는 방법을 제공한다.
➡ 에러 응답을 자유롭게 다룰 수 있다.
➡ 컨트롤러의 메소드에 해당 어노테이션을 적용할 수 있는데, 이는 전역으로 사용할 수
없다. 전역으로 처리하기 위해서는 @RestControllerAdvice(또는 @ControllerAdvice)
어노테이션을 사용해야 한다.
@RestControllerAdvice
➡ Spring 4.3부터 제공하는 애노테이션이다.
➡ @ExceptionHandler를 전역적으로 적용할 수 있게 해준다.
➡ @ControllerAdvice 와의 차이점은 에러 응답을 JSON으로 내려준다는 것이다.
➡ 어노테이션을 적용해 전역적으로 에러를 핸들링하는 Class를 만들어 사용한다.
// ✅ ErrorCode 클래스 : 에러 별 HttpStatus 와 에러 메시지를 정의
@Getter
public enum ErrorCode {
// 발생할 수 있는 에러들을 적어준다.
DUPLICATED_USER(HttpStatus.CONFLICT, "이미 존재하는 사용자입니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 토큰입니다."),
INVALID_PERMISIION(HttpStatus.UNAUTHORIZED, "권한이 없습니다."),
INVALID_INPUT(HttpStatus.BAD_REQUEST, "잘못 입력하셨습니다."),
WRONG_ADDR(HttpStatus.NOT_FOUND,"주소를 잘못입력했습니다.")
;
private final HttpStatus status;
private final String message;
ErrorCode(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
}
// ✅ ErrorResponse 클래스 : 클라이언트에게 전달할 에러의 응답 형태를 정의
@Builder
@Getter
@Setter
public class ErrorResponse {
private String code;
private String message;
}
// ✅ GlobalExceptionAdvise 클래스 : 전역 예외처리를 담당하는 클래스
@RestControllerAdvice
public class GlobalExceptionAdvise extends ResponseEntityExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public ResponseEntity errorHandler(SQLIntegrityConstraintViolationException e) {
e.printStackTrace(); // 에러 로그 추적
return makeResponseEntity(ErrorCode.DUPLICATED_USER);
}
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return makeResponseEntity(ErrorCode.INVALID_INPUT);
}
public ResponseEntity makeResponseEntity(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getStatus()).body(ErrorResponse.builder()
.code(errorCode.name())
.message(errorCode.getMessage())
.build());
}
}
로깅이란 ❓
로깅은 정보를 제공하는 일련의 기록인 로그(Log)를 생성하도록 하는 것을 말한다.
로깅 관련 프레임워크는 대표적으로 log4j, logback, log4j2, 그리고 그것을 통합해서 인터페이스로 제공하는 SLF4J 라이브러리가 있다.
로그의 종류는 아래와 같다. ( 레벨 순 )
1) error : 사용자 요청을 처리하는 중 발생한 문제
2) warn : 처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 문제
3) info : 로그인이나 상태 변경과 같은 정보성 메시지
4) debug :개발시 디버깅 목적으로 출력하는 메시지
5) trace : debug 보다 좀 더 상세한 메세지
특정 로그 레벨을 지정하면, 해당 로그 레벨의 상위 우선순위 로그가 모두 출력된다. 예를 들어, 로그 레벨을 info로 지정하면 info, warn, error 가 모두 출력된다.
로그 레벨을 설정하는 방법 : application.yml 파일 기준
logging:
level:
org.springframework.security: DEBUG
기존 GlobalExceptionAdvise 클래스에 추가한 결과
@RestControllerAdvice
public class GlobalExceptionAdvise extends ResponseEntityExceptionHandler {
✅ private final Logger log = LoggerFactory.getLogger(GlobalExceptionAdvise.class);
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public ResponseEntity errorHandler(SQLIntegrityConstraintViolationException e) {
// 출력 양식은 팀에서 상의하여 정해서 만들 수 있다.
✅ log.error("입력한 email이 중복되었습니다.");
return makeResponseEntity(ErrorCode.DUPLICATED_USER);
}
@Override
protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
// 출력 양식은 팀에서 상의하여 정해서 만들 수 있다.
✅ log.error("입력한 email 또는 password 가 잘못되었습니다.");
return makeResponseEntity(ErrorCode.INVALID_INPUT);
}
public ResponseEntity makeResponseEntity(ErrorCode errorCode) {
return ResponseEntity.status(errorCode.getStatus()).body(ErrorResponse.builder()
.code(errorCode.name())
.message(errorCode.getMessage())
.build());
}
}
System.out.prinln() 을 사용하지 않고 로깅을 사용하는 이유는 무엇일까❓
1) 스레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수
있다.
2) 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지
않는 등 로그를 상황에 맞게 조절할 수 있다.
3) 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의
위치에 남길 수 있다.
4) 파일로 남길 때에는 일별, 특정 용량에 따라 로그를 분할하는 것이 가능하다.
5) println을 썼을 때보다 내부 버퍼링, 멀티 스레드 등의 환경에서 훨씬 좋다.
resources-logback.xml
파일을 만들어서 설정할 내용을 추가해주면 되는데, 양식은 있지만 어떻게 로그를 저장할지에 대한 설정은 각자 알아서 설정해주면 된다. 수업시간에 진행한 설정은 아래와 같이 설정하였다.<configuration scan="true">
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/logFile.%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<Pattern>%d %-5level --- [%thread] - %msg%n</Pattern>
</encoder>
</appender>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d %-5level --- [%thread] - %msg%n</Pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
log
디렉토리에 로그 파일이 생성될 것이고, 파일안에는 아래와 같이 로그들이 기록되는 것을 확인할 수 있다.2023-12-27 20:21:35,543 INFO --- [main] - Starting AopApplication using Java 11.0.2 on DESKTOP-Q0NS9QE with PID 26684 (C:\Users\yhd42\Downloads\aop)
2023-12-27 20:21:35,546 INFO --- [main] - No active profile set, falling back to 1 default profile: "default"
2023-12-27 20:21:36,191 INFO --- [main] - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-12-27 20:21:36,239 INFO --- [main] - Finished Spring Data repository scanning in 38 ms. Found 1 JPA repository interfaces.
2023-12-27 20:21:37,530 INFO --- [main] - Tomcat initialized with port(s): 8080 (http)
2023-12-27 20:21:37,537 INFO --- [main] - Initializing ProtocolHandler ["http-nio-8080"]
2023-12-27 20:21:37,540 INFO --- [main] - Starting service [Tomcat]
2023-12-27 20:21:37,540 INFO --- [main] - Starting Servlet engine: [Apache Tomcat/9.0.76]
2023-12-27 20:21:37,640 INFO --- [main] - Initializing Spring embedded WebApplicationContext
2023-12-27 20:21:37,641 INFO --- [main] - Root WebApplicationContext: initialization completed in 2040 ms
2023-12-27 20:21:38,016 INFO --- [main] - HHH000204: Processing PersistenceUnitInfo [name: default]
2023-12-27 20:21:38,075 INFO --- [main] - HHH000412: Hibernate ORM core version 5.6.15.Final
2023-12-27 20:21:38,225 INFO --- [main] - HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-12-27 20:21:38,350 INFO --- [main] - HikariPool-1 - Starting...
2023-12-27 20:21:38,674 INFO --- [main] - HikariPool-1 - Start completed.
2023-12-27 20:21:38,713 INFO --- [main] - HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
2023-12-27 20:21:39,476 INFO --- [main] - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-12-27 20:21:39,485 INFO --- [main] - Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-12-27 20:21:41,143 WARN --- [main] - spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-12-27 20:21:42,214 INFO --- [main] - Starting ProtocolHandler ["http-nio-8080"]
2023-12-27 20:21:42,343 INFO --- [main] - Tomcat started on port(s): 8080 (http) with context path ''
2023-12-27 20:21:42,369 INFO --- [main] - Started AopApplication in 7.283 seconds (JVM running for 8.085)
2023-12-27 20:22:26,844 INFO --- [http-nio-8080-exec-3] - Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-12-27 20:22:26,845 INFO --- [http-nio-8080-exec-3] - Initializing Servlet 'dispatcherServlet'
2023-12-27 20:22:26,846 INFO --- [http-nio-8080-exec-3] - Completed initialization in 1 ms
2023-12-27 20:22:34,483 WARN --- [http-nio-8080-exec-4] - SQL Error: 1062, SQLState: 23000
2023-12-27 20:22:34,484 ERROR --- [http-nio-8080-exec-4] - Duplicate entry 'test01@naver.com' for key 'Member.UK_9qv6yhjqm8iafto8qk452gx8h'
2023-12-27 20:22:44,455 ERROR --- [http-nio-8080-exec-4] - 입력한 email이 중복되었습니다.
오늘의 느낀점 👀
오늘 수업 중 핵심은 예외 처리가 아닐까 싶다. 결국 자바 프로그래밍때 예외 처리를 해주듯이 스프링에서도 발생할 수 있는 예외들에 대해서 처리를 해줘야 되는데, 그래도 스프링 부트에 기본적인 예외들이 정리되어 있어서 편리하긴 하나, 웹서버를 만들면서 발생하는 에러들은 그때그때 바로 처리를 해줘야 될 것 같다고 생각한다.
어제부터 쇼핑몰 프로젝트를 개인적으로 시작했는데, 만들때 오늘 배운 에러처리를 함께 포함시켜서 기능들을 만들어야겠다.