etc/책 리뷰

클린 아키텍처 를 읽고 SOLID 에 대한 생각과 나의 반성 시간

hojak99 2021. 5. 15. 16:17

지금 2019년 12월부터 이 글을 쓰고 있는 지금까지도 야근과 코드, DB 리펙토링을 하느라 책을 잘 보지 못했다.
그래서 책을 읽으려 노력하고 있으며 [클린 아키텍처] 를 읽고 든 생각을 정리해보려고 한다.
물론 책마다 다를 수 있기 때문에 다른 책도 읽어보긴 해야 할 것 같다.

SOLID: 변경에 유연하고, 유지보수를 쉽게 만들고자 할 때 시스템에 적용 가능한 원칙

 

먼저 해당 책에서는 SOLID 에 대해서 얘기 하는 목차가 있다. 

SRP(단일 책임 원칙: Single Responsibility Principle)

먼저 SRP 부터 소개하는데 내가 평소에 생각하고 있던 것과 다르게 얘기를 한다.
내가 생각하던 SRP 는 "하나의 기능, 메소드는 한 가지의 책임만을 가져야 한다." 였고 책에서는 이게 SRP 원칙이 아니라고 했다.

하나의 모듈은 오직 하나의 액터에 대해서만 책임져야 한다. 
서로 다른 액터가 의존하는 코드를 서로 분리하라고 한다.

 

우발적 중복

먼저 해당 책에서는 SOLID 에 대해서 얘기 하는 목차가 있다. 책에서는 그 예로 CEO, CFO, CTO 가 모두 하나의 급여 메소드를 이용한다고 할 때  CFO 쪽에서 급여에 특정 계산 방식을 추가해달라고 하면 CEO, CTO 에 대해서 예상하지 못한 사이드이펙트가 발생하기 때문에 SRP 를 위반하는 것을 예시로 들었다.

위 내용에 대해서는 나도 겪었던 문제이며 ''서로 다른 액터가 의존하는 코드를 서로 분리해야 한다." 라는 내용에 동의가 된다. 사이드이펙트로 인해서 코드를 분리했던 기억이 있다. 

병합

CTO 팀에서 Employee 테이블을 수정하고, COO 팀에서는 Employee 클래스 내에 급여 메소드를 수정한다고 했을 때 변경사항이 충돌할 수 있기 때문에 코드를 분리해야 한다라는 예시를 들고 있다.

내가 백엔드를 개발할 때 위 예시에 대해서는 느껴본 적이 없었다. 
왜냐하면 스타트업에서 일하다 보니 여러팀에서 하나의 서비스를 개발하는 적도 없었고 같은 팀원이서 개발한다고 해도 동시에 개발한 적이 없기 때문이다. 



먼저, 여러 액터가 사용하는 메소드에 대해서는 entity 클래스에서 분리해야 한다는 이야기는 인정한다. 


지금 회사에서 현재 개발하고 있는 서비스에서도 학습지에 대한 Entity 클래스가 존재한다.
이 때 각각 A,B,C 를 포함하는 하나의 entity 클래스에서 A, B, C 학습지를 정의를 했었다.

그러나 하나의 메소드에서 학습지 종류에 따라 로직이 다른데, 수정 사항이 생겼을 때 해당 메소드를 A 학습지를 기준으로만 구현을 하다 보니 문제가 생겼던 적이 있었다.

그래서 각 학습지 로직을 entity 클래스에서 분리하는 작업을 했었었다. 이 작업을 통해 예상하지 못하는 사이드이펙트를 예방하고, 위에서 얘기하는 병합에 대해서도 해결이 되었던 것 같다.

또, 지금 내가 짠 코드들을 확인 해 보니 하나의 메소드에서만 하나의 역할을 담당하도록만 해 놓았다. 

책을 더 읽다보면 아키텍처에 대한 이야기도 나오는데 각각의 경계를 잘 나누어야 하고 use case 으로도 잘 나누어야 한다고 하는데 기존에 내가 짠 코드들은 use case 별로 클래스를 나누지 않았고 메소드를 기준으로만 나누었다.
그래서 A 클래스가 여러가지 use case 를 포함하고 있다 보니 나중에 유지보수 시 파악하기 힘들 것 같다는 생각이 들었다. 

그래서 클래스들을 각각의 use case 별로 나누었다. 
예를 들어 기존에는 쿠폰 클래스 내에 '쿠폰 적용 후 데이터 insert 기능', '쿠폰 적용 시 상품 금액 조회 기능' 등이 있어서 하나의 use case 를 수정하더라도 다른 use case 에도 영향이 갈 수 있는 구조였다.
 
그래서 '쿠폰 적용 후 데이터 insert 기능' 을 '쿠폰을 적용한다' 라는 use case 로 잡고 따로 클래스로 가져가면서 이 use case 에 관련된 private 메소드까지 가져가는 식으로 변경을 했다.

그럼으로 인해 각 use case 에 대한 메소드 딱 나오고 use case 에 대한 요구사항 수정 필요하면 해당 use case 내에 있는 메소드들만 수정하면 된다. 그렇게 됐을 때 다른 use case 들에 대해서는 영향이 없게 된다.

즉, 이런 것이 SRP 를 지켰다고 할 수 있을 것 같다. 
물론 사람마다 SRP 범위가 다를 수 있다고 생각한다.

 


 

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

