본문 바로가기

카테고리 없음

스프링 핵심 원리 - 기본편(1)

이제는 스프링에 대해 더 자세히 학습한다. 스프링이 무엇인가부터 좋은 객체지향 개발까지 다룰 예정이고
전반적인 원리에 대해서 학습할 예정이다.

 

  • 스프링 == 스프링 DI 컨테이너 기술 / 스프링 프레임워크 / 스프링 생태계 이런 식으로 사용된다.
    • 스프링 프레임워크: 가장 중요! 제일 핵심이다.
      • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트 등스프링 부트 
        • 스프링을 편리하게 사용할 수 있도록 지원
      • 스프링 부트
        • 스프링을 편리하게 사용할 수 있도록 지원
        • 설정 용이, 웹 서버 내장
      • 스프링 세션, 스프링 데이터, 스프링 클라우드,,,

 

좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

 

  • 좋은 객체 지향 = 역할과 구현을 분리
    • 클라이언트에 영향을 주지 않고 새로운 기능을 제공할 수 있다.
    • 유연하고 변경에 용이
    • 내부 구조를 몰라도 된다, 내부 구조가 변경되어도 영향을 받지 않는다.
  • 자바 = 다형성
    • 역할 - 인터페이스
    • 구현 - 인터페이스를 구현한 클래스, 구현 객체
    • 구현보다 인터페이스가 먼저이다!
  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경
    • 클라이언트(요청)와 서버(응답)의 협력 관계
    • 클라이언트에 영향을 주지 않는 변경이 가능하다
    • 인터페이스의 중요성

 

  • SOLID 객체 지향 설계 원칙
    • 단일 책임 원칙
      • 하나의 클래스는 하나의 책임만 가져야 한다.
      • 책임의 모호함 : 변경이 있을 때 파급이 적으면 원칙을 잘 따른 것이다.
    • 개방 폐쇄 원칙
      • 확장에는 열려있고, 변경에는 닫혀있어야 한다.
      • 새로운 클래스를 하나 만들어서 기능 구현
      • 스프링 컨테이너가 다형성을 도와준다.
    • 리스코프 치환 원칙
      • 인터페이스 -> 구현체 : 하위 클래스가 인터페이스의 규약을 다 지켜야 한다.
    • 인터페이스 분리 원칙
      • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 의존관계 역전 원칙
      • 구현 클래스에 의존하지 않고 인터페이스에 의존해야 한다.
      • 역할에 의존하게 한다. (의존한다 = 알고 있다)

다형성만으로는 OCP. DIP를 지킬 수 없다.

 

인터페이스를 도입하면 추상화라는 비용이 발생한다.
-> 기능을 확장할 가능성이 없다면 구체 클래스 사용 후 리팩터링 통해서 인터페이스를 도입하는 것도 방법이다.

 

- 예제 만들기

  • 회원 도메인 설계
    • 요구사항
      • 회원을 가입하고 조회할 수 있다
      • 회원은 일반과 VIP 두 등급이 있다
      • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다 (미확정)
    • 도메인 협력 관계: 기획자들도 볼 수 있는 그림
    • 클래스 다이어그램: 서버는 실행하지 않고 클래스 구현 (정적)
    • 객체 다이어그램: 서버에서 어떻게 진행되는지 (동적)
  • 주문과 할인 설계
    • 요구사항
      • 회원은 상품을 주문할 수 있다.
      • 회원 등급에 따라 할인 정책을 적용할 수 있다.
      • 모든 VIP는 1,000원을 할인해주는 고정 금액 할인
      • 할인 정책은 변경 가능성이 높다. (미확정)

 

- 객체 지향의 원리 적용

  • 새로운 할인 정책 개발

테스트 -> 성공 테스트와 실패 테스트도 해보는 것이 중요하다.

assertions static import사용 -> 알아보기

 

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl코드를 고쳐야 한다.

 

DIP를 지키도록 인터페이스에만 의존하도록 코드를 변경하면 작동하지 않는다!

