Home 인프런 스프링 Spring 핵심 원리 (기본)
Post
Cancel

인프런 스프링 Spring 핵심 원리 (기본)

이 게시물은 김영한 - 스프링 핵심 원리 요약임
모든 코드는 OsoriAndOmori/playground-spring 에 올려둠.
멋대로 만든 프로젝트 구조 이기 떄문에, 강좌 내용은 따라가나 구조는 절대 따라가지 않음..

객체지향 설계와 스프링

  • 스프링을 알기위해선 그 과거를 알아야한다. 그건 ejb.
    • 지옥같은 ejb 의존성. 굳이 찾아서 볼 필요는 없을것 같다.
    • 여기서 탈피하자면서 POJO 라는 개념이 나옴.. 플레인 자바 오브젝트.
  • 스프링은 객체지향을 잘 개발할 수 있게 도와주는 도구인데, 그럼 좋은 객체지향은 뭐냐 대체
  • 좋은 객체지향 설계 원칙 : SOLID 로버트 마틴 아저씨
    • 5가지 원칙이 있다. 그 중 OCP 개방폐쇄(다형성으로 극복), DIP 의존관계(인터페이스에 의존해라) 역전 원칙
    • 결국 좋은 객체지향 설계는 역할(인터페이스)구현(클래스) 에서 역할에 초점을 맞춰야한다. 다형성.
    • 쓰는 클라이언트는 역할(인터페이스)만 보고 개발을 하면 되는것. 구현은 갈아낄 수 있다. (자동차, 테슬라, 현다이)
    • 이 설계의 한계는 역할 잘못 설계하거나 변경해야하면 그냥 개망한다는거 ㅋㅋ java default 꼴 난다.
    • 그런데 스프링이 없을떄 구현 클래스 내 아무리 원칙 잘 지키려고해도 MemberRepository m = new MemoryMemberRepository() 이걸 쓸수 밖에 없음. => DIP 위반
    • 그럼 DIP 를 지키려면 결국 외부에서 저 필드에 주입을 해주는 방법밖에 없음. => 그래서 나온게 스프링 컨테이너이다. 결국 모든 사람은 SOLID 를 지키려면 여기에 도달할 수 밖에 없다.
  • 결국 이상적으로는 모두~ 인터페이스로 먼저 만들어두면, 하부 구현 기술 선택을 최대한 미룰 수 있다. 갈아끼기도 좋다 => 유지보수 좋음
    • 근데 진짜 실무에서 다 구현하기엔 추상화라는 비용이 발생.. 너무 개짜증남. 기능을 확장할 생각이 없으면 구체 클래스, 리펙터링해서 인터페이스 도입해도 될 듯.

맨 땅 자바코드로 스프링 컨테이너 만들어보기 시작

image

  • 순수 자바와 junit 만으로 회원, 주문 시스템으로 개발해보자
  • 멤버, 멤버Service, 멤버Repo, 주문Service, 할인 정책 만들면 끝. interface(역할)과 class(구현) 해서 설계해놓고 하나씩 구현해보자.
  • 다 만들어 놨더니, 할인 정책을 아예 바꾸고 싶다고 나옴 -> 후후 객체지향설계를 잘 해놨지 Policy 만 Service 에 바꿔끼는데 => 이게 문제가 발생함.
    • 클라이언트의 코드를 수정했음… OCP, DIP 다 어겼다.
    • 이 문제를 어떻게 해결할 수 있을까?
      • 누군가가 interface 의 구현체를 생성하고 주입 을 해줘야한다.

관심사의 분리

  • 공연이 있다면, 역할(interface) 가 있고, 배역(class) 이 있는데 공연에서 배우는 누가 데려오는가?
    • 전체 공연 기획자가 하는게 맞다 -> 벗 기존코드는 로미오가 줄리엣을 직접 섭외 해 온 것과 같다.
    • 공연 기획자가 필요하다. 로미오가 공연도하고, 섭외도해야하는 사람이 되었다.
      • 관심사의 분리가 필요하다. 자기 배역에만 충실해야한다.
    • AppConfig.java 에서 만들어서 한 곳에서 만들고 주입을 한다. => 여기서 생성자 주입(DI)
    • MemberService 입장에선 구현클래스 생각없이, 인터페이스가 저장하랬어하고 저장하면되는거야
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public class AppConfig {
        public MemberService memberService () {
            return new MemberServiceImpl(memberRepository());
        }
    
        public OrderService orderService () {
            return new OrderServiceImpl(memberRepository(), discountPolicy());
        }
    
        public MemberRepository memberRepository () {
            return new MemoryMemberRepository();
        }
    
        public DiscountPolicy discountPolicy(){
            return new FixDiscountPolicy();
        }
    }
    
    • 여기서 FixDiscountPolicy -> RateDiscountPolicy 로 변경하면 => 아주 간단하게 정책 변경 됨. 전혀 클라이언트 코드를 변경하지 않음. 구성 변경은 해야지 당연히.
    • 구성 영역만 변경을 하면, 실행영역은 전혀 건드리지 않게 된다. OCP 와 DIP 를 만족하는 코드 완성.
    • 의존성 주입을 사용하면, 정적인 클래스 의존관계(import)를 변경하지 않고 동적인 클래스 의존 관계를 변경 할 수 있다.
      • 결국 AppConfig 가 IoC 컨테이너, DI 컨테이너 임.

