객체지향 5원칙 (SOLID)은 구시대의 유물 ?

*이 포스트는 Robert C. Martin 님의 허락을 받아 blog.cleancoder.com 의 글
Solid Relevance“를 번역한 것입니다. 저작권에 유의하시기 바랍니다.

서론

얼마 전 누군가의 고민이 담긴 메일을 받았습니다. 이런 내용이었죠:

오랫동안 객체지향 5원칙, SOLID principle에 대한 이해도는 저희 채용 프로세스에서 중요한 부분을 차지하고 있습니다. 지원자가 원칙들을 잘 이해하고 있기를 기대하니까요. 그런데 최근에, 이제는 개발에서는 멀어진 관리자 한 분이 그게 올바른 일인지 의문을 제시했습니다. 요즘 개발은 거대한 모놀리식 구조 대신 변경이 쉽고 안전한 마이크로서비스 위주인데, 개방-폐쇄 원칙은 더이상 중요하지 않은 게 아니냐? 이제는 20년 전처럼 상속 관계를 중요시 하지 않는데 리스코프 치환 원칙은 그냥 구닥다리 아니냐? 하는 식이었죠. 저도 이제는 우리가 객체지향 5원칙에 대한 Dan North의 입장, “그냥 심플하게 코딩해라.”를 따르는게 맞지 않나 싶습니다.

그래서 아래처럼 답장을 썼습니다:

객체지향 5원칙, 즉 SOLID 원칙은 90년대나 지금이나 중요합니다 (90년대 이전에도 마찬가지). 왜냐하면 소프트웨어라는 것은 많이 바뀌지 않았거든요. 1945년 앨런 튜링이 전자 컴퓨터를 위해 쓴 최초의 코드도 if 문, while 루프, 대입문으로 이루어져 있고 그건 지금도 별반 다르지 않습니다. 즉 소프트웨어는 여전히 순서(Sequence), 선택(Selection), 반복(Iteration)로 이루어져 있죠.

모든 신세대들은, 자기들의 세상이 이전 세대랑은 완전히 달라졌다고 생각하는 편입니다. 그런데 그건 틀렸습니다. 신세대들의 다음 세대가 와서 세상이 얼마나 바뀌었는지 설명할 때 쯤 자신들이 틀렸었다는걸 깨닫게 되죠.

자, 이제 원칙들 하나하나에 대해서 얘기해봅시다.

단일 책임 원칙 (Single Responsibility Principle)

같은 이유로 변경될 코드들은 모으고. 다른 이유로 변경될 코드들은 흩어라.

이 원칙이 소프트웨어 개발에 더이상 중요하지 않다는 건 상상하기 어렵네요. 우리는 GUI 코드와 비즈니스 로직을 섞어놓지는 않습니다. SQL 쿼리와 통신 프로토콜을 섞어놓지도 않구요. 우리는 한 부분을 수정했을 때 다른 부분에 버그가 생기지 않도록, 다른 이유로 변경될 코드들은 흩어놓습니다. 우리는 다른 이유로 변경될 모듈들끼리 의존성을 가지지 않도록 합니다.

마이크로서비스가 이 문제를 해결해주지는 않습니다. 다른 이유로 변경될 코드들을 섞어놓는다면, 엉킨 마이크로 서비스나 스파게티 마이크로 서비스 집합같은 걸 만들어내게 되죠.

Dan North 의 슬라이드들은 이런 점에 대해 완전히 간과하고 있습니다. 원칙에 대해서 전혀 이해하지 못하고 있다고 생각돼요. 단일 책임 원칙에 대한 그의 주장은 “그냥 심플하게 코딩해라”입니다. 동의해요. 그런데 심플하게 코딩하는 방법 중 하나가 단일 책임 원칙을 잘 지키는 겁니다.

개방-폐쇄 원칙 (Open-Closed Principle)

모듈은 확장에 열려있고, 변경에는 닫혀있어야 한다.

