3장 스프링 프레임워크의 핵심 기능 알아보기

Peter·2023년 3월 29일
0
post-thumbnail

3-1-1 의존성 주입 (Dependency Injection)

의존성 주입: 의존하는 부분을 외부에서 주입하는 것

3-1-2 관점 지향 프로그래밍 (Aspect Oriented Programming)

AOP의 두 가지 요소

  1. 중심적 관심사(Primary Concern): 실현해야 할 기능을 나타내는 프로그램
  2. 횡단적 관심사(Crosscutting-Concerns): 본질적인 기능은 아니지만 유지보수 등의 관점에서 반드시 필요한 기능을 나타내는 프로그램

AOP: 공통 처리 등의 '횡단적 관심사'를 추출하고 프로그램의 여러 곳에서 호출할 수 있게 설정함으로 개발자는 실현해야 할 기능인 '중심적 관심사'에만 집중해서 작성하면 되는 구조

3-2-1 의존성

예시
사용하는 객체 A클래스와 사용되는 객체 B클래스가 있다고 하자
A 클래스에서 B 클래스를 사용하려면 new 키워드를 이용해 B 클래스의 인스턴스를 생성하고 B 클래스의 메서드를 사용한다. 이때 B 클래스에서 구현한 메서드를 변경하면 그 영향으로 A 클래스에서도 해당 메서드를 변경해야 한다. 이런 관계를 'A 클래스는 B 클래스에 의존한다' 라고 한다.

의존에는 두 가지 유형이 있다.
1. 클래스 의존(구현 의존)
2. 인터페이스 의존

3-2-2 클래스 의존

예시
사용하는 객체 A 클래스에서 사용되는 객체 B 클래스의 메서드를 호출하는 경우

