[스프링 핵심 원리]

이도훈·2022년 2월 17일
0

Spring

목록 보기
6/6

1. 객체 지향 설계와 스프링


1-1. 이야기 - 자바 진영의 추운 겨울과 스프링의 탄생

1) EJB(J2EE) -> 하이버네이트 -> JPA

2) JPA구현체들(하이버네이트, EclipseLink, 기타..) => JPA(표준 인터페이스)로

3) 스프링 역사


1-2. 스프링

* 스프링 부트

  • 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용.
  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성.
  • TOMCAT 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨.
  • 손쉬운 빌드 구성을 위한 starter 종속성 제공.
  • 스프링과 3rd parth(외부) 라이브러리 자동 구성.
  • 매트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능 제공.
  • 관례에 의한 간결한 설정.

* 스프링을 왜 만들었을까?

  • 스프링은 자바 언어 기반의 프레임워크
  • 자바 언어의 가장 큰 특징 - 객체 지향 언어
  • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
    ( 스프링의 탄생 배경을 이해해야함. 과거에는 EJB에 매우 종속적으로 JAVA 개발을 진행해야 했기에, 이후 POJO 등 순수한 과거의 자바 스타일로 돌아가자는 말이 있었다.)
    =>
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

1-3. 좋은 객체 지향 프로그램이란?

  • 객체들의 모임 파악, 메시지와 데이터를 request, response

* 유연하고 변경이 용이하다.

  • interface( ex 자동차 역할) vs class( ex k3, 아반떼 등등 자동차 구현)
    : 클라이언트(운전자)는 자동차 구현이 달라지더라도 영향을 받지 않는다.
    => 클라이언트에 영향을 주지 않고, 구현은 무제한적으로 늘어날 수 있다.
    => 다형성 : 유연하고 변경이 용이하다.

* 정리

  • 유연하고 변경이 용이
  • 확장 가능한 설계
  • 클라이언트에 영향을 주지 않는 변경 가능
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요
  • 주의 : 인터페이스 자체가 변하면, 클라이언트, 서버 모두에 큰 영향을 줄 수 있다.

1-4 좋은 객체 지향 설계 5가지 원칙 (SOLID)

* SRP 단일 책임 원칙

Single Responsibility Principle

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는 것은 모호하다.
    (클 수 있고 작을 수 있다. 문맥과 상황에 따라 다르다.)
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따르는 것.

* OCP 개방-폐쇄 원칙

open/closed principle 중요

  • 소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다. => ?
  • 다형성을 활용
    => 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현
    => 즉, 인터페이스나 해당 인터페이스를 통해 구현된 클래스들의 변경 없이, 새로운 구현 클래스에 대한 확장이 가능하다.

문제점

  • MemberService 클라이언트가 구현 클래스를 변경을 해야 한다.
    MemberRepository m = new MemoryMemberRepository(); // 기존 코드
    MemberRepository m = new JdbcMemberRepository(); // 변경 코드
  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
  • 다형성을 활용했지만 ocp 원칙을 지킬 수 없다.
    => 해결책 : 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.
    (컨테이너, di, loc가 이 문제 때문에 필요하다.

* LSP 리스코프 치환 원칙

Liskov Substitution Principle

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
    (ex) 자동차 인터페이스의 엑셀은 앞으로 가는 기능을 가져야 한다. 즉, 뒤로 가는 기능으로 구현하면 해당 원칙을 어기는 것.

* ISP 인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
    사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

* DIP 의존관계 역전 원칙

Dependency Inversion Principle

  • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
  • 쉽게 이야기해서 클라이언트가 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻. 구현보다는 역할에 의존? 신경써라!!
    (의존한다는 뜻 : 해당 코드에 대해서 인지하고 있는 상태다.)

EX) 문제 상황
MemberService는 memberRepository (인터페이스) 뿐만 아니라, JdbcMemberRepository (클래스) 도 인지하는 상태이다.
=> DIP를 위반한 상태
=> 어떻게 해결해야 하나?



<정리>

  • 객체 지향의 핵심은 다형성
  • 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
  • 다형성 만으로는 OCP, DOP를 지킬 수 없다.
  • 뭔가 더 필요하다.

1.5 객체 지향 설계와 스프링

  • 정리 (실무 고민)
    : 하지만 인터페이스를 도입하면 추상화라는 비용이 발생한다.
    기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩토링해서 인터페이스를 도입하는 것도 방법이다. (이는 개발자, 아키텍터의 역량이 중요!!)

.bean 등록

