Spring

Spring Boot 공통 라이브러리 구축 가이드 (공통 모듈)

Written by 개발자서동우 · 6 min read >
Spring Boot 공통 라이브러리 만들기

안녕하세요! Devloo 입니다. 🙂 여러분은 혹시 DRY원칙이라고 들어보셨나요? 저의 다른 글 “매일 실천해야 할 7가지 소프트웨어 개발 원칙“에서 언급한 적이 있는 것 같은데요. 소프트웨어 개발에서 DRY(Don’t Repeat Yourself) 원칙은 효율적인 코딩의 기초입니다. Andy Hunt와 Dave Thomas의 책 『실용주의 프로그래머: 초보자에서 마스터로』에서 처음 소개한 이 원칙은 코드베이스 내에서 반복을 줄이는 것의 중요성을 강조합니다. DRY 원칙을 따르면, 개발자는 중복을 최소화하고 오류의 위험을 줄이며 코드를 더 쉽게 유지관리할 수 있습니다.

DRY 원칙이 특히 빛을 발하는 경우는 마이크로서비스 아키텍처 개발입니다. 마이크로서비스는 여러 서비스가 유사한 기능을 공유하는 경우가 많습니다. 여기에는 유틸리티 함수, 보안 설정, 예외 처리, 공통 비즈니스 로직 등이 포함될 수 있습니다. 이러한 코드를 각 서비스에 중복해서 작성하는 대신, 공통 또는 공유 라이브러리에 캡슐화하는 것이 더 효율적입니다.

Spring Boot에서 공통 라이브러리를 만드는 것은 코드 재사용성을 촉진할 뿐만 아니라, 업데이트와 버그 수정을 한 곳에서만 수행할 수 있어 유지 관리가 크게 단순화되고 버그 발생 가능성도 줄어듭니다. 이 접근 방식은 여러 마이크로서비스가 공통 기능을 일관되게 유지해야 하는 대규모 프로젝트에서 특히 유용합니다.

이 글에서는 Spring Boot에서 공통 라이브러리를 만드는 과정을 안내하며, DRY 원칙을 효과적으로 적용하는 방법을 설명하겠습니다. 공통 라이브러리를 빌드, 패키징 및 통합하는 기본 사항을 다루어 개발 프로세스를 간소화하고 깨끗하고 효율적인 코드베이스를 유지할 수 있도록 도와드리겠습니다. 자 그럼 이제 시작해볼까요 🙂 ?

Spring Boot 공통 라이브러리 구축
사진: UnsplashMax Langelott

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>
        <!-- Spring Boot 버전을 정의하여 포함할 Spring 종속성의 버전을 명시할 필요가 없습니다.
        이는 포함하는 Spring 종속성의 버전과 상호 호환성을 걱정할 필요가 없도록 도와줍니다. -->
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.5</version>
    </parent>
    <groupId>com.yourcompany</groupId>
    <artifactId>sharedlib</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>Commons Library</name>
    <description>공유 유틸리티, 클래스, 설정 등을 포함하는 Spring Boot 라이브러리</description>

    <properties>
        <!-- Java 버전 -->
        <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>
     <!-- lombok, spring data 등과 같은 종속성들을 여기에 추가합니다. -->
    </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>
       <!-- GCP Artifact registry를 사용하는 경우, 아티팩트를 배포하기 위해 다음 확장을 사용합니다.
            그렇지 않은 경우, 'maven-deploy-plugin'을 사용하여 배포할 수 있습니다. Nexus 예시는 여기에서 확인할 수 있습니다: https://www.baeldung.com/maven-deploy-nexus -->
        <extensions>
            <extension>
                <groupId>com.google.cloud.artifactregistry</groupId>
                <artifactId>artifactregistry-maven-wagon</artifactId>
                <version>${artifactregistry-maven-wagon.version}</version>
            </extension>
        </extensions>
    </build>
</project>

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


2. 공통 코드 공유 라이브러리로 이동

어떤 구성 요소를 공유 라이브러리로 옮기면 좋을까요? 몇 가지 예를 들어드리겠습니다. 이후 프로젝트나 사용 사례에 따라 적합한 것을 선택하면 됩니다.

  • 엔터티의 기본 클래스
  • 유틸리티 클래스 (예: 날짜 및 시간 관련 객체를 사람이 읽을 수 있는 형식으로 변환하는 DateTimeUtil 클래스)
  • CORS 구성, WebSocket 구성, 보안 필터, 인증 오류 처리기 등의 공통 보안 설정
  • 공통 예외
  • 비동기 작업, 감사, 스케줄링, 시간대 설정 등의 구성
  • 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) {
        this.id = (id != null) ? 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 Objects.hash(id);
    }
}
예제 2: 유틸리티 클래스
// Utility class for Dates manipulation
package com.yourcompany.sharedlib;
//...다른 의존성 생략...//

public final 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 need to instantiate
    }

    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) {
        // 구체적인 구현은 생략
        return ""; 
    }
}

// Utility class for general purpose
package com.yourcompany.sharedlib;
//...다른 의존성 생략...//

public final class JavaUtility {
    private JavaUtility() {
        // Utility class, no need to instantiate
    }

    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) {
        if (obj == null) {
            throw supplier.get();
        }
        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) {
        return Objects.isNull(email) || 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)
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";

    @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
