Posted:      Updated:

자바 디자인 패턴 스터디를 하며 ‘Java 객체 지향 디자인 패턴’ 교재를 정리한 글입니다.

SOLID 원칙

  • 로버트 마틴이 주장한 다섯가지 설계 원칙
    • SRP: 단일 책임 원칙, Single Resposibility Principle
    • OCP: 개방 폐쇄 원칙, Open Closed Principle
    • LSP: 리스코프의 대입 원칙, Liskov Substitution Principle
    • ISP: 인터페이스 분리 원칙, Interface Segregation Principle
    • DIP: 의존성 역전 원칙, Dependency Inversion Principle

SRP

  • 단일 책임 원칙
  • 클래스(객체)는 단 하나의 책임만 가져야 함

책임

  • 해야 하는 것, 할 수 있는 것
  • 책임 할당 시 어떤 객체보다도 작업을 잘 할 수 있는 객체에 할당해야 함
  • 책임에 수반되는 모든 일을 자신만이 수행해야 함

변경

  • 책임 = 변경 이유
  • 시스템에 새로운 요구사항이나 변경이 있을 때 가능하면 영향 받는 부분을 줄여야 함

책임 분리

  • 한 클래스에 많은 책임을 부여하지 않고 단 하나의 책임만 수행하도록 해서 변경 사유가 될 수 있는 것을 하나로 만들어야 함
  • 여러 책임을 수행하는 클래스가 있다면, 도움을 필요로 하는 코드도 많을 수밖에 없음
  • 변경 사항이 생기면 직접, 간접적으로 사용되는 코드를 모두 다시 테스트해야 함
  • 회귀 테스트: 어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 테스트
    • 시스템에 변경 사항이 발생했을 때 영향을 받는 부분을 적게 해야 함

산탄총 수술

  • 하나의 책임이 여러개의 클래스들로 분산되어 있는 경우에도 설계를 변경해야 할 경우
  • 클래스 하나하나를 모두 변경해야 해서 에러가 발생할 수 있음
  • 로깅, 보안, 트랜잭션 등의 횡단 관심 기능
    • 시스템 핵심 기능(하나의 책임) 안에 포함되는 부가 기능(여러 개의 클래스로 분리)
    • 부가 기능에 변경이 발생하면 부가 기능을 실행하는 모든 핵심 기능에도 변경이 적용되어야 함
  • 해결 방법: 부가 기능을 별개의 클래스로 분리해 책임을 담당하게 함으로 여러 곳에 흩어진 공통 책임을 한 곳에 모아 응집도를 높임

AOP(Aspect-Oriented Programming)

  • 관심 지향 프로그래밍
  • 횡단 관심의 문제를 해결하는 방법 중 하나
  • 횡단 관심을 수행하는 코드를 애스펙트(aspect)라는 특별한 객체로 모듈화하고, 위빙(weaving) 작업을 통해 모듈화한 코드를 핵심 기능에 끼워 넣음
  • 변경 사항이 생길 경우 애스펙트만 수정하면 됨
  • 조인포인트
    • 애플리케이션 실행 중의 특정한 지점
    • 메서드 호출, 메서드 실행 자체, 클래스 초기화, 객체 생성 시점
    • 어떤 지점에서 AOP를 사용해 추가 로직을 삽입할 지 정의하는 핵심 개념
  • 어드바이스
    • 특정 조인포인트에 실행하는 코드
    • Before 어드바이스: 조인포인트 이전에 실행
    • After 어드바이스: 조인포인트 이후에 실행
  • 포인트컷
    • 조인포인트의 집합체
    • 언제 어드바이스를 실행할지 정의할 때 사용
    • 어드바이스를 어떻게 적용할지 상세한 제어 가능
    • 어드바이스 실행 지점을 다양하게 제어할 필요가 있을 때는 복잡하게 구성하여 사용
  • 애스펙트
    • 어드바이스와 포인트컷의 조합
    • 애플리케이션이 가져야 할 로직과 실행해야 하는 지점의 정의
  • 위빙
    • 애플리케이션 코드의 해당 지점에 애스펙트를 실제로 주입하는 과정
    • 컴파일 시점 AOP 솔루션: 컴파일 시점에 위빙이 일어나며, 빌드 중 별도의 과정을 거침
    • 실행 시점 AOP 솔루션: 실행 중에 동적으로 위빙

OCP

  • 개방 폐쇄의 원칙
  • 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계
  • 무엇이 변하는 것인지, 무엇이 변하지 않는 것인지 구분하여 변해야 하는 것은 쉽게 변할 수 있도록 하고, 변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 함

