0208 - 0214

0208

Vue.js

Vue는 무엇인가?

  • 웹 페이지 화면을 개발하기 위한 프런트엔드 프레임워크

뷰의 장점

  1. 배우기 쉽다.
  2. 리액트와 앵귤러에 비해 성능이 우수하고 빠르다.
  3. 앵귤러의 데이터 바인딩 특성과 리액트의 가상 돔 기반 렌더링 특징을 모두 가지고 있다.

뷰의 특징

UI 화면단 라이브러리

  • 뷰는 UI 화면 개발 방법 중 하나인 MVVM 패턴의 뷰 모델에 해당하는 화면단 라이브러리
  • 화면의 요소들을 제어하는 코드와 데이터 제어 로직을 분리하여 코드를 더 직관적으로 이해할 수 있고, 유지 보수가 편해짐
  • MVVM: 모델 - 뷰 - 뷰 모델로 구조화한 개발 방식
  • view: 사용자에게 보이는 화면
  • model: 데이터를 담는 용기. 보통 서버에서 가져온 데이터를 js 객체 형태로 저장
  • 데이터 바인딩: 뷰에 표시되는 내용과 모델의 데이터를 동기화
  • 돔 리스너: 돔의 변경 내역에 대해 즉각적으로 반응하여 특정 로직을 수행하는 장치
  • 뷰 모델: 뷰와 모델의 중간 영역으로 돔 리스너와 데이터 바인딩을 제공하는 영역
    -> 이처럼 뷰는 화면의 요소가 변경되거나 조작이 일어날 때 즉각적으로 반응하여 화면 데이터를 갱신하여 보여주는 역할을 한다.

컴포넌트 기반 프레임워크

  • 뷰의 컴포넌트를 조합하여 화면을 구성할 수 있다.
  • 컴포넌트 방식으로 개발하는 이유는 코드를 재사용하기가 쉽기 때문

리액트와 앵귤러의 장점을 가진 프레임워크

  • 뷰는 앵귤러의 양방향 데이터 바인딩과 리액트의 단방향 데이터 흐름의 장점을 모두 결합한 프레임워크이다.
  • 양방향 데이터 바인딩: 화면에 표시되는 값과 프레임워크의 모델 데이터 값이 동기화되어 한쪽이 변경되면 다른 한쪽도 자동으로 변경되는 것
  • 단방향 데이터 흐름: 컴포넌트의 단방향 통신. 컴포넌트 간에 데이터를 전달할 때 항상 상위 컴포넌트에서 하위 컴포넌트 한 방향으로만 전달하게끔 프레임워크가 구조화
  • 가상 돔 렌더링 방식: 특정 돔 요소를 추가하거나 삭제하는 변경이 일어날 때 화면 전체를 다시 그리지 않고 프레임워크에서 정의한 방식에 따라 화면을 갱신.

0209

0208

프로젝트 진행 중에 게시물을 페이징 해야해서 그와 관련된 내용들을 컴포넌트로 구분하여 개발하였다.

<template>
  <div class="pagination">
    <button
      type="button"
      class="btn_first"
      title="첫 페이지로 이동"
      v-if="first"
      @click="changeLink(1)"
    >
      <span>처음</span>
    </button>
    <button
      type="button"
      class="btn_prev"
      title="이전 5개 목록 페이지로 이동"
      v-if="prev"
      @click="changeLink(startPageIndex - 1)"
    >
      <span>이전</span>
    </button>

    <ul class="paging">
      <li v-for="index in endPageIndex - startPageIndex + 1" :key="index">
        <button
          type="button"
          :class="{ on: startPageIndex + index - 1 == currentPage }"
          title="현재페이지로 이동"
          @click="changePage(index)"
        >
          [mustache] startPageIndex + index - 1 [mustache]
        </button>
      </li>
    </ul>

    <button
      type="button"
      class="btn_next"
      title="다음 5개 목록 페이지로 이동"
      v-if="next"
      @click="changeLink(endPageIndex + 1)"
    >
      <span>다음</span>
    </button>
    <button
      type="button"
      class="btn_end"
      title="마지막 페이지로 이동"
      v-if="end"
      @click="changeLink(pageCount)"
    >
      <span></span>
    </button>
  </div>
</template>