@RequiredArgsConstructor
public class SecurityConfig {

    private final AuthenticationErrorHandler authenticationErrorHandler;

    @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");
        http
                .authorizeRequests(authz -> authz.anyRequest().permitAll())
                .csrf().disable();
        return http.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(String.format("Email [%s] is not in valid format.", email));
    }
}
예제 5: 공통 구성
package com.yourcompany.sharedlib;
//...다른 의존성 생략...//

@Configuration(proxyBeanMethods = false)
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
public class AuditingConfiguration {
    @Bean
    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(ctx -> ctx.getAuthentication())
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .filter(principal -> !ANONYMOUS_USER.equals(principal))
                .map(principal -> principal instanceof Jwt jwtToken ? jwtToken.getSubject() : 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("asyncThreadPoolTaskExecutor")
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(4);
        executor.setThreadNamePrefix("AsyncTask-");
        executor.initialize();
        return executor;
    }
}

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(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() {
        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 setDefaultTimeZone() {
        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. 원격 아티팩트 저장소에 배포하기

이제 마지막 단계인 아티팩트(라이브러리 빌드 파일)를 원격 저장소에 배포할 차례입니다. 이를 통해 마이크로서비스가 라이브러리를 가져올 수 있는 위치를 확보할 수 있습니다.

이번에는 Google Cloud Artifact Registry에 배포하지만, Nexus와 같은 다른 아티팩트 저장소에 배포하는 방식도 매우 유사합니다.

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

Maven 경로에 Maven이 없다면 Maven 래퍼를 사용하여 mvn deploy 또는 ./mvnw deploy 명령어를 실행하세요.

배포 전에 테스트, 검증 설치 등 다른 명령어(단계)가 실행되는 것을 볼 수 있습니다. 놀라지 말고, Maven 공식 문서의 빌드 라이프사이클 소개를 참조하세요.

마지막에는 “Deploy finished”와 같은 명령어가 성공적으로 실행되었음을 나타내는 메시지를 볼 수 있어야 합니다. 만약 “401: Unauthorized” 오류가 발생하면, 환경 변수 GOOGLE_APPLICATION_CREDENTIALS가 올바르게 설정되어 있는지 확인하세요. 자세한 내용은 여기에서 확인할 수 있습니다.

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

이제 아티팩트가 원격 GCP Artifact Registry에 성공적으로 배포되었습니다. 최소한의 설정만으로도 서비스를 위한 종속성으로 사용할 수 있습니다.


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

이제 마지막 단계입니다. 지금까지의 작업을 통해 얻은 이점을 누리기 위해 라이브러리를 마이크로서비스에 포함시켜 봅시다.

먼저, Maven에게 라이브러리를 어디서 찾아야 하는지, 즉 아티팩트를 배포한 저장소를 알려주어야 합니다. 이를 위해 마이크로서비스의 pom.xml 파일에 다음 섹션을 추가합니다:

<dependencies>
    <!-- 다른 종속성들 -->
    ...
    <dependency>
        <groupId>com.yourcompany</groupId>
        <artifactId>sharedlib</artifactId>
        <!-- 배포한 라이브러리의 버전에 따라 변경 -->
        <version>1.0.0</version>
    </dependency>
</dependencies>

<repositories>
    <repository>
        <id>artifact-registry</id>
        <!-- 아티팩트 저장소 URL -->
        <!-- 유효한 URL의 예: artifactregistry://europe-west3-maven.pkg.dev/yourcompany/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>
        <!-- GCP Artifact Registry 사용을 위한 확장 -->
        <!-- Nexus 또는 다른 아티팩트 저장소를 사용하는 경우 이 섹션은 필요 없을 수 있습니다. -->
        <extension>
            <groupId>com.google.cloud.artifactregistry</groupId>
            <artifactId>artifactregistry-maven-wagon</artifactId>
            <version>${artifactregistry-maven-wagon.version}</version>
        </extension>
    </extensions>
    <!-- 다른 확장 및 플러그인 추가 -->
    ...
</build>

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

마지막으로, 공통 라이브러리의 설정, 빈, 구성 요소 및 설정 속성이 Spring AutoConfiguration에 의해 로드되도록 해야 합니다. 이를 위해 메인 클래스 위에 3단계(라이브러리에서 ‘내보낼’ 항목 정의하기)에서 생성한 어노테이션을 추가하면 됩니다.

@SpringBootApplication
@EnableSharedLib // 공유 라이브러리에서 내보낸 모든 설정, 빈, 속성, 구성 요소를 가져오기 위한 커스텀 어노테이션
public class ProjectXApplication {
    public static void main(final String[] args) {
        SpringApplication.run(ProjectXApplication.class, args);
    }
}

이제 여러 마이크로서비스에서 동일한 코드를 작성할 필요가 없어져, 시간을 절약하고 유지 보수 및 개발 비용을 줄이며 버그 발생 가능성도 낮출 수 있게 되었습니다.

이번 글에서는 공통 라이브러리를 구축하는 방법부터 배포, 종속성 설정까지 단계별로 안내했습니다. 이제 직접 공통 라이브러리를 만들어 프로젝트에 적용해 보세요. 개발 과정이 한층 더 효율적이고 체계적으로 바뀔 것입니다.

끝까지 읽어주셔서 감사합니다 :). 궁금한 점이 있으시면 댓글로 남겨주세요.

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *