안녕하세요. 오늘은 잠이 안오네요? 보통 11시쯤부터 졸다가 2시 전에는 자는데, 오늘은 새벽 4시가 지났는데도 잠이 안오네요...그런고로 블로그를 조금 작성하고자 합니다. 평소와 달리 모바일로 작성하니 기분이 묘하네요. 그럼 학습 시작해봅시다.
SOLID를 학습하는 이유
지난 시간에는 좋은 코드를 위한 개발 원칙으로 TDD, KISS, YAGNI에 대해 학습했습니다.
TDD는 테스트로 기능을 검증하는 방식이고, KISS와 YAGNI는 불필요한 복잡성을 줄이는 데 도움을 주는 원칙입니다.
이번 시간에는 객체지향 설계 원칙인 SOLID에 대해 학습해보겠습니다.
프로젝트 규모가 작을 때는 하나의 클래스나 메서드에 여러 역할이 섞여 있어도 당장 큰 문제가 없어 보일 수 있습니다. 하지만 기능이 늘어나고 요구사항이 변경되면, 하나를 수정했을 뿐인데 다른 기능까지 영향을 받는 경우가 생깁니다.
SOLID는 이러한 문제를 줄이고, 변경에 유연하며 유지보수하기 쉬운 객체지향 코드를 작성하기 위한 설계 원칙입니다.
SOLID란?
SOLID는 객체지향 설계에서 자주 언급되는 다섯 가지 원칙의 앞 글자를 모은 용어입니다.
| 약어 | 원칙 | 의미 |
| S | Single Responsibility Principle | 단일 책임 원칙 |
| O | Open-Closed Principle | 개방•폐쇄 원칙 |
| L | Liskov Substitution Principle | 리스코프 치환 원칙 |
| I | Interface Segregation Principle | 인터페이스 분리 원칙 |
| D | Dependency Inversion Principle | 의존성 역전 원칙 |
SOLID는 코드를 무조건 복잡하게 나누라는 의미가 아닌 변경이 발생했을 때 영향을 줄이고 역할을 명확히 나누기 위한 기준입니다.
SRP: 단일 책임 원칙
SRP(Single Responsibility Principle)는 하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다.
여기서 책임이란 단순히 기능 하나만 의미하는 것이 아닌 변경되는 이유를 의미합니다. 하나의 클래스가 여러 이유로 변경된다면 책임이 여러 개 섞여 있을 가능성이 있습니다.
예를 들어 UserService가 회원가입 처리, 이메일 발송, 로그 기록, 파일 저장까지 모두 담당한다면 클래스의 책임이 많아집니다. 이 경우 이메일 발송 방식이 바뀌어도 UserService를 수정해야 하고, 로그 저장 방식이 바뀌어도 UserService를 수정해야 합니다.
그렇기에 회원 관련 로직은 UserService가 담당하고, 이메일 발송은 EmailService, 로그 기록은 LogService처럼 역할을 분리하면 변경 이유를 줄일 수 있습니다.
OCP: 개방-폐쇄 원칙
OCP(Open-Closed Principle)는 확장에는 열려 있고 변경에는 닫혀 있어야 한다는 원칙입니다. 기능을 추가할 때 기존 코드를 계속 수정하기보다 새로운 코드를 추가하는 방식으로 확장할 수 있습니다.
예를 들어 결제 기능에서 카드 결제만 지원하다가 카카오페이, 네이버페이를 추가해야 한다고 가정해봅시다. 결제 방식이 늘어날 때마다 기존 PaymentService 내부의 if문을 계속 수정한다면 기존 코드가 변경될 가능성이 커집니다.
그렇기에 Payment 인터페이스를 만들고 CardPayment, KakaoPayPayment, NaverPayPament처럼 구현체를 나누면 새로운 결제 방식이 추가되어도 기존 코드를 크게 수정하지 않고 확장할 수 있습니다.
LSP: 리스코프 치환 원칙
LSP(Liskov Substitution Principle)는 부모 타입의 객체를 자식 타임의 객체로 바꾸어도 프로그램의 동작이 깨지지 않아야 한다는 원칙입니다. 쉽게 말해 상속이나 구현 관계를 사용할 때, 하위 클래스가 상위 클래스의 약속을 지켜야 한다는 의미입니다.
예를 들어 모든 Notification은 send() 메서드로 알림을 보낼 수 있다고 약속했다고 가정해보겠습니다. EmainNotification, SmsNotification은 send()가 자연스럽게 동작합니다. 그런데 어떤 하위 클래스는 send()를 호출했을 때 예외를 던지거나 아무 동작도 하지 않는다면 Notification이라는 상위 타입으로 사용할 수 없게 됩니다.
그렇기에 LSP는 단순히 사용할 수 있다는 의미가 아닌 상위 타입으로 다뤄도 하위 타입이 기대한 동작을 지켜야 한다는 원칙입니다.
ISP: 인터페이스 분리 원칙
ISP(Interface Segregation Principle)는 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 분리해야 한다는 원칙입니다.
예를 들어 Printer 인터페이스에 print(), scan(), fax() 메서드가 모두 들어 있다고 가정해보겠습니다. 일반 프린터는 print()만 필요하지만 인터페이스를 구현하기 위해 scan()과 fax()까지 억지로 구현해야 할 수 있습니다.
이 경우 Printer, Scanner, Fax처럼 인터페이스를 역할별로 분리하면 필요한 기능만 구현할 수 있습니다.
DIP: 의존성 역전 원칙
DIP(Dependency Inversion Principle)는 상위 모듈이 하위 모듈에 직접 의존하지 않고 추상화에 의존해야 한다는 원칙입니다. 쉽게 말하면 구현 클래스에 직접 의존하기보다 인터페이스나 추상 타입에 의존하도록 설계하라는 의미입니다.
예를 들어 UserService가 MySqlUserRepository 구현체에 직접 의존하면 저장 방식이 바뀔 때마다 Service 코드도 함께 영향을 받을 수 있습니다. 반면 UserRepository 인터페이스에 읜존하면 실제 구현체가 JPA 기반이든 다른 저장소 기반이든 Service는 인터페이스를 통해 동작할 수 있습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User findUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
}
}
UserService는 구체적인 구현체를 직접 생성하지 않고 UserRepository에 의존합니다. 실제 객체 생성과 주입은 Spring 컨테이너가 담당합니다. 이런 구조는 DIP와 의존성 주입을 이해하는 데 도움이 됩니다.
Spring Boot에서 SOLID를 어떻게 볼 수 있을까?
Spring Boot 프로젝트에서는 SOLID 원칙을 여러 곳에서 볼 수 있습니다.
| 원칙 | Spring Boot에서의 예시 |
| SRP | Controller는 요청/응답, Service는 비즈니스 로직, Repository는 DB 접근 담당 |
| OCP | 인터페이스와 구현체를 통해 기능 확장 |
| LSP | 같은 인터페이스 구현체를 동일한 타입으로 사용 |
| ISP | 필요한 기능 단위로 인터페이스 분리 |
| DIP | Service가 구체 클래스가 아닌 Repository 인터페이스에 의존 |
물론 모든 코드를 억지로 원칙에 맞춰 나눌 필요는 없습니다. 하지만 역할이 섞이고 변경 영향이 커질수록 SOLID 원칙을 기준으로 구조를 점검해볼 수 있습니다.
SOLID를 적용할 때 주의할 점
SOLID는 좋은 설계를 위한 원칙이지만 모든 상황에서 무조건 적용해야 하는 규칙은 아닙니다.
- 작은 기능까지 무조건 인터페이스로 나누면 오히려 복잡해질 수 있습니다.
- 변경 가능성이 낮은 코드에 과한 추상화를 적용할 필요는 없습니다.
- 원칙을 지키는 것보다 중요한 것을 현재 문제를 명확히 해결하는 것입니다.
- SOLID는 처음부터 완벽하게 적용하기보다 유지보수 과정에서 구조를 개선할 때 기준으로 삼는 것이 좋습니다.
즉, SOLID는 코드를 복잡하게 만드는 원칙이 아닌 변경이 많아질 때 복잡함을 줄이기 위한 원칙입니다.
오늘은 SOLID에 대해서 학습해보았습니다. 새벽에 썼다가 다시 아침에 이어서 작성하는데, 너무 적게 자서 피곤하네요. 그래도 오늘 잠이 안왔던거였기에 이렇게 졸린 하루를 보내면 저녁에 일찍 자게 되겠죠? 오히려 좋습니다. 다음 시간에는 DRY와 리팩토링으로 돌아오도록 하겠습니다. 수고하셨습니다.
'백엔드 공부' 카테고리의 다른 글
| 아키텍처 패턴(1) - 모놀리식, MSA, SOA란 무엇인가? (0) | 2026.05.24 |
|---|---|
| 개발 설계 원칙(3) - DRY와 리팩토링 (0) | 2026.05.22 |
| CI/CD(2) - CI/CD 도구와 GitHub Actions (0) | 2026.05.19 |
| CI/CD(1) - CI/CD란 무엇인가? (0) | 2026.05.18 |
| 테스트(2) - 테스트 코드 (0) | 2026.05.15 |