<script>
export default {
  props: ["listRowCount", "totalListItemCount", "pageLinkCount", "currentPage"], //listRowCount: 한 페이지당 리스트 개수, totalListItemCount: 총 리스트 개수, pageLinkCount: 링크당 페이지 개수
  data() {
    return {
      pageCount: 0, //페이지 개수
      startPageIndex: 0, //링크에서 페이지 시작 인덱스
      endPageIndex: 0, //링크에서 페이지 마지막 인덱스
      prev: false,
      next: false,
      first: false,
      end: false,
    };
  },
  methods: {
    initUI() {
      this.pageCount = Math.ceil(this.totalListItemCount / this.listRowCount);

      if (this.currentPage % this.pageLinkCount == 0) {
        this.startPageIndex =
          (this.currentPage / this.pageLinkCount - 1) * this.pageLinkCount + 1;
      } else {
        this.startPageIndex =
          Math.floor(this.currentPage / this.pageLinkCount) *
            this.pageLinkCount +
          1;
      }

      if (this.currentPage % this.pageLinkCount == 0) {
        this.endPageIndex =
          (this.currentPage / this.pageLinkCount - 1) * this.pageLinkCount +
          this.pageLinkCount;
      } else {
        this.endPageIndex =
          Math.floor(this.currentPage / this.pageLinkCount) *
            this.pageLinkCount +
          this.pageLinkCount;
      }

      if (this.currentPage <= this.pageLinkCount) {
        this.prev = false;
        this.first = false;
      } else {
        this.prev = true;
        this.first = true;
      }

      if (this.endPageIndex >= this.pageCount) {
        this.endPageIndex = this.pageCount;
        this.next = false;
        this.end = false;
      } else {
        this.next = true;
        this.end = true;
      }

      console.log(`startPageIndex: ${this.startPageIndex}`);
      console.log(`endPageIndex: ${this.endPageIndex}`);
      console.log(`pageCount: ${this.pageCount}`);
    },
    changePage(index) {
      let selectedPage = this.startPageIndex + index - 1;
      this.$emit("update", selectedPage);
    },
    changeLink(index) {
      this.$emit("update", index);
    },
  },
  watch: {
    currentPage: function () {
      this.initUI();
    },
    totalListItemCount: function () {
      this.initUI();
    },
    listRowCount: function () {
      this.initUI();
    },
    pageLinkCount: function () {
      this.initUI();
    },
  },
  created() {
    this.initUI();
  },
};
</script>
  • props로 현재 페이지, 링크 당 페이지, 총 아이템 개수, 페이지당 아이템 개수를 받아서 여러 상황에서 재사용 할 수 있도록 작성하였다.
  • props 데이터가 변경할 때마다 initUI()를 실행시켜서 페이지 관련 변수들을 초기화 시켰다.
  • 상위 컴포넌트에 현재 페이지를 전달하여 메서드를 실행시켜 페이지에 해당하는 목록을 반환하였다.

0210

관심사의 분리

  • 구현체는 본인의 역할을 수행하는 것에만 집중해야 한다.
  • 구현체는 역할을 수행하는 데 있어서 필요한 다른 역할에 있어서 어떤 구현체가 선택되더라도 똑같이 역할을 수행 할 수 있어야 한다.
  • 역할에 맞는 구현체를 지정하는 책임을 담당하는 별도의 기획자가 나올시점이다.
    -> 기획자를 만들고, 구현체와 역할에 맞는 구현체를 지정해주는 책임을 확실히 분리하자.

AppConfig 등장

  • 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
  • 애플리케이션 실제 동작에 필요한 구현 객체를 생성하고, 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.
public class AppConfig {		//AppConfig
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;	// 생성자로 의존성 주입

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

// AppConfig를 실행할 때
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();

// Test코드에서 AppConfig를 실행할 때
MemberService memberService;

@BeforeEach	// 테스트 한 개를 돌릴 때마다 실행
public void beforeEach() {
    AppConfig appConfig = new AppConfig();
    memberService = appConfig.memberService();
}
  • MemberServiceImpl은 이제 더이상 구현 객체에 의존하지 않고 MemberRepository 인터페이스에만 의존한다.
  • 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
  • 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
    -> DIP 완성

AppConfig 리팩터링

  • AppConfig에서 중복되는 코드를 지우고, 역할에 따른 구현이 보이도록 해 준다.
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }


    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), new FixDiscountPolicy());
    }

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    private DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • new MemoryMemberRepository() 부분이 중복 제거되었다. 이제 MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
  • AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
  • 새로운 할인 정책을 적용하더라도 AppConfig에서 discountPolicy()만 수정하면 된다.
    -> 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리되었다. 사용 영역의 코드를 변경할 필요가 없어짐

