웹 어플리케이션과 싱글톤

  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다! => 메모리 낭비가 심하다.
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. => 싱글톤 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

싱글톤 패턴을 적용한 예제 코드

package hello.core.singleton;

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    // private 생성자를 써서 밖에서 new 못 함!
    private SingletonService() {
    }

    public void logic () {
        System.out.println("싱글톤 로직 호출");
    }
}
  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
  3. 딱 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);

    // isSameAs == 인스턴스가 같은지 비교
    assertThat(singletonService1).isSameAs(singletonService2);
}

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. => DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

싱글톤 컨테이너 

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
  • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.

스프링 컨테이너를 사용하는 테스트 코드

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    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("memberService1 = " + memberService2);

    // memberService1 != memberService2
    assertThat(memberService1).isSameAs(memberService2);
}
  • 스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유 해서 효율적으로 재사용할 수 있다.

싱글톤 방식의 주의점

  • 스프링 빈은 항상 무상태(stateless)로 설계하자!

@Configuration 과 싱글톤

 

@Configuration 과 바이트코드 조작의 마법

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

스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext
    • 스프링 컨테이너라고 함
    • 인터페이스임
    • XML 또는 애노테이션 기반의 자바 설정 클래스로 만들 수 있음
  • 스프링 컨테이너 생성 과정
    1. 스프링 컨테이너 생성
    2. 스프링 빈 등록
      • 빈 이름은 메서드 이름을 사용
      • 빈 이름을 직접 부여 가능
        • ex) @Bean(name="memberService2")
    3. 스프링 빈 의존관계 설정 - 준비
    4. 스프링 빈 의존관계 설정 - 완료
      • 스프링 컨테이너는 설정 정보를 참고하여 의존관계를 주입(DI)
  • 스프링 컨테이너 생성 과정 그림

1. 스프링 컨테이너 생성
2. 스프링 빈 등록
3. 스프링 빈 의존관계 설정 - 준비
4. 스프링 빈 의존관계 설정 - 완료

컨테이너에 등록된 모든 빈 조회

// 테스트 코드

 

  • 모든 빈 조회
    • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
    • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
  • 단축키
    • iter + Tab: for 문 자동 완성
    • soutm: 메서드명 출력
    • soutv: 변수명 출력

스프링 빈 조회 - 기본

  • ac.getBean(빈 이름, 타입)
  • ac.getBean(타입)

// 테스트 코드

  • 단축키
    • command + e: 이전 파일로 가기 (최근 파일)

스프링 빈 조회 - 동일 타입 둘 이상

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생
  • 이때는 빈 이름을 지정하자. ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.

// 테스트 코드

 

스프링 빈 조회 - 상속 관계

  • 부모 타입으로 조회하면, 자식 타입도 함께 조회

BeanFactory 와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당
  • getBean() 을 제공
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능

ApplicationContextBeanFactory 

    • BeanFactory 기능을 모두 상속받아서 제공
    • 그 외 수많은 부가 기능 제공

  • 메시지 소스를 활용한 국제화 기능
    • ex) 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수
    • 로컬, 개발, 운영 등을 구분해서 처리
  • 애플리케이션 이벤트
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회
    • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

정리

  • ApplicationContext는 BeanFactory의 기능을 상속 받음
  • ApplicationContext는 빈 관리 기능 + 편리한 부가 기능을 제공
  • BeanFactory를 직접 사용할 일은 거의 없고 부가기능이 포함된 ApplicationContext를 사용
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라고 함

다양한 설정 형식 지원 - 자바 코드, XML

  • 애노테이션 기반 자바 코드 설정
    • new AnnotationConfigApplicationContext(AppConfig.class)
  • XML 설정
    • new GenericXmlApplicationContext(AppConfig.xml)

// xml 예제 코드

스프링 빈 설정 메타 정보 - BeanDefinition

  • 스프링은 어떻게 다양한 설정 형식을 지원하는 것일까? => 핵심은 BeanDefinition 이라는 추상화!
  • 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것이다!
    • XML을 읽어서 BeanDefinition을 만들면 된다.
    • 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
  • BeanDefinition 을 빈 설정 메타정보라 한다.
    • @Bean , <bean> 당 각각 하나씩 메타 정보가 생성된다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.

 

// 예제 코드

