streetprogrammer

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

WEB/SPRING

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

차완호미 2021. 12. 28. 17:00

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

 

- SRP : 단일 책임 원칙 (한 클래스는 하나의 책임만 가져야 한다.)

- OCP : 개방-폐쇄 원칙 (확장에는 열려 있으나 변경에는 닫혀 있어야 한다.)

- LSP : 리스코프 치환 원칙 (부모타입에서 자식타입으로 변경해도 실행 문제가 없어야 한다.)

- ISP : 인터페이스 분리 원칙(인터페이스는 기능에 따라  분리 되어야 한다)

- DIP : 의존관계 역전 원칙 (구현 클래스에 의존하지 말고, 인터페이스에 의존해야 한다.)



기존 설계는 service에서 discountPolicy를 받을떄 new FixDiscountPolicy를 활용하기 때문에 

추상화가아닌 추체클래스도 의존을 한다 => “DIP 위반”

 

또한,

service에서 기존 FixDisCountPolicy가 아닌 RateDisCountPolicy로 변경할 경우 service내부의 코드를 수정해야 한다. => “OCP 위반” 

 

이를 해결하기 위해 AppConfig클래스 활용

 

[AppConfig.class] 

 

AppConfig에서는 구성 정보의 역할과 구현을 명확하게 분리하고 연결함

이렇듯 프로그램에 대한 제어흐름에 대한 모든 권한을 가지고 관리하는것을 제어의 역전 (IoC)라고 한다.

이를 IoC컨테이너 혹은 DI컨테이너 라고 한다. 

 

이러한 AppConfig를 더 효율적으로 관리 해주기 위해 스프링을 사용한다.

 

@Configuration를 AppConfig상위에 해당 어노테이션을 추가하고 매서드 상단에 @Bean을 추가한다.

 

[스프링 생성 및 사용]

 

1.처음 스프링이 시작되면 @Configuration 어노테이션을 확인 하고 해당 클래스 안에  

   @Beand어노테이션들은 스프링 빈 저장소에 추가한다. 

2.이떄 해당 메서드이름이 빈이름으로 자동 등록된다. (beanName은 수정가능 근데 하지마)

3.이후 설정 정보를 참고해서 싱글톤 패턴으로  의존관계가 주입(DI)된다.  

 

// 스프링컨테이너 생성

ApplicationContext ac = new AnnotationcConfigApplicationContext(AppConfig.class);

  • ApplicationContext : 스프링컨테이너를 의미 (인터페이스)
  • AnnotationcConfigApplicationContext : 스프링 구현 클래스

- 모든 빈 출력하기 

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

 

- 빈 유형에 따라 출력하기 

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

 

- 빈 타입으로 조회

@Test
@DisplayName("빈 없이 타입으로만 조회")
void findBeanByType() {
    MemberService memberService = ac.getBean(MemberService.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
  • 이때 해당 타입이 여러개일경우 NoUniqueBeanDefinitionException

 - 빈 이름으로 조회

@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
    MemberService memberService = ac.getBean("memberService", MemberService.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
  • 이때 해당 이름 이 없을 경우 NoSuchBeanDefinitionException 

 

- 모든 특정 타입 빈 조회

@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);
}
  • 부모 빈 타입 조회시 자식 빈들까지 다 나옴 (object를 최고 부모)

[@Congiuration]

목표 : @Congiuration의 역할 알아보기

싱글톤을 유지시키기위해 사용한다.

해당 어노테이션을 사용하면 클래스가 복사되어 가짜 클래스를 사용하게됨

 

[컴포넌트 스캔과 의존관계 자동 주입 시작하기]

목표 : 수동으로 빈등록하지말고 편리한 자동주입을 사용해보자

 

1. @ComponentScan 등록 ( 스프링부트에서는 되어있음)

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
)

public class AutoAppConfig {

}

* Confiuration은 제외하기 위해 excludeFilters를 추가함 why @Confiuration때문에 수동 빈들도 등록되기 떄문에 컴포넌트 스캔이 제대로 작동됐는지 확인이 안됨

 

2. 모든 클래스에 @Component 등록

@Component
public class MemberServiceImpl implements MemberService{

[생성자 주입을 해라!]

목표 : DI할떄 가장 좋은 방법 알아보기

장점 

1. 불변 : 객체를 생성할때 1번만 호출되므로 불변하게 설계할 수 있다.

2. 누락 : 프레임워크 없이 순수한 자바 코드를 단위 테스할때 누락되지 않는다.

3. final : final 키워드 사용가능 값이 설정되지 않는 오류를 컴파일러 시점에서 막아준다.

 

[롬복 라이브러리 사용]

목표 : 더 편리하게 DI와 Getter/Setter , toString 하기

 

1. build.gradle에서 추가

//lombok 라이브러리 추가 시작
compileOnly 'org.projectlombok:lombok'

2. 플러그인 설치 여부 확인 

file > settings > Plugins 에서 lombok 검색 후 설치

 

3. file > settings > Compiler > Annotation Processors 에서 Enable annotation processing 체크

 

사용방법

- 상단에 어노테이션 등록하면 사용할 수 있다.

@Getter
@Setter
@ToString
public class HelloLombok {