IoC, DI, 컨테이너

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
  • AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당하고, 프로그램의 제어 흐름은 AppConfig가 가져간다.
  • 이처럼 프로그램 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라 한다.

프레임워크 vs 라이브러리

프레임워크: 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 (JUnit)
라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리

의존관계 주입 DI(Dependency Injection)

  • 의존관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체 의존 관계 둘을 분리해서 생각해야 한다.

정적인 클래스 의존관계

  • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다.
  • 클래스 다이어그램을 보고 확인 가능

동적인 객체 인스턴스 의존관계

  • 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계
  • 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 참조값을 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.
  • 의존관계 주입을 사용하면 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라고 한다.

스프링으로 전환하기

지금까지는 순수한 자바 코드만으로 DI를 적용했다. 이제는 스프링을 사용해서 바꾸어보자

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), new FixDiscountPolicy());
    }

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

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • AppConfig에 설정을 구성한다는 뜻의 @Configuration을 붙여준다.
  • 각 메서드에 @Bean을 붙여주면 스프링 컨테이너에 스프링 빈으로 등록한다.

스프링 컨테이너 적용

// AppConfig appConfig = new AppConfig(); 기존 코드
// MemberService memberService = appConfig.memberService();

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
  • ApplicationContext를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 등록된 객체를 스프링 빈이라고 한다.
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
  • 이전에는 개발자가 필요한 객체를 AppConfig를 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 스프링 빈을 찾아야 한다. 스프링 빈은 applicationContext.getBean()메서드를 사용해서 찾을 수 있다.

0211

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

스프링 컨테이너의 생성 과정

스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext는 인터페이스이며, 스프링 컨테이너라고 한다.
  • 스프링 컨테이너는 XML기반 또는 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • new AnnotationConfigApplicationContext(AppConfig.class);는 ApplicationContext 인터페이스의 구현체이다.
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 하는데, AppConfig.class를 구성 정보로 지정했다.

스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
  • 빈 이름은 메서드 이름을 사용하고, 이름을 직접 부여할 수도 있지만 항상 다른 이름을 부여해야 한다.

의존관계 설정

  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입한다.
  • 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다.
  • 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다.

컨테이너에 등록된 빈 조회

모든 빈 출력하기

  • 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
  • ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회한다.

애플리케이션 빈 출력하기

  • 스프링 내부에서 사용하는 빈을 제외하고, 등록한 빈만 출력
  • 스프링 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
    • ROLE_APPLICATION: 일반적으로 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
@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);
        }
    }
}

스프링 빈 조회 - 기본

  • 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
  • ac.getBean(빈이름, 타입), ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
  • 구체 타입으로 조회하면 변경시 유연성이 떨어진다
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
    MemberService memberService = ac.getBean("memberService", MemberService.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}

@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByNameX() {
    // MemberService xxxxx = ac.getBean("xxxxx", MemberService.class);
    Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
            ac.getBean("xxxxx", MemberService.class)
    );
}
  • Assertions.assertThrows(예외 클래스, 실행할 예외 람다식)
    -> 예외가 발생하면 JUint에서 정상 동작한다.

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

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생하므로 이때는 빈 이름을 지정해야 한다.
  • ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanByType() {
    Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class); //Map으로 반환
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + " value = " + beansOfType.get(key));
    }
    System.out.println("beansOfType = " + beansOfType);
    assertThat(beansOfType.size()).isEqualTo(2);
}

스프링 빈 조회 - 상속 관계

  • 부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
  • 그래서 최고 부모인 Object타입으로 조회하면, 모든 스프링 빈을 조회한다.

BeanFactory와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공
  • 빈 관리기능 + 편리한 부가 기능을 제공
  • ApplicationContext가 제공하는 부가기능
    • 메시지소스를 활용한 국제화 기능
    • 환경변수
    • 애플리케이션 이벤트
    • 편리한 리소스 조회 등

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

스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까?
-> BeanDefinition으로 추상화

  • 역할과 구현을 개념적으로 나눈 것
  • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 어느 것이든 읽어서 BeanDefinition을 만들어서 설정한다.
  • BeanDefinition을 빈 설정 메타정보라 한다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
    • @Bean, <bean>당 하나씩 메타 정보 생성

0212

싱글톤 컨테이너

