유지보수하기 좋은 코드
어떤 프로그램이든 유지보수는 반드시 뒤따라 옵니다. 버그 수정, 기능 개선, 클라이언트의 요청 등에 따라 프로그램은 계속 바뀝니다.
그런데 만약 수정하기 어렵게 프로그램을 설계했다면? 유지보수하는 입장에서는 지옥일겁니다. 정말 간단한 기능 하나를 추가하려는데 이곳저곳을 수정해야한다면 개발 의욕이 뚝 떨어질 것 같네요. 요구사항 구현을 넘어 코드의 유연함, 확장성까지 생각할 줄 알아야 실력있는 좋은 개발자가 될 수 있다고 생각합니다.
객체지향 프로그래밍 5가지 설계 원칙
그러면 구체적으로 어떤 프로그램이 잘 설계된 프로그램일까요? 객체지향 프로그래밍에서는 설계시 지켜야할 5원칙이 있습니다. 이 원칙들은 프로그램을 잘 설계할 수 있도록 힌트를 줍니다.
- SRP: 단일 책임 원칙
- OCP: 개방-폐쇄 원칙
- LSP: 리스코프 치환 원칙
- ISP: 인터페이스 분리 원칙
- DIP: 의존성 역전 원칙
이 중에서 DI와 관련된 원칙은 OCP와 DIP입니다.
OCP
OCP는 객체 확장에는 개방적이고 수정에는 폐쇄적이어야 한다는 원칙입니다. 이 말은 객체를 수정하지 않고도 확장할 수 있어야 한다는 의미로 받아들일 수 있습니다.
가령 도형을 그려주는 객체 Renderer가 있다고 합시다. 이 Renderer에 그리고 싶은 도형을 넘겨주면 그 도형을 그려줍니다.
classDiagram
Renderer --> 도형
도형 <|-- 원
도형 <|-- 삼각형
<<Interface>> 도형
class Renderer {
+void draw(도형 shape)
}
만약 도형의 종류를 더 추가하고 싶다면, ‘도형’을 구현하는 클래스를 하나 더 만들면 됩니다. ‘사각형’을 하나 추가해볼게요.
classDiagram
Renderer --> 도형
도형 <|-- 원
도형 <|-- 삼각형
도형 <|-- 사각형
<<Interface>> 도형
class Renderer {
+void draw(도형 shape)
}
OCP를 잘 준수하면 이렇게 Renderer를 직접 수정하지 않고도 ‘사각형’을 하나 새로 구현해서 확장할 수 있습니다.
DIP
DIP는 추상적인 것에 의존해야지 구체적인 것에 의존하면 안된다는 원칙입니다.
classDiagram
사용자 --> 갤럭시
이렇게 구체적인 객체에 바로 의존하면 확장성 및 재사용성이 떨어집니다. 향후 스마트폰을 다른 것으로 바꾸려면 관련된 코드를 모두 수정해야할 수도 있습니다.
classDiagram
사용자 --> 스마트폰
스마트폰 <|-- 갤럭시
스마트폰 <|-- 아이폰
스마트폰 <|-- LG폰
<<Interface>> 스마트폰
하지만 이렇게 추상 객체에 의존하면, ‘사용자’ 코드를 최소한으로 수정해서 다른 스마트폰으로 바꿀 수 있습니다. 즉, 스마트폰이 바뀌더라도 유연하게 대처할 수 있습니다. 심지어 DI를 도입한다면, ‘사용자’를 아얘 손대지 않고도 다른 스마트폰으로 바꿀 수도 있습니다.
문제 상황
위의 스마트폰 Class Diagram을 다시 살펴볼게요.
classDiagram
사용자 --> 스마트폰
스마트폰 <|-- 갤럭시
스마트폰 <|-- 아이폰
스마트폰 <|-- LG폰
<<Interface>> 스마트폰
사용자는 스마트폰이라는 인터페이스에 의존하고 있습니다. 이 클래스 구조를 Java 코드로 간단하게 표현해볼게요.
1
2
3
4
5
6
7
8
9
10
11
class 사용자 {
스마트폰 phone; // 어떤 휴대폰을 사용하는지?
}
interface 스마트폰 {}
class 갤럭시 implements 스마트폰 {}
class 아이폰 implements 스마트폰 {}
class LG폰 implements 스마트폰 {}
사용자는 본인이 어떤 휴대폰을 사용하고 있는지 field로 관리합니다. 그런데 phone 필드는 초기화가 되지 않았으므로 null
인 상태입니다. 그럼 아이폰을 사용하도록 코드를 약간 수정해보죠.
1
2
3
class 사용자 {
스마트폰 phone = new 아이폰();
}
코드상 문제가 없어 보입니다. 하지만 이는 OCP 위반입니다. 왜냐하면 다른 휴대폰으로 바꾸기 위해서는 ‘사용자’코드를 직접 수정해야하기 때문입니다.
그러면 ‘사용자’클래스 내부가 아니라 외부에서 객체를 전달 받도록 하면 어떨까요?
1
2
3
4
5
6
7
class 사용자 {
스마트폰 phone;
사용자(스마트폰 usingPhone){
this.phone = usingPhone;
}
}
OCP위반 문제는 해결된 듯 보입니다. 만약 사용자를 싱글턴 객체로 사용하지 않는다면 이제 큰 문제는 없습니다.
하지만 사용자를 싱글턴으로 사용한다면(또는 싱글턴이 아니어도 모든 사용자 객체가 동일한 스마트폰을 사용한다면) 문제가 발생합니다. 즉, 프로그램 전역에 걸쳐 아이폰을 사용하도록 한다면 코드 중복이 많아집니다. 사용자 객체를 생성할 때마다 매번 아이폰 객체를 만들어서 직접 넣어주어야 하죠. 만약 갤럭시로 바뀐다면? 수정해야할 부분이 많아질겁니다.
스프링에서는 Controller, Service, Repository등 싱글턴으로 생성해서 관리해야하는 경우가 많습니다. 이 경우에는 좀 더 세련된 해결 방법이 필요하죠.
DI를 사용하면?
DI는 앞서 했던 의존성 관리를 대신 해줍니다. 앞에서 우리는 객체를 직접 만들어서 생성자를 통해 직접 전달했습니다. 하지만 DI 프레임워크를 사용하면 직접 이러한 객체들을 생성하고 관리해줍니다. Spring은 Bean, Android는 Hilt/Coil 이런 것들이 DI 프레임워크입니다.
의존성 주입을 자동으로 해준다
객체를 주입해달라고 표식을 남기면, 프레임워크가 만든 객체를 자동으로 넣어줍니다. 표식은 보통 Java의 Annotation으로 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
@Service
public class MyService {
MyRepository repo;
// Spring은 이렇게 Autowired annotation으로 의존성 주입이 필요한 지점을 표시합니다.
@Autowired
public MyService(MyRepository repo) {
this.repo = repo;
}
}
그러면 프레임워크가 annotation을 감지해서 생성자/필드/setter 등을 통해 객체를 주입합니다.
싱글턴으로 객체를 관리해준다.
대부분의 DI 프레임워크는 객체를 싱글턴으로 관리합니다. 어떤 클래스에서 의존성 주입을 원하든, 모두 같은 객체를 사용합니다. 물론 이는 사용자의 필요에 따라서 조절할 수 있습니다.
Dependency Cycle을 탐지해준다.
위의 예시에서는 의존성 관계가 비교적 간단했습니다. 나중에 프로그램의 규모가 커진다면 의존성 관계도 그만큼 복잡해집니다. 이렇게 복잡해지면 dependency cycle을 만드는 실수가 종종 나옵니다. 이를 감지하고 사전에 막아주기도 합니다.
아래 그림은 dependency cycle이 발생한 예시입니다.
classDiagram
A --> B
B --> C
C --> D
D --> B
테스트가 더 편해진다.
직접 싱글턴 패턴으로 클래스를 구현하면 테스트하기 어려워집니다. 이유는 아래와 같습니다.
- 유닛 테스트간 의존성 문제. 같은 객체를 초기화하지 않고 여러 테스트에 걸쳐 사용하게 된다.
- Mocking이 어렵다.
하지만 DI를 사용하면 위의 문제가 해결됩니다. 테스트할때는 DI를 쓰지않고 직접 객체를 만들어서 사용하면 되기 때문입니다. 특히 생성자 주입을 사용하면 테스트가 매우 쉬워집니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Service {
MyRepository repo;
@Autowired // 생성자 주입. 테스트할때는 무시된다.
Service(MyRepository repo){
this.repo = repo;
}
}
class Repository implements MyRepository {}
class ServiceTest {
@Test
void unit_test_1() {
Repository repo = new Repository();
Service service = new Service(repo); // 테스트할때는 이렇게 간결하게 직접 주입.
}
}
생성자 주입을 사용하면 이렇게 훨씬 쉽게 테스트할 수 있습니다.
참고
- 『객체지향의 사실과 오해』, 위키북스(2015)
- https://devlog-wjdrbs96.tistory.com/380