😀 Jerry/면접 질문

[1분 면접] SOLID 원칙이란 ?

Jerry_K 2025. 3. 11. 13:08

📌 면접 답변

 

SOLID 원칙은 객체지향 설계의 핵심인 의존성 관리를 위한 원칙이다.

객체지향 프로그래밍을 하면서 지켜야 할 5대 원칙으로, 변경에 용이하고, 유지보수와 확장에 도움이 된다.

 

 

 

SRP (SIngle Responsibility Priciple, 단일 책임 원칙)

하나의 클래스가 여러 가지 기능을 담당하면 안 되고, 하나의 역할(책임)만 수행해야 한다.

SRP를 잘 지키면 변경이 필요할 때 수정할 대상이 명확해진다.

 

SRP 위반 예시

class Report {
    public String generate() {
        return "Report Content";
    }

    public void print() {
        System.out.println("Printing Report...");
    }

    public void saveToFile() {
        System.out.println("Saving Report to File...");
    }
}
  • Report 클래스가 generate, print, saveToFile 모두 처리 
  • 만약 saveToFile의 방식이 변경되면 클래스 전체를 수정 (문제점)

 

올바른 SRP 적용 예시

// 1. Report 생성 역할
class Report {
    public String generate() {
        return "Report Content";
    }
}

// 2. 출력 역할 분리
class ReportPrinter {
    public void print(Report report) {
        System.out.println("Printing: " + report.generate());
    }
}

// 3. 파일 저장 역할 분리
class ReportSaver {
    public void saveToFile(Report report) {
        System.out.println("Saving to file: " + report.generate());
    }
}
  • 각각의 클래스가 하나의 책임만 갖는다.
  • 수정이 필요할 때 관련 클래스만 변경하는 장점
    • 가독성 증가, 유지보수 용이, 유닛 테스트 가능 (장점)

 

 

OCP (Open-Closed Priciple, 개방 폐쇄 원칙)

  • 확장에는 열려있고, 변경에는 닫혀 있어야 함
  • 확장은 새로운 타입을 추가함으로써 새로운 기능 추가를 의미
  • 폐쇄는 확장이 일어날 때 레벨의 모듈이 영향을 받지 않아야 함을 의미
  • 이를 통해 모듈의 행동을 쉽게 변경 가능 

 

OCP 위반 예시

class DiscountService {
    public double calculateDiscount(String type, double price) {
        if (type.equals("STUDENT")) {
            return price * 0.9; // 학생 할인 (10%)
        } else if (type.equals("VIP")) {
            return price * 0.8; // VIP 할인 (20%)
        } else {
            return price; // 할인 없음
        }
    }
}
  • 할인 정책이 변경 될 때마다 calculateDiscount()를 수정해야 함 (문제점)

 

올바른 OCP 적용 예시

// 1. 할인 정책 인터페이스
interface DiscountPolicy {
    double applyDiscount(double price);
}

// 2. 학생 할인 (10%)
class StudentDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price * 0.9;
    }
}

// 3. VIP 할인 (20%)
class VIPDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price * 0.8;
    }
}

// 4. 할인 서비스 (OCP 적용)
class DiscountService {
    private final DiscountPolicy discountPolicy;

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

    public double calculateDiscount(double price) {
        return discountPolicy.applyDiscount(price);
    }
}
  • 이렇게 할 경우 새로운 할인 정책이 필요해도 기존 코드를 수정하지 않고 새로운 클래스를 추가하면 된다.
// 4. 블랙프라이데이 할인 (50%)
class BlackFridayDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price * 0.5;
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        DiscountService studentDiscountService = new DiscountService(new StudentDiscount());
        System.out.println("학생 할인 가격: " + studentDiscountService.calculateDiscount(100));

        DiscountService vipDiscountService = new DiscountService(new VIPDiscount());
        System.out.println("VIP 할인 가격: " + vipDiscountService.calculateDiscount(100));

        DiscountService blackFridayDiscountService = new DiscountService(new BlackFridayDiscount());
        System.out.println("블랙프라이데이 할인 가격: " + blackFridayDiscountService.calculateDiscount(100));
    }
}
  • 새로운 기능이 추가되었는데, 기존 코드 수정 없이 확장이 가능해졌다.
  • OCP가 본질적으로 얘기하는 것은 추상화로, 이 코드에서 추상화는 DiscountPolicy 이다.
    • 추상화를 통해 변하는 것들을 숨기고, 변하지 않는 것들에 의존한다. 
    • 변하지 않는것은 추상화(인터페이스)

 

 