모든 원칙들 중에서도, 누군가 이 원칙에 의문을 제기한다는 건 우리 산업의 미래가 어떻게 될지 저를 공포로 몰아넣네요. 당연히 우리는 기존 코드를 변경하지 않고도 확장할 수 있는 모듈을 만들어야 합니다. 디바이스 독립성을 지원하지 않아서 디스크 파일에 출력하는 방법이 프린터, 화면 혹은 파이프에 출력하는 방법과 완전히 다른 시스템을 상상할 수 있나요? 그런 세부 사항들을 하나하나 처리하려고 if 문으로 도배된 코드를 보고 싶나요?

아니면… 세부 사항들과 추상적인 컨셉들을 분리해두고 싶나요? 비즈니스 로직을 짜증나는 GUI 세부 구현, 마이크로서비스 통신 프로토콜, 멋대로인 데이터베이스 동작과 분리시키고 싶나요? 당연하죠!

또 다시, Dan 의 슬라이드는 이런 사실을 완전히 간과하고 있습니다. 요구 사항이 변경됐을 때, 기존 코드는 부분적으로만 문제가 됩니다. 대부분의 코드는 그대로도 괜찮아요. 그리고 우리는 문제되는 코드를 제대로 동작하도록 수정할 때 멀쩡한 코드는 건드릴 필요가 없도록 보장하고 싶죠. Dan 이 주장하는 해결책은 “그냥 심플하게 코딩해라”입니다. 이번에도 역시 동의합니다. 그리고 아이러니하게도, 심플한 코드는 확장에 열려있고 변경에 닫혀있습니다.

리스코프 치환 원칙 (Liskov Substitution Principle)

어떤 인터페이스를 사용하는 프로그램은 그 인터페이스의 구현체(implementation)에 의해 동작이 오락가락하면 안된다.

사람들은 (저를 포함해서) 이 원칙이 상속에 대한 것이라고 실수했습니다. 아니에요. 이건 서브타이핑에 대한 겁니다. 모든 인터페이스들의 구현체들은, 어떤 한 가지 인터페이스의 서브타입입니다. 모든 덕타입들은, 어떤 한 가지 암묵적 인터페이스의 서브타입입니다. 그리고 모든 베이스 인터페이스의 사용자들은, 명시적이건 암묵적이건, 인터페이스의 의미에 동의해야 합니다. 만약 어떤 한 구현체가 사용자를 혼란스럽게 만들면, if/switch 문이 증식하게 될 겁니다.

*역자주: 리스코프 치환 원칙을 헷갈려하는 분들이 많아서, 위 내용을 예를 들어 설명해보겠습니다. 특정 경로를 삭제하는 메소드를 가진 파일시스템 인터페이스가 있습니다. A 구현체는 경로가 디렉토리이면 무시하는데, B 구현체는 경로가 디렉토리여도 통째로 삭제합니다. 두 구현체가 인터페이스의 의미를 혼란스럽게 만들기 때문에, 사용자는 인터페이스를 사용하기 전에 if 문으로 경로가 디렉토리일 경우를 처리해줘야 합니다. 이런 상황을 막기위해 인터페이스는 “파일인 경우에만 삭제한다” 혹은 “그냥 다 삭제한다” 둘 중 하나의 의미를 명확히 가져야 하고, 모든 사용자들은 이런 의미를 모두가 동일하게 인식하고 있어야 한다. 즉 의미에 동의해야 한다는 말입니다.

이 원칙은 추상화를 뚜렷하고 잘 정의된 상태로 유지하는 것에 대한 것입니다. 이게 시대에 맞지않는 개념이라고는 생각할 수 없죠.

이 원칙에 대한 Dan 의 슬라이드들은 전적으로 옳았습니다. 단순히 핵심을 놓친 것 뿐이죠. 심플한 코드는 서브타입들의 관계를 뚜렷하게 유지하는 코드입니다.

