SOLID 소프트웨어 설계 원칙을 톺아보자!

김동욱·2022년 12월 25일
0

클린코드

목록 보기
2/2
💡 일전에 저명한 개발자인 로버트 마틴의 `클린코드` 를 읽은 적이 있다. 그의 주장에 따르면 `SOLID` 원칙을 준수하면 객체지향 프로그래밍의 십분 활용하여 `유지보수 및 확장`에 용이한 소프트웨어를 만들 수 있다.

1. 단일 책임 원칙

클래스와 함수는 한 개의 책임을 져야한다.

첫번째 이유

class FindAndCreateUser {

	action(userId: string, data?: Users){
		if(data){
			return prisma.users.create({data});
		}
		return prisma.users.findUnique({where:{id: userId}});
	}
}

위 코드를 보면 한 개의 클래스가 User 조회 그리고 생성에 관여한다.

만약에 User 생성 로직에 문제가 있어서 수정을 했다고 하자. 이러면 우리는 두 가지의 기능에 신경을 쓸 수 밖에 없다. (간단한 내용이긴 하지만 손가락이 미끄러져서 실수했을 수도 있으니까)

User 조회 기능 User 생성 기능

💡 책임 단위는 변화 단위와 관계가 깊다. 책임이 다르면 다른 이유로 변화한다. 같은 함수 및 클래스에 있어서 예상치 못한 실수로 인하여 수정을 해야하는 일이 코드를 수정하는 다른 이유가 되게 해서는 안된다.

두번째 이유

class getThumbnailService {
	constructor(
		private readonly s3Service: S3Service,
		private readonly prismaService: PrismaService
	) {}

	getImageUrl(fileId: string){
	const file = await this.prismaService.file.findUnique({where:{fileId}});
	const {s3Path} = file;
	const uri = decodeUri(new URL(s3Path))
	return s3Service.getSignedUrl(uri);
	}
}

위 같은 코드는 DB에서 s3Path를 추출하여 이를 AWS S3로 전송하여 해당 경로에 있는 파일의 signed url을 가져오는 코드이다.

만약 함수의 파라미터가 fileId가 아니라 모종의 이유로 외부에 해당 s3Path가 URL 타입으로 들어온다고 해보자. 그러면 getImageUrl 함수의 내용이 연쇄적으로 수정되어야 할 것이다.

💡 이 현상은 두 개의 책임이 하나의 함수에 긴밀하게 연계되어 있어서 발생한 문제이다.

2. 개방 폐쇄 원칙

확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.

💡 기능을 추가하려면 코드만 추가하게 하라는 뜻이다. 기존 코드 수정 없이

개방 폐쇄 원칙은 변경의 유연함과 관련된 원칙이다. 만약 기존 기능을 확장하기 위해 기존 코드를 수정해 주어야 한다면, 새로운 기능을 추가하는 것이 점점 힘들어진다.

이를 구현하기 위해선 역할과 구현을 분리(추상화)하라는 뜻이다.

위 사진을 보면 무슨 말인지 알 것이다.

하지만 기능 확장이 크게 없을 부분에는 할 필요가 없다. 추상화 라는 개념 자체가 비용일 수도 있다. 코드가 더 늘어나고 추상화하는데 시간도 많이 든다. (정신 건강에 안 좋음)

💡 확장이 많을 부분은 추상화하는 것이 정신 건강에 더 좋다.

3. 리스코프 치환 원칙

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

interface IClass {
	doSomething():void;
}
class SuperClass implements IClass {
	doSomething(){
		//작업
	}
}

class SubClass extends SuperClass {
	// 어쩌구 저쩌구...
}

인터페이스IClass 와 이를 구현한 SuperClass 그리고 이것을 상속 받은 SubClass가 있다고 하자.

function main(cls: IClass){
		cls.doSomething();
}

main(new SuperClass);

그리고 위의 main 함수에 SuperClass를 넣어 doSomething 이라는 작업을 수행했다고 하자.

이때 변화구로

main(new SubClass);

이때도 위와 동일하게 main 함수는 doSomething 이라는 작업이 정상적으로 수행되어야 한다.

💡 여기서 정상적 이라는 뜻은 프로그램의 에러가 발생하지 않음을 말한다. 그러면 오버라이딩을 하지 말라는 뜻인가? 는 아니고 오버라이딩을 하되 그 변경이 벡터의 방향성 에 대한 변경이어서는 안된다. 라는 뜻이다.

4. 인터페이스 분리의 원칙

클라이언트는 인터페이스의 의존하여 소프트웨어를 조작한다.

여기서 인터페이스란 컴퓨터의 자판기, 마우스, 모니터 정도라고 볼 수 있다. 우리는 컴퓨터 내부적인 구조가 어떻게 되어서 모니터에 불이 들어오고 자판기와 마우스로 컴퓨터에 명령을 전해주는지 구체적으로 알지 못한다.

💡 인터페이스 분할이 제대로 되어있지 않으면 다음과 같은 사태가 발생한다.

그림이 의미하는 바는 코드에 하자가 있어 수정했는데 인터페이스 분리가 안되어 있는 관계로 사용자가 이를 알게 되는 것이다.

우리가 흔히 사용하는 어플리케이션에 이런 문제가 발생한다고 생각해보자. 진짜 끔찍하다.



5.의존 역전 원칙

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

상위 차원의 모듈이 하위 차원 모듈에 코드적으로 의존하지 않고 런타임 환경에서 상위 차원 모듈이 하위 차원 모듈을 컨트롤할 수 있게 만들어준다.

💡 각 모듈을 독립적으로 관리 배포할 수 있다. 앞서 설명했는 개방 폐쇄 원칙도 이를 통해 구현이 가능하다.


의존 역전 원칙에 대해서는 매우 복잡하기 때문에 나중에 기회가 되면 다를 것이다.

profile
nestjs 백엔드 개발합니다.

0개의 댓글