Springframework 기본 개념 정리

IoC, Dependency Injection

Bean

자바에서의 JavaBean

  • 데이터를 저장하기 위한 구조체로 자바 빈 규약이라는 것을 따르는 구조체
  • private 프로퍼티와 getter/setter로만 데이터에 접근
    public class Bean {
    private String id;
    private int count;
      
    public String getId() {
      return id;
    }
      
    public void setId(String id) {
      this.id = id;
    }
      
    public int getCount() {
      return count;
    }
      
    public void setCount(int count) {
      this.count = count;
    }
    }
    

스프링에서의 Bean

  • 스프링 IoC 컨테이너: 스프링 빈 또는 기능을 담고 있음
  • 스프링 IoC 컨테이너에 의해 생성되고 관리되는 객체
  • 자바에서처럼 new Object();로 생성하지 않음
  • 각각의 Bean들끼리는 서로를 의존할 수 있음
  • SpringApplicationContext 에서 리스트로 등록된 것 확인 가능
  • 스프링에서의 싱글톤 타입으로 인스턴스화
  • SpringApplicationContext의 설정값이 더해진 상태로 사용됨

Spring Container

ApplicationContext 인터페이스를 통해 제공되는 스프링 컨테이너는 빈 객체의 생성 및 의존성 관리 담당

빈 등록

  • 과거에는 xml로 등록 및 관리
  • 현재는 annotation 기반으로 등록: @Bean, @Controller, @Service
  • 빈 등록 시 정보
    • 클래스 경로
    • 빈의 이름
      • 기본적으로 원래 클래스 이름에서 첫 문자만 소문자로 변경
      • 원하는 이름으로 변경 가능
    • Scope: 빈을 생성하는 규칙
      • Singleton: 컨테이너에 단일로 생성, 처음 스프링을 실행할 때 혹은 처음 필요할 때 생성 후 종료될 때까지 사용
      • prototype: 작업할 때마다 빈을 새로 생성
      • request: http 요청마다 빈을 새로 생성
  • Bean LifeCycle callback
    • callback: 어떤 이벤트가 발생하는 경우, 호출되는 메서드
    • lifecycle callback: 빈을 생성하고 초기화하고 파괴하는 등 특정 시점에 호출되도록 정의된 함수
      • @PostConstruct: 빈 생성 시점에 필요한 작업 수행
        • ex. connection, 자원 소모가 클 경우
      • @PreDestroy: 빈 파괴(주로 앱 종료) 시점에 필요한 작업 수행
        • ex. 빈에서 마무리가 필요한 경우, connection 등 라이브러리 특성에 따라 다름

관점 지향 프로그래밍(AOP)

특정 함수의 호출 전, 후에 공통적인 처리가 필요할 때 사용하며 OOP로 처리하기 까다로운 부분을 AOP를 통해 공통 기능을 추가, 수정, 삭제할 수 있음

  • 로깅
  • 트랜잭션
  • 인증

기본 개념

Aspect

  • 여러 클래스나 기능에 걸쳐서 있는 관점/관심사를 모듈화한 것
  • AOP 중 가장 많이 활용되는 부분: @Transactional (트랜잭션 관리)

Advice

  • AOP에서 실제로 적용하는 기능(로깅, 트랜잭션, 인증 등)

Join Point

  • 모듈화된 특정 기능이 실행될 수 있는 연결 포인트

Pointcut

  • join point 중에서 해당 aspect를 적용할 대상을 뽑을 조건식

Target Object

  • advice가 적용될 대상 객체

AOP Proxy

  • 대상 오브젝트에 aspect를 적용하는 경우, advice를 덧붙이기 위해 하는 작업
  • 주로 CGLIB(Code Generation Library, 실행 중에 실시간으로 코드를 생성하는 라이브러리) 프록시 사용

Weaving

  • advice를 비즈니스 로직에 삽입하는 것

AspectJ 지원

AspectJ는 AOP를 사용하기 위해 필요한 라이브러리로 기본적으로 제공되는 Spring AOP로는 Pointcut 등의 다양한 기법을 사용할 수 없음

Aspect 생성

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class SomeAspect {
  
}