인터페이스 분리 원칙 (Interface Segregation Principle)

사용자가 필요하지 않은 것들에 의존하게 되지 않도록, 인터페이스를 작게 유지하라.

우리는 아직 컴파일 언어를 씁니다. 우리는 아직 어떤 모듈들이 다시 컴파일되고 다시 배포되어야 할지 결정하기 위해 수정 일자에 의존합니다. 이런 사실 아래서는 모듈 A가 컴파일 타임에 모듈 B에 의존하면, 런타임 의존성이 없더라도 모듈 B를 수정했을 때 모듈 A를 재컴파일, 재배포 해야하는 문제를 겪을 수 밖에 없습니다.

이 문제는 특히 Java, C#, C++, go, Swift 등과 같은 정적 타입언어에서 민감합니다. 동적 타입 언어에서는 영향을 덜 받지만, 그렇다고 영향이 없지는 않죠. Maven과 Leiningen의 존재가 그 증거입니다.

이 원칙에 대한 Dan 의 슬라이드에서, 클라이언트는 사용하지 않는 메소드에는 의존하지 않는다는 내용은 거짓입니다. 어떤 메소드가 변경됐을 때 재컴파일, 재배포가 필요하다면 클라이언트는 그 메소드를 사용하지 않더라도 의존하고 있는 겁니다. Dan 의 마지막 주장은 좋습니다. 두 개의 인터페이스를 가진 클래스를 두 개의 클래스로 분리할 수 있다면, 그렇게 하는 건 좋은 아이디어 입니다 (단일 책임 원칙). 그런데 그런 분리는 이뤄질 수 없거나, 원하지 않는 경우가 많죠.

의존 역전 원칙 (Dependency Inversion Principle)

추상화하는 방향으로 의존하라. 상위 레벨 모듈이 하위 레벨 세부 사항에 의존해서는 안된다.

이 원칙을 유용하게 쓰지 않는 아키텍쳐를 떠올리기 어렵네요. 우리는 상위 레벨의 비즈니스 로직들이 하위 레벨 세부 사항에 의존하는 것을 원하지 않습니다. 이게 완전히 당연하게 여겨졌으면 좋겠네요. 우리는 우리에게 돈을 벌어주는 계산들이 SQL 이나 하위 레벨 검증, 포매팅 문제로 더럽혀지는 것을 원하지 않습니다. 상위 레벨의 추상화와 하위 레벨의 세부 사항이 격리되기를 원하죠. 격리는 시스템의 의존 관계를 신중하게 관리해서, 모든 소스 코드 의존 관계가 하위 레벨 세부 사항이 아닌 상위 레벨 추상화를 향하도록 해서 이룰 수 있습니다. 특히 구조적인 경계(architectural boundaries)를 넘나드는 경우에요.

결론

Dan 의 슬라이드들은 전부 “그냥 심플하게 코딩해라”로 끝납니다. 이건 좋은 조언입니다. 그러나 세월이 우리에게 가르쳐준 것이 있다면 그것은 심플함, 즉 단순함은 원칙들을 따른 훈련을 통해 얻어진다는 것입니다. 그 원칙들은 단순함을 정의하는 것들이고, 훈련들은 개발자들이 단순함을 따르는 코드를 작성하도록 만드는 것이죠.

복잡한 난장판을 만드는 가장 좋은 방법은 모두에게 “심플한 게 최고야”라고만 말한 뒤에 그렇게 하려면 어떻게 해야하는지 아무런 안내도 해주지 않는 것이라구요.

4.3 4 votes
Article Rating
구독
Notify of
guest
2 Comments
오래된 순
최근 순 좋아요 순
Inline Feedbacks
View all comments
Taehoon

의존 역전 원칙 (Dependency Inversion Principle) 문단에서 “우리에게 돈을 벌어주는 계산들“이 뭔가싶었는데 “비지니스 로직”을 말하는 것이겠죠?