단위 테스트

  • 클래스를 변경하지 않고도 대상 클래스의 환경을 변경할 수 있는 설계가 중요
  • 실제 데이터가 변경될 위험이 있다면 가짜 객체를 만들어야 함

테스트 더블

  • 모의 객체
    • 테스트 더블의 한 종류
    • 무엇인가를 대신하는 가짜
  • 더미 객체
    • 테스트할 때 객체만 필요하고 기능까지는 필요 없는 경우 사용
    • 메서드 호출 시 정상 동작하지 않고 예외 발생
  • 테스트 스텁
    • 더미 객체에 단순한 기능을 추가
    • 객체의 특정 상태를 가정하여 작성
  • 테스트 스파이
    • 테스트 대상 클래스가 의존하는 클래스로의 간접 출력을 검증하는 데 사용
    • 클래스 실행 동안 의존 클래스로부터의 호출과 호출 결과를 찾아내고 원하는 대로 호출되었는지 검사
  • 가짜 객체
    • 실제 의존 클래스의 기능 전체나 일부를 단순하게 구현해야 함
  • 목 객체
    • 미리 정의한 기댓값과 실제 호출을 비교하여 문제가 있다면 테스트를 실패하게 함

LSP

  • 리스코프 치환 원칙
  • 일반화 관계를 적절하게 사용했는지 점검
  • 일반화 관계에서 최소한 슈퍼 클래스가 제공하는 오퍼레이션을 파생 클래스에서도 제공하는 행위적 일관성이 있어야 함
  • 슈퍼 클래스의 인스턴스 대신 파생 클래스의 인스턴스를 사용해도 프로그램 의미가 변화되지 않음
  • is a kind of 관계를 만족해야 함
  • 피터 코드의 상속 규칙(재정의하지 않음)을 지키면 LSP를 만족시킬 수 있음

DIP

  • 의존 역전 원칙
  • 객체 사이에 서로 도움을 주고 받을 때 생기는 의존 관계의 가이드라인
  • 변화하기 쉬운 것, 자주 변화하는 것보다 변화하기 어려운 것, 거의 변화가 없는 것에 의존
    • 변하기 어려운 것
      • 정책, 전략처럼 어떤 큰 흐름이나 개념같은 추상적인 것
      • 인터페이스
    • 변하기 쉬운 것
      • 구체적인 방식, 사물 등
      • 구체 클래스

의존성 주입

  • 클래스 외부에서 의존되는 것을 대상 객체의 인스턴스 변수에 주입

  • 예제 코드) 아이가 장난감을 로봇에서 레고로 바꾸어 가지고 놀 경우

public abstract class Toy {
    public String toString() {
        return "Toy";
    }
}

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

// 추가 작성
public class Lego extends Toy{
    public String toString() {
        return "Lego";
    }
}

public class Kid {
    private Toy toy;

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

    public void play() {
        System.out.println(toy.toString());
    }
}

public class Main {
    public static void main(String[] args) {
        Toy t = new Robot();
        Kid k = new Kid();
        k.setToy(t);
        k.play();

        // toy 변경
        t = new Lego();
        k.setToy(t);
        k.play();
    }
}
  • Kid, Toy, Robot 등 기존 코드의 변화 없이 장난감을 바꿀 수 있음

역전

  • 의존 관계를 역전시켜야 더 나은 설계가 된다
  • 전체적인 대규모 비즈니스 작업을 총괄하는 추상적인 모듈이 구체적인 작업을 실행하는 모듈 호출

ISP

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

SRP와 ISP의 관계

  • 단일 책임을 수행하지 않고 여러 책임을 수행하는 비대한 클래스를 SRP에 따라 단일 책임을 갖는 여러 클래스들로 분할하고 각자 인터페이스를 제공하면 ISP도 만족시킬 수 있음
  • SRP를 만족하더라도 ISP를 반드시 만족할 수 있는 것은 아님
    • 예시) 게시판의 여러 기능을 구현한 클래스가 있을 경우
      • 글쓰기, 읽기, 수정, 삭제 기능이 있음
      • 클라이언트에 따라 기능을 일부분만 사용할 수 있음
      • 게시판이 게시판에 관련된 책임만 수행 👉 SRP 만족
      • 모든 메서드가 들어있는 인터페이스를 클라이언트에 상관 없이 사용 👉 ISP 위배

댓글남기기