// A 클래스
xxx(){
    B b = new B();
    b.methodX();
// B 클래스
methodX() {
	...
}

설계가 변경돼 사용되는 객체를 C 클래스로 변경하고 methodY 메서드를 호출하도록 변경해야 하면 어떻게 해야할까

// A 클래스
xxx() {
	C c = new C();
    c.methodY();
}
// C 클래스
methodY(){
	...
}

여기서 문제 -> 이처럼 '사용하는 객체' 클래스에서 '사용되는 객체' 클래스의 타입을 직접 지정하면 '사용되는 객체' 클래스를 변경할 경우 이를 이용하고 있는 곳을 모두 수정해야 한다.

3-2-3 인터페이스 의존

예시
I 인터페이스가 있고 그것을 구현한 '사용되는 객체' B 클래스가 있다. '사용하는 객체' A 클래스에서 B 클래스의 methodX 메서드를 호출한다.

  • A 클래스에서 new 키워드를 사용해 B 클래스의 인스턴스를 생성
  • 인스턴스에서 methodX 메서드를 호출

A 클래스에서 인터페이스로 추상화된 I를 이용한다

// A 클래스
xxx(){
	I i = new B();
    i.methodX();
}
// I 인터페이스
methodX();
// B 클래스 implements I
methodX(){
	...
}

설계 변경이 발생해 '사용되는 객체' 클래스를 변경하게 되었다. 어떻게 수정해야 할까

// A 클래스
xxx(){
	I i = new C();
    i.methodX();
}

클래스 의존과 비교해 수정한 곳이 줄었다
이처럼 인터페이스를 구현한 '사용되는 객체' 클래스를 변경하는 경우 다음과 같은 이점이 있다.
1. 인터페이스는 참조를 받는 유형으로 사용할 수 있으므로 변수의 이름을 변경하지 않아도 된다.
2. 인터페이스가 선언된 메서드를 이용하면 클래스가 바뀌어도 메서드명을 변경하지 않아도 된다.

3-2-4 인터페이스에 의존하는 프로그램 만들기

// Calculator 인터페이스
package chapter03.used;

/**
 * 계산 처리
 */
public interface Calculator {
    /**
     * 계산 처리를 함
     * @param x
     * @param y
     * @return Integer
     */
    Integer calc(Integer x, Integer y);
}
// 구현 클래스(AddCalc)
package chapter03.used;

/**
 *  Calculator 구현 클래스<br>
 *  덧셈 처리
 */
public class AddCalc implements Calculator{
    @Override
    public Integer calc(Integer x, Integer y){
        return x + y;
    }
}
// 구현 클래스(SubCalc)
package chapter03.used;

/**
 * Calculator 구현 클래스<br>
 * 뺄셈 처리
 */
public class SubCalc implements Calculator{
    @Override
    public Integer calc(Integer x, Integer y){
        return x - y;
    }
}
// Call 클래스
package chapter03.use;

import chapter03.used.AddCalc;
import chapter03.used.Calculator;
import chapter03.used.SubCalc;

/**
 * 인터페이스 의존 확인
 */
public class Call {
    public static void main(String[] args){
        //Calculator calculator = new AddCalc();
        Calculator calculator = new SubCalc();
        Integer result = calculator.calc(10, 5);
        System.out.println("계산 결과는 (" + result + ")입니다.");
    }
}

인터페이스 의존으로 작성하면 '사용하는 객체' 클래스에서 수정할 부분이 줄어든다. 하지만 의존성 주입을 사용하면 '사용하는 객체'의 클래스를 수정하지 않아도 된다.

3-2-5 DI 컨테이너

의존성 주입: 의존하는 부분을 외부에서 주입하는 것

  • 의존하는 부분: '사용하는 객체' 클래스에 '사용되는 객체' 클래스가 작성된 상태
  • 외부로부터 주입: '사용하는 객체' 클래스의 밖에서 '사용되는 객체' 인스턴스를 주입하는 것

지금까지 인스턴스를 생성하는데 new 키워드를 사용했지만 인스턴스 생성과 같은 작업을 프레임워크에 맡길 수 있고 그 역할을 하는 것이 DI 컨테이너다. 스프링 프레임워크는 임의로 구현한 클래스를 인스턴스로 만들어주는 기능을 제공한다.

3-2-6 다섯 가지 규칙

DI 컨테이너에 인스턴스 생성을 맡기고 다음의 규칙을 지키는 것으로 '사용하는 객체' 클래스를 전혀 수정할 필요가 없게끔 만들 수 있다.

  1. 인터페이스를 이용하여 의존성을 만든다.
  2. 인스턴스를 명시적으로 생성하지 않는다.
  3. 어노테이션을 클래스에 부여한다.
  4. 스프링 프레임워크에서 인스턴스를 생성한다.
  5. 인스턴스를 이용하고 싶은 곳에 어노테이션을 부여한다.

규칙 1

인터페이스를 이용해 의존성을 만든다: 의존하는 부분에 인터페이스를 이용한다

규칙 2

인스턴스를 명시적으로 생성하지 않는다: 인스턴스 생성에 new 키워드를 사용하지 않는다

규칙 3과 규칙 4

-> 인스턴스를 생성하려는 클래스에 인스턴스 생성 어노테이션을 부여한다.

스프링 프레임워크는 시작할 때 프로젝트의 모든 패키지를 스캔한다. 이 기능을 컴포넌트 스캔 이라고 한다.
컴포넌트 스캔 후 스프링 프레임워크는 인스턴스 생성 어노테이션이 부여된 클래스를 추출하고 클래스의 인스턴스를 생성한다.

인스턴스 생성 어노테이션

어노테이션개요
@Controller인스턴스 생성 지시, 스프링 MVC를 이용할 때 컨트롤러에 부여
@Service인스턴스 생성 지시, 트랜잭션 경계가 되는 도메인(서비스) 기능에 부여
@Repository인스턴스 생성 지시, 데이터베이스 액세스(리포지토리) 기능에 부여
@Component위 용도 이외에 클래스를 부여

규칙 5

인스턴스를 이용하고 싶은 곳에 어노테이션을 부여한다
-> 스프링 프레임워크에 의해 생성된 인스턴스를 사용하는 클래스에 참조를 받는 필드를 선언하고 필드에 @Autowired 어노테이션을 부여한다

3-2-7 DI 프로그램 만들기

package com.example.demo.chapter03.used;

/**
 * 인사 인터페이스
 */
public interface Greet {
    /**
     * 인사하기
     */
    void greeting();
}

Greet 인터페이스

package com.example.demo.chapter03.used;

import org.springframework.stereotype.Component;

/**
 * Greet 구현 클래스 <br>
 * 아침 인사 하기
 */
//@Component
public class MorningGreet implements Greet{
    @Override
    public void greeting(){
        System.out.println("--------------");
        System.out.println("좋은 아침입니다");
        System.out.println("--------------");
    }
}

Greet 인터페이스의 구현 클래스 MorningGreet

package com.example.demo.chapter03.used;

import org.springframework.stereotype.Component;

/**
 * Greet 구현 클래스<br>
 * 저녁 인사 하기
 */
@Component
public class EveningGreet implements Greet{
    @Override
    public void greeting(){
        System.out.println("-----------------");
        System.out.println("좋은 저녁입니다.");
        System.out.println("-----------------");
    }
}

Greet 인터페이스의 구현 클래스 EveningGreet

package com.example.demo;

import com.example.demo.chapter03.used.Greet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 스프링 부트 시작 클래스
 */
@SpringBootApplication
public class DemoApplication {

    /**
     * main 메서드
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args)
                .getBean(DemoApplication.class).execute();
    }
    /**
     * 주입하는 곳(인터페이스)
     */
    @Autowired
    Greet greet;

    /**
     * 실행 메서드
     */
    private void execute(){
        greet.greeting();
    }
}

EveningGreet 클래스에 @Component 어노테이션을 부여해 DemoApplication을 실행하면 @Component가 부여된 EveningGreet 클래스의 greeting 메서드가 호출된다.

이는 스프링 프레임워크 기동 시 컴포넌트 스캔에 의해 EveningGreet 인스턴스가 생성되고 @Autowired 어노테이션에 따라 EveningGreet 클래스의 인스턴스가 클래스의 greet 필드에 주입된다. 이를 실행하면 EveningGreet 클래스의 greeting 메서드가 실행된다.

3-3-1 어노테이션을 세 가지 항목으로 설명

어노테이션을 간단히 설명하면 다음 세 가지 항목이 된다.
1. 어노테이션(annotaion)은 주석을 의미하는 영어 표현이다.
2. '@xxx'와 같은 형태로 작성한다.
3. 외부 소프트웨어에 필요한 처리 내용을 전달한다.

어노테이션을 사용하면 에러를 출력하거나 프로그램의 동작을 변하는 등 다양한 것을 할 수 있다.

3-3-2 레이어별로 사용할 인스턴스 생성 어노테이션

레이어(layer): '층'의 의미, 계층 구조로 되어 있는 각 층을 말한다. 쉽게말해 복잡한 전체 내용을 한 번에 정리해 이해하지 말고 계층화해 각 계층별로 대상의 의미를 이해하는 것이다.

레이어개요
애플리케이션 레이어(Application Layer)애플리케이션 레이어는 클라이언트와의 데이터 입출력을 제어하는 레이어다.
도메인 레이어 (Domain Layer)도메인 레이어는 애플리케이션의 중심이 되는 레이어로서 업무 처리를 수행하는 레이어다.
인프라스트럭처 레이어(Infrastructure Layer인프라스트럭처 레이어는 데이터베이스에 대한 데이터 영속성(Persistence Context) 등을 담당하는 레이어다.

위 세 레이어는 '도메인 주도 설계(Domain-Driven Design)'에서 설명한 내용이다.

어노테이션개요
@Controller애플리케이션 레이어의 컨트롤러에 부여
@Service도메인 레이어의 업무 처리에 부여
@Repository인프라 레이어의 데이터베이스 액세스 처리에 부여
@Component@Controller, @Service, @Repository의 용도 이외의 인스턴스 생성 대상 클래스에 부여

레이어별 인스턴스 생성 어노테이션 설명

@Component는 하위 로직을 처리할 때 사용

3-3-3 커스텀 어노테이션

직접 커스텀 어노테이션을 만들 수도 있다.
커스텀 어노테이션을 만들 때는 java.lang.Annotation 인터페이스를 상속하고 만든다. 또한 커스텀 어노테이션을 정의할 때는 전용 자바 파일을 생성할 필요가 있다.
자세한 내용은 @interface 검색

3-4-1 AOP 예제

데이터베이스 액세스 처리에는 예외 발생 시 처리하는 내용이 반드시 포함되어야 한다. 예외 처리를 하지 않으면 프로그램이 중지되고 자바의 경우 예외 처리를 프로그램에 포함하지 않으면 컴파일에 실패한다.

AOP의 고유 용어

용어내용
어드바이스(Advice)횡단적 관심사의 구현(메서드). 로그 출력 및 트랜잭션 제어 등이다.
애스펙트(Aspect)어드바이스를 정리한 것(클래스)이다.
조인포인트(JoinPoint)어드바이스를 중심적인 관심사에 적용하는 타이밍. 메서드(생성자) 실행 전, 메서드(생성자) 실행 후 등 실행되는 타이밍이다.
포인트컷(Pointcut)어드바이스를 삽입할 수 있는 위치. 예를 들어, 메서드 이름이 get으로 시작할 때만 처리하는 조건을 정의할 수 있다.
인터셉터(Interceptor)처리의 제어를 인터셉트하기 위한 구조 또는 프로그램이다. 스프링 프레임워크에서는 인터셉트라는 메커니즘으로 어드바이스를 중심 관심사에 추가한 것처럼 보이게 한다.
타깃(Target)어드바이스가 도입되는 대상을 말한다.

스프링 프레임워크에서 '인터셉터'라는 메커니즘을 사용해 횡단적 관심사(어드바이스)를 중심적 관심사(타깃)에 삽입하는 것처럼 보일 수 있다.

어드바이스의 다섯 가지 종류

어드바이스내용어노테이션
Before Advice중심적 관심사가 실행되기 '이전'에 횡단적 관심사를 실행@Before
After Returning Advice중심적 관심사가 '정상적으로 종료된 후'에 횡단적 관심사를 실행@AfterReturning
After Throwing Advice중심적 관심사로부터 '예외가 던져진 후'로 횡단적 관심사를 실행@AfterThrowing
After Advice중심적 관심사의 '실행 후'에 횡단적 관심사를 실행(정상 종료나 예외 종료 등의 결과와 상관없이 실행)@After
Around Advice중앙적 관심사 호출 전후에 횡단적 관심사를 실행@Around

3-4-2 포인트컷 식

직접 어드바이스를 만드는 경우 패키지, 클래스, 메서드 등 어드바이스 삽입 대상을 조건으로 지정할 수 있다. 지정하는 조건 방법에는 포인트컷 식을 사용한다. 포인트 컷 표현식은 여러 가지가 있지만 이 문서에서는 'execution' 지시자를 설명한다.

와일드카드

와일드카드내용
*(애스터리스크)임의의 문자열을 나타내고, 패키지를 나타낼 때는 임의의 패키지 한 계층을 나타낸다. 메서드의 인수에서는 한 개의 인수를 나타내 반환값으로도 이용할 수 있다.
..(점 두 개)패키지를 나타내는 경우 0개 이상의 패키지를 나타낸다. 메서드의 인수를 포함하는 경우에는 0개 이상의 임의의 인수를 나타낸다.
+(플러스)클래스명 뒤에 기술해 클래스와 그 서브클래스 및 구현 클래스 모두를 나타낸다.

execution 지시자의 구현 예

구현 예내용
execution(*com.example.service.DemoService.*(..))DemoService 클래스의 메서드에 어드바이스를 적용한다.
execution(*com.example.service.DemoService.select*(..))DemoService 클래스의 select로 시작하는 메서드에 어드바이스를 적용한다.
execution(String com.example.service.DemoService.*(..))DemoService 클래스의 반환값이 String 타입인 메서드에 어드바이스를 적용한다.
execution(*com.example.service.DemoService.*(String,..)DemoService 클래스의 첫 번째 인수가 String 타입인 메서드에 어드바이스를 적용한다.
execution(*com.example.service.*.*(..))지정된 패키지 아래의 모든 클래스의 메서드에 어드바이스를 적용한다(서브 패키지는 포함하지 않는다).
execution(* com.example.service..*.*(..))service 패키지 바로 아래와 하위 패키지의 모든 클래스에 어드바이스를 적용한다.
execution(* com.example.service.DemoService.*(*))DemoService 클래스의 인수가 하나인 메서드에 어드바이스를 적용한다.

너무 많고 다 비슷하게 생겼다.

3-4-3 AOP 프로그램 만들기

직접 어드바이스를 만들고 포인트컷 식에서 어드바이스 삽입 위치를 지정해 AOP의 동작 방식을 확인할 수 있는 프로그램을 만들어보자.

package com.example.dependencyinjectionsample.chapter03.aop;

import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SampleAspect {
    
}

어드바이스를 기술하는 클래스에는 @Aspect 어노테이션을 부여한다.
인스턴스를 생성하기 위해 @Component 어노테이션을 부여한다.

package com.example.demo.chapter03.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;

@Aspect
@Component
public class SampleAspect {
    @Before("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
    public void beforeAdvice(JoinPoint joinPoint){
        // 시작 부분 표시
        System.out.println("==== Before Advice ====");
        // 날짜를 출력
        System.out.println(new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date()));
        // 메서드 이름 출력
        System.out.println(String.format("메서드:%s", joinPoint.getSignature().getName()));
    }
}

어노테이션의 인수에는 포인트컷 식인 execution(반환형 패키지.클래스.메서드(인수))를 지정한다.

@After("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
    public void afterAdvice(JoinPoint joinPoint){
        // 시작 부분 표시
        System.out.println("==== After Advice ====");
        // 날짜를 출력
        System.out.println(new SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date()));
        // 메서드명 출력
        System.out.println(String.format("메서드명:%s", joinPoint.getSignature().getName()));
    }

메서드를 실행한 후 호출되는 After Advice

@Around("execution(* com.example.demo.chapter03.used.*Greet.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable{
        // 시작 부분 표시
        System.out.println("==== Around Advice ====");
        System.out.println("처리전");
        // 지정한 클래스의 메서드 실행
        Object result = joinPoint.proceed();
        System.out.println("처리후");
        // 반환값을 돌려줄 필요가 있는 경우에는 Object 타입의 반환값을 돌려준다.
        return result;
    }

메서드 실행 전후에 호출되는 Around Advice


3-4-4 스프링 프레임워크가 제공하는 AOP 기능

트랜잭션 관리에 @Transactional 어노테이션을 사용한다. @Transactional 어노테이션을 부여하여 데이터베이스 액세스 처리 메서드가 정상 종료하면 트랜잭션을 커밋, 예외가 발생하면 롤백한다.

3-4-5 요약

AOP 사고방식

개발 중 동작 상황 확인을 위해 print 찍는 작업은 매우 피곤
또한 프로그램 작성이 완료됐을 때 모든 '디버깅 로그'를 삭제해야 함.
이런 다수의 클래스에 공통으로 필요한 처리를 '횡단적 관심사'라고 한다.


만일 여러 클래스의 메서드에 print를 자동으로 넣어주는 기능 또는 필요가 없어졌을 때 모두 자동으로 삭제해주는 기능이 있다면 편리할 것이다. 이 같은 생각이 AOP 사고방식이다.

AOP의 주요 사항

  • AOP에서는 프로그램을 2개의 요소인 중심적 관심사횡단적 관심사로 구성되어 있다고 생각한다.
  • 중심적 관심사란 구현해야 할 기능을 나타내는 비즈니스 로직이다.
  • 횡단적 관심사란 본질적인 기능은 아니지만 품질이나 유지보수 등의 관점에서 꼭 필요한 기능을 나타내는 프로그램을 말한다.
  • AOP에서는 횡단적 관심사를 분리함으로써 기존 코드를 수정하지 않아도 프로그램 중에 특정 기능(공통 처리)을 추가할 수 있다.
  • 스프링 프레임워크는 다양한 공통 기능을 AOP에서 제공한다.
profile
개발자 지망생. 일단 하고보자

0개의 댓글