개발자로 후회없는 삶 살기

spring PART.순수 자바코드를 스프링으로 전환 본문

[백엔드]/[spring | 학습기록]

spring PART.순수 자바코드를 스프링으로 전환

몽이장쥰 2023. 3. 25. 17:28

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

- IoC, DI와 컨테이너

-> 제어의 역전이란?

예전에는 개발자가 직접 클래스를 호출했는데 이제는 내가 뭔가 코드를 호출하는게 아니고 프레임워크가 대신 호출해주는 것을 말한다. 제어권이 바뀐다고 해서 IoC이다. 예전에는 멤버 서비스 내부코드에서 new 메모리 멤버 레포를 직접 생성했다. 한마디로 개발자가 가져다 쓰는 것이다.

> 반면에 conf가 나온 이후로는 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 app config가 가져간다.

 

ex) 주문 서비스 impl은 서비스 비즈니스 로직에 필요한 인터페이스를 선언을 하지만 어떤 구현 객체들이 거기에 할당되어 실행될지는 이 코드만 봐서는 모른다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.

 

-> 의존관계 주입이란?

주문 서비스 Impl은 멤버 레포와 할인 정책이라는 역할에만 의존하도록 구체는 모르도록 바꿨다. 따라서 주문 서비스 impl 관점에서는 실제 어떤 객체가 들어올지 전혀모르고 실행이 된다. 

 

> ★ 의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계를 분리해서 생각해야한다. ★

 

1. 정적인 클래스 의존관계

클래스가 사용하는 import만 보고 사용하는 객체가 뭔지 의존관계를 쉽게 판단할 수 있다. 어플을 실행하지 않아도 의존관계를 판단할 수 있다.

ex) 주문 서비스 impl은 부모가 주문 서비스이고 멤버 레포와 할인 정책을 참조하고 있구나!를 알 수 있다. 하지만 이런 것으로는 멤버 레포에 메모리가 들어올지 jdbc가 들어올지 이 코드만 보고는 알수가 없다!!(실행시켜 봐야만 알 수 있다.)

 

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

 

실행시켜봐야 알 수 있는 것을 동적인 객체 의존 관계라고 한다. 어플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 의존 관계 주입이라고 한다. 의존관계 주입을 사용하면 클라 코드를 변경하지 않고 클라이언트가 호출하는 대상의 객체를 변경할 수 있다. ( ex) fix를 rate로 변경할 수 있다.)

 

※ IoC 컨테이너와 DI 컨테이너

IoC를 해주는 컨테이너이고 DI를 해주는 컨테이너라고 보면 되고 conf처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 즉 구성 영역을 해주는 것이다. 역할에 배우를 배정하는 것을 appconf가 하는데 얘가 의존 관계 역전을 일으킨다고 해서 IoC 컨테이너라고 한다. DI 컨테이너라고도한다. 같은 말이다. 스프링이 DI 컨테이너 역할을 한다.

 

 

 

- 스프링으로 전환

지금까지는 스프링 1도 없이 순수한 자바코드로 DI를 짠거다. 이제 스프링을 사용해보자

 

-> AppConfig 스프링으로 전환

@configuration이라고 있다. 이걸 딱 붙이고 @Bean을 선언 메서드마다 다 붙여준다. 스프링에서는 구성, 설정 정보에 @conf를 적어주게 되어있다. 이렇게 하면 메서드들이 스프링 컨테이너에 등록이 된다.

 

 

public class MemberApp {
    public static void main(String[] args) {
//        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, "A", Grade.VIP);
        memberService.join(member);
    }
}

> 이제 실제 스프링을 쓰도록 해보자 MemberApp을 자바 버전에서 스프링을 사용하도록 바꿔보자 ApplicationContext가 스프링 컨테이너이다. 얘가 모든 @Bean을 관리해주는 것이다. 얘를 생성하고 파라미터로 AppConfig의 클래스를(설정 정보) 넣어준다. 이렇게 하면 Appconf에 있는 연결 설정 정보를 가지고 스프링이 Bean 붙은 애들을 스프링 컨테이너에 객체로 생성해서 짚어넣는다. 이제는 객체들을 불러올 때 직접 불러오는게 아니라 스프링 컨테이너를 이용해서 불러와야한다. getBean으로 이름을 가져온다.

 

 