새로운 할인 정책 개발

    • 갑자기 기획이 바뀌어 정률 정책 할인도 구현
      • DiscountPolicy 클래스를 구현한 RateDiscountPolicy 클래스
      • package hello.core.discount;
        
        import hello.core.member.Grade;
        import hello.core.member.Member;
        
        public class RateDiscountPolicy implements DiscountPolicy {
        
            private int discountPercent = 10;
        
            @Override
            public int discount(Member member, int price) {
                if (member.getGrade() == Grade.VIP) {
                    return price * discountPercent / 100;
                } else {
                    return 0;
                }
            }
        }
    • 테스트 케이스 짜기
      • command + shift + t: 테스트 클래스 만들기
      • package hello.core.discount;
        
        import hello.core.member.Grade;
        import hello.core.member.Member;
        import org.junit.jupiter.api.DisplayName;
        import org.junit.jupiter.api.Test;
        
        import static org.assertj.core.api.Assertions.*;
        
        class RateDiscountPolicyTest {
        
            RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
        
            @Test
            @DisplayName("VIP는 10% 할인이 적용되어야 한다.")
            void vip_o() {
                // give
                Member member = new Member(1L, "memberVIP", Grade.VIP);
                // when
                int discount = discountPolicy.discount(member, 10000);
                // then
                assertThat(discount).isEqualTo(1000);
            }
        
            @Test
            @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
            void vip_x() {
                // give
                Member member = new Member(2L, "memberBASIC", Grade.BASIC);
                // when
                int discount = discountPolicy.discount(member, 10000);
                // then
                assertThat(discount).isEqualTo(0);
            }
        }

새로운 할인 정책 적용과 문제점

  • 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
  • public class OrderServiceImpl implements OrderService {
    	// private final DiscountPolicy discountPolicy = new FixedDiscountPolicy();
    	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    }
  • DIP 위반
    • 주문 서비스 클라이언트(OrderServiceImpl)는 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
      • 추상(인터페이스) 의존: DiscountPolicy
      • 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
  • OCP 위반
    • 변경하지 않고 확장할 수 있다고 했는데 지금 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 준다.
    • 실제 의존관계 클래스 다이어그램
  • 해결 방법?
    • 인터페이스에만 의존하도록 설계를 변경하자.
    • 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

관심사의 분리

  • AppConfing 의 등장 (= 공연 기획자)
    • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 1. 구현 객체를 생성하고 2. 연결하는 책임을 가지는 별도의 설정 클래스
    • public class AppConfig {
      
          public MemberService memberService() {
              return new MemberServiceImpl(memberRepository());
          }
      
          private MemberRepository memberRepository() {
              return new MemoryMemberRepository();
          }
      
          public OrderService orderService() {
              return new OrderServiceImpl(memberRepository(), discountPolicy());
          }
      
          public DiscountPolicy discountPolicy() {
              return new FixDiscountPolicy();
          }
      }
    • 구현 객체를 생성
    • 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입
      • public class MemberServiceImpl implements MemberService{
        
            private final 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);
            }
        }
    • 설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
    • 단지 MemberRepository 인터페이스만 의존한다.
    • 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfing)에서 결정된다.
    • 클래스 다이어그램
    • 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
    • 테스트 코드
      • package hello.core.member;
        
        import hello.core.AppConfig;
        import org.assertj.core.api.Assertions;
        import org.junit.jupiter.api.BeforeEach;
        import org.junit.jupiter.api.Test;
        
        public class MemberServiceTest {
        
            MemberService memberService;
        
            @BeforeEach
            public void beforeEach() {
                AppConfig appConfig = new AppConfig();
                memberService = appConfig.memberService();
            }
        
            @Test
            void join() {
                // give
                Member member = new Member(1L, "memberA", Grade.VIP);
        
                // when
                memberService.join(member);
                Member findMember = memberService.findMember(1L);
        
                // then
                Assertions.assertThat(member).isEqualTo(findMember);
            }
        }
      • @BeforeEach 는 각 테스트를 실행하기 전에 호출됨
      • command + e: 히스토리 보여줌
      • command + option + m: extract method

새로운 구조와 할인 정책 적용

  • AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.

  • FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고 사용 영역은 전혀 영향을 받지 않는다.

좋은 객체 지향 설계의 5가지 원칙 적용

1. SRP 단일 책임 원칙: 한 클래스는 하나의 책임만

  • SRP 단일 책임 원칙을 따르면서 관심사를 분리함 
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당 
  • 클라이언트 객체는 실행하는 책임만 담당