Pointcut 선언

  • 해당 aspect의 advice가 적용될 join point를 찾기 위한 패턴 또는 조건 생성
  • 포인트 컷 표현식이라 부름
package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class SomeAspect {
  
  @Pointcut("execution(* transfer(..))")
  private void anyTransfer() {}
  
}

Pointcut 결합

package org.xyz;
import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class SomeAspect {
  
  // public 메서드 대상 포인트 컷
  @Pointcut("execution(public * *(..))")
  private void anyPublicOperation() {}
  
  // 특정 패키지 대상 포인트 컷
  @Pointcut("within(com.xyz.myapp.trading..*)")
  private void inTrading() {}
  
  // 위의 두 조건을 결합한 포인트 컷
  @Pointcut("anyPublicOperation() && inTrading()")
  private void tradingOperation() {}
  
}

Advice 정의

포인트컷들을 활용하여 포인트컷 전, 후, 주변에서 실행될 액션 정의

Before advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {
  
  // dataAccessOperation 포인트컷 전에 실행
  @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  public void doAccessCheck() {}
}

After returning advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {
  
  // dataAccessOperation 포인트컷에서 return 발생된 후 실행
  @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()")
  public void doAccessCheck() {}
}

Around advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {
  
  // businessService 포인트컷 전, 후에 필요한 동작 추가
  @Around("com.xyz.myapp.CommonPointcuts.businessService()")
  public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    
    // stop stopwatch
    return retVal;
  }
}

Validation & Data binding

사용자 또는 서버의 요청(http request) 내용에서 잘못된 내용이 있는지 확인하려는 단계로 유효성을 검증한다.

데이터 검증

  • 필수 데이터의 존재 유무
  • 문자열의 길이나 숫자형 데이터의 경우 값의 범위
  • email, 신용카드 번호 등 특정 형식에 맞춘 데이터

비즈니스 검증

  • 서비스 정책에 따라 데이터를 확인하여 검증
    • ex. 배달 요청을 할 때 해당 주문건이 결제 완료 상태인지 확인
  • 경우에 따라 외부 API를 호출하거나 DB의 데이터까지 조회하여 검증하는 경우도 존재

Spring Validation

스프링은 웹 레이어에 종속되지 않은 방법으로 유효성을 검증하려고 하며 주로 두 가지 방법을 활용하며 둘 다 데이터 검증에 가깝다.

Java Bean Validation

  • Java Bean 기반으로 간편하게 개별 데이터 검증
  • 가장 많이 활용되는 방법 중 하나로 어노테이션으로 검증방법 명시
  • 요청 DTO에 어노테이션으로 명시 후, @Valid 어노테이션을 해당 @RequestBody 에 달게 되면 Java Bean Validation을 수행한 후, 문제가 없을 때만 메서드 내부로 진입
  • 검증 실패시, MethodArgumentNotValidExecption 발생
    public class MemberCreationRequest{
      @NotBlank(message="이름을 입력해주세요.")
      @Size(max=64, message="이름의 최대 길이는 64자입니다.")
      private String name;
        
      @Min(0, "나이는 0보다 커야 합니다.")
      private int age;
        
      @Email("이메일 형식이 잘못되었습니다.")
      private int email;
    }
      
    @PostMapping(value = "/member")
    public MemberCreationResponse createMember(@Valid @RequestBody final MemberCreationRequest memberCreationRequest) {
      // logic
    }
    

Spring validator interface

  • Validator 인터페이스 사용
  • supports: 이 validator가 동작할 조건을 정의하며 주로 클래스의 타입 비교
  • validate: 원하는 검증 진행
    public class Person {
      private String name;
      private int age;
    }
      
    public class PersonValidator implements Validator {
      public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
      }
        
      public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
          
        if (p.getAge() < 0) {
          e.rejectValue("age", "negavieValue");
        } else if (p.getAge() > 110) {
          e.rejectValue("age", "too.darn.old");
        }
            
      }
    }
    

Validation 수행 시 주의사항

  • validation이 너무 여러 군데에 흩어져 있으면 테스트 및 유지보수 어려움
  • 중복 검증: 정책 변경 시에 모든 중복 코드를 수정해야 함
  • 다른 검증: 여러 군데서 다른 정책을 따르는 검증이 수행될 수 있음
  • 가능한 validation은 로직 초기에 수행 후, 실패 시에는 exception을 던지는 방향이 처리가 편리함

