
GIF 출처 : https://sigridjin.medium.com/spring-transaction-관리에-대한-메모-f391fd2885b4
그림 자료 출처 : 김영한 - Spring DB 1 / 2 자료
1 ) 객체 지향 설계의 5가지 원칙
2 ) 객체 지향 설계 원칙에 따른 적용
3 ) 스프링 컨테이너
4 ) 스프링 빈 상속 관계
자바를 학습하는 사람이라면 한번 쯤 들어봤을 단어가 있을 것이다. 바로 다형성 ( Polymorphism ) 이다. 다형성은 객체지향 설계의 핵심원리중 하나로 코드 재사용과 인터페이스를 통한 클래스 구현이 스프링에서는 핵심적으로 사용되고 있다.
이뿐만 아니라 스프링에서는 객체 지향의 다양한 중요한 원리들을 기반으로 애플리케이션을 설계하는데 사용된다.
이러한 설계 원리를 정리한 5가지 원칙이 있는데 이를 ' SOLID 5원칙 ' 이라 한다.
1. SRP ( Single Responsibility Principle )
: 하나의 클래스는 하나의 책임만을 가져야한다.
-> 이는 클라이언트 객체는 직접 구현 객체를 생성 , 연결 , 실행하는 다양한 책임을 가지고 있는데 관심사를 분리하여 구현 객체를 생성하고 이에 대한 연결을 Config 를 통해 실행하는 것이다.
- DIP ( Dependency Inversion Principle )
: 프로그래머는 추상화에 의존해야지 , 구체화에 의존하면 안된다.
-> 이는 다형성의 관계에 대한 생각에서 시작한다. 우리가 특정 인터페이스에 의존하여 새로운 클래스를 형성하는 것을 다형성이라고 한다. 이때 ' 의존 ' 하게 되는 대상이 되는 클래스를 ' 추상화 ( 인터페이스 ) ' 라고 하며 의존한 클래스를 ' 구체화 ' 라고 한다.
이때 , 프로그래머는 Service에서 특정 구체화에 의존한 메소드를 작성하지 않아야 한다는 것이다.
- OCP ( Open - Closed Principle )
: 소프트웨어 요소는 확장에 열려있으며 변경에는 닫혀 있어야한다.
-> 이는 클라이언트가 DIP를 지킨 상황에서 Config 파일에서 의존관계를 바꾼다고 하더라도 클라이언트 코드를 바꿀 필요가 없는 환경이여야 한다는 것이다.
4.LSP (Liskov Substitution Principle )
: 하위 타입 객체는 상위 타입 객체에서 가능한 행위를 수행할 수 있어야 한다.
-> 즉, 상위 타입 객체를 하위 타입 객체로 대체하여도 정상적으로 동작 해야 한다는 것이다. 이는 빈의 상속관계와 관련 있는데 스프링 빈 ( Bean ) 관련 포스팅 참고!
- ISP ( Interface Segregation Principle )
: 클라이언트는 자신이 사용하는 메소드에만 의존해야한다. 이때 , 특정 클라이언트를 위해 인터페이스 여러개로 나뉜 분리된 형태로 존재해야한다.
-> 특정 목적을 위해 설계된 인터페이스 여러개로 나뉜 형태로 존재하는 것이 올바른 객체 지향 설계를 위한 방법이다.
그럼 객체 지향 설계 원칙에 맞게 새로운 애플리케이션을 설계해보자.