따라서 누군가 객체를 주입해주어야 한다.

 

  • 관심사의 분리
    • 배우와 배역
    • 그리고 배우를 섭외하는 공연 기획자 == AppConfig
    • 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 클래스이다
  • 생성자를 통해서 객체가 들어간다
    • 생성자 주입

 

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(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

 

생성한 객체의 레퍼런스를 생성자를 통해서 두입하고 연결한다.

객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

 

클라이언트 입장에서는 외부에서 의존관계를 주입한다고 해서

의존관계 주입 혹은 의존성 주입 이라고 한다.

 

  • AppConfig 리팩터링
    • 메서드를 추출해서 역할이 분명하게 드러나게 한다.
    • 전체 구성이 한 눈에 들어온다.

AppConfig에서 할인 정책을 담당하는 구현체를 변경하면 정률 할인 정책제로 바꿀 수 있다.

클라이언트 코드 영역의 변경 없이!

 

  • 적용된 거 살펴보기
    • 단일 책임 원칙
      • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
    • 의존 관계 역전 원칙
      • FixDiscountPolicy라는 구체 클래스에 의존했었음
      • AppConfig가 객체를 클라이언트 대신 생성해서 클라이언트 코드에 의존관계를 주입
    • 개방 폐쇄 원칙
      • 클라리언트 코드를 건들지 않고 변경 가능
      • 사용 영역은 닫혀있다.

 

 

  • IOC (제어의 역전)
    • 제어 흐름에 대한 권한은 모두 AppConfig에 있다. OrderServiceImpl도 AppConfig가 생성한다.
      그렇다면 OrderService 인터페이스의 다른 구현 객체를 생성할 수도 있다.
      이렇게 되면 OrderServiceImpl은 묵묵히 자신의 로직을 실행하고 있다. 이렇듯 제어 흐름을 직접 제어하는 것이 아니라
      외부에서 관리하는 것을 제어의 역전이라고 한다.
    • 프레임워크: 코드를 프레임 워크가 제어하고 대신 실행한다. ex)Junit
    • 라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당한다.
  • DI (의존관계 주입)
    • 정적인 클래스 의존 관계: import 코드만 보고 의존관계를 쉽게 파악할 수 있다.
    • 동적인 클래스 의존 관계: 실행 시점에 결정되는 동적인 객체 의존 관계
    • 실행 시점에 외부에서 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존성 주입 이라고 한다.

 

  • IOC 컨테이너, DI 컨테이너
    • AppConfig처럼 객체를 생성하고 의존관계를 연결해주는 것을 DI(IOC) 컨테이너라고 한다.
    • 애플리케이션 전체에 대한 조립

 

스프링으로 전환하기

스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라 한다. / ApplicationContext는 인터페이스이다.
  • AnnotationConfigApplicationContext가 구현체이다.
  • @Configuration을 붙여서 설정 정보로 사용한다
  • 여기에서 @Bean이 적힌 메서드를 모두 호출해서 반환된 객체들을 스프링 컨테이너에 등록한다.
    • Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.

스프링 컨테이너 생성하기

  • 스프링 컨테이너
    • 스프링 빈 저장소
      • 빈 이름
      • 빈 객체 (new 해서 나오는 거 리턴 받음)
  • 스프링 빈 등록 -> 빈의 이름은 항상 다르게 부여한다! == 메서드 이름을 다르게 작성하자!
  • 빈 의존관계 설정: 스프링이 동적인 객체 의존관계를 연결해준다. / 자바 코드 호출과의 차이가 존재한다
  • 스프링은 빈을 다 생성하고 그 후에 의존관계를 연결해준다.

빈들을 다 출력해보자

@Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }

 

ROLE을 사용하면 애플리케이션에 사용되는 등록한 빈들만 볼 수 있다.

 

 

  • 스프링 빈 조회
    • ac.getBean(빈이름, 타입) 으로 사용한다 (기본)
MemberService memberService = ac.getBean("memberService", MemberService.class);

MemberService memberService = ac.getBean(MemberService.class); //타입으로 조회

MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);//구체 클래스로 조회

assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx", MemberService.class));
                //이름으로 조회할 수 없을 때, 그런 빈은 없다는 예외를 던지는 구문

 

  • 같은 타입이 둘 이상일 때 조회하기
