✔️ DI(Dependency Injection)
*의존성 주입
의존성 주입에 대해 살펴보기 전에 의존성(Dependency)이란 무엇인지에 대한 정의가 필요하다. 프로그래밍에서 이 의존성이란 무엇일까?
의존성/의존 관계란?
1. 코드 레벨(클래스 레벨)의 의존 관계
: Supplier가 변경되면, Client 코드가 영향을 받는다.
2. 사용 의존 관계/오브젝트 의존 관계
: A 오브젝트가 B 오브젝트를 사용하면 A가 B를 의존하고 있다고 표현한다.
이러한 의존 관계에서 A는 B없이 작동할 수 없다. 또한, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다.
스프링의 핵심은 코드 레벨(클래스 레벨)의 의존관계와 런타임 레벨의 오브젝트 의존 관계가 다를 수 있다는 것이다.
그리고 이를 의존성 주입을 통해 만들어낸다. 이제 의존성 주입에 대해 살펴보자.
의존성 주입(Dependency Injection)이란?
관계 설정 책임을 외부에 넘기는 것이다.
예시
예를 들어 가게에서 연필을 판다고 하자. Store에서 Pencil을 사용하고 있으니 의존성이 있다고 표현한다.
public class Store {
private Pencil pencil;
public Store() {
this.pencil = new Pencil();
}
}
그리고 이를 위와 같은 코드로 표현할 수 있다. 만약 연필이 아닌 Food를 팔고 싶고 더 다양한 상품과 의존 관계를 맺고 싶다고 생각해보자. 의존 관계를 인터페이스로 추상화해서 표현할 수 있다.
public interface Product {
}
public class Pencil implements Product {
}
public class Food implements Product {
}
Product 인터페이스를 두고 Food와 Pencil은 Product의 구현체로 둔다.
public class Store {
private Product product = new Pencil();
// ...
}
의존 관계를 인터페이스로 추상화하여 더 다양한 의존 관계를 맺을 수 있게 되었다.
그러나 여전히 조금 불편하다. 다양한 의존관계를 표현할 수 는 있지만, 내부적으로 의존 관계인 Product를 결정하고 있다. 의존 관계가 변경되면 내부 코드의 변경도 필요하다.
이는 현재 관계 설정 책임이 Store 클래스에 있기 때문이다. Store 클래스가 Pencil 클래스에 대해 코드레벨에 의존하는 코드이다.
이는 결국 코드의 유연성이 떨어진다고 볼 수 있다. 관계 설정 책임이 분리 되어 있지 않으면 확장, 변경 그리고 유지보수성에 문제가 생긴다.
의존성 주입을 통한 문제 해결
관계 설정 책임을 분리 시켜 외부에서 의존성을 주입 받으면, 코드 레벨의 의존관계와 런타임 레벨의 의존관계가 다른 프로그램을 만들 수 있다.
public class Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
Store 클래스는 더이상 관계 설정 책임을 가지지 않는다. 오로지 인터페이스에만 의존하고 외부에서 의존성을 주입 받아, 런타임 시(애플리케이션 실행 시점) 의존 관계가 결정된다. 이를 통해 유연한 코드를 만들 수 있다.
생성자 의존성 주입을 권장 하는 이유
의존성을 주입하는 방식은 다양하다. 그러나 생성자 주입을 권장 하는 이유는 다음과 같다.
불변성
Product food = new Food();
Product pencil = new Pencil();
Store store = new Store(pencil);
위 코드의 예시로 살펴보자. Store 객체를 생성하면서 Product의 구현체인 Pencil을 생성자로 주입받는 경우이다. Store 객체가 생성 되는 시점부터 Pencil을 주입 받는단 것이 결정 된 것이다. 다른 구현체로 변경될 수 있는 여지가 없다.
실제로 개발을 하다 보면 의존 관계의 변경이 필요한 상황은 거의 없다고 한다. 수정자 주입이나 일반 메소드 주입을 용하면 불필요하게 수정의 가능성을 열어두어 유지 보수성을 떨어뜨린다.
그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.
컴파일 시점에 검증 가능
public class Store {
private Product product;
public Store(Product product) {
this.product = product;
}
}
/* 다른 어딘가에서 ... */
Store store = new Store();
Store 클래스는 생성자에서 의존성을 주입 받도록 되어 있는데, 다른 어딘가에서 코드를 작성하며 생성자에 주입하는 것을 빼먹었다고 가정해보자. 이럴 경우엔 코드에 빨간 줄이 뜰것이고 컴파일이 되지 않을 것이다.
명확하게 의존성을 주입하기 전까진 컴파일 할 수 없게 되는 것이다.
✔️ IoC(Inversion of Control)
*제어의 역전
public class Store {
private Product product = new Pencil();
// ...
}
어떤 Product를 사용할지에 대한 제어가 Store 클래스에 있었다.
public class Store {
private final Product product;
public Store(Product prduct) {
this.product = product;
}
}
그러나 그 제어를 외부로 넘김으로써 제어권의 역전이 일어난 것이다.
스프링 컨테이너
스프링의 제어의 역전은 스프링 컨테이너와 연관이 있다. 스프링 컨테이너는 스프링 및 스프링 기술의 핵심 컴포넌트이다.
우선, 프레임워크와 라이브러리의 차이를 살펴보자.
📌 프레임워크
원하는 기능 구현에 집중하여 개발할 수 있도록 일정한 형태와 필요한 기능을 갖추고 있는 골격, 뼈대
뼈대 위에서 개발, 사용자는 프레임워크가 정해준 방식대로 클래스나 메소드를 구현한다.
📌 라이브러리
: 특정 기능을 모아둔 코드, 함수들의 집합이며 코드 작성 시 활용 가능한 도구
자주 사용하는 코드의 집합. 언제든지 필요한 곳에서 호출하여 사용할 수 있도록 클래스나 function으로 만들어진다.
프레임워크와 라이브러리의 차이를 살펴보면 제어의 역전을 이해할 수 있다. 라이브러리를 사용할때, 라이브러리 호출은 누가 하는가? 당연히 사용자(=프로그래머)가 한다.
반면에, 프레임워크의 코드는 사용자가 호출하지 않는다. 흔히들 사용하는 스프링 부트를 떠올려보자. 스프링 부트의 메인 메소드에는 @SpringBootApplication 어노테이션이 달려 있다.
메인 메소드를 실행할 때, 이 어노테이션이 존재하기에 Configuration 정보들이 주입이 되고, Bean이 생성되는 등 내가 실행하지 않은 일련의 과정들이 알아서 실행된다.
이는 스프링 컨테이너를 통해서 일어나는 과정들이다. 컨테이너란 무엇을 담는 공간을 뜻한다. 스프링 컨테이너가 담는 것들은 Bean이라고 칭하고, 이 Bean은 오브젝트이다. 정확히 말하면, 애플리케이션의 핵심 기능들을 담당하는 오브젝트이다.
스프링은 컨테이너는 Io/DI 컨테이너라고 불린다. 클래스 외부에서 스프링 컨테이너가 의존성 주입을 통해 오브젝트간의 관계를 맺어주기 때문이다.
이것이 제어의 역전이다. 프로그래머가 아닌 프레임워크가 해당 과정들을 실행해 주는 것으로 제어권이 넘어간 것이다.
정리 : 스프링의 IoC란 무엇인가?
객체의 생성과 의존성 관리를 스프링 컨테이너가 대신하여,
개발자가 아닌 프레임워크가 객체 간 관계를 제어하는 방식이다.
✔️ AOP(관점 지향 프로그래밍)
AOP란?
관심사를 분리하고 모듈화하는 프로그래밍이다. 여기서 관심사란 변경의 관점에서 설명하면, 변경이 되는 시점과 이유가 다르면 관심사가 다르다고 말한다.
코드 = 핵심 관심사 + 횡단 관심사
핵심 관심사는 모듈 별로 다르지만 횡단 관심사는 모듈별로 반복되어 중복 해서 나타난다. 예를 들어, 로깅, 보안, 트랜잭션 관리와 같은 횡단 관심사는 전체 소프트웨어 시스템에서 여러 모듈 또는 컴포넌트에 걸쳐 중복되어 발생하다.
관심사가 다른 코드를 같이 두는것은 좋지 않다. 기능 변경 시 관련 없는 코드까지 수정해야 할 가능성이 커지며, 수정이 필요한 부분을 정확히 찾아내기 힘들어진다. 또한, 한 기능이 중복되고 반복되서 나타난다면, 별도로 분리해서 관리함으로써 재사용성을 높일 수 있다.
이러한 중복을 줄이고 모듈화하기 위해 AOP를 사용한다.
스프링의 AOP
스프링의 AOP는 메소드 앞에서 실행되는 프록시 기반의 구현체이다.
만약 메소드에 로깅하는 코드를 구현한다고 가정해보자. 메소드가 한두개면 문제 없겠지만, 메소드가 10개가 넘어가고 100개가 넘어간다면? 코드의 중복이 엄청나게 될것이다. 이러한 경우에 AOP를 활용할 수 있다.
현재 내 프로젝트의 로깅 AOP 코드이다.
@Aspect // AOP를 적용시키기 위해 필요한 어노테이션
@Component // Bean에 등록을 한다
public class LogAop {
// Pointcut : 내가 AOP를 어디다 걸고 싶은지
// 아래는 PostMethodLog 어노테이션이 달린 메소드에 AOP를 건다는 뜻
@Pointcut("@annotation(dev.flab.studytogether.aop.PostMethodLog)")
public void postMappingLog(){}
// Before : 메소드가 실행 되기 전
@Before("postMappingLog()")
public void logBeforePostMapping(JoinPoint joinPoint){
// 코드 생략
}
// AfterReturning : 메소드가 실행된 후
@AfterReturning(value = "postMappingLog()", returning = "returnObj")
public void logAfterPostMapping(JoinPoint joinPoint, Object returnObj){
// 코드 생략
}
}
POST API를 호출할때 요청, 응답 객체를 로깅하고 싶은데 이를 모든 API 호출 메서드에 넣게 되면 코드의 중복이 어마어마 해진다. 그렇기에 AOP로 설정하여 API 로깅을 한 곳에서 관리 할 수 있게 된다.
✔️ PSA(Portable Service Abstraction)
PSA란?
스프링 프레임워크에서 제공하는 추상화 계층으로, 다양한 서비스 제공자와 통합하기 위한 표준화된 방법을 제공한다.
PSA는 각종 서비스 인터페이스와 구현체 사이에 추상화 계층을 제공하여 개발자가 특정 서비스에 의존하지 않고 이를 교체하거나 다양한 환경에서 호환성 있게 사용할 수 있도록 도와준다.
JDBC
PSA의 주요 예시 중 하나로, 데이터베이스 제공자에 관계없이 일관된 방식으로 데이터베이스 액세스를 수행할 수 있도록 돕는다.
PSA는 스프링의 핵심 원칙 중 하나인 "의존성 역전 원칙 (Dependency Inversion Principle)"을 따르며, 코드의 결합을 줄이고 유지보수성을 향상시키는 데 도움을 준다.
PSA를 사용하면 스프링 애플리케이션을 더 유연하게 만들고 다양한 환경과 서비스와 통합하는 데 도움이 된다.
'Programming > Spring,JPA' 카테고리의 다른 글
[Spring] 예외 처리에만 국한되지 않는 @ControllerAdvice (0) | 2024.10.26 |
---|---|
[Spring] 단일 행이 반환될때는 queryForObject가 항상 정답일까? (0) | 2024.10.26 |
[Spring] 스프링에서 사용되는 디자인 패턴들 (0) | 2024.10.25 |
[JPA] 영속성 컨텍스트(Persistence Context) 내부 구조 살펴보기 (1) | 2024.10.25 |
[JPA] Hibernate 내부 동작을 알고 계신가요?(save 안티패턴 고찰) (3) | 2024.09.05 |