반응형

[0] 스프링

  • 자바 언어 기반의 프레임워크
  • 자바 언어의 가장 큰 특징인 "객체 지향 언어"가 가진 강력한 특징을 살려내는 프레임워크
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

스프링 활용 기술

핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타
웹 기술: 스프링 MVC, 스프링 WebFlux
데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원
기술 통합: 캐시, 이메일, 원격접근, 스케줄링
테스트: 스프링 기반 테스트 지원
언어: 코틀린, 그루비
최근에는 스프링 부트를 통해서 스프링 프레임워크의 기술들을 편리하게 사용

 

[1] 스프링 부트

  • 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
  • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨 손쉬운 빌드 구성을 위한 starter 종속성 제공
  • 스프링과 3rd party(외부) 라이브러리 자동 구성
  • 메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능 제공
  • 관례에 의한 간결한 설정

 

- 객체 지향 프로그래밍

4가지 특징 : 추상화, 캡슐화, 상속, 다형성

 

[3] IoC, DI, 컨테이너

1] IoC (Inversion of Control) : 제어의 역전

Appconfig 등장 이후로 프로그램의 제어 흐름은 AppConfig가 담당하고 구현객체는 자신의 로직을 실행하는 역할만 담당한다.

ex) OrderServiceImp 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행되는지는 알 수 없다 (AppConfig에서 객체가 생성되고 할당된다)

즉, 프로그램에 대한 제어 흐름에 대한 모든 권한이 AppConfiga에게 있는것인데 이처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전 (IoC) 이라고 한다.

 

 

프레임워크 vs 라이브러리

- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다.

ex) JUnit

- 반면 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리이다.

ex) java 객체를 json이나 xml로 변환하려 할때 직접 호출하는 것은 라이브러리를 사용하는 것이다.

 

2] DI (Dependency Injection) : 의존관계 주입

OrderServiceImpl은 DiscountPolicy 인터페이스에 "의존" 하지만 실제 어떤 구현 객체가 사용될지는 모른다.

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

- DI란?

어플리케이션 실행 시점 (런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 DI (의존관계 주입) 이라고 한다.

 

  • 객체 인스턴스를 생성하고 그 참조갓을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

- 정적인 클래스 의존관계

class가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있으며 프로그램을 실행하지 않아도 분석할 수 있다.

-> intelliJ 툴에서 볼수 있다 : Diargrams > show Diagram > show Dependency

 

BUT !! 이러한 정적인 클래스 의존관계로는 실제로 어떤 객체가 OrderServiceImpl에 주입 될지 알 수 없다.

 

- 동적인 객체(인스턴스) 의존관계

어플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계이다.

 

3] IoC컨테이너 혹은 DI컨테이너

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

 

[4] 싱글톤 패턴과 싱글톤 컨테이너

1] 싱글톤 패턴

클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴

구현 예

package hello.core.singleton;
 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() {
}
public void logic() { System.out.println("싱글톤 객체 로직 호출");
} }

단점

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

 

=> 이러한 싱글톤 패턴의 단점을 보완하고, 객체를 단 한개만 생성하게 해주는 것이 스프링의 싱글톤 컨테이너 이다

 

2] 싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.

스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

//Appconfig.java
@Configuration // 구성정보, 설정정보
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
}
// 싱글톤으로 생성되는지 확인하기
	@Test
    @DisplayName("스프링 컨테이너와 싱글톤") void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        //1. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        //2. 조회: 호출할 때 마다 같은 객체를 반환
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        //참조값이 같은 것을 확인
        System.out.println("memberService1 = " + memberService1); System.out.println("memberService2 = " + memberService2);
        //memberService1 == memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

 

스프링은 99%는 싱글톤 빈을 사용하지만 가끔 싱글톤을 사용하지 않고 요청때마다 새로운 객체를 생성하는 기능도 제공한다.

 

3] 싱글톤 사용방식의 주의점

- 무상태로 하기

  • 객체를 딱 하나 생성해서 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다 즉, stateless (무상태) 해야 한다.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
  • 가급적 읽기만 가능해야 한다.

- No 필드 -> 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용하기

필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다. 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

 

4] @Configuration과 싱글톤

AppConfig에 @Configuration 어노테이션을 사용함으로써 싱글톤으로 스프링 빈이 등록되도록 한다.

해당 어노테이션을 사용하면 AppConfig 에 CGLIB가 붙어 싱글톤으로 동작하게 해준다.

만약 해당 어노테이션을 제거하고 @Bean만 사용할 경우 스프링 컨테이너에 스프링빈이 등록되는데 중복된 객체가 생성되어 싱글톤으로 생성되지 않는다.

 

[5] 컴포넌트 스캔

AppConfig 파일로 Configuration과 Bean 어노테이션을 활용해 설정정보를 작성하고 의존관계를 명시하는 방법이 있지만, 등록해야 할 빈이 많게 되면 코드의 유지보수가 복잡해진다. 이럴때 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공하는데 이를 활용할 수 있다. 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.

 

1] @Component 와 @Autowired

@Component : 빈으로 등록할 객체에 대해 해당 어노테이션을 붙인다

@Autowired : 객체가 등록될때 필요한 의존관계를 자동으로 주입해 준다. 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

 

2] 컴포넌트 스캔 기본 대상

  • @Component : 컴포넌트 스캔에서 사용
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용. 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나~ 라고 비즈니스 계층을 인식하는데 도움이 된다.
  • @Repository : 스프링 데이터 접근 계층에서 사용하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration : 스프링 설정 정보에서 사용 + 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리
    를 한다.

 

