SOLID 원칙과 DI

hgh1472·2024년 9월 15일

자바

목록 보기
4/4

SOLID 원칙

SOLID 원칙은 객체 지향적인 설계를 위한 5가지 원칙이다.

객체 지향 설계?

객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 “객체”들의 모임으로 파악한다. 각각의 객체는 메세지를 주고받고, 데이터를 처리할 수 있다.

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

즉, 객체 지향 설계는 객체를 쉽게 변경하거나 갈아끼울 수 있게 한다.

SRP 단일 책임 원칙

Single Responsibility Principle

  • 하나의 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임 ⇒ 변경의 이유 또한 한가지

만약 회원가입 로직을 가지는 Service에서 패스워드를 암호화하는 로직을 직접 구현해서 가지고 있다고 해보자.

public void 회원가입(Stirng id, Stirng password) {
		password 암호화 로직;
		멤버 객체 생성;
		멤버 객체 DB에 저장;
}		

회원가입 로직은 회원가입과 패스워드를 암호화하는 로직을 직접 가지고 있다. 그런데 만약 암호화 알고리즘에 대해 변경사항이 생겼다고 해보자.

그렇다면 암호화 알고리즘을 변경하는데 Service의 회원가입 로직을 수정한다.

이 상황이 가지고 올 수 있는 결과는 대략 다음과 같다.

  • 로그인에서는 복호화 로직을 사용한다.
  • 암호화 알고리즘 변경 ⇒ Service의 회원가입 로직을 수정

변경에 대한 변경 대상이 명확하지 않다. 따라서 암호화에 대한 책임을 분리해야 한다.

public class PasswordEncoder {
		public String encryptPassword(String password) {
				// 암호화 로직
		}
		
		public String decryptPassword(String password) {
				// 복호화 로직
		}
}

위처럼 책임을 분리하면 책임 대상이 명확해진다. 시스템이 커져 많은 의존성이 존재해도 변경 대상은 명확하다.

OCP 개방-폐쇄 원칙

Open/Closed Principle

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
    • 확장에는 열려있다 ⇒ 추가 기능 사항이 있을 때 유연하게 추가할 수 있어야 한다
    • 변경에는 닫혀 있다 ⇒ 구현 객체 변경 시 클라이언트 코드 변경 X
  • 다형성 활용
  • 스프링이 의존관계 주입 ⇒ 클라이언트 코드 변경 X

LSP 리스코프 치환 원칙

Liskov Substitution Principle

  • 프로그램의 객체는 프로그램의 정확성을 깨지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 하위 클래스는 인터페이스 규약을 다 지켜야 한다.
    • 부모 클래스의 원래 의도대로 동작해야 한다.
  • Collection

ISP 인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 한 개보다 낫다.
  • SRP의 인터페이스 버전
  • 목적과 용도에 적합한 인터페이스를 제공
  • 인터페이스가 명확해진다.

DIP 의존관계 역전 원칙

Dependency Inversion Principle

  • 프로그래머는 추상화에 의존한다.
  • 구현 클래스가 아니라 인터페이스에 의존
  • 구현체가 아닌 역할에 의존해야 한다.
  • BookManager bm = new BookManagerImpl();
    • BookManager 인터페이스에 의존하는 것 같지만, BookManagerImpl에도 의존하고 있다.
    • 인터페이스만으로는 실행 X
      • 스프링이 해결
        • IoC : 스프링이 객체를 생성하고 관리하면서 의존관계를 연결
        • BookManager bm; : DI

DI

Dependency Injection

DI는 우리말로 의존관계 주입이라고 할 수 있다.

그렇다면 여기서 의존관계란 무엇일까?

의존관계

의존하다의 뜻은 다음과 같다.

다른 것에 의지하여 존재하다.

그렇다면 자바에서 ‘A가 B에 의존한다’라는 말은 어떤 상황일까?

개발자와 노트북을 예시로 들어보자.

