Gradle 의존성 implementation과 runtimeOnly 의 차이점
Implementation은 코드에서 실제로 해당 라이브러리를 참조하고 싶을 때 사용한다. 예를들면 Redis 라이브러리인 Redisson 라이브러리를 사용한다고 했을 때 아래처럼 사용한다.
redissonClient.getLock()
직접 의존성을 부여해서 getLock()이라는 메서드를 사용하기 때문에 이것은 Implementation로 종속해주어야 한다.
jar파일로 묶일 때 당연히 같이 따라오고, 다른 모듈에서는 사용이 불가능하다. 다른 모듈에서 사용 불가능하다는 이야기는 내가 LikeService 내부에서 RedissonClinet를 사용하고 있는데 PostService에서 LikeService를 참조하여 redissonClient 객체를 쓸 수 없다는 의미이다.
@Service
public class PostService {
private final LikeServiceFacade likeServiceFacade;
public PostService(LikeServiceFacade likeServiceFacade) {
this.likeServiceFacade = likeServiceFacade;
}
public void someMethod() {
// ❌ likeServiceFacade 내부의 redissonClient를 직접 사용하려 하면 안 됨
likeServiceFacade.redissonClient.getLock("post:123").lock(); // 컴파일 에러!
}
}
이제 runtimeOnly에 대해 알아보자. runtimeOnly는 위의 코드에서 처럼 직접적으로 사용되지 않는다.
postgredriver.getConnection // 이렇게 사용하지 않는다.
위처럼 PostgreSQL Driver는 코드에서 직접 getConnection()을 호출하지 않고 JPA가 내부적으로 jdbc 드라이브를 사용만 하기 때문에 이럴땐 컴파일할 때는 필요가 없고, 실행할 때만 있으면 되니까 runtimeOnly를 사용하면 된다. 그렇다고 runtimeOnly를 사용한다고 해서 컴파일할 때 postgresql-driver.jar가 포함되어 있지 않은건 아니다. 포함되어 있다.
드라이버는 포함되어 있으나 컴파일할 땐 필요가 없고 JPA에 의해 실행할 때만 필요하니까 runtimeOnly로 종속되면 된다. 헛갈리면 그냥 실제코드에서 사용하면? Implementation 사용 안 하고 간접적으로 실행만 할 때 사용한다면? runtimeOnly 잘 모르겠으면 그냥 Implementation 사용하면 된다.
Gradle 의존성 implementation과 api 의 차이점
위에서는 컴파일과 실행시점에서 바라봤으면 여기서는 "의존성 전파"를 메인으로 비교해봐야 한다.
Implementation은 Redisson, Spring Boot Starter, ModelMapper 등과 같이 모듈 내부에서만 필요한 것을 의미한다.
api는 Lombok, JPA 공통모듈과 같이 다른 모듈에서도 공통적으로 사용 가능할 때 사용한다고 한다.
그래서 나는 의문이 들어서 내가 했던 프로젝트에 build.gradle을 까보았다.
오잉? lombok이 api가 아니다. compileOnly로 되어 있다. 뭔가 이상해서 다시 자료조사를 해봤다. 알고보니 내가 했던 프로젝트가 MSA기반이 아닌 프로젝트여서 그렇다. 단일 프로젝트는 api를 거의 사용하지 않는다. 왜냐면 api는 의존성 전파를 전체적으로 하기에 빌드속도가 느려지기 때문이고 결합도가 높아지기 때문이다. 다음 사진을 보자.
api는 C가 A까지 사용할 수 있다. 반대로 Implementation은 C가 A를 참조하기 불가능하다.
무슨말이냐면 MSA라면 A라는 모듈만 Lombok 의존성을 가지면 다른 모듈 Service-B,C에서 Lombok을 의존할 필요 없이 A을 참조하여 Lombok을 사용할 수 있는것이다.
하지만 나의 프로젝트는 단일프로젝트였기에 그냥 api가 아닌 compileOnly로 설정되어 있던 것이다. 롬복은 런타임에서는 필요없고, 컴파일할 때만 필요하기에 Implementation가 아닌 compileOnly로 설정되어 있다.
MSA 구조에서 FeignClient 사용예제
그러면 api 구조는 구체적으로 어떨때 사용할까?
예를들어서 service-a(주문 서비스), service-b(결제 서비스)가 있다고하자.
/msa-service
├── service-a (주문 서비스)
│ ├── OrderService.java
│ ├── build.gradle
│
├── service-b (결제 서비스)
│ ├── PaymentService.java
│ ├── build.gradle
│
├── common (공통 모듈, API 정의)
│ ├── PaymentClient.java (FeignClient or RestTemplate)
│ ├── build.gradle
│
├── build.gradle (루트)
service-b (결제 서비스) - PaymentService.java
@Service
public class PaymentService {
public boolean processPayment(String orderId, double amount) {
// 실제 결제 처리 로직 (ex: PG사 연동)
System.out.println("Processing payment for order: " + orderId + " with amount: " + amount);
return true;
}
}
service-b 모듈은 PG사와 연동하는 결제만 담당하는 모듈이다.
common (공통 모듈) - PaymentClient.java (Feign Client)
@FeignClient(name = "payment-service", url = "http://localhost:8081")
public interface PaymentClient {
@PostMapping("/payments")
boolean processPayment(@RequestParam("orderId") String orderId, @RequestParam("amount") double amount);
}
이렇게 지정하면 common 모듈을 통해서 결제 모듈을 사용할 수 있게된다.
주문서비스(service-a)에서는 결제서비스(service-b)를 직접 참조하지 않고 common 모듈(FeignClient)를 통해서 주문 서비스를 제공하는 것이다.
service-a (주문 서비스) - OrderService.java
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
public void createOrder(String orderId, double amount) {
System.out.println("Creating order: " + orderId);
// 주문을 생성하면서 결제 서비스 호출
boolean paymentSuccess = paymentClient.processPayment(orderId, amount);
if (paymentSuccess) {
System.out.println("Order placed successfully!");
} else {
System.out.println("Payment failed. Order canceled.");
}
}
}
이렇게 주문 서비스는 common 모듈을 참조해서 결제 서비스를 불러와 사용한다.
이를 통해 결합도를 낮추고 응집도는 올릴 수 있다. common이라는 공통모듈을 사용하기 때문에 다른 새로운 모듈이 다시 생성되어 common만 또 참조하면 되기에 확장성도 있다.
build.gradle 설정
service-a/build.gradle
dependencies {
implementation project(':common') // 공통 모듈을 의존
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
서비스A(주문서비스)는 common 모듈에 의존하여 서비스B(결제서비스)를 이용한다.
서비스A를 모든 모듈에게 공유하는 것이 아니기 때문에 api를 사용하진 않고 implementation을 사용한다.
service-b/build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
서비스B(결제서비스)는 독립적으로 동작하는 서비스라서 따로 common 모듈에 의존하지 않아도 된다.
common 모듈의 build.gradle
dependencies {
api 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
common 모듈은 common을 참조하는 모든 객체에게 openfeign 서비스를 전파해야하기 때문에 api를 쓴다.
api를 왜 쓰는가.. 를 설명하려고 깊게 들어온 거 같다..
만약에 여기서 service-c 배달서비스가 있다고 했을 때 그냥
// service-c/build.gradle
dependencies {
implementation project(':common') // OpenFeign을 따로 추가하지 않음!
}
common만 추가해주면 common 모듈에 openfeign을 전파해주기 때문에 추가 해주지 않아도
service-c(배달서비스) -> service-b(결제서비스)를 사용할 수 있게 된다.
이것이 implementation, api의 차이다..
정리
implementation은 하나의 모듈에서만 사용가능 하도록 의존성을 추가하는 것이다.
- 예시 라이브러리 : Redisson, Spring Boot Starter
runtimeOnly는 런타임에만 필요한 라이브러리다.
- PostgreSQL Driver
compileOnly는 컴파일 시점에만 필요한 라이브러리다
- Lombok
api는 공통 모듈에서 여러 모듈이 공유해야하는 경우에 사용한다.
- openfeign, Lombok
**참고자료**
https://stackoverflow.com/questions/51127422/gradle-difference-between-implementation-and-runtime
https://docs.gradle.org/current/userguide/declaring_dependencies.html
https://bepoz-study-diary.tistory.com/372
https://k9want.tistory.com/entry/Gradle-dependency-implementation%EA%B3%BC-api-%EC%B0%A8%EC%9D%B4
'Spring' 카테고리의 다른 글
[Spring Boot] Spring 대용량 데이터 페이징 처리하기 2탄(Spring Data JPA + PostgreSQL) (0) | 2025.02.20 |
---|---|
[Spring Boot] Spring 대용량 데이터 페이징 처리하기 1탄(Spring Data JPA + PostgreSQL) (0) | 2025.02.19 |
[Spring] WebClient를 사용 해야하는 이유 [RestTemplate vs WebClient] 성능비교 (0) | 2024.06.08 |
[Spring Boot] Response Entity를 사용하는 이유와 잘 사용하는 법 (0) | 2024.06.01 |
[Spring Boot] @PostConstruct 사용법 (0) | 2024.05.23 |