[6] 의존관계 @Autowired

의존관계는 스프링 빈에 등록된 객체에만 주입할수 있다!! @Component로 등록되어 있어야 한다는 뜻

1] 의존관계 주입 방법 4가지

- 생성자 주입

class 명과 동일한 생성자 메소드 작성하여 해당 메소드에 @Autowired 명시하기

생성자 호출시점에 딱 1번만 호출되는 것이 보장되며 "불변/필수" 의존관계에 사용된다.

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

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

- setter 주입 (수정자 주입)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법

"선택 / 변경" 가능성이 있는 의존관계에 사용한다.

@Component
public class MemberServiceImpl implements MemberService{

    private MemberRepository memberRepository;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
}

- field 주입

코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
DI 프레임워크가 없으면 아무것도 할 수 없다.

=> 사용을 지양한다

@Component
public class MemberServiceImpl implements MemberService{

    @Autowired private MemberRepository memberRepository;
    
}

- 일반 메소드 주입

한번에 여러 필드를 주입 받을 수 있다. 일반적으로 잘 사용하지 않는다.

@Component
public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

    @Autowired
    public void init(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

[7] 롬복 라이브러리

Getter, Setter, ToString, 생성자 주입 등 다양한 기능 지원

1] 생성자 주입 롬복 사용 :  @RequiredArgsConstructor

@RequiredArgsConstructor : final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

사용 전

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

사용 후

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

 

 

[8] 빈 생명주기 콜백

객체의 생성과 초기화 단계적 예시

예시 클래스 생성

package hello.core.lifecycle;

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출 : " + url);
        connect();
        call("초기화 연결 메세지");
    }


    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect : " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + "message : " + message);
    }

    // 서비스 종료시 호출
    public void disconnect() {
        System.out.println("close : " + url);
    }
}

 

위 클래스 객체를 생성하고 초기화 하여 값 확인해보기

package hello.core.lifecycle;

import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

    @Configuration
    static class LifeCycleConfig {

        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

 

위 테스트 코드 결과

/**
 * 생성자 호출 : null
 * connect : null
 * call : nullmessage : 초기화 연결 메세지
 * */

 

- 스프링 빈의 이벤트 라이프사이클

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

 

- 초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출

- 소멸전 콜백 : 빈이 소멸되기 직전에 호출

 

- 스프링의 빈 생명주기 콜백 방법 3가지

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

 

  • 인터페이스(InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruct, @PreDestroy 애노테이션 지원

 

1) 인터페이스(InitializingBean, DisposableBean)

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출 : " + url);
    }


    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect : " + url);
    }

    public void call(String message) {
        System.out.println("call : " + url + "message : " + message);
    }

    // 서비스 종료시 호출
    public void disconnect() {
        System.out.println("close : " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메세지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

 

위 상태로 코드 수정 뒤 테스트 코드를 실행하면 다음과 같다

생성자 호출 : null
connect : http://hello-spring.dev
call : http://hello-spring.devmessage : 초기화 연결 메세지
close : http://hello-spring.dev

Process finished with exit code 0

 

하지만 스프링 전용 인터페이스에 의존적이고, 초기화와 소멸 메소드의 이름을 변경할수 없고, 외부 라이브러리에 적용이 안되어서 잘 사용하지 않는 방법이다.

 

2) 설정 정보에 초기화 메서드, 종료 메서드 지정

1번방법의 implements를 삭제하고 초기화와 소멸 메소드 명을 커스텀한다.

public void init() throws Exception {
    connect();
    call("초기화 연결 메세지");
}

public void close() throws Exception {
    disconnect();
}

 

그리고 나서 해당 클래스가 등록된 Bean의 설정에 초기화와 종료 메소드를 지정한다

@Configuration
static class LifeCycleConfig {

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

 

이후 테스트 코드를 실행하면 다음과 같다.

생성자 호출 : null
connect : http://hello-spring.dev
call : http://hello-spring.devmessage : 초기화 연결 메세지
close : http://hello-spring.dev

Process finished with exit code 0

 

위 방법을 사용하면 외부라이브러리에도 사용 가능하다.

 

3) @PostConstruct, @PreDestroy 애노테이션 지원

생성자와 소멸자 메소드에 어노테이션을 추가한다. 2번에서 추가한 Bean 설정은 제거한다.

@PostConstruct
public void init() throws Exception {
    connect();
    call("초기화 연결 메세지");
}

@PreDestroy
public void close() throws Exception {
    disconnect();
}

 

하지만 이 방법은 외부 라이브러리에는 적용하지 못한다. 

3번 방법을 가장 지향하지만, 외부라이브러리에 적용이 필요하다면 2번 방법을 사용하자!

 

[9] 빈 스코프

1] 스프링에서 지원하는 다양한 스코프

  • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프. 호출할때마다 동일한 스프링빈이 호출된다.
  • 프로토타입 : 스프링 컨테이너가 빈의 생성과 의존곤계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프. 호출할때마다 새로운 빈이 호출된다. 빈이 요청되는 시점 스프링 컨테이너는 프로토타입 빈을 생성하고 필요한 의존관계를 주입한다. @PreDestroy가 동작하지 않는다.
  • 웹 관련 스코프
    • request : 웹 요청 in out 때까지 유지
    • session : 웹 생선 생성~종료 까지 유지
    • application : 웹 서블릿 컨텍스와 같은 범위로 유지
반응형

+ Recent posts