LSP (Liskov Substitution Priciple, 리스코브 치환 원칙)

  • 하위 타입은 언제나 상위 타입으로 교체할 수 있어야 함
  • 즉, 해당 객체를 사용하는 클라이언트는 상위 타입이 하위 타입으로 변경되어도, 차이점을 인식하지 못한 채 상위 타입의 퍼블릭 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다.

 

LSP 위반 예시

// 1. 부모 클래스: 직사각형
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

// 2. 자식 클래스: 정사각형 (직사각형을 상속받음)
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 정사각형이므로 width = height 유지
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height; // 정사각형이므로 width = height 유지
    }
}

// 3. LSP 위반이 발생하는 테스트 코드
public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Square(); // 부모 클래스로 대체

        rect.setWidth(4);
        rect.setHeight(5);

        System.out.println("Expected area: 4 * 5 = 20");
        System.out.println("Actual area: " + rect.getArea()); // 예상과 다름 (LSP 위반)
    }
}
  • 자식 클래스인 Square가 부모 클래스인 Rectangle을 대체 할 수 없음 (문제점)
  • Rectangle을 기대하는 코드에 Square를 넣으면 예상과 다른 동작 발생

 

올바른 LSP 적용 예시

// 1. 공통 인터페이스 생성
interface Shape {
    int getArea();
}

// 2. 직사각형 클래스 (Shape 구현)
class Rectangle implements Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

// 3. 정사각형 클래스 (Shape 구현)
class Square implements Shape {
    private int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int getArea() {
        return side * side;
    }
}

// 4. 테스트 코드 (LSP 만족)
public class Main {
    public static void main(String[] args) {
        Shape rect = new Rectangle();
        ((Rectangle) rect).setWidth(4);
        ((Rectangle) rect).setHeight(5);

        Shape square = new Square();
        ((Square) square).setSide(4);

        System.out.println("Rectangle area: " + rect.getArea()); // 4 * 5 = 20
        System.out.println("Square area: " + square.getArea());   // 4 * 4 = 16
    }
}
  • 이런 경우 Rectangle과 Square를 서로 독립적인 클래스로 나눈다.
  • 그리고 공통 인터페이스 Shape을 만들어 각 클래스가 자신의 규칙대로 구현하도록 설계

 

 

ISP (Interface Segregation Priciple, 인터페이스 분리 원칙)

 

  • 인터페이스는 클라이언트가 필요로 하는 메서드만 제공해야 한다.
  • 불필요한 메드가 포함된 거대한 인터페이스를 만들지 말고, 작은 인터페이스로 나눠야 한다.

 

ICP 위반 예시

// 1. 너무 큰 인터페이스 (SRP 위반)
interface Worker {
    void work();     // 작업 수행
    void cookFood(); // 요리 기능
}

// 2. 개발자 클래스
class Developer implements Worker {
    public void work() {
        System.out.println("개발 중...");
    }

    public void cookFood() { 
        throw new UnsupportedOperationException("개발자는 요리를 하지 않습니다!");
    }
}

// 3. 요리사 클래스
class Chef implements Worker {
    public void work() {
        System.out.println("요리 중...");
    }

    public void cookFood() {
        System.out.println("음식을 요리 중...");
    }
}
  • Worker 인터페이스가 모든 작업자를 위한 인터페이스 
  • 굳이 Developer는 cookFood를 구현할 필요가 없음 (문제점)

 

올바른 ISP 적용 예시

// 1. 작업 인터페이스 (공통)
interface Workable {
    void work();
}

// 2. 요리 인터페이스 (요리 가능한 사람만)
interface Cookable {
    void cookFood();
}

// 3. 개발자 클래스 (요리 기능 없음)
class Developer implements Workable {
    public void work() {
        System.out.println("개발 중...");
    }
}

// 4. 요리사 클래스 (요리 기능 포함)
class Chef implements Workable, Cookable {
    public void work() {
        System.out.println("요리 중...");
    }

    public void cookFood() {
        System.out.println("음식을 요리 중...");
    }
}
  • Worker 인터페이스를 작은 역할별 인터페이스로 분리
  • 인터페이스의 변경이 특정 클래스에 영향을 미치지 않음
  • 클라이언트의 목적과 용도에 적합한 인터페이스 만을 제공해야함 ! 

 

 