* bean 자바 등록과 xml 등록 차이

  • bean 등록에 대한 테스트 (자바 vs xml)
    public class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
	// 자바를 통한 bean 등록
    GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
    // xml를 통한 bean 등록
    
    @Test
    @DisplayName("빈 설정 메타 정보 확인")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("beanDefinitionNAME = " + beanDefinitionName +
                        " beandefinition" + beanDefinition);
            }
        }
    }
}
  • 자바
    -- appconfig 라는 factoryBean을 사용해 중간에 우회하는 과정을 통해 bean을 등록
    -- class명에서 직접적으로 무슨 bean을 등록했는지 확인 불가. (class [null];)
    -- factoryMethod를 확인해 알 수 있음. (factoryMethodName=memberService)
beanDefinitionNAME = appConfig beandefinitionGeneric bean: class [hello.core.AppConfig$$EnhancerBySpringCGLIB$$ae35904e]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null
beanDefinitionNAME = memberService beandefinitionRoot bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=memberService; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionNAME = memberRepository beandefinitionRoot bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=memberRepository; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionNAME = orderService beandefinitionRoot bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=orderService; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionNAME = discountPolicy beandefinitionRoot bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=discountPolicy; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
  • xml
    -- appConfig.xml을 사용해 직접적으로 bean을 등록.
    그래서 class명에서 직접적으로 무슨 bean을 등록했는지 확인 가능하다.
    (class [hello.core.order.OrderServiceImpl];)
beanDefinitionNAME = memberService beandefinitionGeneric bean: class [hello.core.member.MemberServiceImpl]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [appConfig.xml]
beanDefinitionNAME = orderService beandefinitionGeneric bean: class [hello.core.order.OrderServiceImpl]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [appConfig.xml]
beanDefinitionNAME = memberRepository beandefinitionGeneric bean: class [hello.core.member.MemoryMemberRepository]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [appConfig.xml]
beanDefinitionNAME = discountPolicy beandefinitionGeneric bean: class [hello.core.discount.RateDiscountPolicy]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [appConfig.xml]

* 싱글톤 방식은 Stateful 방식이 아닌 stateless 방식으로 구현해야 한다.

  1. stateful 방식(BAD)
public class orderService {
	private int price;
    
    // price의 상태가 유지되늰 stateful 방식 구현
    public void getPrice(String name, int price) {
    	this.price = price;
    }
    
    public int getPrice() {
    	return price;
    }
}
----------------------------------------------------------------
public class orderServiceTest {
	int order1Price = statefulService1.order("A", 10000);
    int order2Price = statefulService2.order("B", 20000);
	int price = statefulService1.getPrice();
	System.out.println("price = " + price);
    // price = 20000
}
  1. stateless 방식 (GOOD)
public class orderService {
	private int price;
    
    // stateless 방식 구현
    public int getPrice(String name, int price) {
    	return price;
    }
    
    public int getPrice() {
    	return price;
    }
}
----------------------------------------------------------------
public class orderServiceTest {
	int order1Price = statefulService1.order("A", 10000);
    int order2Price = statefulService2.order("B", 20000);
	int price = statefulService1.getPrice();
	System.out.println("price = " + price);
    // price = 10000
}

* 스프링 프레임워크에서는 모든 객체에 대한 싱글톤을 유지해준다. (cf. @Configuration 필요)

@Test
    void configurationDeep() {
        ApplicationContext ac= new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean);
        //bean = hello.core.AppConfig$$EnhancerBySpringCGLIB$$e68e7158@710b18a6
        // bean = ??? 클래스맞음?
    }
  • 스프링은 cglib라는 바이트코드 조작 라이브러리를 통해 한 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한다.

  • 클래스#cglib는 스프링 컨테이너에 객체가 없다면 기존 로직과 동일하게 new 를 통해 스프링 컨테이너 등록하지만 객체가 있다면 스프링 컨테이너에서 찾아서 반환함.

  • AppConfig 내 @Configuration 없고 @Bean만 존재한다면 스프링 빈 설정은 되지만 싱글톤 자동 구현이 되지 않는다.
    (마치 싱글톤을 구현하지 않은 자바 소스와 동일하다..)

  • 그냥 설정 정보 있는 곳에는 @Configuration을 필수적으로 사용하자.


* bean 등록과 의존관계 주입

  • 자동으로 bean 등록 및 DI 설정하기
    : AppConfig에서 bean 설정을 따로 구성하지 않고 어노테이션을 활용해 자동으로 빈을 주입하고 의존관계를 형성할 수 있다.
  1. @Component(해당 클래스) : config 설정없이 자동으로 bean 등록해준다.
  2. @Autowired(생성자 메서드) : (Spring bean 안에서 인자의 Type과 동일한 클래스를 찾아) 자동으로 의존관계를 주입해준다.
    (== ac.getBean(클래스이름.class))
  3. spring bean에 등록된 클래스를 출력해주는 @ComponentScan 을 활용해 빈 등록 여부와 의존관계 형성 여부를 확인할 수 있음.

  • bean 등록 중복 (동일 타입, 동일한 이름으로)