스프링 컨테이너와 스프링 빈

  • 빈 전체 생성 -> 빈 의존성 연결 하는 프로세스를 거친다.
  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다. 그런데 자바코드를 보면 생성하면서 의존관계를 만들수밖에 없지않나 싶은데? 좀 나중에 더 설명 예정
  • 빈 가져오기 getBean, getBeanType
    • 부모 타입으로 조회시, 자식들 다 나옴. 당연히 여러개 있으면 NoBeanUniqueException…
    • 빈 설정은 리턴을 interface 로 하는게 유연하니간 참고.
  • BeanFactory 와 ApplicationContext = 스프링컨테이너 = 스프링의 핵심
    • 인터페이스로 기능을 파악하는 개발자가 되어보자..
    • image
    • AnnotationConfigApplicationContext 기준으로 내부적으로 BeanDefinitionReader 가 있음.
      • 실제 자바 코드 읽어서 BeanDefinition 이라는 메타정보를 만들어 저장. 직접 BeanDefinition 구현해서 등록도 가능한데, 그렇게 하진않겠지…

싱글톤

  • private 생성자로 직접 로직추가해서 싱글톤 구현 가능.
  • 싱글톤은 대신 단점이 많다.
    • 보일러플레이트 코드가 많아진다. (클래스별로 getInstance 다 추가)
    • OCP DIP 위반. 클라이언트 코드가 구현클래스에 의존하게 됨.
  • 스프링컨테이너는 객체 인스턴스를 미리 만들어서 집어넣어둠. 어떻게 위 단점을 회피했는지는 안알려주고, 뭐 대충 얘가 해주니 우리가 OCP 를 위반할일은 없다는 느낌인 듯.
  • 주의점
    • ㅋㅋ 당연한거지만 빈에다 멤버변수 만들고 동적으로 바꾸는건 절대 안 됨. Stateless 하게 만들어야함, 읽기만 하는 용도로 만들어야함.
  • @Configuration 과 싱글톤
    • @Configuration 내부 보면 memberService 만들때 한번 new memberRepository 하면서 생성하고 orderService 만들 때 또 memberRepository 생성함. 그럼 싱글톤 꺠지는거 아님?
      • CGLIB 로 기존 클래스를 상속하는 class 를 만들어 메서드 override 만들어서 이를 해결함.
      • 바이트 코드 조작으로 아마 이렇게 바꿔 줄것으로 예상.
        1
        2
        3
        4
        5
        
        @Bean
        public MemberRepository memberRepository () {
           if(스프링 컨테이너에 있으면) return 스프링컨테이너.getbean(memberRepository)
           else return new MemoryMemberRepository();
        }
        
      • @Confiruation 안붙인 설정파일이면, Bean 만들긴 하는데 CGLIB 안써서 만들기 떄문에, 싱글톤이 아니게 됨. 스프링 컨테이너가 관리하는 애도 아니게 됨. 그냥 붙여라.

컴포넌트 스캔

  • 아는거니깐 패스 @ComponentScan, @Service, @Repo, @Controller 관련 이야기

의존관계 자동 주입

  • 생성자 주입을 사용하자
    • 테스트할 때 의존성 명확함 -> 많으면 구조 분리의 신호
    • 불변!
  • @Annotation 을 만들어서 @Qualifier 품으면 좀더 명확하게 쓸 수 있음 (스프링에서 제공하는기능임. 상위 읽어서 쓰는)
    1
    2
    3
    4
    5
    6
    7
    
     @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
    ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Qualifier("mainDiscountPolicy")
    public @interface MainDiscountPolicy {
    }
    
  • 후보 빈이 많을 경우 list 도 map 도 다 받을 수 있음. map 으로 받아서 써야겠음 다음부터
  • 수동(@Bean) 자동(@Component) 등록빈 언제 하는게 좋을까
    • 그냥 느낌대로해 답은 없다.
    • 비즈니스 -> 자동등록
      • 다형성을 지원하는 비즈니스 객체는 수동등록을 고려해보자. (list map 으로 받을것들)
    • 기술지원로직 -> 수동등록이 일반적
      • 근데 스프링부트가 지원 잘하는 Datasource 같은건 그냥 자동등록쓰자
      • 내가 직접 만드는 기술지원 로직은 수동하자

