지나공 : 지식을 나누는 공간

Spring 핵심 원리 기본편 1 본문

Tech/Spring

Spring 핵심 원리 기본편 1

해리리_ 2023. 4. 23. 23:45

Spring 핵심 원리를 오고가면서 쓰윽 들었다. 그래서 그간의 테스트 코드들을 살펴보면서 아주 퀵하게 정리해보려 한다. 이미 너무나 유명한 영한님 강의!ㅎㅎ 좀 오글거리는 소제목들이 있지만 난 이렇게 써야 이해가 잘 가서 부끄러워도 그냥 썼다.

 

예제에 필요한 클래스들을 대충 정리하자면 아래와 같다.

  • Member 관련
    • Member
    • MemberRepository : 멤버 저장 DAO
    • MemberService : 멤버Repository를 호출해서 실제 회원가입과 회원찾기 기능을 해주는 부분
    • Grade enum : 등급별로 멤버들의 할인정책이 바뀌기 때문에 어떤 등급의 멤버인지 등록할 때 사용한다.
  • Order 관련
    • OrderService : 주문 생성에는 멤버정보 / 할인정보 / 상품정보가 필요하다.
      • OrderServiceImpl
    • DiscountPollicy : OrderSerivce에 주입될 할인정책이다. 할인정보를 책임짐.
      • FixDiscountPolicy : 할인정책의 구현체로, 고정 할인값을 가진다.
      • RateDiscountPolicy: 할인정책의 구현체로, 몇 퍼센트를 할인할지 비율로 할인가격을 계산한다.

 

OrderSerivce가 어떤 할인정책을 사용할건지 구체적인 구현체를 알고 있어도 될까?


사실 멤버, 등급은 구성요소고 여기서 가장 포인트가 되는 부분은 OrderService를 제공할 때 어떤 할인정책을 적용할지이다.

스프링컨테이너 없이 그냥 구현을 하자면 필요한 의존관계(특정 할인정책)을 직접 new를 통해 명시해줘야한다. 어떤 구현체(할인정책)를 쓸 것인지 말이다.

 

근데 이렇게 직접적으로 OrderServiceImpl내에 어떤 할인정책을 쓸 건지 FixDiscountPoliciy나 RateDiscountPolicy 같은 구현체 중 하나를 new를 통해 주입해주면, 나중에 내가 할인정책을 바꾸고 싶을때 이 할인정책에 의존하는 order Serivce들의 코드를 바꿔야 한다.

 

이게 바로 객체지향프로그래밍 원칙의 OCP 위반이다. OCP는 Open closed 원칙으로, 변경하지 않고도 기능을 확장할 수 있어야 한다는 원칙이다. 근데 방금까지 설명한 것처럼 구현체를 사용하는 order servcie 내에 직접적으로 어떤 구현체가 쓰이는지 new로 생성해서 주입하는 코드가 있다면, 구현체를 바꾸고싶을 때마다 그 구현체를 쓰고 있는 클라이언트(order service)의 코드들을 바꿔야 하니까, 변경이 필수적이다. 변경에 닫혀있어야 하는데! 변경하지 않고도 바꿀 수 있어야 하는데! 그게 잘 안되는 것이다.

 

그리고 현 상황은 OCP도 위반이지만 동시에 당연히 DIP(의존관계역전)도 위반하고 있는 것이다. DIP를 만족하려면 저수준모듈(구현체)이 고수준모듈(인터페이스)에 의존해야 한다. (==고수준이 저수준에 의존하면 안됨 == 상위 모듈이 하위모듈에 의존하면 안됨) 뭐가 고수준이고 뭐가 저수준인지 헷갈린다면 인텔리제이 다이어그램을 생각하면 될 거 같다. 아무튼 orderSeriveImpl 같은 구현체 즉 저수준 모듈은, 얘네들을 통용하는 인터페이스인 discountPolicy에만 의존해야 한다는 것이다. DIP에서 중요한건 변화하지 않는 것에 의존해야 한다는 점이다. 얘가 어떤 구체적인 값을 가지는지를 설정하는 코드에 의존하면 윗 문단에서 말한 것처럼 OCP도 같이 위반하게 되는 것이다.

 

 

