오늘은 멀티 모듈로 구성한 프로젝트를 실행 먼저 해보도록 하겠다.
실행하기 앞서 필자가 aop를 적용할 때 pointcut을 만들어 둔 클래스가 있는데 패키지와 프로젝트가 바뀌면서 다시 지정해야 한다.
package com.aop.pointcut;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
@Pointcut("execution(* shoppingmall.project..*(..))")
public void allPoint(){} // signature
@Pointcut("within(shoppingmall.project..*Service*)")
public void allService(){}
@Pointcut("within(shoppingmall.project..*Controller*)")
public void allController(){}
@Pointcut("within(shoppingmall.project..*Repository*)")
public void allRepository(){}
@Pointcut("allPoint() && (allService() || allController() || allRepository())")
public void allPointAndMvc(){}
@Pointcut("execution(* *..*Init*.*(..))")
public void initClass(){}
@Pointcut("allPoint() ! initClass()")
public void allPointNotInit(){}
}
위가 기존 사용하던 포인트 컷들이다. 포인트 컷에 대한 포스팅은 아니기에 현제 프로젝트에서 사용할 수 있도록 구성하도록 하겠다.
package com.project.aop.pointcut;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
@Pointcut("execution(* com.project..*(..))")
public void allPoint(){} // signature
@Pointcut("within(com.project..*Service*)")
public void allService(){}
@Pointcut("within(com.project..*Controller*)")
public void allController(){}
@Pointcut("within(com.project..*Repository*)")
public void allRepository(){}
@Pointcut("allPoint() && (allService() || allController() || allRepository())")
public void allPointAndMvc(){}
@Pointcut("execution(* *..*Init*.*(..))")
public void initClass(){}
@Pointcut("allPoint() && ! initClass()")
public void allPointNotInit(){}
}
web과 api의 상위 모듈인 application 모듈을 기준으로 포인트 컷을 작성했다.
설정을 하려고 보니깐 이 aop를 overall이라는 모듈에 두고 설정을 해서 사용하려 하니 이 overall은 pointcut설정에서 application 모듈의 web과 api의 의존성이 필요했다. 의존성을 생성하고 나니 이 overall 모듈을 web과 api가 의존을 하게 되어서 모듈 간 순환 참조가 일어나게 됐다. 이 부분은 생각하지 못한 오류였다. 다른 모듈들은 순환 참조가 일어나지 않지만 이 aop는 순환 참조가 일어나기 때문에 이를 어떻게 할지 고민이 됐다. 시작점부터 실수였기 때문에 이를 인정하고 개선점을 찾았다. 아무리 생각해도 모듈로 구성하는 게 api와 web에서 둘 다 사용하기에 적합하다 생각했지만 순환 참조가 생기니 사용하는 각 모듈에서 생성해서 사용하기로 했다. application의 web과 api가 점점 커지는 것 같아 마음에 들지 않지만 크지 않은 프로젝트이기에 감안하고 api와 web 모두 같은 aop를 사용해서 어쩔 수 없는 중복을 허용하기로 했다.
멀티 모듈을 다시 설계하게 된다면 각 모듈의 역할을 명확히 하고 의존성 구조를 주의해야겠다는 생각이 들었다.
overall은 사라졌으니 이제 main을 포함한 component Scan 패키지를 등록해야 한다.
시간이 오래 걸렸다. 중간에 개인적인 일도 있었지만 멀티 모듈을 구성하고 알아보는 것은 처음이라 web 모듈에 main을 생성하고 실행을 했을 때 필자는 ddl 옵션을 create로 사용한다. 물론 개발 단계에서만 사용하는 것이고 실제 배포 단계에서는 none으로 두고 sql create문을 따로 실행해서 생성한다. web에 main을 만들고 바로 실행을 해보았었다.
@SpringBootApplication
public class MainWebApplication {
public static void main(String[] args) {
SpringApplication.run(MainWebApplication.class, args);
}
}
이와 같이 생성하고 실행했었는데 역시나 실행이 되지 않았다.
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'healthEndpointGroupsBeanPostProcessor' defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]: BeanPostProcessor before instantiation of bean failed
이 오류는 애플리케이션 빈 생성 문제를 나타내는 문제이다.
물론 스캔을 통해서 빈과 엔티티를 스캔해 사용해야 한다는 것은 알고 있다. @SpringBootApplication 애노테이션은 선언된 클래스 패키지와 그 하위 패키지를 스캔해서 사용한다는 것도 알고 있으며 @configuration, @Controller, @Service, @Repository 등등 애노테이션들에 내부에는 @Component를 가지고 있어서 이를 애노테이션을 통해 자동으로 스캔하는 것을 알고 있다. 스프링이나 의존성에 대한 공부가 부족한 탓이겠지만 멀티 모듈을 구성하며 단편적으로는 web 모듈이 다른 모듈을 의존성으로 가지고 있어서 패키지를 import를 통해서 사용하는데 왜 자동으로 스캔을 하지 못할까? 이를 어떻게 해결해야 할까 하는 생각이 들었다.
이전에 스프링에 대해서 공부할 때는 @Configuration 설정 파일에 빈을 정의하고 의존성을 주입해서 사용할 때도 있었다. 이렇게 설정한 설정 파일을 @SpringBootApplication에서 @import를 통해 설정 파일을 바꿔가며 스캔을 수동으로 조절할 때가 있었는데 이때와 같다고 생각했다.
수동으로 조정해서 사용할 때 장점에 대해 배웠었는데 각 모듈의 구성 요소를 분리하고 필요한 모듈을 스캔해 사용할 수 있다. 이 부분이었다.
멀티 모듈로 구성해서 사용하면 직접 스캔 경로를 명시적으로 해야 하는구나 해서 다음은 명시적으로 바꾼 부분을 확인해 보자.
@ComponentScan(basePackages = {
"new-core.database.database-mysql",
"new-core.domain",
"new-core.infra",
"new-core.interceptor",
"application.dto",
"application.web",
"global-utils",
})
@EntityScan(basePackages = "new-core.domain.com.base") // Entity 패키지 경로
@EnableJpaRepositories(basePackages = "new-core.database.database-mysql.com.repository") // Repository 패키지 경로
public class MainWebApplication {
public static void main(String[] args) {
SpringApplication.run(MainWebApplication.class, args);
}
}
엔티티 스캔과 리포지토리 스캔 부분을 별도로 스캔하고 componentscan부분은 의존하는 모듈을 모두 선언했다.
이렇게 구성하고 실행을 해보았지만 실행은 되고 테이블도 만들어지지 않고 웹 페이지에서 엔드포인트로 이동되지 않고 오류 페이지만 띄우게 됐다. 오류 페이지로 이동하면서 로그가 나오지 않는 것으로 보아서 mvc패턴 및 다른 모듈의 클래스 정보가 전혀 사용되지 않았다.
하지만 로그상에 database 연결도 되고 설정에 관한 부분은 문제없어 보였으며 에러페이지도 필자가 400대 에러코드는 하나의 오류 페이지로 이동되게끔 구성했어서 resource 부분의 템플릿인 html파일은 잘 읽어져 오는 것으로 확인했다.
그렇다면 알 수 있는 문제로는 Entity와 database 및 클래스에 관련된 부분을 스캔하지 못하는 것으로 알 수 있었다. aop, interceptor 등 제대로 작동하는 게 하나도 없었기 때문이다.
여기서 시간이 오래 걸렸다. 이전 코드로 필자가 생각하기에는 스캔할 부분을 다 넣어주지 않았나? 이렇게 하면 되는 게 아닌가? 하며 여러 방면으로 찾아보았다. 찾을 때 실수한 부분이 여기서도 나온다. 찾을 때 오류에 관한 부분만 집중적으로 찾았다. 멀티 모듈을 구성하면서 에러가 생기는 부분에만 찾았기 때문에 나의 접근법이 잘못되었다는 것도 몰랐다. 스캔을 하지 못하는 부분이라는 것을 알았으면서 이 부분에 대해 몰두돼서 해결이 늦었다. 스캔하는 부분만 좀 찾아보면 되는 것이었다.
해결한 방법으로는
@SpringBootApplication(scanBasePackages = "com.project")
public class MainWebApplication {
public static void main(String[] args) {
SpringApplication.run(MainWebApplication.class, args);
}
}
해당 코드와 같이 변경했다. 이렇게 구성하면 해당 패키지와 그 하위 패키지의 모든 컴포넌트를 스캔하게 된다.
애초에 되지 않았던 것은 패키지에 대해서 나의 가독성에 맞게만 설정했기 때문에 domain에는 com.base로 되어 있고 database는 com.repository로 시작되고 이렇게 다들 각각 달랐었다. 멀티 모듈을 구성해보지 않았어서 상위 패키지에 대해서 조금 무지한 부분도 있었고 크게 상관이 없을 거라 생각했었다. 부끄러운 생각이지만 이런 부분에서도 일관성을 갖추는 게 유지보수나 프로젝트에서 어떤 컴포넌트가 있는지 찾을 수 있는 효과를 얻을 수 있다. 즉, 각 모듈이 독립적으로 관리되기 때문에, 어떤 모듈이 어떤 다른 모듈에 의존하는지 명확하게 정의되어 있지 않기 때문이다. 스프링 부트가 자동으로 모든 모듈을 스캔하도록 하면, 불필요한 클래스까지 스캔하게 되어 성능 저하가 발생할 수 있고, 의도하지 않은 클래스가 빈으로 등록되는 등 예기치 않은 문제가 발생할 수 있다.
위와 같이 명시적으로 스캔할 패키지를 통일해서 스캔하면 스캔이 제대로 작동해서 서버가 켜지고 실행을 확인할 수 있었다.
해결법과 이전과 같이 구성하는 부분에서도 더 알아보자.
@ComponentScan: 컴포넌트(예: @Service, @Repository, @Controller 등)를 찾아 빈으로 등록한다. 멀티 모듈 프로젝트에서는 각 모듈에 분산된 컴포넌트를 찾기 위해 여러 패키지를 지정해야 한다.
@EntityScan: JPA 엔티티를 찾아 등록한다. 엔티티 클래스가 여러 모듈에 분산되어 있다면, 각 모듈의 패키지를 지정해야 한다.
@EnableJpaRepositories: JPA 리포지토리를 찾아 등록한다. 리포지토리 인터페이스가 여러 모듈에 분산되어 있다면, 각 모듈의 패키지를 지정해야 한다.
이렇게 더 세부적으로 컨트롤이 필요할 땐 세부적으로 컨트롤하게끔 구성하면 될 것 같다. 여기까지 오늘 일어난 트러블 슈팅에 대해서 알아보았다. 이 부분을 트러블 슈팅으로 따로 다뤄볼까 하다가 오늘 포스팅 내용이 너무 빈약하고 별게 없어서 그대로 진행했다. 필자가 부족한 부분이고 더 깊게 알아봤어야 하고 문제점을 알고 있었지만 문제점에 대해 해결법을 늦게 찾은 점이 스스로에게 좀 실망하게 되었다. 문제 해결 능력이 중요한 게 개발자기 때문에 이를 해결을 잘했어야 하는데 안타깝다. 하지만 이번 실패로 다음번엔 더 성장해서 더 나은 문제 해결 능력을 갖추도록 하겠다.
오늘 포스팅은 여기까지이다. 이번 포스팅에 실행까지 다 해서 api 모듈도 실행하게 만들고 저번에 말 한 핵심 비즈니스 로직이랑 유스케이스 로직을 분리하기로 했었는데 아쉽게도 내일로 미루게 되었다. 내일은 모두 할 수 있게끔 하겠다. 오래 걸리지 않을 것이라 금방 해치우고 도커에 대해서 좀 더 알아보고자 한다. 멀티 모듈을 구성한 만큼 각각 모듈 배포를 할 수 있게끔도 해보고 싶기 때문이다. 도커에 대해서는 이미 한번 다룬 적이 있지만 아직 사용은 미숙하기 때문에 한 번 더 공부해서 각 모듈을 배포를 가능하게끔 구성해 보도록 하겠다.
'spring' 카테고리의 다른 글
멀티 모듈 kafka 추가하기 (1) | 2024.06.19 |
---|---|
단일 모듈 프로젝트 멀티 모듈 구성하기 -7 (1) | 2024.06.18 |
단일 모듈 프로젝트 멀티 모듈 구성하기 -5 (1) | 2024.06.15 |
단일 모듈 프로젝트 멀티 모듈 구성하기 -4 (0) | 2024.06.15 |
단일 모듈 프로젝트 멀티 모듈 구성하기 -3 (1) | 2024.06.14 |