내가 생각하고 있는 OCP 의 정의와 책에서 소개하는 OCP 의 정의가 같았다.

소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

책임이 서로 다른 레이어에 대해서는 interface 를 호출해서 A 에서 바뀌어도 다른 레이어인 B 에는 영향이 가지 않는 식으로 개발하는 것에 대해서 동의한다.

그래서 위에서 얘기한 쿠폰 금액 계산 및 적용 기능에서도 OCP 에서 소개하는 것처럼 "확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다." 를 지키도록 수정을 했다.

우선 쿠폰에는 A, B, C 타입의 쿠폰으로 나뉘어져 있고 각 쿠폰마다 할인 계산 방식이 서로 다르다.

OCP 준수

위 사진은 간단하게 그린 flow chart 이다. 네이밍은 별로라도 넘어가도록 하자.
설명하자면 팩토리 패턴을 이용하여 "쿠폰 가격 계산 service" 에서 "쿠폰 가격 계산 facetory" 를 이용하여 "쿠폰 가격 계산 interface" 를 반환하도록 했다. 그러므로 나중에 쿠폰 타입이 D, F, G 이렇게 추가되더라도 "쿠폰 가격 계산 service" 에서는 영향이 가지 않게 된다. 물론 A, B, C 쿠폰 계산 구현체에도 영향이 가지 않게 된다는 이야기이다.

이렇게 하면 확장에는 열려있고 변경에는 닫혀있다는 것을 직접 구현할 수 있다.

 

 


 

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

부모 객체를 자식 객체로 변환할 수 있어야 한다. 서로 타입을 치환할 수 있어야 하는데 각 메소드가 서로 다른 행동을 한다면 이는 LSP 를 위반했다는 뜻으로 보면 된다.

 


 

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

소프트웨어 설계자는 사용하지 않는 것에 의존하지 않아야 한다.

내가 책을 읽고 이해한 것은 역할보다는 역할 안에 행위로 인터페이스를 구분한다는 것이다. 물론, 내가 말하는 행위가 역할이 될 수도 있다. 그렇게 된다면 역할에 대해 더 작은 역할로 분리가 된다는 뜻으로 볼 수 있을 것 같다. 물론 때에 따라 다를 수 있다. spring security 에서 UserDetails 인터페이스와 같이 말이다. 

ISP 위반

위 사진 처럼 구현을 하게 된다면, 달리기 선수가 수영하기 메소드를 구현하게 되어 버린다. 그렇다면 달리기 선수는 실제 사용하지 않는 것에 의존되는 것이라고 생각한다. 그래서 이렇게 역할 내에 행위를 정의하지 않고 아래 사진과 같이 행위로 인터페이스를 구분해야 한다고 생각한다. 그래야만 유연하며 확장성 있고 영향이 없이 개발할 수 있다고 생각하기 때문이다.

ISP 준수

 

 

DIP (의존성 역전 법칙: Dependency Inversion Principle)

DIP 에서 말하는 유연성이 극대화된 시스템이란 소스 코드 의존성이 추상에 의존하여 구체에는 의존하지 않는 시스템이다.

여기서 말하는 DIP 에 대해서 얘기할 때 java 의 String 클래스는 예외적으로 두고 있다. 왜냐하면 String 클래스에 대해서는 소스 코드 의존성을 벗어날 수 없고, 벗어나서도 안되기 때문이라고 하고 있다.

그래서 DIP 를 논할 대 운영체제나 플랫폼 같이 안정성이 보장된 환경에 대해서는 무시하는 편이라고 한다.우리가 의존하지 않도록 피하고자 하는 것은 변동성이 큰 구체적인 요소이다.  

위에서 예로 들었던 OCP 부분을 확인해보면 DIP 와 굉장히 유사하다고 볼 수 있다. OCP 이야기를 하면서 예를 든 사진을 보면 해당 사진도 추상에 의존하고 구체에는 의존하고 있지 않기 때문이다. 그래서 이 두 가지의 차이를 검색해보니 다음과 같이 들 수 있다. 

if (couponType is A) {
	(ACouponService) interface.process()
} else if (couponType is B) {
	(BCouponService) interface.process()
} 

 

위 코드는 DIP 를 준수했다고 볼 수 있다. 왜냐하면 구체를 의존하지 않고 추상에 의존하고 있기 때문이다. 그러나 OCP 를 준수했다고 볼 수는 없다. couponType 이 추가됐을 때 해당 구현체가 변경되기 때문이다. 이렇게 된다면 변경에 닫혀있지 않다고 볼 수 있기에 이러한 부분에서 OCP, DIP 의 차이점을 볼 수 있다.

 

 


 

이로써 SOLID 에 대한 부분을 모두 정리하며 나의 반성 시간을 가져보았다. 먼저 예전부터 든 생각이긴 하지만, SOLID 원칙을 모두 지키면서 개발하기에는 결코 쉬운 일이 아니라고 생각한다. 그러나 알면서도 하지 않는다면 그것은 잘못 행동이라고 생각한다. 예를 들어 '이 구현체 부분을 interface 로 추상화해서 사용하면 나중에 구현체가 변경되더라도 코드를 변경하지 않아도 될텐데' 라고 생각하면서도 귀찮아서 그냥 구현체를 사용해버리는 식으로 한다는 것처럼 말이다. 

 

반응형