티스토리 뷰

300x250

SOLID란 무엇일까?

Java를 공부하다 보면 객체지향 설계 원칙으로 SOLID라는 단어를 자주 보게 됩니다. 처음에는 영어 약자라서 어렵게 느껴지지만, 사실은 유지보수하기 좋은 코드를 만들기 위한 5가지 기준이라고 이해하면 됩니다.

이번 글에서는 Java 기준으로 SOLID가 무엇인지, 왜 중요한지, 그리고 각 원칙을 예시와 함께 쉽게 정리해보겠습니다.


1. SOLID란?

SOLID는 객체지향 설계의 5가지 핵심 원칙의 앞글자를 모은 말입니다.

  • S: SRP, 단일 책임 원칙
  • O: OCP, 개방-폐쇄 원칙
  • L: LSP, 리스코프 치환 원칙
  • I: ISP, 인터페이스 분리 원칙
  • D: DIP, 의존 역전 원칙

이 원칙들은 복잡해 보이지만 결국 한 방향을 가리킵니다.

코드를 수정하기 쉽고, 확장하기 쉽고, 덜 깨지게 만들자

즉, SOLID는 예쁜 코드를 만들기 위한 장식이 아니라 변화에 강한 설계 기준입니다.


2. 왜 SOLID가 중요할까?

처음에는 SOLID 없이도 코드가 잘 동작할 수 있습니다. 하지만 프로젝트가 커지고 기능이 늘어나면 문제가 생깁니다.

예를 들어 아래 같은 상황을 떠올려볼 수 있습니다.

  • 결제 수단이 카드만 있다가 카카오페이, 네이버페이로 늘어남
  • 회원 등급 정책이 계속 바뀜
  • 알림 방식이 이메일에서 문자, 푸시까지 확장됨
  • 여러 명이 함께 같은 코드를 수정하게 됨

이때 설계가 잘못되어 있으면 한 군데 수정했는데 다른 기능까지 함께 깨지는 상황이 자주 발생합니다. SOLID는 이런 문제를 줄이는 데 도움을 줍니다.

728x90

3. SRP - 단일 책임 원칙

개념

하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다. 더 정확히 말하면 변경 이유가 하나여야 한다는 뜻에 가깝습니다.

나쁜 예시

public class UserService {
    public void saveUser(User user) {
        // 사용자 저장
    }

    public void sendWelcomeEmail(User user) {
        // 가입 환영 이메일 발송
    }

    public void generateReport() {
        // 사용자 통계 리포트 생성
    }
}

위 클래스는 너무 많은 일을 합니다.

  • 사용자 저장
  • 이메일 발송
  • 통계 리포트 생성

저장 정책이 바뀌어도 수정해야 하고, 메일 정책이 바뀌어도 수정해야 하고, 리포트 형식이 바뀌어도 수정해야 합니다. 즉, 변경 이유가 너무 많습니다.

개선 예시

public class UserService {
    public void saveUser(User user) {
        // 사용자 저장
    }
}

public class EmailService {
    public void sendWelcomeEmail(User user) {
        // 이메일 발송
    }
}

public class ReportService {
    public void generateReport() {
        // 리포트 생성
    }
}

이제 각 클래스가 맡은 역할이 명확해졌습니다.

한 줄 정리

한 클래스에 이것저것 몰아넣지 말고 역할별로 나누자.


4. OCP - 개방-폐쇄 원칙

개념

확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙입니다. 쉽게 말하면 새 기능은 추가할 수 있어야 하지만, 기존 코드는 최대한 덜 건드려야 한다는 뜻입니다.

나쁜 예시

public class DiscountService {
    public int getDiscount(String grade) {
        if (grade.equals("VIP")) {
            return 2000;
        } else if (grade.equals("GOLD")) {
            return 1000;
        }
        return 0;
    }
}

등급이 추가될 때마다 if-else를 계속 수정해야 합니다. 이런 구조는 정책이 늘어날수록 더 복잡해집니다.

개선 예시

public interface DiscountPolicy {
    int discount();
}

public class VipDiscountPolicy implements DiscountPolicy {
    public int discount() {
        return 2000;
    }
}

public class GoldDiscountPolicy implements DiscountPolicy {
    public int discount() {
        return 1000;
    }
}
public class DiscountService {
    private final DiscountPolicy discountPolicy;

    public DiscountService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    public int getDiscount() {
        return discountPolicy.discount();
    }
}

이제 새로운 할인 정책이 생기면 기존 코드를 크게 수정하지 않고 구현체만 추가하면 됩니다.

한 줄 정리

새 기능은 덧붙이고, 기존 코드는 덜 건드리는 구조가 좋다.


5. LSP - 리스코프 치환 원칙

개념

자식 클래스는 부모 클래스를 대체할 수 있어야 한다는 원칙입니다. 즉, 상속을 썼다면 하위 클래스가 상위 클래스의 기대 동작을 깨면 안 됩니다.

예시

public class Bird {
    public void fly() {
        System.out.println("하늘을 난다.");
    }
}

그런데 펭귄은 새지만 날 수 없습니다.

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("펭귄은 날 수 없습니다.");
    }
}

이 경우 PenguinBird처럼 사용하려고 하면 기대 동작이 깨집니다. 즉, 부모 타입으로 믿고 썼는데 실제 동작이 달라집니다.

