SOLID 리뷰
저자: Guerth Castro (github.com/GuerthCastro). MIT 라이선스에 따라 사용이 허가되었습니다.
리뷰 프로세스
코드가 리뷰를 위해 제공되면:
- 언어와 계층을 식별합니다 (C# 도메인 엔터티, 애플리케이션 핸들러, Angular 서비스, React 컴포넌트 등).
- 다섯 가지 원칙 각각을 독립적으로 평가합니다.
- 아래 형식을 사용하여 구조화된 보고서를 생성합니다.
- 위반 사항을 심각도별로 정렬합니다: 치명적(아키텍처 위반) > 주요(유지보수성 저하) > 사소(스타일 선호).
- 모든 치명적 또는 주요 위반 사항에 대해 구체적인 리팩토링 버전을 제공합니다.
보고서를 불필요하게 길게 만들지 마십시오. 원칙이 실제로 충족되면 PASS라고 표시하고 넘어갑니다.
다섯 가지 원칙
S: 단일 책임 원칙 (Single Responsibility Principle)
하나의 클래스, 하나의 변경 이유.
찾아야 할 위반 사항:
- 클래스가 데이터 액세스와 비즈니스 로직을 모두 수행합니다.
- 서비스가 이메일을 보내고, 데이터를 검증하고, 데이터베이스에 씁니다.
- 컴포넌트가 데이터를 가져오고, 형식을 지정하고, UI 상태를 관리합니다.
- 핸들러가 여러 개의 관련 없는 작업을 조정합니다.
- 여러 도메인에 걸친 10개 이상의 메서드를 가진 갓 클래스.
위반. OrderService는 검증, 지속성, 알림을 수행합니다. 알림 호출이 속하지 않는 부분입니다.
public class OrderService(IOrderRepository repository, IEmailService email)
{
public async Task<bool> AddItem(long orderId, long productId)
{
Order order = await repository.GetById(orderId);
if (order.ItemCount >= order.MaxItems)
{
return false;
}
await repository.AddItem(orderId, productId);
await email.SendOrderUpdated(orderId);
return true;
}
}
수정: 알림 서비스를 추출하고 핸들러가 두 호출을 조정하도록 합니다.
O: 개방-폐쇄 원칙 (Open/Closed Principle)
확장에는 열려 있고, 수정에는 닫혀 있습니다.
찾아야 할 위반 사항:
- 모든 기능이 추가될 때마다 커지는
if/else if체인 또는 타입 문자열이나 열거형에 대한switch블록. - 새로운 보고서 유형을 추가하려면 기존 보고서 서비스를 편집해야 합니다.
- 새로운 결제 방법을 추가하려면 체크아웃 핸들러를 편집해야 합니다.
- 추상화가 필요한 곳에 구체적인 의존성.
위반. 새로운 내보내기 형식은 이 메서드를 편집해야 함을 의미합니다.
public byte[] ExportInvoice(string format, IEnumerable<InvoiceLine> lines)
{
if (format == "pdf")
{
return GeneratePdf(lines);
}
if (format == "excel")
{
return GenerateExcel(lines);
}
throw new NotSupportedException(format);
}
수정: 컨테이너에서 키로 해결되는 IInvoiceExporter 추상화와 PdfInvoiceExporter 및 ExcelInvoiceExporter 구현.
L: 리스코프 치환 원칙 (Liskov Substitution Principle)
하위 타입은 기본 타입을 대체할 수 있어야 합니다.
찾아야 할 위반 사항:
- 파생 클래스가 상속된 멤버에 대해
NotImplementedException을 던집니다. - 재정의가 예상된 동작을 변경합니다: 무음 no-op, 다른 반환 형태, 새로운 예외 유형.
- 일부 구현에서 의미 있게 구현할 수 없는 멤버를 선언하는 인터페이스.
- 동작에 도달하기 위해 구체적인 타입으로 캐스팅:
if (repository is SqlOrderRepository sql).
I: 인터페이스 분리 원칙 (Interface Segregation Principle)
어떤 클래스도 사용하지 않는 멤버를 구현하도록 강요받아서는 안 됩니다.
찾아야 할 위반 사항:
- 15개 이상의 멤버가 있는 거대한 인터페이스에서 구현체가 3개만 사용하는 경우.
IRepository<T>가 모든 엔터티에GetAll,GetById,Create,Update,Delete,Paginate,Search,BulkInsert를 강제하는 경우.- 읽기와 쓰기가 혼합된 서비스 인터페이스에서 소비자가 한쪽만 필요한 경우.
위반. 읽기 전용 보고 소비자가 Create, Update, Delete를 강제로 보게 됩니다.
public interface IOrderRepository
{
Task<Order> GetById(long id);
Task<IEnumerable<Order>> GetAll();
Task Create(Order order);
Task Update(Order order);
Task Delete(long id);
}
수정: IOrderReader와 IOrderWriter로 분할합니다. 둘 다 필요한 곳에서 합성합니다.
D: 의존성 역전 원칙 (Dependency Inversion Principle)
구체화가 아닌 추상화에 의존하십시오.
찾아야 할 위반 사항:
- 비즈니스 로직 내부에서
new로 서비스나 리포지토리를 인스턴스화하는 경우. - 서비스 내부의
SqlConnection또는NpgsqlConnection에 대한 직접적인 의존성. - 애플리케이션 계층이 인프라 타입을 임포트하는 경우.
- 변경 가능한 상태를 보유하는 정적 서비스 클래스.
위반. 핸들러가 자체 구체적인 리포지토리와 연결을 생성합니다.
public class GetOrderHandler
{
public async Task<OrderDto> Handle(long id)
{
OrderRepository repository = new(new SqlConnection("..."));
Order order = await repository.GetById(id);
return order.MapToDto();
}
}
수정: 기본 생성자를 통해 IOrderRepository를 주입하고 구성 루트의 컨테이너에 등록합니다.
보고서 형식
각 리뷰에 대해 이 구조를 생성하십시오:
## SOLID 리뷰: [ClassName 또는 파일명]
**언어/계층:** C# 애플리케이션 핸들러, Angular 서비스 등
### S: 단일 책임
상태: PASS / 위반 (치명적, 주요, 사소)
발견: [클래스가 단일 책임을 넘어 수행하는 작업]
수정: [구체적인 조치]
### O: 개방-폐쇄
상태: PASS / 위반
발견: ...
수정: ...
### L: 리스코프 치환
상태: PASS / 위반 / 해당 없음 (상속 없음)
발견: ...
### I: 인터페이스 분리
상태: PASS / 위반 / 해당 없음 (인터페이스 없음)
발견: ...
### D: 의존성 역전
상태: PASS / 위반
발견: ...
수정: ...
### 우선 순위 위반 사항
1. [치명적] DIP: 핸들러가 구체적인 리포지토리를 인스턴스화함
2. [주요] SRP: 서비스가 알림 이메일도 보냄
### 리팩토링 버전
[전체 수정된 코드 블록]
클린 아키텍처 계층 위반 사항
SOLID 외에도 이러한 교차 문제를 지적하십시오:
- 도메인 엔터티가 애플리케이션이나 인프라 타입을 임포트하는 경우 (의존성 방향 위반).
- 컨트롤러에 핸들러에 속하는 비즈니스 로직이 포함된 경우.
- 리포지토리가 도메인 엔터티 대신 DTO를 반환하는 경우. 매핑은 애플리케이션에 속합니다.
- 인프라가 애플리케이션이나 도메인에 선언된 인터페이스를 구현하는 대신 외부 API를 직접 호출하는 경우.
- 핸들러가
HttpContext에 접근하여 인프라 관심사를 애플리케이션 계층으로 유출하는 경우. - 잘못된 어셈블리에서의 DI 등록으로 인해 계층이 컴파일을 위해 하위를 참조해야 하는 경우.