@Component("빈 등록 이름")
public class CarServiceImpl implements CarService {
	~~~
}
// name이 설정되어있지 않다면 클래스명의 맨 앞 글자만 소문자로 바뀐 이름이 빈에 등록된다.
// (ex) carServiceImpl
  1. 자동 bean 등록 vs 자동 bean 등록 시
    : BeanDefinitionStoreException 발생
@Component("service")
public class serviceA {}
---------
@Compoent("service")
public class serviceB {}

- result 
: BeanDefinitionStoreException: Failed to parse configuration class [hello.core.AutoAppConfig]
  1. 자동 bean 등록 vs 수동 bean 등록 시
    : 수동 빈 등록이 우선권을 가진다.
// 자동 bean 등록
@Component("service")
public class serviceA {} 
---
// 수동 bean 등록
@Configuration
public class AppConfig {
	@Bean(name = "service")
    public ServiceA serviceA() {
    	return new xxxServiceA();
    }
}
- result : 문제 없음
- 스프링 부트로 사용 시, 자동 빈 등록과 수동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다. 
-> 빈등록 충돌에 대해 정확히 인지하지 못할 가능성이 크고 추후에 더 큰 문제 발생의 가능성이 있기 때문.

  • 의존관계 주입
    : 의존관계를 주입하는 방식에는 크게 4가지 방식이 있다.
    - 생성자 주입
    - 수정자 주입(setter 주입)
    - 필드 주입
    - 일반 메서드 주입
  1. 생성자 주입 (자주 사용)
    : 빈을 등록하면서 의존관계 주입이 함께 일어난다.
    (빈을 등록할 때 해당 클래스의 생성자를 호출하기 때문에 동시에 DI 주입까지 일어남)
    • 생성자 호출 시 딱 1번만 호출되는 것이 보장된다.
    • '불변', '필수' 의존관계에 사용
	"불변"
    set method를 통해 생성자를 변하게 만들지 않는다.
    "필수"
    // final 로 설정되어 있으므로 생성자 값이 1개 명시되어야 함을 의미
    private "final" MemberRepository memberRepository;
    @Autowired
    public AServiceImpl(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
  1. 수정자 주입
    : set method(@Autowired) 를 통해 의존관계 주입
    • 선택적으로 의존관계 주입
    • 변경 가능성이 있는 의존관계에 사용
  2. 필드 주입
    : 필드 인젝션은 권장하지 않는다고 intellj에 명시됨
    • 문제점
      : ex) 이후 순수한 자바코드 테스트를 하고 싶을 때, 각 service에서 활용할 dao, repository에 대한 구현객체를 넣을 수 있는 방법이 없다.
      -> 결국 set method를 통해 바꿔야 한다. (이런 경우 수정자 주입을 하는게 낫다.)

  • 동일한 자바 빈이 2개 이상 설정되어 있는 경우
    : 한 서비스 클래스에서 스프링 빈 안의 구현체들을 사용하고자 할 때, 동일한 인터페이스로 만들어진 구현체 클래스들은 동일한 빈 이름을 가지므로 exception이 발생하게 된다.
    (동일한 이름을 가진 두 개의 자바 빈을 만들려고 하기 때문에, 스프링 컨테이너는 이를 거부하고 exception을 발생시킴.)

    => 해결방법 ? - 3가지 존재

  1. @Autowired 는 TYPE에 의한 DI를 수행한다. 하지만 위의 문제 상황과 같이 동일한 type의 빈이 2개 이상 있을 경우, type에 의한 di를 수행하고 파리미터명에 의해서도 수행하기 때문에 생성자 인자 값을 구현체명으로 만들면 의도한대로 DI를 수행할 수 있다.

  2. @Quilifier 사용

    1) @Quilifier끼리 매칭 ex) @Quilifier("A_Repository") -> A_Repository type의 클래스 DI
    2) 빈 이름 매칭, A_Repository 라는 이름을 갖는 클래스 DI
    3) 2)도 실패하면 NoSuchBeanDefinitionException 발생

  3. @Primary 사용
    : 동일한 타입의 대상에 대해 @Autowired가 있을 경우, @Primary로 우선순위를 확인해 DI 가능
profile
back-end developer

0개의 댓글