AppConfig의 등장 :
new로 직접 구현체를 만들어주는 건 내가 혼자 다 해줄게!


객체지향원칙을 지키도록 수정하면 이제는 각각의 service나 클래스들이 인터페이스에만 의존해야 하고, 실제 구현체는 런타임에 생성자로 주입되도록 만들 수 있다. 사실 결론부터 말하자면 스프링이 이런 주입을 자동으로 해 주는 건데, 지금 아직 스프링 컨테이너를 도입하기 전이라고 가정하자면 이 주입을 도맡아 해주는 역할의 클래스가 필요하다. 그걸 우린 AppConfig로 만들어줄 수 있다. 이게 관심사의 분리다. 애플리케이션의 전체 동작 방식을 구성하기 위해 구현체 생성과 연결을 책임지는 AppConfig 클래스를 만들어서 각각의 관심사를 분리할 수 있다. 실제 구현체들을 사용하는 영역과, 그 사용할 구현체들을 만들고 필요한 곳에 넣어주는 구성 영역으로 구분할 수 있다.

AppConfig가 구현체 생성과 연결을 책임지고, ServiceImpl 같은 저수준모듈은 MemberRepository같은 고수준 모듈에만 의존한다. 더이상 구현체와 같은 저수준 모듈에 의존하는 부분이 없다.

 

OrderServiceImpl 같이 구현체를 주입받고 싶은 애들은 생성자로 주입만 받도록 만들고, 구현체의 생성과 실제 주입 행위를 하는 쪽은 AppConfig가 다 처리한다.

 

스프링 컨테이너 활용 with AppConfig


위에서 만든 AppConfig 같은 구성영역의 설정 정보 파일을 통해서 스프링을 시작할 수 있다. AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. IoC는 제어의 역전으로, OrderServiceImpl이 할인정책들을 사용하는 주체이니까 얘가 걔네들을 제어해야 할 것 같은데, 오히려 본인은 무슨 할인 정책을 쓸지 모르고 외부에서 수동적으로 주입을 받는다는 걸 떠올리면 된다. 제어가 역전된 것이다. DI는 의존관계 주입이라는 말로, 실제 어떤 구현체가 사용되는지 알 수 없고 의존관계를 밖에서 주입받는 개념이다.

 

그럼 이제 우리도 IoC, 뭐 DI 실현을 위해 스프링 컨테이너를 적용할 수 있다. AppConfig 같은 설정 정보에 이제는 스프링컨테이너가 관리할 수 있게 그들의 언어로 몇가지를 추가해주면 된다. 스프링컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용하고, 여기서 @Bean이라 적힌 메소드를 모두 호출해서 거기서 반환되는 객체를 스프링컨테이너에 빈으로 등록한다.

 

  • ApplicationContext를 스프링 컨테이너라고 한다.
  • 스프링 컨테이너를 생성할 때는 @Configuration이 붙은 AppConfig 같이 구성 정보를 설정해줘야 한다.
  • 스프링 빈은 메서드 이름을 사용하는데, 직접 부여할 수도 있다. 항상 빈 이름은 모두 달라야 한다. 안 그러면 다른 빈이 무시되거나 기존 빈을 덮어버리는 등 오류가 발생한다.

 

스프링컨테이너가 싱글톤을 보존하는 방법


스프링 프로그램에서 매 요청마다 빈이 새롭게 생성되면 메모리 낭비가 심해진다. 그래서 스프링컨테이너는 빈을 싱글톤으로 관리한다. 그런데 싱글톤이 마냥 좋기만한 것은 아니다. 싱글톤의 문제점은 아래와 같다.

  • 싱글톤을 구현하기 위한 코드가 추가로 작성되어야 한다. 생성자는 private 하게 바꾸고 매번 같은 객체가 반환되도록 하는 코드가 필요하다.
  • 요청이 들어올 때마다 이미 만들어진 구현 객체를 들고있다가 반환해야 하니까 클라이언트가 구현체 클래스에 의존한다. 여기서부터 벌써 DIP원칙을 위반했고, DIP가 위반되다보면 OCP가 위반될 확률이 높아진다. 구현체 클래스를 바꿀 일이 생기면 그때 OCP가 깨질 테니까.