세팅된 노트북이 주어졌을 때, 개발자는 노트북에 따라 작업하는 방식이 달라질 수 있다. 즉, 노트북이 바뀌면 개발자의 작업 방식도 달라지게 된다. 따라서 개발자는 노트북에 의존한다고 할 수 있다.

예를 들어 노트북A에는 이클립스가 설치되어 있고, 노트북 B에는 인텔리제이가 설치되어 있다고 해보자. 같은 자바 파일을 작업하더라도, IDE가 다르기 때문에 방식은 다를 수 있다.

class Developer {
		private SamsungNoteBook samsungNoteBook;
		
		public Developer() {
				samsungNoteBook = new SamsungNoteBook();
		}
}

위 구조는 삼성 노트북만 의존할 수 있는 구조로 구성되어 있다. 더 다양한 노트북을 의존받을 수 있게 하려면 인터페이스를 이용한다.

class Developer {
		private NoteBook notebook;
		
		public Developer() {
				notebook = new SamsungNoteBook();
				// notebook = new MacBook();
		}
}

interface NoteBook {
		// 노트북 관련 메소드
}

class SamsungNoteBook implements NoteBook {
}

위처럼 인터페이스를 이용해 추상화를 하게 되면, 더 다양한 의존관계를 맺을 수 있고, 구현체와의 결합도가 낮아진다.

의존관계 주입

그렇다면 의존관계 주입은 무엇일까?

위의 코드에서는 Developer 가 직접 어떤 NoteBook 을 사용할지 선택한다.

그런데 회사에서 노트북을 제공한다고 생각해보자. 즉, Developer 가 노트북을 선택하는 것이 아니라, 어떤 노트북을 선택할지 외부에서 결정해주는 것이다.

이처럼 의존관계를 외부에서 주입해주는 것을 Dependency Injection(의존관계 주입)라고 한다.

  • 인터페이스만 의존하고 있는 상태
  • 런타임 시점에 제 3자가 의존관계 결정

의존관계 주입

생성자 주입

  • 생성자를 통해서 의존관계 주입
  • 생성자 호출 시점에 딱 1번만 호출되는 것 보장
  • 불변, 필수 의존관계에 사용
@Component
public Developer {
private NoteBook noteBook;
		@Autowired
		public Developer(NoteBook notebook) {
				this.noteBook = noteBook;
		}
}

생성자가 1개만 있으면 @Autowired를 생략해도 자동 주입된다.

setter 주입

  • setter를 통해서 의존관계 주입
  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용
@Component
public Developer {
private NoteBook noteBook;
		@Autowired
		public void setNoteBook(NoteBook noteBook) {
				this.noteBook = noteBook;
		}
}
❓ **자바빈 프로퍼티?**

자바에서는 필드 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해 값을 읽거나 수정하는 규칙이 존재

필드 주입

  • 필드에 바로 주입
  • 코드가 간결하지만 외부에서 변경 불가 X
  • DI 프레임워크가 없으면 사용할 수 없음
@Component
public Developer {
		@Autowired
		private NoteBook noteBook;
}

일반 메서드 주입

  • 일반 메서드를 통해 주입
  • 한번에 여러 필드 주입
  • 일반적으로 잘 사용 X
@Component
public Developer {
		private NoteBook noteBook;

		@Autowired
		public void init(NoteBook noteBook) {
				this.noteBook = noteBook;
		}
}

IoC

Inversion of Control

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 구현체를 생성하고 연결한다.

즉 구현 객체가 프로그램의 제어 흐름을 스스로 조종한다.

반면 DI를 이용한 경우, 구현체는 자신의 로직을 실행하는 역할만 담당한다. Developer는 필요한 NoteBook 인터페이스를 호출하지만, 어떤 NoteBook이 들어올지는 모른다.

프로그램에 대한 제어 흐름은 외부가 가지고 있다. Developer는 어떤 NoteBook이 오든지 자신의 로직을 실행할 뿐이다.

이처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.

프레임워크 = 프레임워크가 내가 작성한 코드를 제어하고 대신 실행

라이브러리 = 내가 작성한 코드를 내가 직접 제어의 흐름을 담당

참고자료

https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

0개의 댓글