결합도(Coupling)는 (클래스, 모듈, 함수 등) 간의 상호 의존성의 정도를 나타낸다.
📖 서론
1. 유지보수의 용이성: 낮은 결합도는 한 컴포넌트의 변경이 다른 컴포넌트에 미치는 영향을 줄여, 유지보수를 용이하게 해 준다.
2. 재사용성의 증가: 각 컴포넌트가 독립적으로 기능하므로, 다른 곳에서 재사용하기가 쉬워진다.
3. 테스트의 용이성: 컴포넌트 간 결합도가 낮으면 독립적인 테스트가 가능해져, 테스트 과정이 간소화된다.
결합도(Coupling)는 소프트웨어 개발, 특히 객체 지향 및 프레임워크 사용과 관련된 중요한 주제다. 스프링 프레임워크를 사용하는 컨텍스트에서 결합도를 낮추는 방법을 이야기하는 것은 매우 중요하다고 생각한다.
특히 결합도는 소프트웨어 컴포넌트 간의 상호 의존성을 얼마나 효율적으로 관리하는지를 나타내는 지표로써도 사용된다. 스프링 프레임워크에서 결합도를 낮추는 전략을 사용하는 것은 애플리케이션의 유연성, 확장성 및 유지보수성을 향상시켜 준다.
지금부터 설명할 인터페이스 기반 프로그래밍, 의존성 주입, 프로파일 설정, 이벤트 기반 프로그래밍, AOP와 같은 기술적 방법들은 모두 결합도를 관리하는 데 있어 핵심적인 역할을 한다.
1. 인터페이스 기반 프로그래밍으로 결합도 낮추기
인터페이스 기반 프로그래밍이란?
인터페이스를 통해 구체적인 클래스보다 추상화된 계약에 의존하게 한다. 이렇게 하면 구현 클래스가 변경되더라도 인터페이스를 사용하는 코드에는 영향을 덜 미치게 된다.
예시코드로 이해해 보자
지금부터 PaymentService 인터페이스를 작성할 텐데 이 인터페이스는 결제 처리의 추상적인 계약을 정의한다. 구체적인 결제 방법(예: PayPal, 신용카드, 무통장 입금 등)에 대해서는 명시하지 않는다.
public interface PaymentService {
void processPayment();
}
다음으로 PaymentService 인터페이스를 구현하는 PaypalPaymentService를 구현한다.
이 클래스는 PaymentService 인터페이스를 구현한다. 여기서 중요한 점은 OrderService가 PaypalPaymentService 클래스를 직접 참조하지 않는다는 것이다. 대신 PaymentService 인터페이스에 의존한다.
@Service
public class PaypalPaymentService implements PaymentService {
@Override
public void processPayment() {
// PayPal 결제 처리 로직
}
}
마지막으로 OrderService에서는 PaymentService 인터페이스를 의존하도록 코드를 작성한다.
OrderService는 구체적인 결제 방법(구현체 코드)을 몰라도 된다. 그저 PaymentService 인터페이스의 processPayment메서드를 호출하기만 하면 되는 것이다. 이는 OrderService와 구체적인 결제 서비스 간의 결합도를 낮춘 것이다. (스프링이 인터페이스에 알아서 구현체를 주입해 준다.)
@RequiredArgsConstructor
@Service
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void completeOrder() {
// 주문 완료 로직
paymentService.processPayment();
}
}
인터페이스 기반 프로그래밍의 역할은 무엇일까?
1. 추상화를 통한 유연성
인터페이스는 특정 구현과의 직접적인 결합을 제거하고, 대신 추상화된 계약에 의존하게 만든다. 이렇게 함으로써, 구현체가 변경되거나 다양한 구현체가 생겨나도, 인터페이스를 사용하는 코드는 영향을 받지 않게 된다.
2. 유지보수 및 확장성 개선
시스템의 한 부분(예: PaymentService의 구현체)을 수정하거나 교체해도, 이 인터페이스에 의존하는 다른 부분(예: OrderService)은 변경할 필요가 없게 되었다. 이렇게 됨으로써 애플리케이션의 유지보수성과 확장성이 크게 개선되었다.
3. 결합도와 응집도의 균형
인터페이스 기반 프로그래밍은 결합도를 낮추면서도 응집도는 높게 유지할 수 있게 해 준다. 즉, 각 구성 요소는 서로 독립적으로 유지되면서도, 같은 기능 또는 비즈니스 로직을 중심으로 잘 조직될 수 있게 된다.
이러한 방식으로 인터페이스 기반 프로그래밍을 활용하면, 소프트웨어 설계에서 결합도를 효과적으로 낮출 수 있다. 이는 애플리케이션의 유연성과 확장성을 높이는 데 큰 도움이 되며, 유지보수를 간소화하고 장기적인 관리가 용이한 코드베이스를 구축하는 데 중요한 역할을 한다.
2. 의존성 주입(Dependency Injection)으로 결합도 낮추기
의존성 주입(Dependency Injection)이란?
스프링에서 의존성 주입(Dependency Injection)은 결합도를 낮춰준다. 의존성 주입은 클래스가 직접적으로 다른 클래스의 인스턴스를 생성하는 대신 외부(대개는 스프링 프레임워크)에서 필요한 의존성을 제공받는 방식이다. 이 방식은 구현체에 대한 직접적인 의존성을 줄이고 유연성을 증가시킨다.
의존성 주입과 결합도 감소 이해하기
1. 구체적인 클래스 대신 인터페이스 의존
일반적으로 스프링에서는 구체적인 클래스보다는 인터페이스에 의존하는 것이 권장됩니다. 그러나 필요에 따라 구체적인 클래스에 대한 의존성 주입도 가능합니다. 인터페이스 의존이 일반적으로 더 유연하지만, 특정 상황에서는 구체적인 클래스에 의존하는 것이 더 적합할 수 있습니다.
2. 의존성 주입 방식
@Autowired 어노테이션을 사용하거나 생성자 주입 방식을 사용할 수 있습니다. 생성자 주입은 클래스 간 명확한 의존 관계를 설정하고, 의존성 주입이 런타임에 이루어지므로 안정성을 높입니다.
3. 결합도 감소
의존성 주입을 사용하면 클래스는 필요한 의존성을 외부에서 제공받기 때문에, 해당 의존성의 구체적인 구현을 몰라도 됩니다. 이것은 클래스 간의 결합도를 낮추고, 한 부분의 변경이 다른 부분에 미치는 영향을 줄입니다.
예시코드로 의존성 주입 이해하기
아래의 코드는 의존성 주입을 사용하여 UserService가 UserRepository에 의존하는 방법을 보여주는 예시다. 여기서 @RequiredArgsConstructor 어노테이션(Lombok 사용)은 필요한 모든 final 필드에 대한 생성자를 자동으로 생성한다. 따라서 @Autowired 어노테이션을 생략할 수 있다. 이렇게 생성된 생성자를 통해 스프링은 UserRepository의 인스턴스를 UserService에 주입한다.
@RequiredArgsConstructor
@Service
public class UserService {
// @Autowired는 생략해도 된다. (Lombok사용 및 생성자가 1개라서)
@Autowired
private final UserRepository userRepository;
}
이 방식은 UserService가 UserRepository의 구체적인 구현을 알 필요가 없게 만들고, UserRepository의 구현이 변경되어도 UserService를 수정할 필요가 없게 한다. 이는 UserService와 UserRepository 간의 결합도를 현저하게 낮추는 결과를 가져온다.
3. 프로파일(Profile)을 통해 결합도 낮추기
프로파일(Profile)을 통해 결합도 낮추기
스프링 프로파일은 애플리케이션의 구성을 다양한 환경에 맞게 분리할 수 있는 방법이다. 예를 들어, '개발(dev)', '테스트(test)', '운영(prod)' 같은 환경별로 다른 설정을 적용할 수 있다.
프로파일로 결합도를 낮추는 방법 이해하기
1. 환경에 따른 구현체 분리
프로파일을 사용하면, 같은 인터페이스에 대해 환경별로 다른 구현체를 제공할 수 있다. 이렇게 함으로써 개발 환경과 운영 환경에서 각기 다른 동작을 하는 컴포넌트를 쉽게 관리한다. 예를 들어, 개발 환경에서는 실제 상용의 데이터베이스(MySQL) 대신에 인메모리 기반 저장소(H2)를 사용하는 구현체를 적용할 수 있고, 운영 환경에서는 실제 데이터베이스(PostgreSQL)를 사용하는 구현체를 적용할 수 있다.
2. 유연한 구성 변경
프로파일을 활용하면, 애플리케이션의 구성을 변경하고자 할 때 코드 수정 없이 환경 설정만으로 쉽게 바꿀 수 있다. 이는 코드의 변경을 최소화하면서도 다양한 환경에 대응할 수 있게 해준다.
예시코드를 통해서 이해하기
아래의 코드에서 DataSourceConfig 인터페이스에는 개발(DevDataSourceConfig)과 운영(ProdDataSourceConfig) 환경에 맞는 구현체가 존재한다. 이렇게 개발 환경과 운영 환경에서 서로 다른 데이터 소스 구성을 적용할 수 있다.
public interface DataSourceConfig {
DataSource createDataSource();
}
@Profile("dev")
@Configuration
public class DevDataSourceConfig implements DataSourceConfig {
// 개발 환경용 데이터 소스 구성
}
@Profile("prod")
@Configuration
public class ProdDataSourceConfig implements DataSourceConfig {
// 운영 환경용 데이터 소스 구성
}
프로파일을 사용함으로써, 우리는 환경별로 다른 구성을 쉽게 분리하고 관리할 수 있다. 이는 코드의 변경 없이도 다양한 환경에 유연하게 대응할 수 있게 해 주며, 이는 결국 결합도를 낮추는 효과를 가져온다. 프로파일은 환경에 맞춰 필요한 구현체만을 활성화시키므로, 코드 베이스를 깔끔하게 유지하며 다양한 설정을 효율적으로 관리할 수 있다.
4. 이벤트 기반 프로그래밍(Event-Driven Programming)으로 결합도 낮추기
이벤트 기반 프로그래밍(Event-Driven Programming)으로 결합도 낮추기
이벤트 기반 프로그래밍은 애플리케이션의 구성 요소들이 이벤트를 통해 서로 소통하는 방식이다. 이벤트는 특정 상황이나 조건이 발생했을 때 발생하는 신호나 알림 같은 것이라고 생각하면 된다. 스프링에서는 이벤트 리스너와 이벤트 발행 기능을 제공한다. 이를 통해 시스템의 다양한 부분이 서로 직접적으로 의존하지 않고도 상호작용할 수 있게 된다.
이벤트를 통한 결합도 감소의 원리
1. 독립적인 컴포넌트 상호작용
이벤트를 사용하면 각 컴포넌트(예: 서비스, 리스너)가 서로 직접 의존하지 않고도 작업을 수행할 수 있다. 즉, 한 컴포넌트의 변경이 다른 컴포넌트에 영향을 미치지 않는다.
2. 이벤트 중심의 통신
컴포넌트는 이벤트를 발행(예: 주문 완료)만 하고, 누가 이 이벤트를 처리할지는 신경 쓰지 않는다. 이렇게 이벤트 중심으로 통신하면, 각 컴포넌트는 자신의 역할에만 집중할 수 있으며, 결합도가 낮아진다.
3. 유연한 확장성 및 유지보수성
새로운 이벤트 리스너를 추가하거나 기존 리스너를 변경하는 것이 쉬워지기 때문에, 시스템을 더 유연하게 확장하고 유지보수할 수 있다.
예시 코드를 통해 이벤트 기반 프로그래밍 이해하기
이벤트 클래스 정의: 주문 완료 시 발생하는 이벤트를 나타내는 클래스를 정의한다. 이 클래스는 주문 정보를 담고 있다.
스프링 프레임워크의 최신 버전에서는 이벤트를 만들기 위해 ApplicationEvent 클래스를 확장할 필요가 없다. 대신, 일반 POJO(Plain Old Java Object)를 이용하여 이벤트를 정의할 수 있게 되었다. 이벤트를 발행할 때는 ApplicationEventPublisher를 사용하여 이벤트를 발행하면 된다.
public class OrderCompleteEvent {
private final Order order;
public OrderCompleteEvent(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
}
이벤트 리스너 구현:
주문 완료 이벤트를 감지하고 처리하는 리스너다. 이 리스너는 주문 완료와 관련된 로직만 처리하고, 주문 서비스의 다른 부분과는 독립적으로 작동하게 된다.
@Component
public class OrderEventListener {
// 이벤트가 발행되면 이 메서드가 동작한다.
@EventListener
public void onOrderComplete(OrderCompleteEvent event) {
// 주문 완료 이벤트 처리 로직
Order order = event.getOrder();
// 주문 관련 처리를 여기서 수행
}
}
이벤트 발행:
주문 서비스에서 주문이 완료되면 이벤트를 발행한다. 이 서비스는 누가 이 이벤트를 처리할지, 어떻게 처리할지 알 필요가 없다. 이벤트 발행 후의 처리는 리스너의 책임이 된다.
@RequiredArgsConstructor
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public void completeOrder(Order order) {
// 주문 완료 처리 로직
// 로직 완료 후 마지막에 스프링 이벤트 발행
eventPublisher.publishEvent(new OrderCompleteEvent(this, order));
}
}
이렇게 이벤트 기반 프로그래밍은 각 컴포넌트가 독립적으로 작동하면서도 필요할 때 서로 소통할 수 있게 해서 결합도를 낮춰준다. 이는 스프링 애플리케이션의 유연성과 유지보수성을 크게 향상시키는 중요한 요소다.
5. AOP(Aspect-Oriented Programming)로 결합도 낮추기
AOP를 통해 결합도 낮추기
AOP는 관심사의 분리(Separation of Concerns)를 위한 프로그래밍 패러다임이다. AOP를 사용하면 로깅, 보안, 트랜잭션 관리 등과 같은 크로스 커팅(cross-cutting) 관심사를 주요 비즈니스 로직으로부터 분리하여 모듈화 할 수 있다.
AOP의 장점
1. 결합도 감소: 비즈니스 로직과 크로스 커팅 관심사를 분리함으로써, 각각의 코드가 서로에게 덜 의존적이 된다.
2. 코드 재사용 증가: 로깅, 보안 체크 등의 기능을 여러 곳에서 재사용할 수 있다.
3. 유지보수성 향상: 관심사가 중앙집중화되므로, 변경사항이 발생할 때 한 곳에서만 수정하면 된다.
AOP의 적용 예시
로깅 Aspect: 이 예시에서, LoggingAspect 클래스는 서비스 메서드가 호출되기 전후에 로깅을 수행한다. @Before 어노테이션은 메소드 실행 전에, @After 어노테이션은 메소드 실행 후에 로직을 수행하도록 지정해 준다.
@Aspect
public class LoggingAspect {
@Before("execution(* com.yourapp.service.*.*(..))")
public void logBeforeServiceMethods(JoinPoint joinPoint) {
// 서비스 메소드 실행 전 로깅
System.out.println("Method 호출 전: " + joinPoint.getSignature().getName());
}
@After("execution(* com.yourapp.service.*.*(..))")
public void logAfterServiceMethods(JoinPoint joinPoint) {
// 서비스 메소드 실행 후 로깅
System.out.println("Method 호출 후: " + joinPoint.getSignature().getName());
}
}
AOP를 활용하면 크로스 커팅 관심사를 비즈니스 로직으로부터 분리할 수 있어, 결합도를 낮추고 코드의 재사용성과 유지보수성을 높일 수 있다.
이벤트 드리븐 아키텍처를 적용해서 느슨한 결합을 만들어본다.👇🏻👇🏻
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
[Spring] 톰캣과 스프링: 웹 요청의 라이프사이클 이해하기 (27) | 2023.12.31 |
---|---|
스프링에서 느슨한 결합 만들기: 이벤트 기반 아키텍처 적용 (37) | 2023.12.25 |
[Spring] RuntimeException의 메시지 (0) | 2023.12.21 |
가볍게 알아보는 디자인 패턴 - 퍼사드 패턴(Facade Pattern) (3) | 2023.12.11 |
가볍게 알아보는 디자인 패턴 - 팩토리 메서드 패턴(Factory Method Pattern) (1) | 2023.12.11 |