[Spring] 객체 지향 설계와 스프링 (SOLID)

2025. 2. 10. 07:00코딩 도구/백엔드 개발 (Backend Development)

반응형

좋은 객체 지향 설계의 5가지 원칙 (SOLID)

김영한님의 "스프링 핵심 원리 - 기본편"을 수강하고 정리했습니다. 

1. SOLID 원칙이란?

SOLID 원칙은 클린 코드의 창시자로 유명한 로버트 마틴(Robert C. Martin)이 정리한 좋은 객체 지향 설계의 5가지 원칙이다. 이 원칙을 따르면 유지보수성과 확장성이 뛰어난 코드를 작성할 수 있다.

SOLID는 다음과 같은 다섯 가지 원칙의 약어다.

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

2. 단일 책임 원칙 (SRP: Single Responsibility Principle)

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

핵심 개념

  • 클래스는 단 하나의 책임(변경의 이유)만 가져야 한다.
  • 책임이 많아지면, 변경이 발생할 때 여러 기능에 영향을 미쳐 유지보수가 어렵다.

적용 예시

잘못된 예시: UI 변경과 비즈니스 로직이 섞여 있는 클래스

public class ReportGenerator {
    public void generateReport() {
        // 데이터 수집
        // 데이터 처리
        // UI에 출력 (HTML, PDF 등)
    }
}

개선된 예시: UI 로직과 데이터 처리 로직을 분리

public class ReportDataCollector { /* 데이터 수집 관련 코드 */ }
public class ReportProcessor { /* 데이터 처리 관련 코드 */ }
public class ReportRenderer { /* UI 출력 관련 코드 */ }

3. 개방-폐쇄 원칙 (OCP: Open/Closed Principle)

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

핵심 개념

  • 새로운 기능을 추가할 때 기존 코드를 변경하지 않고 확장할 수 있어야 한다.
  • 다형성을 활용하여 구현한다.

적용 예시

잘못된 예시: 새로운 저장소를 추가하려면 기존 코드를 수정해야 한다.

public class MemberService {
    private MemberRepository memberRepository = new MemoryMemberRepository();
}

개선된 예시: 인터페이스를 활용하여 확장 가능하도록 변경

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

이제 MemoryMemberRepository 대신 JdbcMemberRepository를 주입하면 새로운 기능을 추가할 수 있다.


4. 리스코프 치환 원칙 (LSP: Liskov Substitution Principle)

"하위 클래스는 기반 클래스(부모 클래스)의 기능을 깨뜨리지 않으면서 대체 가능해야 한다."

핵심 개념

  • 부모 클래스의 규약을 하위 클래스가 준수해야 한다.
  • 하위 클래스가 부모 클래스의 동작을 변경하면 예기치 않은 오류가 발생할 수 있다.

적용 예시

잘못된 예시: 원과 사각형이 같은 방식으로 동작하지 않는다.

class Rectangle {
    int width, height;
    void setWidth(int width) { this.width = width; }
    void setHeight(int height) { this.height = height; }
}

class Square extends Rectangle {
    void setWidth(int width) { this.width = width; this.height = width; }
    void setHeight(int height) { this.width = height; this.height = height; }
}

위 코드는 Rectangle을 사용하는 코드에서 Square로 바꿀 경우 의도한 동작이 깨질 수 있다.

개선된 예시: 별도의 인터페이스 도입

interface Shape { int getArea(); }
class Rectangle implements Shape { /* 구현 */ }
class Square implements Shape { /* 구현 */ }

이렇게 하면 Shape을 사용하는 코드에서 RectangleSquare를 안전하게 교체할 수 있다.


5. 인터페이스 분리 원칙 (ISP: Interface Segregation Principle)

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

핵심 개념

  • 하나의 커다란 인터페이스보다 작고 구체적인 인터페이스 여러 개로 분리하는 것이 좋다.
  • 클라이언트가 필요하지 않은 기능을 강제로 구현하지 않도록 한다.

적용 예시

잘못된 예시: 범용 인터페이스로 인해 불필요한 메서드를 구현해야 한다.

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    public void work() { /* 작업 수행 */ }
    public void eat() { /* 로봇은 먹을 수 없음! */ }
}

개선된 예시: 역할에 따라 인터페이스를 분리한다.

interface Workable { void work(); }
interface Eatable { void eat(); }

class Human implements Workable, Eatable {
    public void work() { /* 작업 수행 */ }
    public void eat() { /* 식사 */ }
}

class Robot implements Workable {
    public void work() { /* 작업 수행 */ }
}

6. 의존관계 역전 원칙 (DIP: Dependency Inversion Principle)

"구체적인 구현이 아닌 추상화(인터페이스)에 의존해야 한다."

핵심 개념

  • 구체 클래스에 의존하지 않고 인터페이스(추상 클래스)에 의존해야 한다.
  • 이를 통해 유연하고 확장 가능한 설계를 할 수 있다.

적용 예시

잘못된 예시: 구현 클래스에 직접 의존

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

개선된 예시: 인터페이스를 활용하여 의존성 주입

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

이제 MemoryMemberRepository뿐만 아니라 JdbcMemberRepository도 쉽게 교체할 수 있다.

 
반응형