Spring 개요

뾰족머리삼돌이·2023년 12월 22일
0

Spring

목록 보기
1/11

Spring

객체지향 애플리케이션 개발 프레임워크

간단하게 말하자면 좋은 객체지향 어플리케이션을 개발하도록 환경을 제공해주는 기술이다
Spring Framework의 핵심 개념으로 IoC/DI, AOP, PSA이 있고 스프링 MVC와 같은 웹기술을 제공한다.
이 외에도 DB접근 기술과 테스트 등 개발에 필요하고 유용한 기술들을 제공한다

Spring Framework와 Spring Boot는 엄연히 다르다
Spring Boot에서는 내장 WAS를 제공하고 기본적인 종속성을 제공해주며 AutoConfiguration을 통한 자동설정을 지원한다

IoC/DI

Spring 삼각형이라고 불리는 3대요소중의 하나다
제어의 역전과 의존성주입을 의미하며 객체 생명주기을 프로그램에게 맡기는 형태를 띈다

public class MemberService{
	
    private static final MemberService instance = new MemberService();
    
	private MemberService(){}
    
    public static MemberService getInstance(){
    	return this.instance;
    }
}

예를들어 위와같은 service 클래스의 객체를 사용하려면 MemberService.getInstance()를 호출해서 객체를 얻는 과정이 필요하며, 객체 생성과 사용을 위해 개발자가 직접 코드를 작성해야한다

@Service
public class MemberService{

}

@RestController
public class MemberController{
	private final MemberService memberService;
    
    @Autowired
    public MemberController(MemberService memberService){
    	this.memberService = memberService;
    }
}

Spring 에서는 객체생성을 위한 싱글톤코드의 작성과 사용을 위한 getInstance()의 호출 모두 프레임워크에서 제공한다
이를통해 반복적인 코드의 작성이 아닌 서비스 로직에 대한 집중을 할 수 있다

심지어 Lombok 라이브러리를 이용한다면 @Autowired와 생성자를 작성하지않고 @RequiredArgsConstructor를 이용하여 service객체를 생성할 수 있다

IoC Container

Container는 특정 요소들을 관리( 생성, 사용, 소멸.. )해주는 역할을 한다
Tomcat은 Servlet을 의미하며 Spring에서는 주로 Java 객체, 그 중에서도 Bean들을 관리하는 역할을 맡는다

Bean은 @Component 가 붙어있는 클래스들을 의미한다

Spring에서는 이러한 IoC Container의 역할을 BeanFactory가 맡고있으며, 좀 더 추상화된 ApplicationContext가 주로 이용된다

실제로 Spring Boot에서 디버깅을 진행해보면 WebApplicationContext에서 관리되는 Bean들을 확인할 수 있다

DispatcherServlet

DispatcherServlet의 경우 특이하게도(?) WAS와 IoC Container 모두에서 관리된다

ApplicationContext vs DispatcherServlet : 컨텍스트 간의 관계
위 블로그에 따르면 WebApplicationContext == BeanFactory가 먼저 생성되고 DispatcherServlet이 이를 부모로 가지고 있는다

SpringBoot 에서 dispatcherServlet의 생성로그는 request를 보내면 찍히게되는데, 이 로그는 두번 찍힌다

  1. Spring DispatcherServlet 으로 등록
  2. Servlet 으로 등록

따라서 양쪽 컨테이너 모두 DispatcherServlet을 참조하는 것을 알 수 있다

의존성 주입 ( DI )

Spring에서 관리해주는 Bean들을 주입받는 작업을 의존성 주입이라고 부른다
보통 생성자를 통한 주입을 사용하며, 기본적으로 @Autowired를 통해 주입받을 수 있다

기본적으로 클래스 명칭과 비교해서 일치하는 이름의 Bean을 주입받으며,
필드주입 / setter 주입 / 생성자 주입의 3가지 방법이 존재한다

먼저 Bean으로 등록하기 위해서는 @Component 애노테이션을 사용하면되는데 보통 @Controller, @Service, @Repository를 주로 이용한다


1. 필드주입

@RestController
public class MemberController{ 
	
