26/07/06 IL(I Learned)

Spring 객체지향 설계와 의존성 주입

1) 계층 분리(Layered Architecture)

가장 먼저 도메인 객체(Book), 서비스(Service), 저장소(Repository)를 분리하여 객체지향적인 구조를 설계하는 방법을 학습하였다.

각 계층의 역할

계층역할
Book데이터를 표현하는 객체(Domain)
Repository데이터를 저장하고 조회하는 역할
Service프로그램의 비즈니스 로직 처리
Main프로그램 실행 및 서비스 호출

Service는 데이터를 직접 저장하지 않고 Repository에게 저장을 요청하며, Main은 Service만 호출하도록 역할이 분리되어 있다.

이를 통해 각 클래스가 하나의 책임만 가지도록 설계하는 것이 객체지향 설계의 기본이라는 것을 이해하였다.


2) Interface를 사용하는 이유

BookRepository는 저장 기능만 정의하고 실제 저장 방법은 구현하지 않는다.

BookRepository
        ▲
        │
MemoryBookRepository

이처럼 인터페이스를 사용하면 구현체를 자유롭게 변경할 수 있으며, Memory 저장 방식에서 Database 저장 방식으로 변경하더라도 Service는 수정하지 않아도 된다.

즉, 구현이 아닌 추상화에 의존하도록 설계하는 것이 객체지향 설계의 중요한 원칙임을 이해하였다.


3) Repository 구현

MemoryBookRepository에서는 HashMap을 이용하여 간단한 메모리 저장소를 구현하였다.

HashMap

1 → Book

2 → Book

3 → Book

save()는 HashMap의 put()을 이용하여 저장하고,

findBook()은 get()을 이용하여 조회한다.

실제 데이터베이스 대신 HashMap을 사용하지만 Repository의 역할 자체는 동일하다는 점을 이해하였다.


4) 생성자 주입(Dependency Injection)

Step2에서는 Service가 Repository를 직접 생성하지 않고 생성자를 통해 전달받도록 변경하였다.

기존 방식

private BookRepository repository = new MemoryBookRepository();

변경 후

public BookServiceImpl(BookRepository repository)

이 구조에서는 BookServiceImpl이 어떤 Repository를 사용할지 알 필요가 없으며, 외부에서 필요한 구현체를 전달받기만 한다.

이를 생성자 주입(Constructor Injection) 이라고 하며, 객체 간의 결합도를 낮추는 중요한 방법이라는 것을 학습하였다.


5) Spring Bean과 ApplicationContext

Spring에서는 객체를 직접 생성하지 않고 Spring Container가 생성 및 관리한다.

ApplicationContext를 생성하면 Spring은 설정 클래스를 읽고 Bean을 생성한다.

ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);

이후에는

BookService service =
context.getBean(BookService.class);

처럼 필요한 객체를 Spring Container에게 요청하여 사용할 수 있다.

이를 통해 객체 생성 책임이 개발자에서 Spring으로 이동한다는 점을 이해하였다.


6) @Configuration과 @Bean

AppConfig 클래스에서는 Spring에게 어떤 객체를 Bean으로 등록할 것인지 알려준다.

@Configuration

은 설정 클래스라는 의미이며,

@Bean

은 Spring이 관리할 객체를 등록하는 역할을 한다.

BookRepository Bean을 생성한 뒤,

BookService Bean 생성 시 Repository를 전달하여 객체 간의 의존성을 연결하는 과정을 확인하였다.


7) @Component와 Component Scan

Step3부터는 @Bean 대신 @Component를 이용한 자동 Bean 등록 방식을 학습하였다.

@Component

가 붙은 클래스를

@ComponentScan

이 자동으로 탐색하여 Bean으로 등록한다.

즉,

@Component

↓

Component Scan

↓

Spring Bean 등록

이라는 흐름으로 동작한다는 것을 이해하였다.

실무에서는 @ComponentScan을 사용하는 방식이 가장 일반적이다.


8) Bean Scope

Bean이 몇 개 생성되는지를 결정하는 Scope에 대해서도 학습하였다.

Singleton

Spring의 기본 Scope이며 객체를 하나만 생성한다.

getBean()

↓

같은 객체 반환

따라서

bean1 == bean2

는 true가 된다.