public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
    void findBeanBytypeDuplicate(){
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);

    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType(){
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + "value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {


        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }

}

 

  • 상속 관계에서의 조회
    • 최고 부모인 object로 조회를 하면 모든 스프링 빈을 조회한다!
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회 시, 자식이 둘 이상 있으면 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate(){
        DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회 시, 자식이 둘 이상 있으면 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName(){
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for(String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for(String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }


    @Configuration
    static class TestConfig {

        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }

 

 

  • BeanFactory
    • 스프링 컨테이너의 최상위 인터페이스이다.
    • getBean()을 제공한다
    • ApplicationContext
      • BeanFactory의 기능을 모두 상속 받아서 제공한다.
      • 국제화 기능: 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로
      • 환경변수: 로컬 / 개발 / 운영 등을 구분해서 처리
      • 애플리케이션 이벤트: 이벤트를 발행하고 구독하는 모델을 편리하게 지원
      • 편리한 리소스 조회: 파일, 외부 등에서 리소스를 편리하게 조회
  • 둘 다 스프링 컨테이너라고 부른다!

 

  • XML로 설정하는 법 알아보기
    • 지금까지는 자바 코드로 설정하였다.
    • 레거시 프로젝트들에 자주 작성되어 있다.
    • 자바 코드가 아니기 때문에 resources 파일에 작성해준다.
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    <bean id="memberRepository"
          class="hello.core.member.MemoryMemberRepository" />

 

전에 했던 AppConfig에 작성하여 빈을 등록했던 방법과 유사하게 작동한다.

 

void xmlAppContext(){
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }

 

GenericXmlApplicationContext를 사용하여 전과 같은 형식으로 빈을 받아올 수 있다.

 

 

  • 스프링 빈 메타 정보 BeanDefinition
    • 다양한 설정 정보를 BeanDefinition으로 추상화해서 사용한다.
    • 직접 생성해서 스프링 컨테이너에 등록 가능하다.

 

싱글톤 컨테이너

 

스프링이 없는 경우: 클라이언트 요청에 따라 객체가 생성된다?

순수 DI 컨테이너는 요청이 올 때마다 객체를 새로 생성한다.

 

=> 해당 객체가 딱 1개만 생성해놓고  인스턴스를 공유하는 해결 방법을 사용한다. (싱글톤 패턴)

 

  • 싱글톤 패턴
    • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
    • 객체 인스턴스를 2개 이상 생성하지 못하도록 막는다.
package hello.core.singleton;

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }
    
    private SingletonService(){
        
    }
    
    public void login(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

 

static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.

static영역에 대해 다시 한 번 짚고 넘어가자.

Static 영역의 핵심 특징

  • 생명 주기 (Lifecycle):
    • 생성: 프로그램이 시작되고 클래스가 로딩될 때 생성됩니다.
    • 소멸: 프로그램이 종료될 때 메모리에서 해제됩니다.
    • 참고: 일반 객체(Heap 영역)는 Garbage Collector(GC)에 의해 수시로 관리되지만, Static 영역은 GC의 주 관리 대상이 아닙니다.
  • 공유 (Sharing):
    • 생성된 모든 객체(Instance)가 하나의 메모리 주소를 공유합니다.
    • 따라서 new 연산자로 객체를 생성하지 않아도 클래스 이름으로 바로 접근이 가능합니다.

 

1개의 객체 인스턴스만 존재해야 하므로,
생성자를 private으로 막아서 외부에서 new로 키워드로 객체 인스턴스가 생성되는 것을 막는다.

 

@Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);
        
        assertThat(singletonService1).isSameAs(singletonService2);
    }

 

싱글톤 객체로 검증할 수 있다!

같은 인스턴스를 반환하고 있다. / 참고로 싱글톤 패턴을 구현하는 방법은 여러가지이다.

 

  • 단점
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 클라이언트가 구체 클래스에 의존하게 된다. -> DIP, OCP 위반할 가능성 존재 ex) .getInstance()코드
    • 유연성이 떨어진다. 인스턴스를 애초에 박아버리기 때문이다.

 

  • 스프링 컨테이너의 싱글톤 컨테이너 역할
    • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
    • 빈 이름과 빈 객체로 나누어서 저장하는데 여기서 객체를 공유해서 사용한다.
    • 추가 코드나 priavte을 사용하지 않아도 된다.
@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContatiner(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        
        assertThat(memberService1).isSameAs(memberService2);
    }

=> 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 재사용한다.

(스프링의 빈 등록은 기본적으로 싱글톤이지만, 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.)

 

  • 싱글톤 방식의 주의점
    • 싱글톤 => 클라이언트가 하나의 객체 인스턴스를 공유한다. 따라서 상태를 유지해서는 안된다.
      • 특정 클라이언트가 값을 바꾸게 하면 안된다.
      • 가급적 읽기만 가능해야 한다.
      • 스프링 빈에 공유하는 값을 설정하면 큰 장애가 발생할 수 있다!!
public class StatefulService {

    private int price; //상태를 유지하는 필드

    public void order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        this.price = price;
    }

    public int getPrice(){
        return price;
    }
}

 

void statefulServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig{

        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }
    }

 

A의 결과가 20,000원이 나와버리는 상황이 발생한다.

스프링 빈을 이용하면 같은 객체를 사용하는데 이러면 클라이언트가 값을 임의로 변경하게 할 수 있다.

따라서 항상 무상태로 설계해야 한다.

 

  • @Configuration과 싱글톤
    • 클래스명에 CGLIB이 붙으면서 클래스명이 복잡해져 있다
    • AppConfig를 상속받은 임의의 클래스를 만들고, 그 클래스를 스프링 빈으로 등록한 것이다!
    • 이름은 appConfig이지만 인스턴스는 AppConfigCGLIB 이런 게 들어있다
      => 임의의 클래스를 만들고 해당 클래스로 싱글톤이 되도록 조작한다.
  • AppConfig@CGLIB 예상

memoryMemberRepository가 스프링 컨테이너에 있으면 찾아서 반환

없으면 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록한다

 

  • @Configuration을 붙이지 않을 때
    • AppConfig에 작성한 출력 코드가 3번이나 호출된다.
    • 다른 부분에서 싱글톤이 깨진 것이다!
    • 결론적으로 @Configuration을 붙이면 스프링은 프록시 객체를 생성하여 사용한다.

 


 

지금까지 객체 지향과 이를 위한 스프링이 제공하는 컨테이너의 기능에 대해 학습했다.

다음부터는 컴포넌트 스캔부터 스프링이 제공하는 기술들을 차근차근 배워갈 예정이다.