728x90
SOLID란?
SOLID 원칙은 객체 지향 설계에서 유지보수성과 확장성을 높이기 위한 다섯 가지 핵심 원칙이다. Robert C. Martin(일명 Uncle Bob)이 정리한 이 원칙들은 실제로도 많은 OOP 기반 시스템에서 코드 품질과 구조 개선에 기여한다.
내가 직접 설계하고 리팩토링한 인증 서비스(auth-service)와 결제 서비스(escrow-service)에도 이 원칙들을 적용하며 실무적 감각을 쌓았다.
S - 단일 책임 원칙 (Single Responsibility Principle, SRP)
- 정의: 하나의 클래스는 하나의 책임만 가져야 한다.
- 효과: 변경의 이유가 하나뿐이므로 코드 수정 시 다른 기능에 영향을 주지 않음.
예시 적용
- RegisterUserUsecase / LoginUserUsecase / VerifyResetCodeUsecase 등 유즈케이스를 분리하여 각 기능이 독립적으로 변경될 수 있도록 함.
- UserRepository는 DB 접근만, UserService는 비즈니스 로직만 담당하도록 구조화.
코드 예시
변경 전 (하나의 서비스에 모든 책임이 집중)
class UserService {
constructor(private readonly ormRepository: Repository<User>) {}
async register(data: RegisterUserDto) {
const user = this.ormRepository.create(data);
return await this.ormRepository.save(user);
}
async login(email: string, password: string) {
// 비밀번호 비교 로직 포함
}
async verifyResetCode(code: string) {
// 인증 코드 검증 로직 포함
}
}
변경 후 (책임을 Usecase 단위로 분리)
class RegisterUserUsecase {
constructor(private readonly userRepository: IUserRepository) {}
async execute(dto: RegisterUserDto) {
const user = UserFactory.create(dto);
return await this.userRepository.save(user);
}
}
class VerifyResetCodeUsecase {
constructor(private readonly resetCodeRepository: IResetCodeRepository) {}
async execute(dto: VerifyResetCodeDto) {
const storedCode = await this.resetCodeRepository.getCode(dto.email);
if (storedCode !== dto.code) throw new Error('인증 실패');
}
}
O - 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
- 정의: 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
- 효과: 기존 코드를 건드리지 않고 새로운 기능을 추가할 수 있다.
예시 적용
- 알림 기능 예시: NotificationService 인터페이스 정의 후, EmailNotification, SMSNotification 클래스를 별도로 구현.
- 실제 인증 서비스에서 세션 저장소 변경 시 ISessionRepository 인터페이스만 유지하고 구현체를 바꿔도 영향이 없도록 설계함.
코드 예시
변경 전 (구현에 직접 의존)
class SessionService {
private sessionStorage = new RedisClient();
save(session: Session) {
this.sessionStorage.set(session.id, session);
}
}
변경 후 (인터페이스 기반 구조로 확장 가능)
interface ISessionRepository {
save(session: Session): Promise<void>;
}
class RedisSessionRepository implements ISessionRepository {
save(session: Session) {
redisClient.set(session.id, session);
}
}
class SessionService {
constructor(private readonly sessionRepository: ISessionRepository) {}
save(session: Session) {
return this.sessionRepository.save(session);
}
}
L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
- 정의: 서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
- 효과: 다형성이 안전하게 작동함.
예시 적용
- User → AdminUser, SellerUser처럼 사용자 타입을 분리할 때 공통된 부모 인터페이스나 클래스를 통해 기능이 유지되도록 구현.
코드 예시
abstract class User {
constructor(public readonly id: string) {}
abstract getRole(): string;
}
class AdminUser extends User {
getRole() {
return 'admin';
}
}
class RegularUser extends User {
getRole() {
return 'user';
}
}
function printUserRole(user: User) {
console.log(user.getRole());
}
I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
- 정의: 하나의 일반적인 인터페이스보다, 여러 개의 구체적인 인터페이스를 사용하는 것이 낫다.
- 효과: 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않게 된다.
예시 적용
- IUserAuthRepository, IUserProfileRepository처럼 사용자 관련 인터페이스를 관심사별로 분리.
- Redis 기반 인증 코드 검증 기능은 IResetCodeRepository로 따로 분리.
코드 예시
interface IUserAuthRepository {
findByEmail(email: string): Promise<User | null>;
save(user: User): Promise<void>;
}
interface IResetCodeRepository {
getCode(email: string): Promise<string | null>;
saveCode(email: string, code: string): Promise<void>;
}
D - 의존성 역전 원칙 (Dependency Inversion Principle, DIP)
- 정의: 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다.
- 효과: 유연하고 테스트 가능한 구조가 된다.
예시 적용
- 모든 유즈케이스는 실제 구현체가 아닌 인터페이스(IUserRepository, ISessionRepository, IMailService 등)를 주입받음.
코드 예시
interface IMailService {
sendVerificationCode(email: string, code: string): Promise<void>;
}
class MailService implements IMailService {
sendVerificationCode(email: string, code: string) {
// SMTP 전송 구현
}
}
class SendResetCodeUsecase {
constructor(private readonly mailService: IMailService) {}
async execute(email: string, code: string) {
await this.mailService.sendVerificationCode(email, code);
}
}
정리
원칙 | 정의 | 실무 적용 포인트 |
SRP | 하나의 책임만 | 유즈케이스/서비스/레포 분리 |
OCP | 확장엔 열려, 변경엔 닫힘 | 인터페이스 도입, 구현체 분리 |
LSP | 부모 타입으로 자식 교체 가능 | 인터페이스 기반 설계 |
ISP | 인터페이스는 작게, 명확하게 | 관심사 분리로 인터페이스 구성 |
DIP | 구현보다 추상화에 의존 | DI 기반 구조, 테스트 용이성 |
마무리
SOLID는 "깨끗한 코드"의 철학이자, 실무에서 서비스 유지보수성과 테스트 가능성을 획기적으로 높여주는 무기이다.
단순히 "좋은 구조"라는 이유만으로 쓰기보다는, 실제 팀 협업과 기능 분리, 테스트 편의성에서 어떤 효과를 가져다주는지 느끼며 적용하는 것이 중요하다.
실무에서 한 번이라도 서비스 구조를 리팩토링해본 개발자라면, SOLID 원칙은 이론이 아니라 생존 전략이다.
반응형
'백엔드 개발 > API · 아키텍처 설계' 카테고리의 다른 글
DTO → Command → UseCase 패턴으로 본 구조적 백엔드 설계 (0) | 2025.05.26 |
---|---|
UserFactory로 객체 생성 책임을 분리한 이유 – SRP 기반 설계 적용기 (0) | 2025.05.09 |
Procedural 구조에서 Use Case 구조로, 내가 구조를 다시 짠 이유 (0) | 2025.05.08 |
[MSA] API Gateway는 왜 필요한가? – 인증, 보안, 확장성 중심의 설계 가이드 (0) | 2025.04.24 |
사이드 프로젝트 채팅 시스템 설계 Q&A – 안드로이드 & 백엔드 협업 정리 (2) | 2024.02.14 |