[Java] 객체 지향 설계의 핵심! SOLID 원칙

2025. 5. 14. 19:40·Back end/Java
반응형

좋은 소프트웨어를 만들기 위해 꼭 알아야 할 설계 원칙, 바로 SOLID입니다.

이 원칙은 클린 코드로 유명한 로버트 마틴(Robert C. Martin)이 정리한 다섯 가지의 핵심 원칙으로, 객체 지향 설계의 방향성을 제시합니다.

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

이제 각 원칙을 하나씩 살펴보겠습니다.


✅ 1. SRP - 단일 책임 원칙 (Single Responsibility Principle)

한 클래스는 하나의 책임만 가져야 한다.

‘책임’이라는 개념이 추상적으로 느껴질 수 있지만, 핵심은 변경의 이유가 하나여야 한다는 것입니다. 즉, 클래스가 여러 역할을 하면, 하나의 변경이 여러 기능에 영향을 줄 수 있습니다. 이는 유지보수를 어렵게 만듭니다.

예를 들어, UI를 변경하거나 객체의 생성 방식을 바꾸는 작업이 클래스 내부의 다른 로직에 영향을 주지 않도록 설계하는 것이 SRP를 잘 지킨 사례입니다.


✅ 2. OCP - 개방-폐쇄 원칙 (Open/Closed Principle)

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

어떻게 보면 말장난 같지만, 핵심은 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 것입니다. 이를 위해 다형성(polymorphism)을 적극적으로 활용할 수 있습니다. 인터페이스를 정의하고, 새로운 구현 클래스를 만들어 기존 코드를 수정하지 않고도 기능을 확장할 수 있습니다.

❗ OCP를 지키지 않은 예시

// 클라이언트 코드에서 직접 구현체를 선택
MemberRepository repo = new MemoryMemberRepository(); // 나중에 변경하려면?
repo = new JdbcMemberRepository(); // 기존 코드 수정 필요!

이처럼 클라이언트가 직접 구현체를 선택하면, 구현이 변경될 때마다 클라이언트 코드도 함께 수정해야 하므로 OCP 위반입니다.

➡ 이를 해결하려면, 구성 객체를 외부에서 주입받도록 설계하고, 객체를 생성하고 연결해주는 조립기(Configurator) 또는 DI 컨테이너가 필요합니다.


✅ 3. LSP - 리스코프 치환 원칙 (Liskov Substitution Principle)

“프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.”

핵심은 부모 타입을 사용하는 코드에서 자식 타입을 넣어도 동작이 동일하게 유지되어야 한다는 것입니다. 다형성을 제대로 활용하려면 하위 클래스가 부모 클래스의 계약(행동 규칙)을 깨지 않도록 해야 합니다.

❗ 잘못된 예 – LSP 위반 (자동차 인터페이스의 엑셀 기능)

예시: 자동차 인터페이스의 엑셀 기능

public interface Car {
    void accelerate();  // 앞으로 가야 함
}

이제 누군가가 이 인터페이스를 구현합니다.

public class BackwardCar implements Car {
    @Override
    public void accelerate() {
        // 뒤로 움직이는 구현!
        System.out.println("뒤로 갑니다");
    }
}

Car 인터페이스를 사용하는 클라이언트는 accelerate()를 호출하면 앞으로 간다는 전제를 믿고 사용할 것입니다. 그런데 BackwardCar는 뒤로 가도록 구현했습니다. 이는 인터페이스의 규약을 깨뜨린 것이며, 프로그램의 정확성을 해칩니다.

✅ LSP를 지키려면?

  • 하위 클래스는 부모 클래스의 기능을 확장하거나 정의된 동작을 일관되게 유지해야 합니다.
  • 하위 클래스는 부모 클래스와 동일한 의미의 행동을 가져야 하며, 기대 동작을 벗어나면 안 됩니다.

✅ 4. ISP - 인터페이스 분리 원칙 (Interface Segregation Principle)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.

ISP의 핵심은 하나의 커다란 범용 인터페이스보다는, 클라이언트의 역할에 맞는 여러 개의 작은 인터페이스로 분리하자는 것입니다. 이렇게 하면, 클라이언트가 자신과 관계없는 메서드를 강제로 구현하지 않아도 되며 불필요한 의존성을 줄일 수 있습니다.

❗ 나쁜 예: 하나의 거대한 자동차 인터페이스

public interface Car {
    void drive();
    void refuel();
    void repair();
}

운전자는 drive()만 필요하고, 정비사는 repair()만 필요합니다. 그런데 두 클라이언트 모두 Car 인터페이스를 구현하거나 참조해야 한다면, 불필요한 의존성이 생기고, 변경에 쉽게 휘둘리는 구조가 됩니다.

✅ 좋은 예: 인터페이스 분리

public interface Drivable {
    void drive();
}

public interface Repairable {
    void repair();
}

