Development/Code

Spring Boot: 스프링 부트 공통 라이브러리 구축하기 (공통 모듈)

Danny Seo 2024. 7. 2. 17:09

소프트웨어 개발 세계에서 DRY(Do not Repeat Yourself) 원칙은 효율적인 코딩의 핵심입니다. 앤디 헌트와 데이브 토마스가 그들의 기념비적인 책 "The Pragmatic Programmer"에서 만들어낸 DRY 원칙은 코드베이스 내의 반복을 줄이는 것의 중요성을 강조합니다. 이 원칙을 준수함으로써 개발자는 중복을 최소화하고 오류 발생 가능성을 줄이며 코드를 더 유지 보수하기 쉽게 만들 수 있습니다.

 

DRY 원칙이 빛을 발하는 일반적인 시나리오는 마이크로서비스 아키텍처의 개발입니다. 마이크로서비스는 여러 서비스가 유사한 기능을 공유하는 경우가 많습니다. 이러한 기능은 유틸리티 함수, 보안 구성, 예외 처리, 공통 비즈니스 로직 등 다양합니다. 이러한 코드를 여러 서비스에 중복시키는 대신, 공용 또는 공유 라이브러리에 코드를 캡슐화하는 것이 더 효율적입니다.

 

스프링 부트에서 공용 라이브러리를 만드는 것은 코드 재사용성을 촉진할 뿐만 아니라, 업데이트 및 버그 수정을 한 곳에서만 수행할 수 있어 유지 보수가 훨씬 간편해지고 버그도 줄어듭니다. 이 접근 방식은 여러 마이크로서비스가 공통 기능을 일관되고 최신 상태로 유지해야 하는 대규모 프로젝트에서 특히 유용합니다.

 

이 글에서는 스프링 부트에서 공유 라이브러리를 만드는 과정을 안내하며, DRY 원칙을 효과적으로 적용하는 방법을 보여드리겠습니다. 공유 라이브러리를 구축하고 패키징하며 스프링 부트 프로젝트에 통합하는 기본 사항을 다루어 개발 프로세스를 간소화하고 깨끗하고 효율적인 코드베이스를 유지할 수 있도록 하겠습니다.

 

이제 지루한 설명은 그만하고 시작하겠습니다 :)

Spring Boot: 스프링 부트로 공통 모듈 구축하기

1. 공유 라이브러리 프로젝트 설정하기

우선 공유 라이브러리를 위한 새로운 Maven 프로젝트를 생성합니다. 여러분이 선호하는 IDE(IntelliJ IDEA를 추천) 또는 Spring Initializr를 사용하여 이 작업을 할 수 있습니다. 필요한 종속성을 선택하세요. 프로젝트를 생성한 후 초기 디렉토리는 다음과 같이 생겼을 것입니다.

shared-library
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── yourcompany
│   │   │           └── sharedlib
│   │   │               └── ...
│   │   └── resources
│   │       └── application.yml
├── pom.xml

 

이제 pom.xml 파일을 열어 다음과 같이 수정하세요:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
    </parent>
    <groupId>com.vaskojovanoski</groupId>
    <artifactId>sharedlib</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>Commons Library</name>
    <description>Spring Boot Library containing shared utilities, classes, configurations and similar.</description>

    <properties>
        <maven.compiler.release>21</maven.compiler.release> 
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <artifactregistry-maven-wagon.version>2.2.1</artifactregistry-maven-wagon.version>
        <artifact-repository-url>YOUR_REMOTE_ARTIFACT_REPOSITORY_URL_HERE</artifact-repository-url>
    </properties>

    <dependencies>
        <!-- Your dependencies go here, like lombok or spring data etc.. -->
    </dependencies>

    <distributionManagement>
        <snapshotRepository>
            <id>artifact-registry</id>
            <url>${artifact-repository-url}</url>
        </snapshotRepository>
        <repository>
            <id>artifact-registry</id>
            <url>${artifact-repository-url}</url>
        </repository>
    </distributionManagement>

    <build>
        <extensions>
            <extension>
                <groupId>com.google.cloud.artifactregistry</groupId>
                <artifactId>artifactregistry-maven-wagon</artifactId>
                <version>${artifactregistry-maven-wagon.version}</version>
            </extension>
        </extensions>
    </build>
</project>

이제 Java 21 버전으로 애플리케이션을 빌드하고, 원격 저장소(이 경우 GCP Artifact Registry)에 아티팩트를 배포하도록 Maven을 설정했습니다. 이제 공용 코드를 라이브러리로 이전할 준비가 되었습니다.

