일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 인프런
- 자료구조
- 카카오코테
- SQL
- 자바
- 정처기
- Til
- CS
- Kotlin
- doitandroid
- MySQL
- 오블완
- 혼공파
- 기술면접
- 티스토리챌린지
- 정보처리기사
- Android
- 안드로이드스튜디오
- 안드로이드
- 코테
- 코틀린
- 프로그래머스
- groupby
- join
- 혼공단
- 알고리즘
- 혼공챌린지
- java
- 스터디
- select
- Today
- Total
Welcome! Everything is fine.
[STUDY] Spring IoC/DI와 객체지향 설계 원칙 본문
⭐ 객체지향 설계가 중요한 이유
객체 지향 프로그래밍은 객체를 중심으로 시스템을 구성하는 방식이다. 현실 세계의 사물이나 사건을 하나의 객체로 보고, 이런 객체들 간의 상호작용을 중심으로 프로그래밍한다. 복잡한 시스템일수록 기능을 분리하고, 역할을 명확히 나눈 구조가 필요하다. 객체지향은 이러한 확장성과 유지보수성을 높이기 위한 핵심 설계 방식이다. Spring은 객체지향 설계를 전제로 만들어진 프레임워크로, 특히 IoC와 DI는 객체 간의 관계를 유연하게 구성하고 관리하는 데 중요한 역할을 한다.
💡 OOP 특징과 설계 원칙
객체 지향의 4가지 핵심 개념
추상화( Abstraction )
불필요한 세부 사항을 감추고 핵심적인 동작이나 속성만 드러내는 것
- List 인터페이스를 사용할 때 내부 구조는 몰라도 add()나 get() 같은 기능만 사용하면 된다.
캡슐화( Encapsulation )
객체의 내부 상태를 감추고, 외부에서는 정해진 방식으로만 접근하게 하는 것
- 필드를 private으로 감추고 getter/setter를 사용하는 방식으로 캡슐화할 수 있다.
- 추상화와 캡슐화는 모두 '정보 은닉'을 기반으로 하지만, 추상화는 '무엇을 제공할지', 캡슐화는 '무엇을 감출지'에 초점을 둔다.
상속( Inheritance )
기존 클래스를 확장하여 새로운 클래스를 정의하는 것
- 공통 기능을 부모 클래스에 정의해두고, 자식 클래스는 이를 물려받아 재사용할 수 있다.
다형성( Polymorphism )
한 객체가 여러 타입의 객체로 취급될 수 있는 능력
- 다형성을 활용하면 중복을 제거하고, 변경사항이 생겨도 코드를 크게 고치지 않고 재사용할 수 있다.
SOLID 원칙
SRP ( 단일 책임 원칙 )
하나의 클래스는 하나의 책임만 가져야 한다.
OCP ( 개방-폐쇄 원칙 )
확장에는 열려 있고, 변경에는 닫혀 있어야 한다.
LSP ( 리스코프 치환 원칙 )
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
ISP ( 인터페이스 분리 원칙 )
클라이언트에 맞는 인터페이스를 분리해 제공해야 한다.
DIP ( 의존 역전 원칙 )
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다.
🧐 IoC와 DI 이해하기
제어의 역전( Inversion of Control, IoC)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것.
- 객체 생성이나 의존성 관리 같은 책임을 프레임워크가 대신 담당하는 개념이다.
🗣️ IoC란 무엇인가요? 왜 필요한가요?
IoC는 제어의 역전(Inversion of Control)을 뜻하며, 객체 생성이나 의존성 관리 같은 책임을 개발자가 아니라 프레임워크가 대신 담당하는 개념입니다. Spring에서는 IoC 컨테이너가 객체를 생성하고, 필요한 의존성을 주입해줍니다. 이로 인해 객체 간 결합도가 낮아지고, 테스트나 유지보수가 용이해집니다.
의존성 주입( Dependency Injection, DI)
객체 간의 의존 관계를 외부에서 주입받는 방식
- Spring에서는 생성자, 필드, 세터 주입 방식이 있다.
- DI를 통해 구현체가 바뀌어도 클라이언트 코드는 변경되지 않아, 결합도를 낮추고 테스트가 쉬워진다.
🗣️ DI(의존성 주입)는 왜 필요한가요? 직접 new로 만들면 안 되나요?
직접 new로 객체를 생성하면 클래스 내부에서 구현체에 강하게 결합됩니다. 반면 DI는 외부에서 의존성을 주입받아 추상화에 의존하게 만듭니다. 이를 통해 DIP(의존 역전 원칙)를 지킬 수 있고, 구현체 교체나 테스트가 유연해집니다. 예를 들어, 실제 서비스 대신 Mock 객체를 주입해 테스트할 수 있습니다.
아래 코드는 <스프링 핵심 원리 - 기본편>에 나오는 예시 중 일부인데, 직접 구현체에 의존하는 구조여서 잘못된 예시로 가져왔다.
OrderServiceImpl이 discountPolish 인터페이스 뿐만 아니라 RateDiscountPolish인 구체 클래스도 함께 의존하고있다. (DIP 위반) 따라서 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다. (OCP 위반)
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolish discountPolish = new RateDiscountPolish();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolish.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
예시에서는 AppConfig 클래스를 통해 애플리케이션에 필요한 구현 객체를 생성하고, 생성자를 통해 의존성을 주입했다.
이처럼 객체를 생성하고 의존 관계를 연결하는 구성을 IoC 컨테이너 또는 DI 컨테이너라고 한다.
AppConfig는 구현체 변경 시 해당 설정만 수정하면 되므로, 클라이언트 코드의 변경 없이 유연하게 확장할 수 있다.
다만, 이는 개념 설명을 위한 수동 설정 예시이며, 실제 Spring Boot 환경에서는 @ComponentScan과 생성자 주입(@Autowired 또는 Lombok)을 통해 자동 주입이 이뤄지므로 AppConfig를 직접 작성할 필요는 없다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public static MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolish());
}
@Bean
public DiscountPolish discountPolish() {
return new RateDiscountPolish();
}
}
OrderServiceImpl은 추상화에만 의존하도록 변경되었고, 이를 통해 DIP와 OCP 원칙을 만족하며, DI와 IoC 구조도 함께 실현된다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolish discountPolish;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolish discountPolish) {
this.memberRepository = memberRepository;
this.discountPolish = discountPolish;
}
// ...
}