이렇게 나누면 운전 클라이언트는 Drivable만, 정비 클라이언트는 Repairable만 사용하면 됩니다. 각 인터페이스가 변경되더라도 서로 영향을 미치지 않기 때문에 유지보수가 쉬워집니다.


✅ 5. DIP - 의존관계 역전 원칙 (Dependency Inversion Principle)

“구현이 아닌 추상화에 의존하라”

“클래스는 인터페이스에 의존해야지, 구체 클래스에 의존하면 안 된다.”

DIP는 유연하고 변경에 강한 설계를 위해 매우 중요한 원칙입니다. 우리가 흔히 말하는 의존성 주입(DI, Dependency Injection) 기법은 이 DIP 원칙을 실천하는 방법 중 하나입니다.

❗ DIP를 위반한 예

public class MemberService {
    private final MemoryMemberRepository repository = new MemoryMemberRepository();
}

MemberService는 구체 클래스인 MemoryMemberRepository에 직접 의존하고 있습니다. 이럴 경우, 저장소를 JdbcMemberRepository로 바꾸려면 서비스 코드 자체를 수정해야 하는 문제가 생깁니다.

✅ DIP를 지키는 구조(생성자 주입)

public class MemberService {
    private final MemberRepository repository;

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

이제 MemberService는 오직 MemberRepository 인터페이스에만 의존합니다. 이처럼 인터페이스에만 의존하고 실제 구현 객체는 외부에서 주입받으면 DIP를 만족할 수 있습니다.


🔁 마무리

객체지향에서 핵심은 다형성입니다. 하지만 다형성만으로는 SOLID 원칙을 완전히 지킬 수 없습니다.

  • 다형성만 사용하면 구현 객체를 변경할 때 여전히 클라이언트 코드도 수정해야 합니다.
  • OCP와 DIP를 지키기 위해서는 구성과 생성의 책임을 분리하는 설계가 필요합니다.
  • 그래서 역할(인터페이스)에 의존하고, 구현체 주입은 외부에서 이루어지도록 구성해야 합니다.

역할과 구현을 명확히 분리하고, 인터페이스 기반으로 설계하면 변화에 유연한, 유지보수가 쉬운 구조를 만들 수 있습니다.

반응형

'Back end > Java' 카테고리의 다른 글

[Java] 환경변수 설정하기 (windows)  (0) 2025.05.21
[Java] Java란 무엇인가?  (0) 2025.05.20
[Java] 다형성과 역할-구현 분리로 이해하는 객체 지향 프로그래밍  (0) 2025.05.13
[Java] List<Map<String, String>> 형식을 String[] 형식으로 변경하는 방법  (0) 2024.08.02
[Java] JUnit과 AssertJ를 활용한 효과적인 단위 테스트 작성 방법  (0) 2024.06.05
'Back end/Java' 카테고리의 다른 글
  • [Java] 환경변수 설정하기 (windows)
  • [Java] Java란 무엇인가?
  • [Java] 다형성과 역할-구현 분리로 이해하는 객체 지향 프로그래밍
  • [Java] List<Map<String, String>> 형식을 String[] 형식으로 변경하는 방법
Kim-SooHyeon
Kim-SooHyeon
개발일기 및 알고리즘, 블로그 운영에 대한 글을 포스팅합니다. :) 목표: 뿌리 깊은 개발자 되기
    반응형
  • Kim-SooHyeon
    soo_vely의 개발로그
    Kim-SooHyeon
  • 전체
    오늘
    어제
    • 분류 전체보기 (253)
      • 알고리즘 (108)
        • 자료구조 (3)
        • Java (104)
        • Python (1)
      • Back end (70)
        • Spring Project (27)
        • Java (21)
        • API (1)
        • Python (0)
        • Django (3)
        • Linux (1)
        • 서버 (2)
        • 에러로그 (11)
        • 부스트 코스 (1)
      • Front end (9)
        • HTML, CSS (4)
        • JavaScript (4)
        • JQuery (0)
      • 기타 프로그래밍 (4)
        • Android Studio (1)
        • Arduino (2)
        • Azure Fundamental(AZ-900) (1)
      • 개발도구 (23)
        • Git (12)
        • SVN (0)
        • Eclipse (2)
        • 기타 Tool (9)
      • Database (16)
        • Oracle (10)
        • MySQL (0)
        • H2 Database (3)
        • ORM & JPA (1)
      • 자격증 (10)
        • 컴활 1급 (7)
        • 컴활 2급 (2)
        • SQLD (1)
      • 기타 (13)
        • 블로그 운영 (6)
        • 문서 (1)
        • 기타 (6)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring
    1차원 배열
    Git
    문자열
    jpa
    BOJ
    단계별풀기
    github
    백준 자바
    for문
    백준
    오라클
    배열
    Oracle
    java
    springboot
    solved.ac
    알고리즘
    백준알고리즘
    구현
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kim-SooHyeon
[Java] 객체 지향 설계의 핵심! SOLID 원칙
상단으로

티스토리툴바