    @Autowired
   	private MemberService memberService;
}

가장먼저 필드주입의 예시이다
필드주입의 경우 간단하다는 장점이 있지만 순환참조와 final 불가능, 테스트코드 작성의 단점때문에 추천되지 않는다


2. setter 주입

@RestController
public class MemberController{
	private MemberService memberSerivce;
    
    @Autowired
    public void setMemberService(MemberService memberService){
    	this.memberSerivce = memberService;
    }
}

다음으로 setter주입의 예시이다
필드주입과 마찬가지로 순환참조와 final 불가능, 테스트코드 작성의 단점때문에 추천되지 않는다
추가적으로 외부에서 객체를 수정할 수 있다는 위험성이 있다


3. 생성자 주입

@RestController
public class MemberController{
	private final MemberService memberSerivce;
    
    @Autowired
    private MemberController(MemberService memberService){
    	this.MemberService = memberService;
    }
}

마지막인 생성자 주입으로 앞선 방법의 단점이 해결된다
Lombok의 @RequiredArgsConstructor를 이용하면 더 간결하게 작성할 수 있다

이러한 주입방법에서 차이점이 생기는 이유는 주입방식의 차이때문이다
필드와 setter 주입의 경우, 1. Bean을 먼저 생성하고 2. 의존성을 주입받는다
생성자 주입의 경우, 1.생성자에 필요한 Bean을 찾고 2. Bean을 생성한다

ComponentScan

Spring에서는 어떻게 Bean들을 찾아서 등록하는 걸까?
과거에는 직접 xml파일에 Bean으로 등록할 클래스들을 입력하는 방식을 사용해야했다
하지만 최근에는 @ComponentScan 애노테이션을 이용해서 해당 애노테이션이 붙은 클래스 이하의 모든 클래스들을 탐색하는 방법을 사용한다


실제로 @SpringBootApplication에는 위와같이 애노테이션이 붙어있는 모습을 확인할 수 있는데 ComponentScan 대상에서 제외할 수 있는 옵션또한 존재한다

Scope

이러한 Bean들은 기본적으로 Singleton으로 등록되지만 필요에 따라서는 Prototype으로 등록할 수 있다

@Component @Scope("prototype")
public class Proto{ ... }

@Scope 를 통해서 설정가능하며, 주입 받을 때마다 매번 다른 객체가 생성된다
유의할 사항으로 Singleton 객체내에 Prototype의 Bean을 주입받은 경우 항상 동일한 객체가 호출되므로, proxyMode 설정을 추가로 진행해야한다

@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Proto { ... }

이는 프록시를 통해서 매번 새로운 bean을 주입받는 형식으로 동작한다

AOP

관점 지향 프로그래밍의 약자로 특정 작업들 이전과 이후에 항상 동일한 작업을 수행해야하는 경우 사용된다
동적 프록시를 이용하여 구현되며, Dynamic Proxy, CGLib, AspectJ 등이 사용된다

Spring에만 있는 개념은 아니다

주로 메서드 동작 전후에 로그를 찍거나 데이터 검증의 용도로 사용되며 관련된 용어로 Target, Advice, JoinPoint, PointCut이 있다

Target ⇒ AOP를 적용할 대상
Advice ⇒ AOP로 처리할 로직
JoinPoint ⇒ 어느시점에 AOP를 적용시킬 것인지
PointCut ⇒ AOP를 어느 Target 에 적용시킬건지

또한 AOP를 적용시키는 시점으로 3가지가 존재한다
컴파일 / 로드타임 / 런타임

컴파일은 리플랙션 등을 이용해서 AOP를 적용을 시킨 상태로 바이트코드를 생성하는 방식이다

  • 장점
    ⇒ 로드타임과 런타임에 아무런 부하없음
    ⇒ AspectJ의 도움을 받을 수 있음
  • 단점
    ⇒ 내부적으로 컴파일을 한번 더 시킨다

로드타임클래스파일을 로딩시키는 시점에 AOP를 적용시키는 방식이다 ( Load Time Weaving )

  • 장점
    ⇒ 다양한 문법 적용가능 ( AspectJ의 도움 )
  • 단점
    ⇒ 클래스파일 로딩 시 약간의 부하
    ⇒ Load Time Weaver라는 개념..