    private String name;
    private int age;

 

lombok으로 생성자 만드는방법

-현재 클래스에서 final로 만들어진 변수의 생성자를 자동으로 만들어준다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
    /**
     * lombok 사용하기 : @RequiredArgsConstructor통해서 생성자를 생략해도 자동으로 만들어짐
     * 단, final 필수
     */
    private  final MemberRepository memberRepository;
    private  final DiscountPolicy discountPolicy;

[조회 빈이 2개 이상 -문제]

목표 : 조회된 빈이 두개 이상일떄 발생하는 오류 해결하기 

 

1. @Autowired 필드명

- 생성자 주입시 필드 명을 통해서 원하는 구현체의 빈을 찾아올수 있다.

private  final MemberRepository memberRepository;
private  final DiscountPolicy discountPolicy;

public OrderServiceImpl(MemberRepository memberRepository,
                         DiscountPolicy rateDiscountPolicy) { // discountPolicy -> rateDiscountPolicy
    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}

2. @Qualifire ( 추가 구문자 )

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{

- @Quilifier 등록 후 사용

public OrderServiceImpl(MemberRepository memberRepository,
                        @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

* 만약 못찾으면 해당 이름의 스프링 빈을 찾는다 그래도 없으면 예외

 

3. @Primay

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{

* @Primay로 등록하면 해당 클래스가 우선권을 가지게 된다. 주로 많이 사용됨


[조회한 빈이 모두 필요할 떄, List, Map]
목표 : 클라이언트가원하는 구현객체 이용하기 (동적으로 빈 선택하기)

 

1. DiscountPolicy 인터페이스를 Map으로 받기 (정액제 , 할인제)

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

2. 클라이언트가 원하는 discount 구현체를 선택 후 discount 결과값 넘겨주면 Map에서 해당데이터 리턴 

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

    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}

*클래스를 바로 new AnnotationConfigApplicationContext에 등록하면 빈등록이 됨

 

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

목표 : 자동을 쓰다가 언제 수동 빈 등록을 사용하면 좋을지 알아보기

 

결론 : 자동을 주로 사용하자 why 자동으로도 OCP, DIP를 지킬수있다.

*명확히 나타내려면 수동을 써야함.

 

[빈 생명주기 콜백]

목표 :  원하는 어플리케이션이 종료되면 빈도 사용종료

@Test
public void lifeCycleTest(){
    ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
    NetworkClient client = ac.getBean(NetworkClient.class);
    ac.close();
}

*스프링을 종료시키려면 기존 ApplicationContext 말고 ConfigurableApplicationContext가 필요

*스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입-> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

* 객체를 생성하는 부분과 초기화는 분리해야됨 초기화의 경우 외부 커넥션을 연결하는 등 무거운 동작을 수행하기때문

 

[인터페이스로 초기화 소멸전 콜백]

public class NetworkClient implements InitializingBean, DisposableBean {

InittializingBean : 의존관계 주입이 끝난 후 실행하는 메서드를 가지고 있음DisposableBean :  종료전 콜백 메서드를 가지고 있음

@Override
public void afterPropertiesSet() throws Exception {
    System.out.println("NetworkClient.afterPropertisSet");
    connect();
    call("초기화 연결 메세지");
}
@Override
public void destroy() throws Exception {
    disconnect();
}

단점 (사용안함)- 스프링 전용 인터페이스로 스프링전용 인터페이스에 의존한다.- 초기화 소멸 메서드의 이름을 변경할 수 없다.- 외부 라이브러리에 적용할 수 없다.

 

[빈 등록 초기화,소멸 메서드]

@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient(){
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-core.dev");
        return networkClient;
    }
}

* @Bean에다가 초기화메서드 이름과 종료메서드 이름을 등록해 사용한다.

public void init(){
    System.out.println("NetworkClient.afterPropertisSet");
    connect();
    call("초기화 연결 메세지");
}
public void close(){
    disconnect();
}

장점 

- 메서드 이름을 자유롭게 지정

- 스프링 빈이 스프링 코드에 의존하지 않는다

- 외부라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

추가로 @Bean의 destoryMethod는 디폴트로 종료메서드를 찾아서 동작시킴 (shutdow, close 등 )

만약 해당 기능을 false로 놓고 싶으면 destoryMethod = ""로 등록하세요.

 

[어노테이션을 이용한 방법]

@PostConstruct/@PreDestroy 어노테이션을 사용하면됨

@PostConstruct
public void init(){
    System.out.println("NetworkClient.afterPropertisSet");
    connect();
    call("초기화 연결 메세지");
}
@PreDestroy
public void close(){
    disconnect();
}

단점- 외부라이브러리에서 적용 불가능 잠정- 가장 많이 사용하고있음 그냥 이거쓰세요.

 

[빈스코프]  (빈 생명주기)

목표 : 스코프에 대한 기초개념을 학습

 

싱글톤 스코프 :  컨테이너의 시작과 종료까지 유지되는 가장 넒은 범위의 스코프이다. 기본적으로 싱글톤스코프를 지원

프로토 타입 스코프 : 빈의 생성과 의존관계 주입 까지만 관여한다.

웹 관련 스코프 

1. request : 웹 요청이 들어오고 나갈떄 까지 유지되는 스코프

2. session : 웹세션이 생성되고 종료될 떄 까지 유지되는 스코프

3. application : 웹의 서블릿 컨텍스와 같은 범위로 유지되느 스코프

 

[프로토타입 스코프]

목표 : 프로토타입 스코프의 심화개념 학습

@Scope("prototype")
static class PrototypeBean {
    @PostConstruct
    public void init(){
        System.out.println("PrototypeBean.init");
    }
    @PreDestroy
    public void destory(){
        System.out.println("PrototypeBean.destory");
    }
}

프로토타입의 경우 요청할떄마다 새로운 인스턴스를 생성해서 의존관계 부여 후 초기화 처리한뒤 반환한다. 이후로 관여안함 즉 종료는 클라이언트가 마무리 지어야함 

 

[프로토타입 스코프와 싱글톤 빈이 함꼐 사용시 문제점]

- 싱글톤안에 프로토타입이 요청될 경우 단 한번의 요청 후 변경되지 않아 사용자가 원하는 프로토타입의 효과를 볼수가없다.  이를 해결하려면 해당 매서드에서 applicationConext~~를 불러야함 이건 너무 무식

 

[프로토타입 빈 - 싱글톤 빈과 사용시 Provider로 문제해결]

목표 : 원하는 빈만 찾기(DL : dependency lookup) 위해서 Provider을 이용해 프로토타입 빈을 찾아 이용한다.

1. ObjectProvider 방법 

@Scope("singleton")
static class ClientBean{

    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;

    public int logic(){
        PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return  count;
    }
}

하지만 스프링에서 만들어주기떄문에 스프링에 의존적이다.

 

2. JSR303 Provider 방법

* build.gradle에 추가한다.

implementation 'javax.inject:javax.inject:1'
    @Scope("singleton")
    static class ClientBean{

        @Autowired
//        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
        private Provider<PrototypeBean> prototypeBeanObjectProvider;
        public int logic(){
//            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return  count;
        }
    }

* 프로토 타입빈은 매번 사용할 떄 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용한다. 하지만 드믈다.

 

[웹스코프] - 종료메서드까지 호출해줌

 

1. request : http 요청마다 들어오고 나갈때까지 유지되는 스코프

- 동시에 2명이 요청하면 각각 다른 스프링빈이 생성되서 사용할 수 있음

- 요청이 같은경우 같은 스프링빈을 사용한다.

2. session : http Session과 동일한 생명주기를 가지는 스코프

3. application 서블릿 컨텍스트와 동일한 생명주기 

4. websocket : 웹소켓과 동일한 생명주기

 

[request 스코프 예제 만들기]

목표 : 예제를 통해 request 스코프 이해하기

상황 : 여러 요청을 로그로 남길때 request 스코프와 UUID를 활용해 개발해보자

문제 : request 스코프 자체는 사용자가 요청시 만들어지는데 스프링이 작동될떄는 존재하지않음 => 에러 발생

해결 : Provider를 활용해 해결해보자

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
//    private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        String requestURL = request.getRequestURL().toString();
        myLogger.setReqeustURL(requestURL);

 

[스코프와 프록시]

목표 : proxyMode를 추가하면 기본의 Provider를 사용안해도된다.

프록시는 가짜를 만들어줌 이떄 TARGET_은 대상이 class일경우 CLASS interface일 경우 INTERFACE로 지정

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
@Service
@RequiredArgsConstructor
public class LogDemoService {

//    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
    private final MyLogger myLogger;

    public void logic(String id) {
//        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

 

 

포트변경properties에서 

server.port= 9090 

 

 

 

Comments