2. DIP 의존관계 역전 원칙: 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안 된다.

  • AppConfig 가 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입 (= DI)

3.  OCP 개방-폐쇄 원칙: 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야

  • AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy 로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨

IoC, DI, 그리고 컨테이너

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한 마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다. 
  • 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다.

의존관계 주입 DI(Dependency Injection)

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.

스프링으로 전환하기

  • AppConfig 코드
    • @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();
          }
      }
  • MemberApp 에 스프링 컨테이너 적용
    • public class MemberApp {
      
          public static void main(String[] args) {
             	// MemberService memberService = new MemberServiceImpl();
              // AppConfig appConfig = new AppConfig();
              // MemberService memberService = appConfig.memberService();
      
              ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
              MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
      
              Member member = new Member(1L, "memberA", Grade.VIP);
              memberService.join(member);
      
              Member findMember = memberService.findMember(1L);
              System.out.println("new member = " + member.getName());
              System.out.println("find member = " + findMember.getName());
          }
      }
  • ApplicationContext 스프링 컨테이너라고 한다.
  • 기존에는 개발자가 AppConfig 를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링
    컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean
    이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에
    등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. (memberService, orderService)
  • 스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.

1. 이야기

  • EJB 엔티티빈 (ORM) --화남--> 하이버네이트 --자바표준--> JPA
  • 표준 인터페이스 JPA <--JPA 를 구현-- 하이버네이트 등

2. 스프링 역사

  • 스프링 이름은 전통적인 EJB라는 겨울을 넘어 새로운 시작이라는 뜻으로 지었다고 함!

3. 스프링 생태계

  • 스프링 부트
    • 스프링을 편하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
    • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨
    • 손쉬운 빌드 구성을 위한 starter 종속성 제공
    • 스프링과 3rd party 라이브러리 자동 구성

4. 스프링을 왜 만들었냐?

  • 객체 지향 언어의 강력한 특징을 잘 살려냄!
  • 객체 지향 특징
    • 추상화
    • 캡슐화
    • 상속
    • 다형성
  • 다형성
    • 역할구현의 분리
    • 장점: 클라이언트가 내부 구조를 몰라도 됨
    • 다른 대상으로 대체 가능. 유연. 변경 용이
    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스, 구현 객체
    • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.
    • 즉, MemberService 를 변경하지 않고, MemberRepository <- MemoryMemberRepository || JdbcMemberRepository 가 가능하다.
    • 인터페이스를 잘 설계하는 것이 중요! (잘하는 개발자)
  • 스프링은 다형성을 극대화해서 이용할 수 있도록 지원!
    • ex) IoC, DI

6. 좋은 객체 지향 설계의 5가지 원칙 (SOLID)

6.1. SOLID

  1. SRP: 단일 책임 원칙 (Single Responsibility Principle)
  2. OCP: 개방-폐쇄 원칙 (Open/Closed Principle)
  3. LSP: 리스코프 치환 원칙 (Liskov Subsitution Principle)
  4. ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)
  5. DIP: 의존관계 역전 원칙 (Despendency Inversion Principle)

6.2. SRP 단일 책임 원칙 (Single Responsibility Principle)

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것.

6.3. OCP 개방-폐쇠 원칙 (Open/Closed Principle)

  • 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 문제점: 클라이언트 변경 필요
    •  
    • public class MemberService { private MemberRepository memberRepository = new MemoryMemberRepository(); }
    • public class MemberService {
      	// private MemberRepository memberRepository = new MemoryMemberRepository();
      	private MemberRepository memberRepository = new JdbcMemberRepository();
      }
  • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
  • 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
  • 객체를 생성하고 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다. => 스프링 컨테이너의 역할. DI, IoC 컨테이너.

6.4. LSP 리스코프 치환 원칙 (Liskov substitution principle)

  • 프로그램의 객체는 프로그램의 정확성을 개뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스의 인터페이스 규약을 다 지켜야 한다는 것. 다형성을 지원하기 위한 원칙. 인터페이스를 구현한 구편체는 믿고 사용하려면, 이 원칙이 필요하다.
  • ex) 자동차 인터페이스의 엑셀은 앞으로 가라는 기능. 뒤로 가게 구현하면 LSP 위반. 느리더라도 앞으로 가야함.

6.5. ISP 인터페이스 분리 원칙 (Interface segregation principle)

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

 

+ Recent posts