2. 공용 코드를 공유 라이브러리로 이전하기

어떤 구성 요소를 공유 라이브러리로 옮기기에 좋은 후보일까요? 몇 가지 예를 들어드리겠습니다. 프로젝트/사용 사례에 따라 어떤 것을 옮길지 결정하세요.

  • 엔티티의 기본 클래스
  • 유틸리티 클래스(예: 날짜 관련 및 날짜 시간 관련 객체를 사람이 읽을 수 있는 형식으로 변환하는 DateTimeUtil 클래스 등)
  • CORS 구성, 웹소켓 구성, 보안 필터, 인증 오류 핸들러 등 공통 보안 구성
  • 공통 예외
  • 비동기 작업, 감사, 스케줄링, 시간대 구성 등
  • ObjectMapper, Clock 등 재사용 가능한 빈

예제 1: 엔티티의 기본 클래스

/// Base super class ///
package com.yourcompany.sharedlib;
@Getter
@ToString
@MappedSuperclass
public abstract class BaseEntity implements Serializable {
    @Id
    @Column(name = "id", unique = true, nullable = false)
    protected UUID id = UUID.randomUUID();


    public void setId(final UUID id) {
        if (Objects.nonNull(id)) {
            this.id = id;
        } else {
            this.id = UUID.randomUUID();
        }
    }
}

----------------------------------------------------------------------------

/// Audited base super class ///
package com.yourcompany.sharedlib;
@Getter
@Setter
@RequiredArgsConstructor
@ToString(callSuper = true)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditedEntity extends BaseEntity {

    @CreatedBy
    @Column(name = "created_by")
    protected String createdBy;

    @LastModifiedBy
    @Column(name = "modified_by")
    protected String lastModifiedBy;

    @CreatedDate
    @Column(name = "created_date")
    protected ZonedDateTime createdDate;

    @LastModifiedDate
    @Column(name = "last_modified_date")
    protected ZonedDateTime lastModifiedDate;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
        AuditedEntity that = (AuditedEntity) o;
        return id != null && Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
}

예제 2: 유틸리티 클래스

/// Utility class for Dates manipulation ///
package com.yourcompany.sharedlib;
public abstract class DateUtil {
    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy 'at' hh:mm a z");

    private DateUtil() {
        // Utility class, no reason to instantiate this class
    }

    public static ZonedDateTime fromTimestamp(final Long bookedTimeTimestamp) {
        return ZonedDateTime.ofInstant(Instant.ofEpochSecond(bookedTimeTimestamp), ZoneId.of("UTC")).withZoneSameInstant(ZoneId.systemDefault());
    }

    public static String prettifyDateTime(final ZonedDateTime dateTime, final ZoneId timezone) {
        return dateTime.withZoneSameInstant(timezone).format(DATE_TIME_FORMATTER);
    }

    public static long minutesBetweenDates(final ZonedDateTime olderDate, final ZonedDateTime newerDate) {
        return ChronoUnit.MINUTES.between(olderDate, newerDate);
    }

    public static String prettifyMinutesToHumanReadableString(Long minutes) {
        // some code here to prettify minutes to human readable string;
    }
}

----------------------------------------------------------------------------

/// Utility class for general purpose ///
package com.yourcompany.sharedlib;
public abstract class JavaUtility {
    private JavaUtility() {
        // Utility class, no reason to instantiate this class
    }

    public static void validateNotNull(Supplier<?> supplier, String fieldName) {
        if (Objects.isNull(supplier.get())) {
            throw new MissingFieldException(fieldName);
        }
    }

    public static void validateNotNull(Object field, String fieldName) {
        if (Objects.isNull(field)) {
            throw new MissingFieldException(fieldName);
        }
    }

    public static String capitalizeFirstLetter(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }
        return Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();
    }

    public static <T, E extends RuntimeException> T requireNonNull(T obj, Supplier<E> supplier) {
        final RuntimeException exception = supplier.get();
        if (obj == null) {
            throw exception;
        }
        return obj;
    }
}


----------------------------------------------------------------------------

/// Validators ///
package com.yourcompany.sharedlib;
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[_a-zA-Z0-9-]+(\\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*$");

    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (Objects.isNull(email)) {
            return true;
        }
        return EMAIL_PATTERN.matcher(email).matches();
    }
}

