SOLID Principle

Park Young Jun

Y3S Study group : https://github.com/y3s-study

2018.07.10

자바를 사용해 만든 소프트웨어는

객체 지향 소프트웨어인가?

객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이 존재할까?

S.O.L.I.D

What is SOLID?

Robert C. Martin

2000년대 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 제시

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

OCP (Open Close Principle) : 개방 폐쇄 원칙

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

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

DIP (Dependency Inversion Principle) : 의존 역전 원칙

Core Principle?

High Cohesion

&

Loose Coupling

SOLID 원칙이 잘 적용된 소프트웨어는...

  • 이해하기 쉽고

  • 리팩토링 및 유지보수가 수월하고

  • 논리적으로 정연하다

SRP (Single Responsibility Principle)

"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다." - Robert C. Martin

Responsibility (책임)

  • SRP에서 말하는 책임의 기본 단위는 "객체"

  • 객체는 단 하나의 책임만 가져야 한다.

public class Student {
    public Course getCourse() {/*...*/}
    public void addCourse(Course course) {/*...*/}
    public void save() {/*...*/}
    public Student load() {/*...*/}
    public void printOnReportCard() {/*...*/}
    public void printOnAttendanceBook() {/*...*/}
}

너무 많은 책임(역할)을 수행하는 Student 클래스

클래스가 SRP 원칙을 지키지 않은 경우

Student 클래스의 역할(책임)

  • 수강과목 추가 및 조회
  • 데이터베이스에 학생 정보 저장 및 조회
  • 성적표 출력
  • 출석부 출력
public class Student {
    public Course getCourse() {/*...*/}
    public void addCourse(Course course) {/*...*/}
    public void save() {/*...*/}
    public Student load() {/*...*/}
    public void printOnReportCard() {/*...*/}
    public void printOnAttendanceBook() {/*...*/}
}
class Student {
    public Course getCourse() {/*...*/}
    public void addCourse(Course course) {/*...*/}
}

class StudentRepository {
    public void save(Student student) {/*...*/}
    public Student load() {/*...*/}
}

class ReportCard {
    public void print(Student student) {/*...*/}
}

class AttendanceBook {
    public void print(Student student) {/*...*/}
}

학생

학생 Repository

성적표

출석부

SRP를 적용해 개선한 코드

메서드가 SRP 원칙을 지키지 않은 경우

public class 강아지 {
    final static Boolean 수컷 = true;
    final static Boolean 암컷 = false;
    
    Boolean 성별;
    
    void 소변보다() {
        if (this.성별 == 수컷) {
            // 한 쪽 다리를 들고 소변을 본다.
        } else {
            // 뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
        }
    }
}

메서드가 SRP를 지키지 않을 경우 나타나는 대표적인 냄새가 바로

분기 처리를 위한 if문!

SRP를 적용해 개선한 코드

abstract class 강아지 {
    abstract void 소변보다();
}

class 수컷강아지 extends 강아지 {
    void 소변보다() {
        // 한 쪽 다리를 들고 소변을 본다.
    }
}

class 암컷강아지 extends 강아지 {
    void 소변보다() {
        // 뒷다리 두 개로 앉은 자세로 소변을 본다.
    }
}

여러 개의 클래스에 책임이 분산된 경우

  • 로깅, 보안, 트랜잭션과 같은 횡단 관심 기능이 대표적
  • 클래스 하나 하나를 모두 변경하지 않으면 정상적으로 동작하지 않고 에러 발생

관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)

  • 횡단 관심 문제를 해결하는 방법
  • 횡단 관심을 수행하는 코드를 에스펙트(Aspect)라는 객체로 모듈화 하여 위빙(Weaving)이라는 작업을 통해 모듈화한 코드를 핵심 기능에 끼워넣는다.
  • 기존의 코드를 변경하지 않고 핵심 기능에서 부가 기능을 효과적으로 사용할 수 있다.

관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)

@Component
public class UserService {
    public void execute() {
        System.out.println("UserService execute()");
    }
}

UserService 클래스의 execute() 메소드가 실행될때 앞, 뒤에서 로깅을 남기고 싶다면?

@Component
public class UserService {
    private static Logger log = LoggerFactory.getLogger(UserService.class);

    public void execute() {
        log.info("### before logging ###");
	System.out.println("UserService execute()");
        log.info("### after logging ###");
    }
}