Prototype

Prototype Scope는 getBean()을 호출할 때마다 새로운 객체를 생성한다.

getBean()

↓

새 객체 생성

따라서

bean1 == bean2

는 false가 된다.

Singleton은 프로그램 전체에서 하나의 객체를 공유하며, Prototype은 독립적인 객체가 필요할 때 사용된다는 차이를 이해하였다.


9) Bean 생명주기(Lifecycle)

Bean이 생성되고 소멸되는 과정도 학습하였다.

객체 생성

↓

@PostConstruct

↓

객체 사용

↓

@PreDestroy

Singleton Bean은 생성과 소멸 모두 Spring이 관리하지만,

Prototype Bean은 생성만 Spring이 담당하며 소멸은 직접 관리해야 한다는 차이를 알게 되었다.


10) 의존성 주입 방식 비교

Spring에서 사용하는 세 가지 의존성 주입 방식을 비교하였다.

방식특징
생성자 주입가장 권장되는 방식
필드 주입테스트 어려움, final 사용 불가
Setter 주입선택적인 의존성에 적합

생성자 주입은 객체 생성 시 반드시 필요한 의존성이 모두 주입되므로 가장 안전한 방식이라는 점을 이해하였다.

또한 Lombok의

@RequiredArgsConstructor

를 사용하면 생성자를 직접 작성하지 않아도 final 필드를 포함한 생성자가 자동 생성된다는 것도 학습하였다.


11) 순환 참조(Circular Dependency)

객체가 서로를 참조하는 순환 참조 문제도 실습하였다.

A

↓

B

↓

A

생성자 주입에서는 Spring이 Bean 생성 단계에서 순환 참조를 감지하여 예외를 발생시키기 때문에 문제를 빠르게 발견할 수 있다.

반면 필드 주입이나 Setter 주입에서는 객체 생성은 가능하지만, 서로의 메서드를 계속 호출하여 StackOverflowError가 발생할 수 있다는 점을 확인하였다.

이를 통해 생성자 주입이 권장되는 또 하나의 이유를 이해하였다.


직면한 문제

학습 초기에는 Spring이 객체를 어떻게 생성하는지, Bean과 일반 객체의 차이가 무엇인지 명확하게 이해되지 않았다.

의존성 주입 역시 단순히 "객체를 넣어준다" 정도로만 이해하고 있었기 때문에 생성자 주입과 필드 주입의 차이가 잘 와닿지 않았다.

특히 순환 참조 예제에서는 생성자 주입에서는 애플리케이션 시작 단계에서 예외가 발생하는 반면, 필드 주입에서는 실행 중 StackOverflowError가 발생하는 이유를 이해하는 데 어려움을 겪었다.


해결을 위해 시도한 방법

Main → Service → Repository → HashMap으로 이어지는 호출 흐름을 단계별로 추적하면서 객체가 생성되는 순서와 메서드가 호출되는 과정을 분석하였다.

Step1부터 Step4까지 변경된 부분만 비교하면서 왜 코드가 수정되었는지 확인하였고,

특히 생성자 주입, 필드 주입, Setter 주입을 각각 비교하여 장단점을 정리하였다.

Singleton과 Prototype은 getBean()을 여러 번 호출했을 때 반환되는 객체를 비교하면서 Scope의 차이를 확인하였다.

마지막으로 순환 참조 예제를 통해 생성자 주입과 필드 주입에서 발생하는 문제를 직접 확인하면서 Spring이 생성자 주입을 권장하는 이유를 이해할 수 있었다.


느낀 점

이번 학습을 통해 Spring은 단순히 객체를 대신 생성해주는 프레임워크가 아니라, 객체 간의 관계를 관리하고 의존성을 연결해주는 컨테이너라는 것을 이해하게 되었다.

또한 객체지향 설계에서는 구현보다 추상화에 의존하도록 설계하는 것이 유지보수성과 확장성을 높인다는 점도 알게 되었다.

특히 생성자 주입이 단순히 권장되는 코딩 스타일이 아니라 테스트 용이성, 객체 불변성 보장, 순환 참조 방지 등 다양한 장점을 가진 설계 방식이라는 것을 실습을 통해 체감할 수 있었다.

profile
예 마 함 가보입시더

0개의 댓글