코드 한 줄의 기록

Java 설계 원칙 완전 정복: 초보자를 위한 SOLID 입문 가이드 본문

JAVA

Java 설계 원칙 완전 정복: 초보자를 위한 SOLID 입문 가이드

CodeByJin 2025. 10. 13. 08:06
반응형

객체지향 설계에서 안정성·유연성·확장성을 보장하는 SOLID 원칙은 코드 품질을 높이는 필수 지침이다.
이 글에서는 SOLID 각 원칙의 개념과 Java 예제, 그리고 학습 팁을 공유하며 함께 공부할 수 있도록 안내합니다.

SOLID란 무엇인가?

최근 Java 프로젝트를 진행하면서 코드가 복잡해지고 기능 추가가 어려워지는 경험을 자주 했다. 그때마다 클린 아키텍처리팩토링 이론을 찾아보다가 마주한 것이 바로 SOLID 원칙이다.
 
SOLID는 객체지향 설계의 다섯 가지 핵심 원칙을 뜻하는 약어로, 미국의 로버트 C. 마틴(“아저씨 마틴”)이 제안했다.
- S: Single Responsibility Principle (단일 책임 원칙)
- O: Open/Closed Principle (개방-폐쇄 원칙)
- L: Liskov Substitution Principle (리스코프 치환 원칙)
- I: Interface Segregation Principle (인터페이스 분리 원칙)
- D: Dependency Inversion Principle (의존 역전 원칙)
이 원칙들을 따르면 코드 가독성과 유지보수성이 크게 향상된다.
지금부터 하나씩 살펴보자.

S: Single Responsibility Principle

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

개념

  • 클래스가 변경될 이유(Responsibility)가 하나여야 한다.
  • 여러 기능을 하나의 클래스에 몰아넣으면 변경 시 서로 다른 기능이 서로에게 영향을 준다.

예제

class UserReport {
    public String generateReport(User user) {
        // 리포트 생성 로직
    }
    public void saveToFile(String report, String path) {
        // 파일 저장 로직
    }
}

 
위 코드에서 리포트 생성과 저장 책임이 섞여 있다.
 
해결책

class UserReportGenerator {
    public String generateReport(User user) { ... }
}
class ReportFileSaver {
    public void saveToFile(String report, String path) { ... }
}

 
이렇게 역할을 분리하면 각 클래스 변경이 독립적이다.

O: Open/Closed Principle

> 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
 

개념

  • 기존 코드를 수정하지 않고 기능을 추가할 수 있어야 한다.
  • 인터페이스나 추상 클래스를 활용해 구현체를 교체할 수 있도록 설계한다.

예제

interface DiscountPolicy {
    double applyDiscount(double price);
}
class NormalDiscount implements DiscountPolicy { ... }
class SpecialDiscount implements DiscountPolicy { ... }

class Order {
    private DiscountPolicy discountPolicy;
    public Order(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
    public double calculatePrice(double price) {
        return discountPolicy.applyDiscount(price);
    }
}

 

새 할인 정책 추가 시 DiscountPolicy 구현체만 추가하면 된다. Order 클래스는 수정 불필요하다.

L: Liskov Substitution Principle

> 자식 클래스는 부모 클래스의 대체 가능해야 한다.
 

개념

  • 부모 타입을 사용하는 곳에 자식 타입을 넣어도 문제가 없어야 한다.
  • 사전(Precondition)은 강화하면 안 되고, 사후(Postcondition)는 약화하면 안 된다.

예제

class Rectangle {
    protected int width, height;
    public void setWidth(int w) { width = w; }
    public void setHeight(int h) { height = h; }
    public int getArea() { return width * height; }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w);
    }
    @Override
    public void setHeight(int h) {
        super.setWidth(h);
        super.setHeight(h);
    }
}

 

SquareRectangle의 대체가 불가능하다. setWidth 호출만으로 높이까지 바뀌므로 영역 계산이 예기치 않게 동작한다.

 

해결: RectangleSquare를 공통 추상 타입에서 분리하거나, Square를 별도 구현체로 처리하자.

I: Interface Segregation Principle

> 인터페이스는 클라이언트별로 구체화해야 한다.
 

개념

  • 사용하지 않는 메서드 때문에 구현체가 비대해지면 안 된다.
  • 역할별로 인터페이스를 작게 쪼개서 필요 인터페이스만 의존하도록 설계한다.

예제

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

class Robot implements Worker {
    @Override public void work() { ... }
    @Override public void eat() {
        // 로봇은 식사 안 함 → 불필요 메서드 구현  
    }
}

 

로봇은 eat()을 구현할 이유가 없다.

 
해결

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

class Human implements Workable, Eatable { ... }
class Robot implements Workable { ... }

 

이렇게 쪼개면 로봇은 Workable만 구현하면 된다.

D: Dependency Inversion Principle

> 상세(구현체)에 의존하지 말고 추상(인터페이스)에 의존하라.
 

개념

  • 고수준 모듈이 저수준 모듈에 의존하지 않도록, 둘 다 추상에 의존해야 한다.
  • 구체 클래스 대신 인터페이스나 추상 클래스에 의존성을 주입(DI)한다.

예제

class MySQLConnection { ... }
class UserRepository {
    private MySQLConnection conn = new MySQLConnection();
    // 강하게 결합돼 있어 교체 어려움  
}

 
위 코드는 MySQL에 종속적이다.
 
해결

interface DBConnection { ... }
class MySQLConnection implements DBConnection { ... }
class OracleConnection implements DBConnection { ... }

class UserRepository {
    private DBConnection conn;
    public UserRepository(DBConnection conn) {
        this.conn = conn;
    }
}

 

이제 UserRepository는 구현체 교체나 테스트용 목 객체 주입이 자유롭다.

SOLID 적용 팁

  • TDD(테스트 주도 개발)와 함께 사용하면 설계가 자연스럽게 SOLID에 가까워진다.
  • 의존성 주입(DI) 프레임워크(Spring, Guice 등)를 활용해 D 원칙을 실천하자.
  • 리팩토링할 때 리스코프 치환 위반이나 단일 책임 위반 지점을 우선 점검하라.
  • 처음부터 완벽하려 하기보다는, 테스트 작성 → 작은 리팩토링 → SOLID 준수를 반복하며 개선한다.

SOLID 원칙은 처음에는 다소 추상적이고 복잡하게 느껴질 수 있다. 하지만 하나씩 적용해보면 코드가 훨씬 유연하고 유지보수하기 쉬워진다는 것을 체감할 것이다.
함께 SOLID 기반 설계를 연습하며, 더 읽기 좋고 확장 가능한 Java 프로젝트를 완성해보자.

“좋은 코드란, 변경이 쉬운 코드다.” – 로버트 C. 마틴

 
이 글을 참조하며 직접 예제 프로젝트에 SOLID 원칙을 적용해보고, 느낀 점이나 궁금한 사항을 댓글로 남겨주세요. 서로의 경험을 공유하며 더 나은 설계 방법을 찾아봅시다!

Java 패키지와 모듈 시스템: 체계적인 코드 구조화 가이드

개발을 하다 보면 클래스가 점점 많아지고 프로젝트가 복잡해집니다. 이때 코드를 어떻게 정리하고 관리할지가 중요한 문제가 되는데요. 저도 처음 Java를 배울 때 이 부분이 많이 헷갈렸습니다.

byteandbit.tistory.com

반응형