실무 활용 패턴

  • 요청 DTO에서 Java Bean Validation으로 단순 데이터 1차 검증(유무, 범위, 형식 등)
  • 로직 초기에 비즈니스 검증 수행 후, 실패 시에는 CustomException 예외를 던지도록 처리
  • Spring validator의 장단점
    • 장점: Java Bean Validation에 비해 좀 더 복잡한 검증 가능
    • 단점
      • Validation을 수행하는 코드를 찾기가 상대적으로 어려움
      • 완전히 데이터만 검증하는 것이 아니므로 일부 비즈니스적인 검증이 들어가는 경우가 있음
      • 비즈니스 검증 로직이 여러 군데로 흩어지므로 잘못된 검증을 수행할 가능성이 높아짐

Data Binding

사용자나 외부 서버의 요청 데이터를 특정 도메인 객체에 저장해서 request에 담아주는 것

Converter<S, T> Interface

S(Source) 타입을 받아서 T(Target) 타입으로 변환해주는 인터페이스

  • PathParameter나 특수한 경우의 데이터를 특정 객체에 담고 싶은 경우
  • converter를 만들어서 Spring에 빈으로 등록
  • 스프링 내에 ConversionService라는 내장된 서비스에서 converter 구현체 빈들을 converter 리스트에 등록
  • 외부 데이터가 들어오면 Source Class Type -> Target Class Type 이 컨버터에 등록된 형식과 일치하면 해당 컨버터가 동작하는 원리 ```java package org.springframework.core.convert.converter;

public interface Converter<S, T> { T convert(S source); }


```java
// 예제: 파라미터에 json 형식 문자열이 담겨오는 경우, 해당 문자열을 바로 특정 DTO에 담도록 사용
// GET /user-info
// x-auth-user: {"id": 1, "name": "John"}

// User object
public class XAuthUser {
  private int id;
  private String name;
}

@GetMapping("/user-info")
public UserInfoResponse getUserInfo(@RequestHeader("x-auth-user") XAuthUser xAuthUser) {
  // logic
}

// converter를 빈으로 등록
@Component
public class XAuthUserConverter implements Converter<String, XAuthUser> {
  @Override
  public XAuthUser convert(String source) {
    return objectMapper.readValue(source, XAuthUser.class);
  }
} 
Formatter
  • 특정 객체와 String 간의 변환 담당
  • Spring 빈으로 등록하면 자동으로 ConversionService에 등록시켜 주므로 필요에 따라 자동으로 동작하게 됨 ```java package org.springframework.format.datetime;

public final class DateFormatter implements Formatter { public String print(Date date, Locale locale) { return getDateFormat(locale).format(date); }

public Date parse(String formatted, Locale locale) throws ParseException { return getDateFormat(locale).parse(formatted); } }


## Resource
- classpath 내부 접근이나 상대 경로 등의 java.net.URL의 한계를 해결하게 위해 스프링에서 추가로 구현
```java
// Resource interface
public interface Resource extends InputStreamSource {
  boolean exists();
  
  boolean isReadable();
  
  boolean isOpen();
  
  boolean isFile();
  
  URL getURL() throws IOException;
  
  URI getURI() throws IOException;
  
  File getFile() throws IOException;
  
  ReadableByteChannel readableChannel() throws IOException;
  
  long contentLength() throws IOException;
  
  long lastModified() throws IOException;
  
  Resource createRelative(String relativePath) throws IOException;
  
  String getFilename();
  
  String getDescription();  
}

Resource 구현체 목록

  • UrlResource
    • java.net.URL을 래핑한 버전
    • 다양한 종류(ftp, file, http 등의 prefix로 접근 유형 판단)의 리소스에 접근 가능
    • 기본적으로 http(s)로 원격 접근
  • ClassPathResource
    • 소스코드를 빌드한 결과(기본적으로 target/classes 폴더)인 classpath 하위의 리소스 접근 시 사용
  • FileSystemResource
    • 파일을 다루기 위한 리소스 구현체
  • ServletContextResource, InputStreamResource, ByteArrayResource
    • Servlet 어플리케이션 루트 하위 파일, InputStream, ByteArrayInput 스트림을 가져오기 위한 구현체

