[Java] Spring AOP 에서 Custom Exception 사용하기

Spring AOP에서 Custom Exception 사용하기

Spring AOP는 CglibAopProxy 클래스를 이용하여 동작합니다.

아래 코드와 같이 super.process() 를 try-catch로 처리하고 있고, Runtime Exception은 exception 을 그대로 던질 수 있기 때문에

Custom Exception은 Exception 클래스가 아니라 RuntimeException을 상속받아서 사용해야 합니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

@Override
@Nullable
public Object proceed() throws Throwable {
try {
return super.proceed();
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) ||
KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) {
// Propagate original exception if declared on the target method
// (with callers expecting it). Always propagate it for Kotlin code
// since checked exceptions do not have to be explicitly declared there.
throw ex;
}
else {
// Checked exception thrown in the interceptor but not declared on the
// target method signature -> apply an UndeclaredThrowableException,
// aligned with standard JDK dynamic proxy behavior.
throw new UndeclaredThrowableException(ex);
}
}
}

[Java]Spring REST Docs HTML이 생성되지 않을때

백기선님의 스프링부트 강좌를 수강하는중에 Spring REST Docs를 이용하여 HTML을 생성하려하는데,

아무리 빌드를 해도 ascii\html\index.html이 생성되지 않았습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
오후 11:58:18: Executing task 'build'...

> Task :compileJava
> Task :processResources
> Task :classes
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses

> Task :test
2019-12-02 23:58:35.629 INFO 24376 --- [ Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-12-02 23:58:35.629 INFO 24376 --- [ Thread-7] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-12-02 23:58:35.630 INFO 24376 --- [ Thread-7] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2019-12-02 23:58:35.630 INFO 24376 --- [ Thread-5] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2019-12-02 23:58:35.630 INFO 24376 --- [ Thread-7] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2019-12-02 23:58:35.630 INFO 24376 --- [ Thread-5] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2019-12-02 23:58:35.637 INFO 24376 --- [ Thread-5] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2019-12-02 23:58:35.642 INFO 24376 --- [ Thread-5] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
2019-12-02 23:58:35.733 ERROR 24376 --- [ Thread-7] .SchemaDropperImpl$DelayedDropActionImpl : HHH000478: Unsuccessful: drop table event if exists
2019-12-02 23:58:35.734 INFO 24376 --- [ Thread-7] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown initiated...
2019-12-02 23:58:35.739 INFO 24376 --- [ Thread-7] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown completed.

> Task :asciidoctor NO-SOURCE
> Task :bootJar
> Task :jar SKIPPED
> Task :assemble
> Task :check
> Task :build

BUILD SUCCESSFUL in 18s
5 actionable tasks: 5 executed
오후 11:58:36: Task execution finished 'build'.

cli를 들여다보니, 위와 같이 노출이 되는데 자세히 들여다보면 > Task :asciidoctor NO-SOURCE 가 있습니다.

의존성문제인줄알고 버전도 변경 하여 보고 build.gradle 파일의 코드가 잘못되었거나,

버전이 올라가면서 변경점이 있는지 체크해보았으나 다른점이 없어 검색을 하였더니
asciidoctor sourceDirectory가 Maven 플러그인에서는 src/main/asciidoc이지만, Gradle 플러그인은 sourceDirectory가 /src/docs/asciidoc 였습니다.
또한 Spring-REST-Docs에 의해 생성되는 경로도 아래 이미지와 같이 달랐습니다

JUnitRestDocumentation rule

Maven을 사용해 본 적이 없어서 gradle과 플러그인도 동일할줄 알았는데,

빌드 결과물도 다른 디렉토리에 생성되고 실행가능한 명령어들도 다른것을 알 수 있었습니다.

참고자료


[Java]해당 클래스의 서비스는 어디서 주입되나요??

회사 업무중 스프링 서비스 코드 푸시를 하였습니다.

다른 팀원들에게

인터페이스를 파라미터로 정의한것은 확인하였는데,

혹시 해당 컨트롤러에 주입은 어디에서 되나요?`

라는 질문을 들었습니다.

1
2
3
4
5
6
@RestController
@Slf4j
@RequiredArgsConstructor
public class YourController {
private final YourService yourService;
}

위와같이 컨트롤러가 선언되어있었습니다.(물론 예제입니다.)

자세히 보기

[Java]@JsonProperty이 왜 동작을 안하지?

1
2
3
class anonymousDTO {
String whatYouWant;
}

대부분 자바의 변수를 생성할때 위와 같이 CamelCase를 많이 쓰는것으로 알고있습니다…..(본인이 잘못 생각하는것일 수 있습니다.)

필자 또한 위와 같이 camelCase를 사용하였으나 API 통신 및 응답을 리턴할때에는

hyphen uppercase가 필요했습니다.

자세히 보기

[Java]@AllArgsConstructor의 잘못된 사용

Consider defining a bean of type java.lang.String in your configuration.

스프링부트로 서비스를 개발중에 @Value 어노테이션을 이용하여 application.yml 파일에 저장해둔 환경변수에 접근하고 싶었는데,

아래와 같은 오류가 발생하였습니다.

1
2
3
Parameter 2 of constructor in com.hodory.v1.service.MyService required a bean of type 'java.lang.String' that could not be found.

Consider defining a bean of type 'java.lang.String' in your configuration.
자세히 보기

[Java]스프링부트 회원 기존 비밀번호 체크하기

회원 비밀번호 변경 로직을 작성중에 현재 비밀번호와 새 비밀번호를 입력받아,

기존 비밀번호가 맞는지 체크하는 로직을 넣고 싶었습니다.

1
2
3
4
5
6
7
8
9
10
String currentPassword = new BCryptPasswordEncoder().encode(request.getCurrentPassword());	

final User persistUser = userRepository.findUserByIdAndPassword(userId, currentPassword)
.orElseThrow(() -> new EntityNotFoundException("회원정보를 찾을 수 없습니다."));

if(!currentPassword.equals(persistUser.getPassword())) {
logger.info("changePassword is Not Equal Current Password");
return new ResponseEntity<>(UserRegisterResult.ERROR.getResponseBody(),
HttpStatus.FORBIDDEN);
}

new BCryptPasswordEncoder().encode(password);로 암호화 한 패스워드를 저장했기 때문에,

회원을 찾을때도 이렇게 하면 되겠다고 생각해서 위와 같은 코드를 작성하였는데,

테스트중 계속하여 EntityNotFoundException이 발생하였습니다.

디버깅으로 체크하였더니 String currentPassword = new BCryptPasswordEncoder().encode(request.getCurrentPassword()); 부분에서 매번 다른 비밀번호가 currentPassword에 들어갔습니다.

왜 다른 값이 나오는지 알고 싶어 BCryptPasswordEncoder 클래스 파일을 열어보았는데,

encode 부분에서 rawPasswordsalt값을 생성하여 두개의 값으로 패스워드를 해싱하고 있었습니다.

해당 클래스 파일안에 matches(CharSequence rawPassword, String encodedPassword)라는 함수가 있었고,

클래스의 인터페이스를 확인하였더니,

1
2
3
4
5
6
7
8
9
10
11
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);

라는 주석을 확인하였고, 구현체가 아닌 인터페이스를 사용하고자
org.springframework.security.crypto.password.PasswordEncoder 를 의존성 주입하여,

1
2
3
4
5
6
7
8
final User persistUser = userRepository.findById(userId)
.orElseThrow(() -> new EntityNotFoundException("회원정보를 찾을 수 없습니다."));
if(!passwordEncoder
.matches(request.getCurrentPassword(), persistUser.getPassword())) {
logger.info("changePassword is Not Equal Current Password");
return new ResponseEntity<>(UserRegisterResult.ERROR.getResponseBody(),
HttpStatus.FORBIDDEN);
}

이와 같이 수정하였고, 기대했던 결과대로 수행되었습니다.

아직 자바와 스프링이 많이 서툴러서 코드를 작성하는 시간보다 검색해보는 시간이 많아 더 어려운거 같습니다.

잘못된 부분이 있다면 코멘트 부탁드리겠습니다.