Springframework 기본 개념 정리
05 Sep 2021IoC, 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 등 라이브러리 특성에 따라 다름
- @PostConstruct: 빈 생성 시점에 필요한 작업 수행
관점 지향 프로그래밍(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 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일 수 있음을 명시