> name에 내가 컨테이너에서 뭘 꺼낼건지를 적고 꺼낼 것의 class를 쓴다.  컨테이너에는 메서드이름으로 등록이 된다. 그래서 이름은 이거고 type은 멤버 서비스야 라고 하는 것이고 type을 정하면 반환 타입이 정해진다. 돌려보면 잘 돌아간다. 보면 스프링 빈에 등록하는 것이 로그가 남는다.

 

-> 정리

AppicationContext를 스프링 컨테이너라고 한다. 기존에는 개발자가 appconf를 사용해서 직접 객체를 생성하고 Di를 했지만 이제부터는 스프링 컨테이너를 통해서 사용한다. 스프링 컨테이너는 @Configuration이 붙은 설정 정보로 한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너로 등록된 객체를 스프링 빈이라 한다. 

 

> 스프링 빈은 @bean이 붙은 메서드를 스프링 빈의 이름으로 쓴다. 스프링 컨테이너를 통해서 스프링 빈을 찾아야한다. getBean 메서드를 통해서 원하는 스프링 빈을 찾을 수 있다. 이제부터는 스프링에게 환경정보를 던져주고 찾을 때는 스프링 컨테이너를 통해서 찾아온다. 스프링이 환경정보를 가지고 필요한 것들을 다 관리를 하고 getBean으로 가져올 수 있다.

 

 

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

드디어 스프링에 대해서 설명한다. 이제 진짜 스프링 그 자체에 대해서 알아보자 

 

=> 스프링 컨테이너 생성

생성되는 과정을 알아보자 new AnnotationConfigApplicationContext(AppConfig.class);를 하면 스프링 컨테이너가 반환이 된다. appcontext를 스프링컨테이너라고 얘기를 하고 인터페이스이다. 인터페이스를 구현한 것 중 하나가 AnnotationConfigApplicationContext 인 것이다.(구현체) 

 

-> 스프링 컨테이너 생성 과정

1. new AnnotationConfigApplicationContext에 appconfig.class의 정보를 준다. 그러면 구성 정보가 지정이 된거다.

 

2. 그러면 스프링 컨테이너가 만들어지고 그 안에 스프링 빈 저장소가 있다. key는 빈 이름, 값은 빈 객체가 된다. 

 

3. 스프링 컨테이너가 생성이 되면서 스프링 빈 저장소에 스프링 빈을 등록을 한다. appconf에 @Bean이 붙은 것을 보고 죄다 호출을해서 메서드이름을 빈 이름인 키로 지정하고 빈 객체를 return 반환 객체로 등록을 한다. 우리는 4개니깐 4개를 스프링 빈에 등록을 한다. 

 

4. 스프링 의존 관계 설정 준비를 한다. 스프링 빈을 등록을 하고 의존관계를 넣어준다. 멤버 서비스의 의존 관계를 멤버 레포를 넣어줬으니 메모리 멤버 레포지토리가 셋팅이 된다. 동적인 객체 인스턴스 의존관계를 스프링이 연결을 해준다. 스프링 컨테이너는 설정정보를 참고해서 의존관계를 주입한다. 단순히 자바 코드를 호출하는 것 같지만, 차이가 있고 싱글톤 컨테이너와 관련이 있다.

 

-> 정리

정리해보자면, 스프링 빈을 등록을 다 하고 연결을 촥 하는 것이다. 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관게 주입도 한 번에 처리가 된다. 생성자가 호출되면서 다른 생성자를 호출하기 때문이다. 하지만 이걸 스프링을 하면 다 나뉘어져서 들어간다.

 