빈 생명주기 콜백

  • 스프링 빈은 간단하게 아래와 같은 라이프사이클을 갖는다.
    • 기본적으론 객체 생성 -> 의존관계 주입인데
    • 생성자 주입의 경우 생성하면서 바로 의존관계 설정해줌.
  • 빈 초기화라는 말은 결국 빈 생성만 한걸 말하진않음 -> 의존관계 설정까지 다하고 callback method 를 줘서 다 만들고 초기화할 수 있게 해줌.
    • 스프링 컨테이너가 종료되기 전에 소멸 콜백을 줄 수도 있음.

객체 생성과 초기화를 분리하자 생성 = 진짜 클래스 그냥 만드는거, 초기화 = 네트워크 연결 및 무거운 로직 단일 책임의 원칙!

  • 스프링 생명주기 콜백
    • 인터페이스 InitializingBean, DisposableBean
      • 스프링 전용 인터페이스임. annotation 정도면 모르겠는데 코드레벨까지 가져와야하는 단점이 좀 있음.
      • 내가 코드를 고칠 수 없는 외부 라이브러리에는 당연히 적용할수가 없음.
      • InitializingBean -> affterPropertiesSet
      • DisposableBean -> destroy
      • 더 나은 방법들이 있어서 이제 잘 사용하진 않음 (2003년에나 만들었던 방법)
    • @BeandestroyMethod, initMethod
      • destroyMethod 는 INFERRED 값이 기본값으로 약간 알아서 close shutdown 메서드 동작 시켜줌
    • @PostContruct, @PreDestroy 스프링 권장사항
      • javax.annotation 에 들어있음. 자바진영 그냥 공식 JSR-250 자바 표준

빈 스코프

  • 종류
    • 싱글톤
    • 프로토타입 : 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리함. @PreDestroy 호출해주지않음.
    • request : 요청에서 하나
  • 프로토타입을 싱글톤과 같이 사용하는 경우 문제점 (싱글톤 빈 A에서 프로토타입 빈B를 주입받아 사용하는 경우)
    • 아마 개발자의 의도는 호출할떄마다 B의 프로토타입 새로운 객체가 생성되길 바라는건데,
    • 이렇게되면 최초 싱글톤 빈 A 만들때, 그냥 Prototype 빈을 호출해서 들고 있는것이 될 뿐 의도대로 동작하는 것은 아님.
    • 결국 그냥 싱글톤 빈들을 사용하는것 밖에 안됨.
  • 해결 방법 : ObjectProvider(스프링), Provider(자바표준) 사용
    • 얘네는 대리자 정도임. 가운데 프록시라고 생각하면되고, .get() 으로 매번 가져올수있음.
    • 스프링 컨테이너가 이걸 뜨면서 bean 으로 등록하나봄

자바 표준과 스프링 표준 겹치면 그냥 스프링꺼 쓸래. 어차피 프레임워크 바꾸면, 그냥 처음부터 다시 만드는 세상인데 ㅎㅎ

  • 웹 스코프
    • 웹 환경에서만 동작하는 빈 스코프 얘도 웹 요청이 와야 스프링에서 bean 만들어서 던져줘야하니까
    • 웹 스코프 빈도 Provider 를 통해 감싸서 빈에서 받아야한다!!
1
2
3
4
5
@Controller
  @RequiredArgsConstructor
  public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
  • 스코프 프록시
    • 아 근데 Provider 로도 감싸고 싶지가 않아 그냥 쓰고싶어 하는놈들이 만들어낸 기능
    • Enhancer 로 짝퉁만들어넣은다음에, 실제 호출할때 Provider 동작하는것 처럼 하게 해줌.
      1
      2
      3
      4
      
      @Component
      @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
      public class MyLogger {
      }
      
  • 아무튼 대부분의 비즈니스 문제는 싱글톤으로 대부분 해결이 가능한데, 굳이 막 쓰진 말자. 유지보수 정말 어려워 질수도 있음.
This post is licensed under CC BY 4.0 by the author.

정해진 미래

함수형 프로그래밍과 모나드