관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)

@Aspect
@Component
public class LoggingAspect {
    private static Logger log = LoggerFactory.getLogger(LoggingAspect.class);
	
    @Before("execution(* net.y3s.designpatternexample.chapter03.srp.UserService.*(..))") 
    public void loggingBefore(JoinPoint joinPoint) {
        log.info("### before logging ###");
    }
	
    @After("execution(* net.y3s.designpatternexample.chapter03.srp.UserService.*(..))") 
    public void loggingAfter(JoinPoint joinPoint) {
	log.info("### after logging ###");
    }	
}
@Component
public class UserService {
    public void execute() {
        System.out.println("UserService execute()");
    }
}

UserService에는 핵심 비즈니스 로직만 남길 수 있다 (SRP 만족)

SRP와 가장 관계가 깊은 객체 지향의 특성?

Abstraction (추상화)

애플리케이션의 경계를 정하고 추상화를 통해 클래스의 속성과 메서드를 설계할 때

반드시 단일 책임 원칙을 고려하는 습관을 들이자!

OCP (Open Closed Principle)

"소프트웨어 엔티티는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다." - Robert C. Martin

Open Closed Principle

  • 개방 폐쇄 원칙
  • 확장에는 열려(Open)있고, 변경에는 닫혀(Colsed)있어야 한다.
  • 기존의 코드를 변경하지 않으면서 새로운 기능을 추가 할 수 있도록 설계가 되어야 한다.

 

Open Closed Principle

도서관 대여 명부 출력 기능이 추가되어야 한다면?

Open Closed Principle

  • 간단한 방식으로 도서관 대여 명부 클래스를 만들어 SomeClient가 이용하게 함
  • 이 방식은 SomeClient를 수정해야 하기 때문에 OCP를 위반한다.

OCP를 만족하는 설계

  • 변하는 것과 변하지 않는 것을 구분하는 것이 중요하다.
  • SomeClient는 추상적인 모듈(인터페이스)에 의존
  • 인터페이스에서 구체적인 출력 매체를 캡슐화

OCP와 JDBC

DB가 Oracle에서 MySQL로 바뀌더라도 애플리케이션을 수정할 필요가 없다.

OCP와 가장 관계가 깊은 객체 지향의 특성?

Polymorphism (다형성)

OCP를 무시하고 프로그램을 작성하면 객체지향의 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.

LSP (Liskov Substitution Principle)

"서브타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다."

- Robert C. Martin

LSP (Liskov Substitution Principle)

  • 일반화 관계(상속)에 대한 원칙
  • 상위 클래스와 자식 클래스 사이의 행위는 일관성이 있어야 한다.
  • LSP를 만족하면 상위 인스턴스 대신 하위 클래스의 인스턴스로 대체해도 프로그램의 의미가 변하지 않는다.

객체 지향 상속(일반화 관계)의 조건

하위 클래스 is a kind of 상위 클래스 

LSP를 만족하지 않는 예

  1. 원숭이 is a kind of 포유류 ?
    • 원숭이는 알을 낳지 않는다. (O)
  2. 오리너구리  is a kind of 포유류 ?
    • ​​오리너구리는 알을 낳지 않는다. (X)

상위 타입 : 표유류는 알을 낳지 않는다.

LSP를 만족하지 않는 예

public class Bag {
    private int price;

    public void setPrice(int price) {
        this.price = price;
    }

    public int getPrice() {
        return this.price;
    }
}   
class DiscountedBag extends Bag {
    private double discountedRate = 0;

    public DiscountedBag(double discountedRate) {
        super();
        this.discountedRate = discountedRate;
    }

    @Override
    public void setPrice(int price) {
        super.setPrice(price - (int)(discountedRate * price));
    }
}
Bag bag = new Bag();
bag.setPrice(50000);
System.out.println(bag.getPrice());
Bag bag = new DiscountedBag(5);
bag.setPrice(50000);
System.out.println(bag.getPrice());

LSP 위반/만족 사례 비교

LSP 위반

LSP 만족

할아버지 = new 딸();
동물 = new 박쥐();

LSP와 가장 관계가 깊은 객체 지향의 특성?

Inheritance/Extends (상속)

LSP는 객체 지향의 상속이라는 특성을 올바르게 활용하면 자연스럽게 만족하게 된다.

ISP (Interface Segregation Principle)