런타임Target Bean을 생성할때 ProxyBean를 만들어서 Proxy에서 AOP를 적용시키는 방식이다

  • 장점
    ⇒ 간단한 문법
    ⇒ 추가설정 X
  • 단점
    ⇒ proxy 빈을 생성하는 과정에서 약간의 성능이슈
    ⇒ AspectJ의 도움을 못받음

사용예시

Target ⇒ AOP를 적용할 대상
Advice ⇒ AOP로 처리할 로직
JoinPoint ⇒ 어느시점에 AOP를 적용시킬 것인지
PointCut ⇒ AOP를 어느 Target 에 적용시킬건지

@Aspect
@Component
public class MyAspect {

    @Before("execution(* me.ddings.spring_fw.AOP.UserService.*())")
    public void validateMy(JoinPoint joinPoint) throws Throwable {
        System.out.println("joinPoint.getArgs()으로 얻은 데이터 검증!!!");
    }

    @Around("execution(* me.ddings.spring_fw.AOP.UserService.*())")
    public Object logMy(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        System.out.println("로그 찍음!!!!");
        Object retVal = proceedingJoinPoint.proceed();
        System.out.println("로그 찍음!!!!");
        return retVal;
    }

}

코드를 하나씩 살펴보면
@Around("execution(* me.ddings.spring_fw.AOP.UserService.*())")
에서 Around() 안에 들어가는 부분이 PointCut ( AOP를 적용할 타겟 ) 이다

메서드안에 처리하는 로직은 Advice이며, JoinPoint는 PointCut을 통해 가져온 Target의 특정 시점을 의미한다
여기서 실제로 진짜메소드를 실행하는건 ProceedingJoinPointproceed()했을때다

JoinPointproceed() 기준으로 이전, 리턴 후 등을 체크해서 실행된다
AOP를 적용하기 위해서는 클래스 선언 시 @Aspect를 사용하면 되며, Spring AOP에서는 JoinPoint를 메소드 실행시 만 지원한다

좀 더 세분화된 JoinPoint가 필요하다면 Java의 AspectJ를 사용해야한다
Spring AOP와 AspectJ 비교하기

AOP 동작시점 종류

@Aspect
@Component
public class MyAspect {

    @Before("@annotation(LogginProcess)") // 이전
    public void beforeAnno(JoinPoint joinPoint){
        System.out.println("@Before =>" + joinPoint.getSignature());
    }

    @After("@annotation(LogginProcess)") // 
    public void afterAnno(JoinPoint joinPoint){
        System.out.println("@After =>" + joinPoint.getSignature());
    }

    @AfterReturning("@annotation(LogginProcess)")
    public void afterReturningAnno(JoinPoint joinPoint){
        System.out.println("@AfterReturning =>" + joinPoint.getSignature());
    }

    @AfterThrowing("@annotation(LogginProcess)")
    public void afterThrowingAnno(JoinPoint joinPoint){
        System.out.println("@AfterThrowing =>" + joinPoint.getSignature());
    }

    @Around("@annotation(LogginProcess)")
    public Object logMy(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        System.out.println("로그 찍음!!!!");

        Object retVal = null;
        try {
            retVal = proceedingJoinPoint.proceed();
        }catch(Exception e){
            e.printStackTrace();
        }

        System.out.println("로그 찍음!!!!");
        return retVal;
    }

}

Spring AOP는 메서드 동작시에만 AOP를 적용할 수 있으며 총 5가지의 동작시점이 존재한다

  1. @AroundproceedingJoinPoint.proceed()를 기준으로 시작 전/후에 실행코드를 작성할 수 있다
  2. @BeforeproceedingJoinPoint.proceed()가 호출되기 직전에 끼어들어서 실행된다
  3. @AfterRetuningproceedingJoinPoint.proceed()가 끝나고나면 실행된다
  4. @After@AfterRetuning 이후에 실행된다
  5. @AfterThrowing ⇒ Exception이 발생하면 @AfterRetuning 대신에 실행된다