Spring ResourceLoader

스프링 프로젝트 내 파일 등의 리소스에 접근할 때 사용하는 기능

  • 기본적으로 applicationContext에서 구현되어 있음
  • 프로젝트 내 파일(주로 classpath 하위 파일)에 접근할 일이 있을 경우 활용
  • 대부분의 사전에 정의된 파일들은 자동으로 로딩되도록 되어 있으나, 추가로 필요한 파일이 있을 때 활용 가능
    @Service
    public class ResourceService {
      @Autowired
      ApplicationContext ctx;
        
      public void setResource() {
        Resource myTemplate =
          ctx.getResource("classpath:some/resource/path/myTemplate.txt");
        ...
      }
    }
    

ResourcePatternResolver

  • ApplicationContext에서 ResourceLoader를 불러올 때 사용하는 인터페이스
  • 위치 지정자 패턴(“classpath:***”, “file:***”, “http:”) 에 따라 자동으로 리소스 로더 구현체 선택
    public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
      ...
    }
    

ApplicationContexts & Resourcepaths

  • applicationContext(스프링 설정)을 이루는 설정값 가져오는 방법
  • 현재는 잘 사용하지 않지만 이전 버전에서 사용하던 방법
    ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");
        
    Applicationcontext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");
      
    Applicationcontext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");
      
    Bear bear = (Bear) ctx.getBean("bear");
    

SpEL(Spring Expression Lanugage)

Expression Language는 짧고 간단한 문법을 통해 필요한 데이터나 설정 값을 얻어올 수 있게 하는 특별한 형태의 표현식에 가까운 언어로 그래퍼 접근 등이 가능하다. SpEL은 스프링의 모든 영역에서 사용할 수 있다. 주로 @Value("${config.value}") 와 같은 방식으로 설정값을 주입받을 때 사용

SpEL의 값 평가(evaluation)

  • SpelParser는 "" 안에 들어가 있는 문자열을 평가해서 결과값 생성
  • ‘Hello World’는 문자열 리터럴이 되며, concat이라는 메서드도 호출 가능
  • String 객체를 new로 생성해서 사용 가능 ```java ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(“‘Hello World’”); String message = (String) exp.getValue(); // “Hello World”

// 실제로 잘 사용하지는 않음 Expression expWow = parser.parseExpression(“‘Hello World’.concat(‘!’)”); String messageWow = (String) expWow.getValue(); // “Hello World!”

Expression expString = parser.parseExpression(“new String(‘hello world’).toUpperCase()”); String messageString = expString.getValue(String.class);


#### Bean의 Property를 설정할 때 사용하는 방식
- 기본적으로 `#{\<expression string>}` 방식으로 property 설정
- application.properties 또는 application.yml의 값을 가져올 때는 `${\<property name>}` 방식으로 가져옴
  ```java
  @Component
  public class SimpleComponent {
    @Value("#{ 1+1 }")
    int two;
    
    @Value("#{ 2 eq 2}")
    boolean isTrue;
    
    @Value("${ server.hostname }")
    String hostName;
    
    @Value("#{ ${ server.hostname } eq 'www.server.com'}")
    boolean isHostSame;
  }

Null Safety

null 안정성을 높이는 방법

  • 아래와 같은 코드를 만들지 않는 방법
  • 혹은 널 체크를 확인하지 않아서 발생하는 NPE(Null Pointer Exception)을 방지하는 방법
  • IDE의 도움으로 1차적인 문제를 방지
    public void method(String request) {
      if (request == null) return;
        
      ...
    }
    

@NonNull annotation

  • 해당 값이나 함수 등이 null이 아님을 나타내는 어노테이션
  • org.springframework.lang.NonNull 사용
  • 메서드 파라미터에 붙이는 경우: null이라는 데이터가 들어오는 것을 사전에 방지
  • 프로퍼티에 붙이는 경우: null을 저장하는 경우에 경고

@Nullable annotation

  • @NonNull 과 반대로 해당 데이터가 null일 수 있음을 명시