개선 방향

public interface Bird {
    void eat();
}

public interface FlyingBird extends Bird {
    void fly();
}

이제 펭귄은 Bird, 참새는 FlyingBird로 표현할 수 있어 구조가 더 자연스러워집니다.

한 줄 정리

상속은 이름이 비슷해서가 아니라, 진짜로 대체 가능할 때만 써야 한다.


6. ISP - 인터페이스 분리 원칙

개념

클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다는 원칙입니다. 즉, 큰 인터페이스 하나보다 작고 필요한 인터페이스로 나누는 것이 좋습니다.

나쁜 예시

public interface Machine {
    void print();
    void scan();
    void fax();
}

출력만 가능한 프린터도 이 인터페이스를 구현하면 사용하지 않는 메서드까지 억지로 구현해야 합니다.

public class SimplePrinter implements Machine {
    public void print() {
        System.out.println("출력");
    }

    public void scan() {
        throw new UnsupportedOperationException();
    }

    public void fax() {
        throw new UnsupportedOperationException();
    }
}

개선 예시

public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public interface Fax {
    void fax();
}
public class SimplePrinter implements Printer {
    public void print() {
        System.out.println("출력");
    }
}

public class MultiFunctionPrinter implements Printer, Scanner, Fax {
    public void print() {}
    public void scan() {}
    public void fax() {}
}

한 줄 정리

큰 인터페이스 하나보다, 필요한 기능만 가진 작은 인터페이스가 낫다.


7. DIP - 의존 역전 원칙

개념

구체적인 구현 클래스보다 추상화에 의존해야 한다는 원칙입니다. 쉽게 말하면 CardPayment 같은 구현체보다 Payment 같은 인터페이스에 의존하자는 뜻입니다.

나쁜 예시

public class OrderService {
    private CardPayment cardPayment = new CardPayment();

    public void pay() {
        cardPayment.pay();
    }
}

이 구조에서는 결제 방식이 바뀌면 OrderService도 직접 수정해야 합니다.

개선 예시

public interface Payment {
    void pay();
}
public class CardPayment implements Payment {
    public void pay() {
        System.out.println("카드 결제");
    }
}
public class KakaoPayment implements Payment {
    public void pay() {
        System.out.println("카카오페이 결제");
    }
}
public class OrderService {
    private final Payment payment;

    public OrderService(Payment payment) {
        this.payment = payment;
    }

    public void pay() {
        payment.pay();
    }
}

이제 OrderService는 구현체 변경에 훨씬 유연하게 대응할 수 있습니다.

한 줄 정리

구현 클래스에 직접 묶이지 말고, 추상화된 규약에 의존하자.


8. SOLID 5원칙 한 번에 정리

  • SRP: 한 클래스는 한 가지 역할에 집중
  • OCP: 확장은 쉽게, 기존 코드 수정은 최소화
  • LSP: 상속했다면 부모 대신 자식이 자연스럽게 들어가야 함
  • ISP: 인터페이스는 작고 필요한 만큼만 분리
  • DIP: 구현보다 추상화에 의존

아래처럼 기억하면 이해가 더 쉽습니다.

SOLID는 유지보수하기 좋은 객체지향 구조를 만들기 위한 기준이다.


9. SOLID 적용할 때 주의할 점

SOLID가 중요하다고 해서 무조건 클래스를 잘게 쪼개는 것이 정답은 아닙니다. 작은 프로젝트에서 과하게 적용하면 오히려 구조가 복잡해질 수 있습니다.

예를 들어,

  • 클래스 수가 지나치게 많아지고
  • 인터페이스가 불필요하게 늘어나고
  • 코드 흐름 파악이 더 어려워질 수 있습니다

따라서 SOLID는 무조건 외워서 적용하는 규칙보다는, 유지보수성과 확장성이 필요한 상황에서 설계를 점검하는 기준으로 보는 것이 좋습니다.


10. 실무에서 이렇게 점검하면 좋다

실무에서는 SOLID를 약자로 외우기보다 아래 질문으로 점검하면 훨씬 도움이 됩니다.

  • 이 클래스가 너무 많은 일을 하고 있지 않은가?
  • 새 기능 추가할 때 기존 코드를 계속 수정하고 있지 않은가?
  • 상속 구조가 정말 자연스러운가?
  • 인터페이스가 너무 크지는 않은가?
  • 구현체에 직접 의존해서 결합도가 높아지지 않았는가?

이 질문에 자주 걸린다면 설계를 한 번 다시 보는 것이 좋습니다.


마무리

SOLID는 객체지향 설계를 잘하기 위한 핵심 원칙이지만, 처음부터 완벽하게 지키는 것은 쉽지 않습니다. 중요한 것은 약자를 외우는 것이 아니라 왜 필요한지 이해하는 것입니다.

한마디로 정리하면, SOLID는 변경에 유연하고 유지보수하기 좋은 코드를 만들기 위한 설계 원칙입니다.

Java로 객체지향을 공부하고 있다면 문법 학습에서 끝나지 말고, SOLID 같은 설계 원칙까지 함께 익혀두면 훨씬 더 좋은 코드를 만들 수 있습니다.

728x90
댓글
반응형
최근에 올라온 글
글 보관함
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30