웹 애플리케이션과 싱글톤

  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
  • 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다. 메모리 낭비가 심하다.
    -> 싱글톤 패턴을 이용하여 객체를 딱 1개만 생성하고, 공유하도록 설계하면 된다.

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성하는 것을 보장하는 디자인 패턴
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.
public class SingletonService {
    //1. static 영역에 객체를 딱 1개만 생성
    private static final SingletonService instance = new SingletonService();

    //2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용
    public static SingletonService getInstance() {
        return instance;
    }

    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {

    }
}
  • 싱글톤 패턴을 적용하면 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 수 많은 문제점들을 가지고 있다.

싱글톤 패턴 문제점

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

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다. 스프링 빈이 바로 싱글톤으로 관리되는 빈

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스플이 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 이런 기능 때문에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 상태를 유지하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다.
    • 특정 클라이언트에 의존적인 필드가 있거나, 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
    -> 공유 필드는 정말 조심해야 한다. 스프링 빈은 항상 무상태로 설계하자

Configuration과 싱글톤

AppConfig를 보면 각각 2번 new MemoryMemberRepository 호출해서 서로 다른 인스턴스가 생성되어야 하는데 어떻게 싱글톤을 유지할까?

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

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
  • 스프링이 자바 코드까지 조작하기는 어렵다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
  • AnnotationConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 등록된다.
  • @Configuration을 클래스에 붙이면 AppConfig를 상속받은 다른 클래스를 만들고, 그 클래스를 스프링 빈으로 등록하여 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장한다.
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • @Configuration을 붙이지 않으면 AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록되어 싱글톤을 보장하지 않는다.

0213

컴포넌트 스캔

컴포넌트 스캔과 의존관계 자동 주입

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean>을 통해서 설정 정보에 직접 등록할 빈을 나열했다.
  • 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능
  • 의존관계를 자동으로 주입하는 @Autowired 기능
@Configuration
@ComponentScan(
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
  • 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.
  • 기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.
  • 컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록되기 때문에, 설정정보는 스캔 대상에서 제외했다.
  • 컴포넌트 스캔은 이름 그대로 @Component애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.

@ComponentScan

  • @ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
  • 빈 이름 직접 지정
    @Component("memberService2")이런식으로 이름을 부여하면 된다.

@Autowired 의존관계 자동 주입

  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 이 때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
  • 생성자에 파라미터가 많아도 다 찾아서 자동 주입한다.

탐색 위치와 기본 스캔 대상

탐색할 패키지의 시작 위치 지정

  • 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸려서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
@ComponentScan(
	basePackages = "hello.core",	//이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
)
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
  • 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

관례

  • 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것. 스프링 부트도 이 방법을 제공
  • 또한 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다.
  • 스프링 부트를 사용하면 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례
    -> 이 안에 @ComponentScan이 들어있다.

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.

  • @Controller: 스프링 컨트롤러로 인식
  • @Service: 스프링 비즈니스 로직에서 사용
  • @Repository: 스프링 데이터 접근 계층에서 사용. 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration: 스프링 설정 정보에서 사용. 스플이 빈이 싱글톤을 유지하도록 추가 처리

필터

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정
@Configuration
@ComponentScan(
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
        excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {

}
  • includefiltersMyIncludeComponent 애노테이션을 추가하여 BeanA가 스프링 빈에 등록된다.
  • excludeFiltersMyExcludeComponent 애노테이션을 추가하여 BeanB는 스프링 빈에 등록되지 않는다.

@Component면 충분하기 때문에, includefilters를 사용할 일은 거의 없다. 특히 스프링 부트는 컴포넌트 스캔을 기본으로 제공하여, 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
-> 두 가지 상황이 있다.

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

자동 빈 등록 vs 자동 빈 등록

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.

수동 빈 등록 vs 자동 빈 등록

  • 이 경우 수동 빈 등록이 우선권을 가진다.
    -> 수동 빈이 자동 빈을 오버라이딩 해버린다.
  • 개발자가 의도적으로 이런 결과를 기대했다면, 자동보다는 수동이 우선권을 가지는 것이 좋지만, 현실은 여러 설정들이 꼬여서 이런 결과가 만들어진다.
    -> 정말 잡기 어려운 버그가 만들어진다. 애매한 버그
  • 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

0214

의존관계 자동 주입

다양한 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

  • 생성자를 통해서 의존 관계를 주입받는 방법
  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존관계에서 사용
  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입
  • @Autowired는 스프링 빈에서만 동작한다.

수정자 주입

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법
  • 선택, 변경 가능성이 있는 의존관계에 사용
  • @Autowired의 기본 동작은 주입할 대상이 없으면 오류 발생. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required=false)로 지정
  • 자바빈 프로퍼티 규약 - getter, setter메서드를 통해 값을 읽거나 수정

필드주입

  • 필드에 바로 주입하는 방법
  • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점
  • DI 프레임워크가 없으면 아무 것도 할 수 없다.
  • 사용하지 말자

일반 메서드 주입

  • 일반 메서드를 통해서 주입
  • 한번에 여러 필드를 주입 받을 수 있다. 잘 사용하지 않는다.

생성자 주입을 선택하는 이유

불변

  • 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.
  • 수정자 주입을 사용하면, 누군가 실수로 변경할 수 있기 때문에 좋은 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 한 번 호출되므로 불변하게 설계할 수 있다.

누락

  • 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행할 때 수정자 주입을 사용하면 의존 관계가 모두 누락하기 때문에 Null Point Exception이 발생한다.
  • 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생한다. 그래서 어떤 값을 필수로 주입해야 하는지 바로 알 수 있다.

final 키워드

  • 생성자 주입을 사용하면 필드에 final 키워드를 사용해서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다.
  • 컴파일 오류는 가장 빠르고 좋은 오류이다.

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required = false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • @Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
static class TestBean {
    @Autowired(required = false)
    public void setNoBean1(Member member) {
        System.out.println("member = " + member);
    }

    @Autowired
    public void setNoBean2(@Nullable Member noBean2) {
        System.out.println("noBean2 = " + noBean2);
    }

    @Autowired
    public void setNoBean3(Optional<Member> noBean3) {
        System.out.println("noBean3 = " + noBean3);
    }
}
  • Member는 스프링 빈이 아니다.

롬복과 최신 트랜드

_개발을 하다 보면, 대부분 다 불변이고 생성자에 final키워드를 사용하게 된다. _

  • 롬복 라이브러리가 제공하는 @RequiredArgsConstructor기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
  • 롬복이 자바 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}
  • 최근에는 생성자를 딱 1개 두고, @Autowired를 생략하여 Lombok라이브러리를 더하면 사용하는 기능을 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.