"클라이언트는 자신의 사용하지 않는 메서드에 의존 관계를 맺으면 안된다."

- Robert C. Martin

ISP (Interface Segregation Principle)

  • 인터페이스 분리 원칙
  • 클라이언트 자신이 사용하지 않는 기능에는 영향을 받지 않아야 한다.
  • 인터페이스를 클라이언트에 특화 되도록 분리시키는 설계 원칙

복합기 클래스 다이어그램

복합기 클래스에 ISP 원칙 적용

  • 클라이언트 마다 관심있는 메서드만 인터페이스로 제공
  • 클라이언트 자신이 사용하지 않는 메서드의 변화로 인한 영향을 받지 않음

ISP와 SRP의 관계

  • SRP : 하나의 역할(책임)만 하는 다수의 클래스로 분할
  • ISP : 하나의 역할(책임)만 하는 다수의 인터페이스로 분할
  • SRP와 ISP는 같은 문제에 대한 두 가지 다른 해결책
  • 특별한 경우가 아니라면 SRP를 적용하는 것이 더 좋은 해결책이다.

DIP (Dependency Inversion Principle)

"고차원 모듈은 저차원 모듈에 의존하면 안된다.

이 두 모듈은 모두 다른 추상화된 것에 의존해야 한다."

 

"추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다."

 

"자주 변경되는 구체(Concrete) 클래스에 의존하지 마라."

 

- Robert C. Martin

DIP (Dependency Inversion Principle)

  • 의존 관계를 맺을때의 가이드라인
  • 자주 변하는 것 보다는 변화하기 어려운 것에 의존하라는 원칙

 

변하기 쉬운 것, 변하기 어려운 것

변하기 쉬운 것

변하기 어려운 것

  • 구체적인 방식
  • 사물
  • 구체 클래스
  • 정책, 전략과 같은 큰 흐름
  • 개념
  • 추상적인것
  • 인터페이스

장난감 클래스에 DIP 적용

  • 구체적인 장난감은 변하기 쉬움
  • 아이가 장난감을 가지고 노는 사실은 변하기 어려움

DI (Dependency Injection)

  • 의존성 주입(DI)란 클래스 외부에서 의존되는 것을 대상 객체의 인스턴스에 주입하는 기술
  • DIP를 만족하면 이 DI라는 기술로 변화를 쉽게 수용할 수 있는 코드를 만들 수 있다.
  • DI를 이용하면 대상 객체를 변경하지 않고도 대상 객체에서 외부 의존 객체를 바꿀 수 있다.
  • 대표적 DI 프레임워크 : Spring Framework

DI (Dependency Injection)

class Kid {
    private Toy toy;

    void setToy(Toy toy) {
        this.toy = toy;
    }

    void play() {
        System.out.println(toy.toString());
    }
}
abstract class Toy {
    public abstract String toString();
}

class Lego extends Toy {
    @Override
    public String toString() {
        return "Lego";
    }
}

class Robot extends Toy {
    @Override
    public String toString() {
        return "Robot";
    }
}

DI (Dependency Injection)

public class Client {
    public static void main(String[] args) {
        Kid kid = new Kid();
        Toy toy = new Lego();

        kid.setToy(toy);
        kid.play();

        toy = new Robot();
        kid.setToy(toy);
        kid.play();
    }
}
  • 다른 장난감을 가지고 놀고 싶으면 새 장난감을 주입(Injection)해주면 된다.
  • Kid, Toy, Robot, Lego 등 기존 코드의 영향을 받지 않고 바꾸는게 가능하다.

정리 - 객체지향과 SOLID

  • SOLID는 객체 지향을 올바르게 프로그램에 녹여내기 위한 원칙
  • SOLID는 객체 지향 4대 특성을 제대로 활용하면 당연히 나타나는 결과
  • SoC : Separation Of Concerns(관심사의 분리)
  • SoC를 적용하면 자연스럽게 SRP, ISP, OCP에 도달하게 된다.
  • 스프링은 SoC를 통해 SOLID를 극한까지 적용하고 있다.
  • SOLID 원칙을 적용하면 소스파일의 개수는 더 많아진다.
  • 늘어 나는 소스 파일에 대한 부담보다 얻는 혜택이 더 많다.

Thank you!

SOLID Principle

By Young Jun Park (박영준)

SOLID Principle

SOLID 원칙을 설명합니다.

  • 205