❓PointCut 종류

  1. execution ( 표현식 )

    표현식을 표기하는 방법은 다음와 같다
    [ 접근제한자 ] 리턴타입 클래스경로.메소드이름(파라미터 개수)
    접근제한자는 말 그대로 public, private 등을 의미하고
    리턴타입은 해당 메소드의 리턴타입,
    클래스경로는 FQCN,
    파라미터개수는 ..는 0개 이상을 의미한다

  2. within ( FQCN )

    1번에서 설명한 표현식을 사용해도 된다

  3. bean( bean이름 )

    반드시 bean으로 등록한 클래스의 첫글자를 소문자로 입력 ( 카멜표기법 ) 해야한다

**@annotation**

@Retention(RetentionPolicy.CLASS) // 기본 CLASS ( 생략가능 )
public @interface LoginProcess {
}
@Around("@annotation(LoginProcess)")
public Object logMy(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
    System.out.println("로그 찍음!!!!");
    Object retVal = proceedingJoinPoint.proceed();
    System.out.println("로그 찍음!!!!");
    return retVal;
}

Bean으로 설정한 클래스에서 해당 애노테이션이 달린 메소드들을 찾아서 AOP를 적용해준다

Spring AOP PointCut 표현식 정리

⚠️ 번외. Interface타입이 있으면 Interface타입으로 주입받아라

AOP를 사용할 때, SpringBoot가 아닌경우에 기본적으로 생성되는 Proxy객체는 Interface의 구현체이다

만약 현재 클래스 설계상에서 A ( interface )B ( class ) 가 implements 하고있고
B는 Bean으로 등록되어있다고 생각해보자

이 상황에서 A를 구현하는 모든 sub class들에 AOP를 적용시키고 싶어서 A에 AOP를 적용시켰다
이때 Spring에게 B타입의 객체를 주입해달라고 하면 에러가 발생한다
그 이유는 Interface의 구현체로 생성된 Proxy타입의 객체B타입의 객체와 다르기 때문이다

코드로 나타내보면 아래와 같다

public class B implements A{ ... }

public class Proxy implements A{ ... }

여기서 B 객체 = new Proxy() 의 상황인거다

하지만 A타입의 객체로 Bean을 주입해달라고 하면 정상작동 하는데, 이 경우에는 다음과 같이 Proxy가 생성된다

public class Proxy extends B{ ... }

즉, B클래스를 상속하는 Proxy가 생성된다

SpringBoot는 기본적으로 클래스기반으로 Proxy를 생성해서 에러가 발생하지 않는다.

PSA

Portable Service Abstraction

추상화를 통해 여러서비스를 묶어서 제공하는 것으로, @Transactional을 예로 들 수 있다

요청 처리를 Servlet과 비교해본다면

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// ...
		super.doGet();
}
  1. Get요청이 들어오면 request에 담긴 데이터가 한글일 경우 Encoding 설정
  2. url을 판단해서 처리하는 로직을 가진 메소드로 분기
  3. 결과로 얻어낸 데이터에 한글이 들어있을 경우 response Encoding 설정
  4. client에게 보여줄 페이지 작성
  5. 응답을 foward로 할지 redirect로 할지 결정

등의 작업을 모두 작성해줘야한다


반면 Spring에서는 어떻게 처리하는지 살펴보자

@GetMapping("/login")
public String login(){
    return "/";
}
  • @GetMapping을 다는 순간부터 url에 맞는 메서드로 받을 수 있고
  • foward , redirect도 response를 거칠필요없이 기본으로 forward / “redirect:” 를 달면 redirect로 응답할 수 있다
  • 동적 페이지도 미리 만들어놓은 HTML에 데이터를 끼워넣기만 하면된다
  • 전역설정으로 전체 encoding도 설정할 수 있다

즉, Spring은 사용자가 메인 로직에만 집중할 수 있도록 환경을 조성해줘서 코드를 거의 변경하지않고 여러 웹 스택기술을 사용할 수 있게 해주며 이러한 구조를 PSA가 적용되었다 라고 볼 수 있다

0개의 댓글