그래서 싱글톤을 쓰더라도 스프링에서는 단점을 보완해서 쓰고 있다. 어떻게 보완하는지 아래에 정리했다.

  • 객체 인스턴스를 스프링컨테이너에 빈으로 등록해놓고 거기에 있으면 반환하고 없으면 새로 등록하고... 이런식으로 동작하기 때문에 싱글톤 패턴을 코드로 적용할 때처럼 추가 코드를 작성하지 않더라도 싱글톤을 적용할 수 있다.
  • 지저분한 싱글톤 구현 코드가 없으니 구현체에 의존할 일도 없고 DIP, OCP, private 생성자의 문제점으로부터 자유롭다.
  • 싱글톤은 컨테이너에 등록되어서 공유자원처럼 쓰이는 거라서 stateless하게 유지되어야 한다. 특정 변수값을 가지게 되면 공유 필드가 생기면서 동시 접근 시 문제가 발생할 수 있다.

아무튼 이런 식으로 단점을 보완하고 있다. 그런데 아래에 첨부한 AppConfig 파일을 보면 이상한 점이 있다. 스프링컨테이너는 설정정보를 읽은 뒤 거기에 bean이라 선언된 메소드들을 모두 호출해서 반환된 객체를 빈으로 등록한다고 했다. 그럼 memberRepository를 생성하는 코드는 세 번이나 실행되는건데, 어떻게 해서 memberRepository라는 이름의 빈 하나로, 즉 싱글톤으로 유지되게 할까?

세 가지 빈 모두 동일한 객체임을 알 수 있다. 싱글톤 유지가 잘 됐다.

memberRepository를 생성하는 코드는 세 번 호출되는 것 같은데 막상 로그를 찍어보면 memberRepository는 딱 한번만 호출되고 있다. 원래는 세번 호출되는게 맞는데, 스프링이 클래스의 바이트코드를 조작하는 라이브러리를 사용한다는 것에 그 비밀이 들어있다. @Configuration으로 선언한 AppConfig도 사실 빈으로 등록되는데, 그 값을 출력해보면 일반적이진 않은 값이 나온다. CGLIB라는 단어가 보인다. 일반적인 클래스라면 위에 출력한 객체들처럼 그냥 AppConfig라고 나와야하는데 뭔가 막 다른 CGLIB라든지 Spring이라든지 복잡한 단어들이 붙어있다.

Configuration 파일도 빈으로 등록된다. 그걸 가져와보니 CGLIB라는 값이 붙은 객체가 나온다.

내가 만들어 놓은 AppConfig가 아니라 실제로는 스프링이 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서 AppConfig를 상속받은 임의의 다른 클래스를 만들고 이 클래스를 스프링 빈으로 등록한 것이다. 아마도 바이트코드를 조작하는 코드로 내부에 작성되어 있을 것이다. 스프링 빈이 이미 등록된 객체에 대해 생성을 요구하면 기존의 객체를 스프링 컨테이너에서 찾아서 반환하고, 그게 아니라면 새로 생성해서 컨테이너에 등록하는 기능이 구현되도록 바이트코드를 조작한다.

 

근데 이제 이런 예상코드는 영한님이 알려주신 내용이고,,, 나는 이 바이트코드가 어떻게 생겼는지 직접 봐야겠다. 찾을 수 있기를!ㅠㅠㅎㅎ

언제나 두근거리는 코드 뜯어보기..... 오늘은 여기까지ㅎ

일단 싱글톤에 대한 설명까지 하고 이 글을 마무리한다. 다음에는 저 AppConfig 가 어떻게 생긴 빈으로 등록됐는지 내부 한번 뜯어보고, 컴포넌트 스캔과 빈 생명주기, 빈 스코프 등을 정리해야겠다.

 

추가) AppConfig CGLIB를 눈으로 확인하려 했는데 아직 프록시에 대한 지식이 부족해서 미루게 됐다. 스프링 프록시 다음 번 강의까지 다 듣고나서 찾아야겠다.. ^_^...

 

728x90
Comments