+ 스프링 컨테이너를 생성하고 설정 정보를 참고해서 스프링 빈도 등록하고 의존관계도 설정했다. 이제 스프링 컨테이너에서 데이터를 조회해보자 지금까지 등록한 게 빈으로 잘 등록이 됐나? 보는 것이다./

 

 

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

뭔가 등록이 된 거 같은데 진짜 잘 된건지 보자 test 코드로 한번짜보자 패키지를 만들고 ApplicationContextTest 클래스를 만든다. 여기에 빈을 조회하는 코드를 작성할 것이다.

 

-> 모든 빈 출력

public class ApplicationContextInfoTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
        }
    }

    @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);
            }
        }
    }
}

getBeanDefinitionNames라고 해서 꺼낼 수 있다. 하면 빈에 등록된 이름을 가져오고 getBean으로 빈 객체를 가져온다. bean이 getBean으로 가져온 빈 객체이고 beanDefinitionName이 스프링 빈에 등록된 빈 이름이다. 

 

 

> 실행해보면 스프링이 자체적으로 필요해서 등록한 빈들이있다. 내가 한 것만 하고 출력하고 싶으면  getBeanDefinitaion으로 빈 하나하나에 대한 정보를 꺼내고 role이라는게 있는데 ROLE_APPLICATION이 스프링 내부가 아닌 개발자가 만든 것이고 둘이 같은 경우만 출력을 하게 한다. 결과를 보면 빈 이름 memberService의 등록된 빈 객체는 MemberServiceImpl이라는게 보인다. 이걸 만들어두면 앞으로 필요할 때 어떤 빈들이 등록되어있는지 볼 수 있겠다./

 

- 스프링빈 조회 기본 방법

아까처럼 다 조회하는 경우는 잘 없다. 기본적인 방법은 getBean으로 한다. 만약 조회할 빈이 등록이 안되어있다면 없다는 에러가 뜬다.

 

-> 빈 이름으로 조회

@Test
@DisplayName("빈 이름으로 조회")
public void findBeanByName(){
    MemberService memberService = ac.getBean("memberService", MemberService.class);
    System.out.println("memberService = " + memberService);
    System.out.println("memberService.getClass() = " + memberService.getClass());
}

getBean 방법이 아까 memberApp에 짠 하나만 불러서 반환하는 것이다. 이름으로 조회하는 법을 짠다. getclass를 하면 클래스의 타입이 나온다. 이렇게 인터페이스.class로 조회를 하면 인터페이스의 구현체가 대상이 되는데 구체타입(모든 자식객체가 다 대상이 된다. 근데 MemberService는 없네?라고 생각할 수 있는데 스프링 빈에 등록된 것만 조회가 되고 빈에 등록한 것들이 다 객체일 테니 인터페이스인 MemberService 부모는 조회가 안 될 것이다.)으로 조회할 수도 있다.

 

@Test
@DisplayName("빈 이름으로 조회")
public void findBeanByName2(){
    MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
    System.out.println("memberService = " + memberService);
    System.out.println("memberService.getClass() = " + memberService.getClass());
}

이렇게하면 안 좋다. 왜냐면 항상 역활과 구현을 나누고 역할에 의존하도록 해야하는데 구현체가 대상이 되면 구현에 의존하는 것이다.

 

 

-> 빈 타입으로만 조회

@Test
@DisplayName("빈 이름 없이 타입으로만 조회")
public void findBeanByType(){
    MemberService memberService = ac.getBean(MemberService.class);
    Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}

 

검증을 Assertions로 해보면 isInstanceOf로 멤버 서비스가 멈버 서비스 impl의 객체면 성공한다고 코드를 짜는 것이다. 이름없이 타입으로만 조회해보자 이름을 빼도 조회가 가능하다. 같은 타입이 여러개인 경우는 좀 곤란해진다.

 

 

-> 실패 테스트

    @Test
    @DisplayName("빈 이름으로 조회 X")
    public void findBeanByNameX(){
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("m", MemberService.class));
    }
}

