ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SOLID 원칙 완벽 정리
    Java 2025. 6. 18. 17:47

    객체지향 설계의 정수, SOLID 원칙 완벽 정리

    우리가 Java로 객체지향 프로그래밍을 할 때 가장 많이 듣는 조언 중 하나는
    "유지보수가 쉬운 코드를 작성하라", 또는 "확장에 유연한 구조를 설계하라"는 말이다.

    하지만 이걸 실제로 어떻게 해야 할까?

    그 해답이 바로 로버트 마틴(Robert C. Martin, Uncle Bob)이 제안한
    SOLID 원칙 속에 담겨 있다.

    이번 글에서는 SOLID 원칙이 무엇이고, 왜 중요한지, 그리고 실무에서 어떻게 적용할 수 있는지 차근차근 정리해보겠다.

     

    SOLID란 무엇인가?

    SOLID는 다섯 가지 객체지향 설계 원칙의 앞글자를 딴 약어이다:

    원칙 설명

    SRP 단일 책임 원칙 (Single Responsibility Principle)
    OCP 개방-폐쇄 원칙 (Open/Closed Principle)
    LSP 리스코프 치환 원칙 (Liskov Substitution Principle)
    ISP 인터페이스 분리 원칙 (Interface Segregation Principle)
    DIP 의존관계 역전 원칙 (Dependency Inversion Principle)

    1. SRP – 단일 책임 원칙

    "한 클래스는 오직 하나의 변경 이유만 가져야 한다."

    이 원칙은 가장 기본이면서도 가장 무시되기 쉬운 원칙이다.
    여기서 말하는 "책임"은 기능이라기보다는 변경의 이유를 말한다.

    예시

    public class MemberService {
        public void createMember() {
            // 회원 생성
        }
    
        public void printMemberPage() {
            // 회원 정보 UI 출력
        }
    }
    • 만약 디자인팀에서 UI를 변경하자고 하면 printMemberPage()를 수정해야 함
    • 동시에 비즈니스 로직의 정책이 바뀌면 createMember()도 수정해야 함
      → 즉, 두 가지 이유로 변경될 수 있는 클래스이므로 SRP 위반이다.

    클래스가 변경되는 이유가 여러 개라면, 책임도 여러 개다.

    실무 팁

    • 책임의 기준은 ‘조직 구조’ 또는 ‘업무 영역’과도 맞닿아 있다.
    • UI 관련 클래스, 도메인 로직 클래스, 저장소 클래스 등을 분리하자.

    2. OCP – 개방-폐쇄 원칙

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

    즉, 새로운 기능이 추가되어도 기존 코드를 수정하지 않도록 설계해야 한다는 것이다.
    처음 들으면 말장난 같지만, 객체지향의 다형성을 활용하면 이 원칙을 실현할 수 있다.

    문제 예시

    public class MemberService {
        private MemberRepository repository = new MemoryMemberRepository(); // Memory에서 JDBC로 바꾸려면?
    
        public void register() {
            repository.save();
        }
    }
    
    • JdbcMemberRepository로 바꾸려면 기존 클래스 코드를 수정해야 한다 → OCP 위반

    해결 방법: 역할과 구현 분리

    public class MemberService {
        private final MemberRepository repository;
    
        public MemberService(MemberRepository repository) {
            this.repository = repository;
        }
    }
    

    → 이처럼 생성자를 통해 구현체를 주입하면, 확장은 가능하면서 기존 코드는 건드리지 않아도 된다.

    실무 팁

    • Spring에서는 @Configuration 클래스에서 Bean으로 주입하는 구조가 OCP를 자연스럽게 만족시킨다.
    • 전략 패턴, 의존성 주입(DI) 기법과도 밀접한 관련이 있다.

    3. LSP – 리스코프 치환 원칙

    "프로그램의 정확성을 깨뜨리지 않고 하위 타입으로 대체할 수 있어야 한다."

    이 원칙은 다형성이 안전하게 작동하기 위한 전제조건이다.
    하위 클래스가 상위 클래스의 계약(계약=기대되는 행동)을 지키지 않으면, 전체 프로그램의 신뢰성이 무너진다.

    예시

    interface Car {
        void accelerate();
    }
    
    class SportsCar implements Car {
        public void accelerate() {
            System.out.println("앞으로 빠르게 달린다.");
        }
    }
    
    class WrongCar implements Car {
        public void accelerate() {
            System.out.println("뒤로 간다."); // 🚨 문제 발생!
        }
    }
    
    • accelerate()는 앞으로 가는 동작을 기대하지만, 뒤로 가게 구현한 것은 LSP 위반이다.
    • 하위 클래스는 느리더라도 행동의 의미(앞으로 가는 동작)는 같아야 한다.

    실무 팁

    • 오버라이딩을 할 때는 항상 ‘기존 동작의 기대’를 충족하는지 확인하자.
    • 테스트 코드가 LSP 위반을 빠르게 잡아주는 역할을 할 수 있다.

    4. ISP – 인터페이스 분리 원칙

    "범용 인터페이스 하나보다는, 클라이언트에 맞는 여러 인터페이스로 나누는 것이 낫다."

    하나의 거대한 인터페이스에 모든 기능을 몰아넣으면, 일부 기능만 필요한 클라이언트가
    불필요한 의존을 하게 되고, 인터페이스가 바뀔 때 불필요한 영향을 받게 된다.

    예시

    interface Car {
        void drive();
        void repair();
    }
    
    • 운전자 클라이언트는 drive()만 필요
    • 정비사 클라이언트는 repair()만 필요
      → 하지만 하나의 인터페이스로 묶여 있으면 둘 다 의존해야 한다

    개선된 구조

    interface Drivable {
        void drive();
    }
    
    interface Maintainable {
        void repair();
    }

    → 이제 각 클라이언트는 자신에게 필요한 인터페이스만 구현하거나 사용할 수 있다.

    실무 팁

    • 클라이언트가 어떤 기능만 사용하는지 관찰하고, 그 기준에 맞춰 인터페이스를 분리하자.
    • 유지보수성과 교체 가능성을 크게 향상시킬 수 있다.

    5. DIP – 의존관계 역전 원칙

    "추상화(인터페이스)에 의존하라. 구체화(구현 클래스)에 의존하지 마라."

    이 원칙은 구현체가 아니라 역할에 의존하는 설계를 의미한다.
    즉, 클래스는 구현 클래스가 아니라 인터페이스에 의존해야 한다는 것이다.

    문제 예시

    public class MemberService {
        private MemberRepository repository = new MemoryMemberRepository(); // DIP 위반
    }
    
    • MemberService는 인터페이스 없이 구체 클래스에 의존하고 있음 → 변경 시 유연하지 못함

    개선 방법

    public class MemberService {
        private final MemberRepository repository;
    
        public MemberService(MemberRepository repository) {
            this.repository = repository;
        }
    }
    

    → 이제 MemberService는 구현체가 어떤 것이든 상관없이 작동함
    즉, 의존성 역전이 이루어진 구조이다.

    실무 팁

    • Spring에서는 @Autowired, 생성자 주입, 설정 클래스 등을 통해 자연스럽게 DIP를 지킬 수 있다.
    • 역할(인터페이스)을 중심으로 설계하는 습관이 중요하다.

     


    정리하며

    객체지향의 핵심은 다형성이다.
    하지만 다형성만으로는 SOLID를 실현할 수 없다.

    • 단순히 인터페이스만 도입한다고 끝이 아니다.
    • OCP와 DIP를 만족하려면, 객체 생성과 연결을 별도로 분리(설정자)해야 한다.
    • 이상적으로는 모든 설계에 인터페이스를 부여해 역할과 구현을 철저히 나눠야 한다.
    • 이는 마치 공연을 설계하는 것과 같다.
      배역은 고정되어 있고, 배우는 유연하게 교체할 수 있는 구조.

    실무에서는 어떻게 적용할까?

    • 모든 곳에 인터페이스를 도입하면 코드가 지나치게 복잡해질 수 있다.
    • 기능이 명확하고, 확장 가능성이 낮은 부분은 처음엔 구현 클래스를 직접 사용해도 된다.
    • 나중에 확장이 필요해질 때 리팩토링을 통해 추상화를 도입하는 것도 좋은 전략이다.

    마무리

    SOLID 원칙은 개발자가 객체지향을 "제대로" 쓰기 위한 기준이다.
    단순히 규칙을 외우는 게 아니라, 왜 이 원칙이 필요한지,
    어떤 상황에서 이를 지키지 않으면 어떤 문제가 생기는지 고민하며 적용하는 것이 중요하다.

    이제부터는 클래스를 작성할 때마다 한번쯤 이렇게 자문해보자.

    "이 클래스는 하나의 책임만 가지는가?"
    "변경 없이 확장이 가능한 구조인가?"
    "인터페이스만으로 동작하도록 설계했는가?"

    이 질문들에 ‘예’라고 자신 있게 대답할 수 있다면,
    당신은 이미 SOLID한 개발자다.

     

Designed by MSJ.