----------------------------------------------------------------------------

package com.yourcompany.sharedlib;
@Documented
@Constraint(validatedBy = EmailValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEmail {
    String message() default "Invalid email format";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

예제 3: 공통 보안 구성

/// CORS Config ///
package com.yourcompany.sharedlib;
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(final CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("https://*.mydomain.com", "http://localhost:4200", "https://myapp.mydomain.com,", "https://myapp.mydomain.com/**")
                .allowedHeaders("*")
                .allowedMethods("*")
                .maxAge(86400);
    }
}

----------------------------------------------------------------------------

/// Security Filter for multi-tenance purposes, selecting tenant based on 'X-Tenant' header value ///
package com.yourcompany.sharedlib;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnProperty(prefix = "application", name = "multitenancy-enabled", havingValue = "true")
@Log4j2
public class DataSourceRoutingFilter extends GenericFilterBean {

    public static final String HEADER_NAME = "X-Tenant";

    public DataSourceRoutingFilter() {
        log.info("Adding DataSourceRoutingFilter to existing filter chain");
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        final String dataSourceHeader = httpServletRequest.getHeader(HEADER_NAME);

        try {
            TenantContext.setCurrentTenant(TenantEnum.fromString(dataSourceHeader));
        } catch (final IllegalArgumentException exception) {
            TenantContext.setCurrentTenant(TenantEnum.DATASOURCE_PRIMARY);
        } finally {
            chain.doFilter(request, response);
            TenantContext.clear();
        }
    }
}

----------------------------------------------------------------------------

/// Common Security Filter Chain configuration ///
package com.yourcompany.sharedlib;

@Configuration(proxyBeanMethods = false)
@EnableMethodSecurity
@EnableWebSecurity
@Log4j2
public class SecurityConfig {

    private final AuthenticationErrorHandler authenticationErrorHandler = new AuthenticationErrorHandler(new ObjectMapper());

    public SecurityConfig() {
    }

    @ConditionalOnProperty(prefix = "application.auth0", name = "endpoint-security-enabled", havingValue = "true")
    @Bean
    public SecurityFilterChain auth0FilterChain(final HttpSecurity http) throws Exception {
        log.warn("Auth0 Security enabled");
        http
                .csrf().disable()
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/api/public/**", "/swagger-ui/**", "/swagger-ui**", "/api-docs/**", "/api-docs**", "/favicon.ico", "/swagger-resources/**", "/webjars/**", "/resources").permitAll()
                        .requestMatchers("/api/**").authenticated())
                .cors(Customizer.withDefaults())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .jwt(Customizer.withDefaults())
                        .authenticationEntryPoint(authenticationErrorHandler));
        return http.build();
    }

    @ConditionalOnMissingBean(name = "auth0FilterChain")
    @Bean
    public SecurityFilterChain defaultFilterChain(final HttpSecurity http) throws Exception {
        log.warn("Auth0 Security disabled - exposing all endpoints");
        return http.authorizeRequests()
                .anyRequest().permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}

----------------------------------------------------------------------------

/// Error handler for failed authentication ///
package com.yourcompany.sharedlib;
@RequiredArgsConstructor
public class AuthenticationErrorHandler implements AuthenticationEntryPoint {

    private final ObjectMapper mapper;

    @Override
    public void commence(
            final HttpServletRequest request,
            final HttpServletResponse response,
            final AuthenticationException authException
    ) throws IOException {
        final var errorMessage = ErrorMessage.from("Requires authentication");
        final var json = mapper.writeValueAsString(errorMessage);

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(json);
        response.flushBuffer();
    }
}
----------------------------------------------------------------------------
/// Error response wrapper object ///
package com.yourcompany.sharedlib;
public record ErrorMessage(String message) {

    public static ErrorMessage from(final String message) {
        return new ErrorMessage(message);
    }
}

예제 4: 공통(일반) 예외

package com.yourcompany.sharedlib;
public class MissingFieldException extends RuntimeException {
    public MissingFieldException(String fieldName) {
        super(String.format("Field '%s' must be provided", fieldName));
    }
}  

----------------------------------------------------------------------------

package com.yourcompany.sharedlib;
public class InvalidEmailFormatException extends IllegalArgumentException {
    public InvalidEmailFormatException(String email) {
        super("Email [" + email + "] is not in valid format.");
    }
}

예제 5: 공통(일반) 설정

package com.yourcompany.sharedlib;

@Configuration(proxyBeanMethods = false)
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
public class AuditingConfiguration {
    @Bean // Makes ZonedDateTime compatible with auditing fields
    public DateTimeProvider auditingDateTimeProvider() {
        return () -> Optional.of(ZonedDateTime.now());
    }

}

----------------------------------------------------------------------------

package com.yourcompany.sharedlib;
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
    private static final String ANONYMOUS_USER = "anonymousUser";

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .filter(principal -> !principal.equals(ANONYMOUS_USER))
                .map(it -> {
                    if (it instanceof Jwt jwtToken) {
                        return jwtToken.getSubject();
                    }
                    return null;
                });
    }
}

----------------------------------------------------------------------------

package com.yourcompany.sharedlib;

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "application.async", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableAsync
@Log4j2
public class AsyncConfiguration implements AsyncConfigurer {
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (throwable, method, obj) -> {
            log.error("Exception Caught in Thread - " + Thread.currentThread().getName());
            log.error("Exception message - " + throwable.getMessage());
            log.error("Method name - " + method.getName());
            for (Object param : obj) {
                log.error("Parameter value - " + param);
            }
            throwable.printStackTrace();
        };
    }

    @Primary
    @Bean(ASYNC_THREAD_POOL_EXECUTOR_NAME)
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(1);
        threadPoolTaskExecutor.setMaxPoolSize(4);
        threadPoolTaskExecutor.setThreadNamePrefix("AsyncTask-");
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}


----------------------------------------------------------------------------

package com.yourcompany.sharedlib;

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "application.scheduling", name = "enabled", havingValue = "true", matchIfMissing = false)
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class SchedulingConfiguration {
    @Bean
    public LockProvider lockProvider(final DataSource dataSource) {
        return new JdbcTemplateLockProvider(dataSource);
    }

}

예제 6: 재사용이 가능한 공통 빈

package com.yourcompany.sharedlib;

@Configuration(proxyBeanMethods = false)
public class CommonLibraryBeanFactory{
    @Bean
    public Clock clock() {
        return Clock.systemUTC();
    }

    @Bean
    public ObjectMapper objectMapper() {
        final ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
        return mapper;
    }

    @PostConstruct
    public void init() {
        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
    }
}

3. 라이브러리에 '내보낼' 항목 정의하기

Spring Boot AutoConfiguration에 포함시킬 빈과 구성을 명확히 정의하는 것이 중요합니다. 이렇게 하면 우리 라이브러리를 통합하는 프로젝트가 라이브러리에서 가져오고 사용하는 구성 요소를 정확히 알 수 있습니다.

이를 위해 새로운 클래스를 생성하고 @AutoConfiguration으로 주석을 달고, @Import 주석을 사용하여 통합 프로젝트에서 사용할 수 있는 모든 구성과 구성 요소를 지정할 수 있습니다. @ComponentScan 및 @ConfigurationPropertiesScan의 조합을 사용할 수도 있지만, Spring Boot 라이브러리를 살펴보면 항상 @Import 접근 방식이 선호됨을 알 수 있습니다. 이 방법은 이 구성 클래스에 등록된 구성과 구성 요소를 명확하게 보여줍니다.

package com.yourcompany.sharedlib;

@AutoConfiguration
@Import({
        GcpPubSubAutoConfiguration.class,
        CommonLibraryBeanFactory.class,
        AsyncConfiguration.class,
        AuditingConfiguration.class,
        AuditorAwareImpl.class,
        DataSourceRouting.class,
        SchedulingConfiguration.class,
        DataSourceRoutingFilter.class,
        CorsConfig.class,
        SecurityConfig.class,
        SwaggerConfig.class,
        ...
        ...
        ...
})
public class SharedLibConfiguration {
}

 

구성 클래스에 이 모든 구성을 추가할 때 사용할 수 있는 커스텀 주석을 만들어 우리의 작업(그리고 이 라이브러리를 사용하는 사람들의 작업)을 더 쉽게 만들어 보겠습니다. 이 주석이 어떻게 작동하는지 기사는 뒷부분에서 자세히 다룰 것입니다.

package com.yourcompany.sharedlib;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SharedLibConfiguration.class)
public @interface EnableSharedLib {
}

4. 원격 아티팩트 저장소에 배포하기

이제 마지막 단계는 아티팩트(라이브러리 빌드 파일)를 원격 저장소에 배포하는 것입니다. 이렇게 하면 마이크로서비스가 라이브러리를 가져올 수 있는 장소를 확보하게 됩니다.

 

pom.xml 파일을 지시에 따라 구성했다면, 즉 <distributionManagement><extension> 섹션이 올바르게 설정되어 있다면, 단일 명령문으로 라이브러리를 배포할 준비가 된 것입니다.

 

Maven wrapper를 사용 중이며 Maven이 경로에 없으면 mvn deploy 또는 ./mvnw deploy 명령을 실행하십시오.

 

배포 전에 테스트, 검증, 설치 등 다른 명령(단계)이 실행되는 것을 확인할 수 있습니다, 자세한 내용은 공식 Maven 문서의 빌드 생명주기를 참조해주세요.

 

마지막에는 “Deploy finished”와 같은 명령이 성공적으로 실행되었다는 메시지를 확인할 수 있습니다. 만약 “401: Unauthorized” 오류가 발생한다면, CREDENTIALS 관련 환경 변수가 잘 설정되어 있는지 확인하세요.

 

참고: 버전 X.Y.Z로 라이브러리를 한 번만 배포할 수 있으며, 버전에 -SNAPSHOT 접미사가 추가된 경우에는 동일한 버전으로 여러 번 라이브러리를 배포할 수 있습니다.

 

이제 아티팩트가 원격 아티팩트 레지스트리에 배포되었습니다. 최소한의 설정만으로 서비스에서 이를 종속성으로 사용할 수 있습니다.

5.  프로젝트에 라이브러리 종속성 추가하기

이제 마지막 단계로, 우리가 만든 라이브러리를 마이크로서비스에 포함하여 그 혜택을 누려봅시다. 이를 위해 먼저 Maven에게 우리가 만든 라이브러리를 어디서 찾아야 하는지 알려줘야 합니다. 즉, 아티팩트를 배포한 저장소를 알려줘야 합니다. 이를 위해 마이크로서비스의 pom.xml 파일을 다음과 같이 수정합니다:

<dependencies>
<!-- Your dependencies -->
...
...
...
    <dependency>
       <groupId>com.yourcompany</groupId>
       <artifactId>sharedlib</artifactId>
        <!-- Change depending on what version of the library you deployed -->
       <version>1.0.0</version> 
     </dependency>
</dependencies>    

<repositories>
        <repository>
            <id>artifact-registry</id>
            <!-- Artifact registry URL. -->
            <!-- Example of valid URL: artifactregistry://europe-west3-maven.pkg.dev/vaskojovanoski/project-x-artifacts -->
            <url>artifactregistry://YOUR_GCP_REGION_HERE-maven.pkg.dev/YOUR_GCP_PROJECT_HERE/YOUR_ARTIFACT_REPO_NAME_HERE</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <build>
        <extensions>
            <!-- Extension specific for usage of GCP Artifact registry -->
            <!-- In case you have Nexus or different Artifact repository this section will probably be redundant -->
            <extension>
                <groupId>com.google.cloud.artifactregistry</groupId>
                <artifactId>artifactregistry-maven-wagon</artifactId>
                <version>${artifactregistry-maven-wagon.version}</version>
            </extension>
        </extensions>
        <!-- Your other extensions and plugins go here -->
        ...
        ...
        ...
      </build>

이제 Maven 프로젝트를 동기화하면 원격 저장소에서 공유 라이브러리를 성공적으로 가져올 수 있을 것입니다.

 

마지막으로, 공유 라이브러리의 설정, 빈(beans), 컴포넌트, 구성 속성(configuration properties)이 Spring AutoConfiguration에 의해 자동으로 로드되도록 해야 합니다. 이를 위해 3단계에서 만든 어노테이션을 메인 클래스 위에 추가하면 됩니다.

@SpringBootApplication
@EnableSharedLib // We applied our custom annotation to import all the configurations / beans / properties / components that we exported from our shared library.
public class ProjectXApplication {
    public static void main(final String[] args) {
        SpringApplication.run(ProjectXApplication.class, args);
    }
}

 

이제 여러 마이크로서비스에서 동일한 코드를 작성하지 않아도 되어 많은 시간을 절약하고, 유지보수 및 개발 비용을 줄이며, 버그 발생 가능성을 낮췄습니다. 

 

지금까지 스프링 부트로 공통 모듈을 만드는 방법을 살펴보았습니다. 이 글이 도움이 되셨다면 좋겠습니다.

 

끝까지 읽어주셔서 정말 감사합니다. (_ _)