이름으로 조회했는데 없는 테스트를 만들자 이 경우는 없는 빈이라고 예외가 터진다. 이런 예외가 터지는 테스트는 assertThrows를 쓴다. 무조건 예외가 터져야 성공이라는 test를 만들 것이다. 람다를 써서 getBean 로직을 쓰면 이 예외가 터져야한다는 뜻이다.

 

- 동일한 타입이 둘 이상

타입으로만 조회할 때 같은 타입이 여러개면 곤란하다고 했는데 보자 둘 이상이면 오류가 발생하니 이름을 지정해야한다. 진짜 그런지 테스트해보자 

 

> 이렇게 코드를 짜면 스프링 입장에서는 나 뭐를 선택해야하지?하고 예외가 터진다. 

 

-> 같은 타입이 둘 이상일 때 조회

@Test
@DisplayName("둘 이상")
public void findBeanByTypeDup(){
    assertThrows(NoUniqueBeanDefinitionException.class,
            () -> ac.getBean(MemberRepository.class));
}

역시 예외가 터지는 test는 throws를 한다. 같은 타입이 여러개 있다면 이름을 지정해줘라

 

 

-> 특정 타입을 모두 조회하려면

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

 

getBeansOfType을 쓰면 Map으로 키, 벨류로 나온다. 당연히 키가 객체 이름일 것이고 벨류가 빈 객체이다. 검증은 개수가 2개 있나 보는 것으로 하자/ 이거를 알아야하는 이유는 autowired에도 이런게 있기 때문이다.

 

 

 

- 상속관계

빈을 조회할 때 부모 타입으로  조회했을 떄 자식이 여러개 있으면 기본 대원칙이 있다. 스프링에서 부모 타입으로 조회를하면 자식 빈들은 다 같이 조회가 된다. 자식 타입은 그냥 다 끌려나온다고 생각하면 된다.

> 따라서 Object 타입을 조회하면 모든 스프링 빈을 조회한다. 1로 조회하면 1234567이 다 나온다. 무슨 말이냐면 그냥 위와 같은 상황인 것이다. 부모 타입으로 조회하면 그 밑에 모든 타입이 동일한 타입으로 돼서 둘 이상있는 경우가 되어버린다. 따라서 이름을 지정해야한다. 

 

-> 예제를 만든다.

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

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

할인 정책으로 조회하면 자식 타입 2개가 조회가 되도록 해보자 

 

1. 부모 타입으로 조회시 자식이 둘 이상 발생하면 중복 오류가 발생한다.

// 이름을 지정하거나
    
    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상있음ㅕㄴ, 빈 이름을 지정하면 된다.")
    public void findBeanDup(){
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }
}


// 타입을 구체적으로 바꾼다.
    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상있음ㅕㄴ, 빈 이름을 지정하면 된다.")
    public void findBeanDup(){
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(FixDiscountPolicy.class));
    }

여기서 타입을 할인 정책으로 하면 두개가 조회가 돼서 타입으로만 조회했는데 같은 타입이 여러개라 오류가 발생하는 상황이 된다. 이러면 위와 같은 상황이 된 거라서 그냥 throws로 예외가 발생한다고 테코를 쓰면 되고 이를 해결하려면 이름을 지정하면 된다. 또한 구체적인 타입으로 fix라고 해도 에러가 안뜬다.

 

2. 부모 타입으로 모두 조회해보자

fix와 rate가 둘 다 나온다.

 

-> 정리

이렇게 해서 조회하는 것을 다 봤다. 이 정도만 알아두면 된다. 뒤에가면 getBean을 할 일이 거의 없다. 스프링 컨테이너가 자동으로 DI 해주는 걸 쓰거나 DI 부분을 아예 명시를 안한다. 근데 이를 설명을 한 이유는 기본 기능이기 때문이다. 거의 쓸 일은 없지만 알고 있어야 자동 DI에서 문제없이 쓸 수 있기 때문이다.

 

Comments