SpringMVC 구조로 개발을 할 때 Service 계층을 Service, ServiceImpl 즉 인터페이스와 구현체로 구분하여 개발을 하던 중 이것이 단순히 관습적인 구조로 인한 것인지 아니면 정말 근거를 가지고 작성을 했던 건지 스스로를 되돌아보게 되었다.
이 주제는 정말 다양한 측면에서 생각해 볼 수 있는 문제이고, 상황에 따라 수많은 의견이 분분한 주제이므로 정답은 없다고 생각한다.
하지만 이번 주제에 대해서든, 그게 아니더라도 어떠한 구조든 적용했을 때는 그만한 근거를 가지고 이유를 알고 사용해야 유의미하다고 생각하기에 이번 글에서 그 이유에 대해 정리해보려고 한다.
객체지향 설계의 관점에서 본 Service, ServiceImpl
객체지향 설계의 대표적인 원칙인 SOLID를 먼저 살펴볼 필요가 있다. 이 중에서도 특히 OCP(Open Closed Principle), ISP(Interface Segregation Principle), DIP(Dependency Inversion Principle)가 서비스 계층에서 인터페이스와 구현체를 나누는 이유와 직결된다.
OCP (개방 폐쇄 원칙)
OCP는 확장에는 열려있고 변경에는 닫혀 있어야 한다는 원칙이다. 기능이 확장될 때 기존 코드에 대한 수정이 최소화되어야 하며, 인터페이스를 통한 추상화는 이런 확장성을 보장해준다.
간단한 Java 코드 예제를 확인해 보자.
public interface Shape {
double area();
}
@AllArgsConstructor
public class Rectangle implements Shape {
private double width;
private double height;
public double area() {
return width * height;
}
}
위와 같이, Shape라는 인터페이스가 있고, Rectangle 클래스는 이를 상속받아 사각형의 넓이를 반환한다.
여기서 Circle 클래스를 추가(확장)하고 싶으면 다음과 같이 인터페이스를 상속받아 쉽게 확장할 수 있다.
@AllArgsConstructor
public class Circle implements Shape {
private double radius;
public double area() {
return Math.PI * radius * radius;
}
}
실제 Shape 인터페이스를 활용하는 예제를 확인해 보자.
public class Desk implements Shape {
private final Shape shape = new Rectangle();
...
}
만약 Shape를 사각형이 아닌 원으로 바꾸고 싶다면 선언부 코드의 수정이 불가피하다. 이는 구현체(Rectangle)에 의존하고 있기 때문에 DIP 위반이다.
DIP (의존 역전 원칙)
DIP는 구체적인 클래스에 의존하지 말고, 추상화된 인터페이스에 의존하라는 원칙이다. 이렇게 추상화에 의존함으로써 구현체에 대한 변화가 일어나도 코드가 유연하게 대처할 수 있다.
위 예제에서 Rectangle을 주입하기 위해 Rectangle에 의존성이 생기는 게 불가피하다. DIP를 만족시키려면 다음과 같이 코드가 작성되어야 한다.
public class Desk implements Shape {
private Shape shape;
public Desk(Shape shape) {
this.shpae = shape;
}
...
}
이로써 구현체(구체화)에 대한 의존이 없어지고 인터페이스(추상화)에만 의존되어 있는 것을 알 수 있다.
하지만 Desk의 객체를 생성할 때 생성자에 구현체를 주입해줘야 한다는 한계점이 존재한다.
이러한 한계점은 프레임워크의 도움을 받으면 쉽게 해결할 수 있다.
DI (Dependency Injection)
코드로 직접 선언하지 않고 프레임워크가 의존성을 주입해 준다면 OCP도 만족할 수 있게 된다.
이를 가능하게 하는 것이 Spring의 DI(의존성 주입)이다.
Spring은 DI Container를 통해 인스턴스의 생명주기를 관리한다. 설정 파일 혹은 어노테이션을 기반으로 빈(bean)을 생성해, 애플리케이션 전역에서 사용할 수 있다.
개발자가 직접 종속성을 관리하지 않고 프레임 워크에서 직접 해주기 때문에 IoC(Inverse of Container)라고도 불린다.
덕분에, 개발자는 비즈니스 로직에만 집중할 수 있다.
위 예시에서, Desk 클래스에서 동그라미 모양으로 만들고 싶다면 다음과 같이 Circle 클래스를 빈으로 등록하면 된다.
@Component
@AllArgsConstructor
public class Circle implements Shape {
private double radius;
public double area() {
return Math.PI * radius * radius;
}
}
@Component 어노테이션을 추가함으로써, 컴포넌트 스캔의 대상이 되어 빈으로 등록된다.
Desk 클래스의 생성자에 빈으로 등록된 Circle이 주입되면서 원하는 대로 동작할 수 있다.
혹은, Rectangle로 변경하고 싶다면 @Qulifier, @Primary 같은 어노테이션을 활용해 원하는 인스턴스를 빈으로 등록할 수 있다.
이처럼, 인터페이스로 추상화시키는 것은 변화에 더욱 유연하게 대처할 수 있도록 만든다.
~Service, ~ServiceImpl을 분리하는 것은 전략 패턴(Strategy Pattern)이라고 할 수 있다.
인터페이스에 해야 할 역할을 정의하고, 이를 상속받아 각 전략에 맞게 기능을 구현하는 것이다.
인터페이스로 분리한 덕분에, 구현체에 의존하지 않고 개발할 수 있다. 이는 결합도(Coupling)를 낮춰서 변화가 생겨도 유연하게 받아들일 수 있다.
결국 인터페이스로 추상화시킴으로써 객체지향의 설계 원칙을 지킴과 동시에 Java의 다형성까지 활용할 수 있다.
결론
물론, 과하게 높은 수준의 추상화는 오히려 역효과를 불러일으킨다. 쉬운 것도 어렵게 돌아가게 만들 수 있기 때문이다.
당장 주어진 상황뿐 만 아니라, 앞으로의 변화 가능성을 판단해야 한다. 이는 해당 도메인에 대한 깊은 이해를 요구한다.
따라서, 적정선의 추상화가 어렵고 기준이 명확하지 않기 때문에 인터페이스와 구현체로 분리하는 것에 대한 의견이 분분하다고 생각한다.
그럼에도 불구하고, 추상화는 객체지향의 중요한 특징 중 하나로 뽑히는 만큼 포기할 수 없다.
코드의 재사용성을 늘리고, 독립적으로 클래스 내부를 수정할 수 있다는 특징이 있기 때문이다.
특히 나 같이 경험이 적은 개발자는 장기적인 상황을 예측하기 힘들기 때문에, 여러 상황에 대비해야 한다.
SOLID와 같이 원칙이 주어지면 최대한 지키는 쪽이 장기적으로 봤을 때 안정적이라는 판단을 내렸다.
'SPRING' 카테고리의 다른 글
[Spring] @ModelAttribute와 @RequestParam 더 깊이 이해하기 (2) | 2024.08.28 |
---|---|
[스프링 MVC] - 핸들러 매핑과 핸들러 어댑터의 구조 이해 (1) | 2024.01.15 |
의존관계 주입 - 자동, 수동의 올바른 기준 (0) | 2023.08.11 |