필자가 OrderService 라는 Service 클래스를 개발한다고 가정해보자. 그럼 Service 클래스를 인터페이스로 생성한 뒤 OrderServiceImpl을 사용하는 것은 자연스러울 것이다.
그 이후에 해당 Service 클래스는 어떠한 기능을 담은 서비스인지 아직 결정되지 않은 와중에 할인을 해주는 정책을 적용한 서비스로 결정했다고 가정해보자.
그럼 DiscountPolicy 라는 인터페이스 즉, ' 추상화' 에 의존하는 것은 DIP 원칙에 의해 옳은 의존이다. 이후 해당 인터페이스는 FixDiscountPolicy 와 RateDiscountPolicy 라는 두 가지 ' 구현체 ' 로 구현되었다고 가정해보자.
그럼 OrderServiceImpl은 다음과 같은 원칙을 통해 코드를 구성해야한다.
- 구현체인 FixDiscountPolicy , RateDiscountPolicy에 의존하지 말 것
- DiscountPolicy는 생성자를 통해서 의존성을 주입받을 것.
- 의존성 주입 ( DI ) 는 Config 파일을 통해서 이루어질 것.
코드를 작성하면 다음과 같다.
package hello.core.Member;
public class MemberServiceImpl implements MemberService{
private MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
해당 코드의 경우 철저히 자바 코드로만 이루어져 있으며 오직 인터페이스인 MemberService에만 의존하고 있다. 또한 memberRepository라는 클래스 또한 생성자를 통해서 철저히 의존성을 주입받도록 코드를 구성하고 있다.
다음은 MemberRepository 인터페이스를 의존하고 있는 MemoryMemberRepository 클래스의 코드이다. 이 또한 OrderServiceImpl과 마찬가지로 생성자를 통해서 의존성을 주입받도록 코드를 구성하고 있다.
package hello.core.Member;
import java.util.HashMap;
import java.util.Map;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long,Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(),member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
그렇다면 어떤 파일 혹은 클래스에 의해 의존성을 주입받고 있는가?
바로 Configuration 파일에 의해 의존성을 주입받고 있다. Configuration 파일을 통해 의존성을 주입해주어야 하는 이유는 무엇인가?
만약 Impl클래스나 다른 Serivce , Repository 클래스에서 다른 클래스들의 의존관계를 관리한다고 가정해보자. 그럼 해당 클래스를 제외한 나머지 클래스는 SOLID 5원칙을 잘 지키고 있을 것이다. 왜냐하면 해당 클래스 또한 서비스를 정의하는 클래스 이기 때문이다.
그렇기에 애플리케이션의 서비스 로직을 담거나 리포지토리의 기능을 담은 클래스가 아닌 의존성을 주입해줄 수 있는 파일이 별도로 존재해야한다.
package hello.core;
import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.Member.MemberRepository;
import hello.core.Member.MemberService;
import hello.core.Member.MemberServiceImpl;
import hello.core.Member.MemoryMemberRepository;
import hello.core.Order.OrderService;
import hello.core.Order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
해당 코드는 앞서 언급한 OrderImplservice , MemoryMemberRepository 클래스에 의존성을 주입해주는 Appconfig 파일의 코드이다. 이처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 ' IoC 컨테이너 ' 혹은 ' DI 컨테이너 ' 라 한다.
코드들을 보면 @Configuration , @Bean 이라는 어노테이션이 존재한다. 이는 스프링 빈과 연관이 있다.
앞서 AppConfig 파일을 통해 DI를 완수했다. 그러나 AppConfig를 통해서 직접 빈을 조회하는 방식은 AppConfig 내부를 직접 살펴보고 구조를 알아야한다.
그렇기 때문에 IoC 컨테이너와 마찬가지로 ' 스프링 컨테이너 ' 에 Config 파일을 담아 컨테이너 내부에서 빈을 조회할 수 있다. 
다음은 스프링 컨테이너의 구조를 보여주는 그림이다. 스프링 컨테이너에는 1. 빈 이름 2. 빈 객체의 정보를 담고 있으며 해당 저장소의 정보를 AppConfig.class를 통해 주입하여 채우게 된다.
이때 , 빈의 이름은 항상 다른 이름을 부여해야 하며 , 다른 빈이 무시되거나 기존 빈을 덮어버리게 되면 설정에 따라 오류가 발생한다.
package hello.core.beanfind;
import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAll(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean(){
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//Role ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
if(beanDefinition.getRole()==BeanDefinition.ROLE_APPLICATION){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + " object = " + bean);
}
}
}
}
해당 코드를 보면 ApplicationContext라는 인터페이스의 구현체인 AnnotationConfigApplicationConetext 클래스의 객체 ( 스프링 컨테이너 ) 를 통해 Config.class 를 주입받고 해당 컨테이너에 존재하는 모든 빈을 출력하는 애플리케이션 테스트이다.
- 스프링 빈 조회하기
스프링 컨테이너에서 스프링 빈을 찾는 기본적인 방법은 다음과 같다.
- ac.getBean ( 빈 이름 , 타입 )
- ac.getBean ( 타입 )
해당 조회 대상 빈이 없으면 예외가 발생한다.
NoSuchBeanDefinitionException: No bean named 'xxxxx' available
만약 동일 타입이 둘 이상 존재하는 경우 빈 이름 지정을 통해 중복을 막을 수 있다.
따라서 getBeandsOfType() 메소드를 통해 해당 타입의 모든 빈을 조회할 수 있기 때문에 타입을 조회한 뒤 빈 이름을 적어 넣는 1번 타입을 사용하면 된다.
스프링 빈의 경우 부모 타입을 조회하면 자식 타입 또한 함께 조회된다. 그래서 모든 자바 객체의 최고 부모인 ' Object ' 타입으로 조회하면 모든 스프링 빈을 조회하게 된다.
다음과 같은 관계 구조도를 가지고 있는 빈들의 관계가 있다고 가정해보자.

만약 2번을 조회하게 되면 2번의 자식 타입인 4 , 5 번 또한 조회되게 된다는 것이다. 이때 , 부모 타입을 조회할 때 둘 이상 존재하면 중복 오류가 발생하기 때문에 4 , 5 와 같이 다른 빈 이름으로 지정해주어야 한다.