DIP (Dependency Inversion Priciple, 의존성 역전 원칙)

  • 상위 수준의 모듈이 하위 수준의 모듈에 의존해서는 안됨 
    • 즉, 고수준인 클라이언트가 하위 모듈들에 의존하면 안되고 추상화된 인터페이스에 의존해야 함
    • 다른 말로 비즈니스와 관련된 부분이 세부 사항에 의존하지 않는 설계 원칙 
  • 모두 추상화에 의존을 강조해야 한다.

 

DIP 위반 예시

// 1. MySQLDatabase 클래스 (저수준 모듈)
class MySQLDatabase {
    public void connect() {
        System.out.println("MySQL 데이터베이스 연결됨");
    }

    public void save(String data) {
        System.out.println("MySQL에 '" + data + "' 저장 완료");
    }
}

// 2. UserService 클래스 (고수준 모듈) - 직접 MySQLDatabase에 의존 (DIP 위반)
class UserService {
    private MySQLDatabase database;

    public UserService() {
        this.database = new MySQLDatabase(); // 특정 DB에 강하게 의존
    }

    public void saveUser(String username) {
        database.connect();
        database.save(username);
    }
}

// 3. 실행 코드
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserService();
        userService.saveUser("Kim");
    }
}
  • UserService가 MySQLDatabase에 직접 의존하고 있다. 
  • 다른 DB로 변경할 경우 UserService를 수정해야 하는 DIP 위반이 발생 (문제점)

 

올바른 DIP 적용 예시

// 1. 추상화 계층 (인터페이스)
interface Database {
    void connect();
    void save(String data);
}

// 2. MySQL 구현체 (저수준 모듈)
class MySQLDatabase implements Database {
    public void connect() {
        System.out.println("MySQL 데이터베이스 연결됨");
    }

    public void save(String data) {
        System.out.println("MySQL에 '" + data + "' 저장 완료");
    }
}

// 3. PostgreSQL 구현체 (새로운 DB 추가)
class PostgreSQLDatabase implements Database {
    public void connect() {
        System.out.println("PostgreSQL 데이터베이스 연결됨");
    }

    public void save(String data) {
        System.out.println("PostgreSQL에 '" + data + "' 저장 완료");
    }
}

// 4. UserService가 구체적인 DB가 아닌 인터페이스(Database)에 의존 (DIP 적용)
class UserService {
    private Database database;

    public UserService(Database database) { // 의존성 주입 (Dependency Injection)
        this.database = database;
    }

    public void saveUser(String username) {
        database.connect();
        database.save(username);
    }
}

// 5. 실행 코드
public class Main {
    public static void main(String[] args) {
        Database mysqlDB = new MySQLDatabase();
        UserService userService1 = new UserService(mysqlDB);
        userService1.saveUser("Kim");

        Database postgresDB = new PostgreSQLDatabase();
        UserService userService2 = new UserService(postgresDB);
        userService2.saveUser("Lee");
    }
}
  • 인터페이스를 만들어 UserService가 추상화에 의존하도록 한다.
  • 이제 어떤 DB든 쉽게 교체가 가능하다.

📌 내 답변

SOLID 원칙은 객체지향 설계에서 지켜야 할 원칙이다. 

S : 객체는 단일 책임을 가진다. 

O : 확장은 쉽고, 코드 변경 사항은 작게 해야 한다.

L : 리스코브 원칙 

I, D :  ... ??  

 

 

SOLID 원칙에 대해 대충 알고는 있었지만, 막상 대답을 하려하니 잘 나오지 않았다... 

 

SOLID 원칙은 가장 객체 지향의 기본적인 원칙으로,  핵심은 결국 추상화와 다형성이다.

이 원칙을 통해 유연하고 확장가능한 애플리케이션을 개발 할 수 있다. 

잘 알아두자 !! 


[출처 및 참고 자료]

 

https://www.maeil-mail.kr/

 

매일메일 - 기술 면접 질문 구독 서비스

기술 면접 질문을 매일매일 메일로 보내드릴게요!

www.maeil-mail.kr

매일 메일의 면접 질문 정리

 

 

https://blog.siner.io/2020/06/18/solid-principles/

 

 

[번역] 그림으로 보는 SOLID 원칙

SOLID 원칙과 관련된 좋은 그림예시가 있어서 이를 번역하면서 예제코드를 추가하였습니다. 만약 당신이 객체지향 프로그래밍과 친숙하다면, 당신은 SOLID 원칙에 대해 들어보았을 것 입니다. 이

blog.siner.io

SOLID 원칙 그림 출처