조회 빈이 2개 이상일 경우

  • @Autowired는 타입으로 조회한다.
  • 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다 -> NoUniqueBeanDefinitionException
  • 하위 타입으로 지정하면 DIP를 위배하고 유연성이 떨어진다.
  • 스프링 빈을 수동 등록해서 문제를 해결할 수도 있지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

Autowired 필드 명 매칭

  • @Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름을 추가 매칭한다.
  • 필드명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때만 추가로 동작하는 기능

Qualifier 사용

  • @Qualifier는 추가 구분자를 붙여주는 방법
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  1. @Qualifier끼리 매칭
  2. 빈 이름 매칭
  3. NoSuchBeanDefinitionException 예외 발생

Primary 사용

  • @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다
  • @Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여주어야 한다는 것

우선순위

  • @Primary는 기본값처럼 동작하는 것이고, @Qualifier는 상세하게 동작한다.
  • 스프링은 자동보다는 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선 순위가 높다.
  • 따라서 @Qualifier의 우선권이 높다.
  • 메인에서 @Primary를 사용하고 서브에서 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy")이렇게 문자를 적으면 컴파일 시 체크가 안된다. 어노테이션을 만들어서 문제를 해결할 수 있다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
  • 애노테이션에는 상속이라는 개념이 없다. 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.

조회한 빈이 모두 필요할 때

예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류를 선택할 수 있다고 가정해보자

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);	//스프링 컨테이너에 설정 파일 등록

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPolicy = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPolicy).isEqualTo(2000);
    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;


        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}
  • DiscountService는 Map으로 모든 DiscountPolicy를 주입받는다.
  • Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

자동, 수동의 올바른 실무 운영 기준

  • 스프링은 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원할 뿐더러, 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있기 때문에 기본적으로는 자동 빈 등록을 사용하자
  • 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.
  • 비즈니스 로직 중에서 다형성을 적극 활용할 때는 수동 빈 등록을 고민해보자
    • 조회한 빈이 모두 필요할 때 Map, List의 경우 어떤 빈들이 주입될 지 Map<String, DiscountPolicy>코드만 보고 한번에 쉽게 파악하기 힘들다.
    • 이런 경우 수동으로 빈을 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는 것이 좋다.

태그:

카테고리:

업데이트: