728x90

우리가 Spring하면 IOC(제어반전)와 AOP(절단면 지향 프로그래밍)가 가장 먼저 떠오를 수 있다.

스프링의 base 이고 훌륭한 디자인 덕분에 스프링이 많은 framework들 중에서도 남다른 인기를 장기간 이어 왔었다.

그 외에도 spring을 사용시 유연한 확장력장점 때문에 spring은 스펀지마냥 또다른 제3자 애플리케이션을 품?에 쉽게 끌어안을수 있는것이다. rocketmq, mybatis, redis 등등...

이번 글에서는  spring 에서 가장 많이 쓰이는 11가지 확장점에 대해 알아보자.


1. 커스텀 인터셉터(Interceptor)


SpringMVC interceptor 는 Spring interceptor에 비해 HttpServletRequest, HttpServletResponse 등 웹 객체 인스턴스를 가져올 수 있다. Spring mvc interceptor의  최상위 인터페이스는 HandlerInterceptor 이며 내무 함수는 아래와 같다.

  • preHandle 타겟 메서드가 실행되기 전에
  • postHandle 타겟 메서드를 실행한 후
  • afterCompletion 요청을 완료할 때 실행.

HandlerInterceptorAdapter 추상 클래스를 오버라이딩하여 구현 . 권한 인증, 로그, 통계구현 등 케이스에서 interceptor를 사용해볼만하다.

  • step1: HandlerInterceptorAdapter 정의 
public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String requestUrl = request.getRequestURI();
        if (checkAuth(requestUrl)) {
            return true;
        }

        return false;
    }

    private boolean checkAuth(String requestUrl) {
        System.out.println("권한인증");
        return true;
    }
}
  • step2:  정의된 Interceptor 를 빈에 등록
@Configuration
public class WebAuthConfig extends WebMvcConfigurerAdapter {
 
    @Bean
    public AuthInterceptor getAuthInterceptor() {
        return new AuthInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}

2. 빈객체 얻는 3가지

BeanFactoryAware 사용하기 

@Service
public class PersonService implements BeanFactoryAware {
    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public void add() {
        Person person = (Person) beanFactory.getBean("person");
    }
}

ApplicationContextAwar 사용하기 

@Service
public class PersonService2 implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public void add() {
        Person person = (Person) applicationContext.getBean("person");
    }
}

ApplicationListener 사용하기

@Service
public class PersonService3 implements ApplicationListener<ContextRefreshedEvent> {
    private ApplicationContext applicationContext;
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        applicationContext = event.getApplicationContext();
    }

    public void add() {
        Person person = (Person) applicationContext.getBean("person");
    }
}

3. global Exception 처리


아래의 심플한 코드가 있다.

@RequestMapping("/test")
@RestController
public class TestController {

    @GetMapping("/add")
    public String add() {
        int a = 10 / 0;
        return "성공";
    }
}

성공여부에 따른 분기처리를 아래와같이 자주 한다.

@GetMapping("/add")
public String add() {
    String result = "성공";
    try {
        int a = 10 / 0;
    } catch (Exception e) {
        result = "data exception!";
    }
    return result;
}

 

 

뭐 이거 하나만 처리한다고 치면 괜찮은 코드인듯 싶다. 하지만 controller가 수십개, 수백게 되면 이런 처리를 일일해주기는에는 너무 큰 노가다 일뿐다. Spring에서 기막한 어노테이션을 지원한다. 바로 RestControllerAdvice 이다. 이름만 봐도 어느정도 예측가능하다. 아래 코드를 보자.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public String handleException(Exception e) {
        if (e instanceof ArithmeticException) {
            return "data Exception!";
        }
        if (e instanceof Exception) {
            return "Service Intenerl Error!";
        }
        retur nnull;
    }
}

처음 이 기능을 보고 1주일 변비가 뻥뚤린 기분이였다.

 

4. 클래스 유형 전환

Spring 은 3가지 타입전환방식이 있다.

Converter<S,T>:S 를 T로 전환.

ConverterFactory<S, R>:S를 R의 자식클래스로 전환

GenericConverter:정교한 Converter구현이 필요한 경우, GenericConverter 사용한다.

step1: 샘플 domain 생성

@Data
public class User {

    private Long id;
    private String name;
    private Date registerDate;
}

step2: 구현

public class DateConverter implements Converter<String, Date> {

    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    public Date convert(String source) {
        if (source != null && !"".equals(source)) {
            try {
                simpleDateFormat.parse(source);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

step3: 정의된 클래스 전화객체를 빈에 등록한다.

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DateConverter());
    }
}

step4: 사용해보기 

@RequestMapping("/user")
@RestController
public class UserController {

    @RequestMapping("/save")
    public String save(@RequestBody User user) {
        return "success";
    }
}

5. Config import 하기

5.1 일반클래스

public class A {
}

@Import(A.class)
@Configuration
public class TestConfiguration {
}

@Import 로 A 클래스를 주입하면 , spring은 자동으로 A클래스에 대하여 객체를 생성할것이고. 따라서 @Autowired 를 이용하여 주입하면 된다.

@Autowired
private A a;

어 ? @Bean 하지 않고도 객체 생성 가능하네...

5.2 config 클래스

해당 import 방식은 제일 복잡하다. 왜냐면 @Configuration 주입은 기타 다른 주입방식을 겹쳐서 사용하는걸 지원하기 때문이다. 아래처럼 말이다.

public class A {
}

public class B {
}

@Import(B.class)
@Configuration
public class AConfiguration {

    @Bean
    public A a() {
        return new A();
    }
}

@Import(AConfiguration.class)
@Configuration
public class TestConfiguration {
}

5.3 ImportSelector

해당방식은 ImportSelector 인터페이스를 구현해야 한다.

public class AImportSelector implements ImportSelector {

private static final String CLASS_NAME = "com.sue.cache.service.test13.A";
    
 public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{CLASS_NAME};
    }
}

@Import(AImportSelector.class)
@Configuration
public class TestConfiguration {
}

이 방식의 장점은 리턴이 배열로 되어 있어 여러개를 동시에 import 할수 있다는것.

5.4 ImportBeanDefinitionRegistrar

해당방식은 ImportBeanDefinitionRegistrar 인터페이스를 구현한다.

public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(A.class);
        registry.registerBeanDefinition("a", rootBeanDefinition);
    }
}

@Import(AImportBeanDefinitionRegistrar.class)
@Configuration
public class TestConfiguration {
}

registerBeanDefinitions 에서 BeanDefinitionRegistry 의 등록객체를 얻을수 있다. 때문에 BeanDefinition 수동으로 생성 & 등록을 조작할수 있다.


6. 프로젝트 실행시

일반적으로 우리는 그냥 기본 실행을 하지만 필요시 해당 프로젝트의 설정대로 시작해야 하는 경도 있다 . 예를 들ㅇ면 시스템설정,로컬캐쉬 , 초기화 등 설정을 해야 하는 경우도 있다. Spring은 CommandLineRunner 과 ApplicationRunner 를 제공한다.

@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    private LoadDataService loadDataService;

    public void run(ApplicationArguments args) throws Exception {
        loadDataService.load();
    }
}

ApplicationRunner 를 구현하여 run() 메서드를 재정의 하면 된다.

만일 프로젝트에 ApplicationRunner 가 여러개 면 @Order(n) 나 @Priority 로 순서를 지정할수 있다.


7.BeanDefinition

Spring IOC 에서 bean을 초기화 하기전에 BeanDefinitionReader가  bean 들의 정보를 읽는다. 이때 BeanDefinition 객체에 저장하고 이것을 이용해 bean 을 생성하게 된다.

그럼 BeanDefinition 의 속성를 수정하려면??

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(User.class);
        beanDefinitionBuilder.addPropertyValue("id", 123);
        beanDefinitionBuilder.addPropertyValue("name", "hi this kim");
        defaultListableBeanFactory.registerBeanDefinition("user", beanDefinitionBuilder.getBeanDefinition());
    }
}

postProcessBeanFactory 메서드에서 BeanDefinition 관련 객체를 얻을수 있고 그걸 수정하면 된다.

8. Bean 초기화

가끔 Bean 들이 초기화 되고 난후 나의 로직을 끼워넣고 싶다 .이럴때 사용하는것이 바로 BeanPostProcessor 이다.

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof User) {
            ((User) bean).setUserName("hi this kim!");
        }
        return bean;
    }
}
  • postProcessBeforeInitialization 초기화 전 호출 됨.
  • postProcessAfterInitialization  초기화 후 호출 됨.

9. 초기화 방법

9.1 @PostConstruct 사용하기

@Service
public class AService {
    @PostConstruct
    public void init() {
        System.out.println("===초기화===");
    }
}

9.2 InitializingBean 인터페이스 구현

@Service
public class BService implements InitializingBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("===초기화===");
    }
}

10. 컨테이너 닫기

DisposableBean 인터페이스를 구현하고 destroy()를 재정의 한다.

@Service
public class DService implements InitializingBean, DisposableBean {
 
    @Override
    public void destroy() throws Exception {
        System.out.println("DisposableBean destroy");
    }
 
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("InitializingBean afterPropertiesSet");
    }
}

11. 사용자 정의 도메인

동일 스레드의 spring 컨테이너에서 생성도니 빈은 하나! 로 하고 싶을 경우??

step1: scope 인터페이스 구현 

public class ThreadLocalScope implements Scope {
    private static final ThreadLocal THREAD_LOCAL_SCOPE = new ThreadLocal();

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object value = THREAD_LOCAL_SCOPE.get();
        if (value != null) {
            return value;
        }

        Object object = objectFactory.getObject();
        THREAD_LOCAL_SCOPE.set(object);
        return object;
    }

    @Override
    public Object remove(String name) {
        THREAD_LOCAL_SCOPE.remove();
        return null;
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
    }

    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Override
    public String getConversationId() {
        return null;
    }
}

step2: 새로 정의한 scope를 spring 컨테이너에 등록

@Component
public class ThreadLocalBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        beanFactory.registerScope("threadLocalScope", new ThreadLocalScope());
    }
}

setp3: 새로정의한 scope 사용

@Scope("threadLocalScope")
@Service
public class CService {
    public void add() {
    }
}

 

끝!

728x90

특정 메서드 실행 시간 ,종료 시간을 측정하고 싶을때 우리는 일반적으로 아래와 같은 식으로 작성했었다.

@Around("webLog()")
public Object 실행시간측정해보기(ProceedingJoinPoint joinPoint) throws Throwable {
    long 시작시간 = System.currentTimeMillis();
    long 종료시간 = System.currentTimeMillis();
    webLog.setSpendTime((int) (종료시간 - 시작시간));
}

하지만 spring 에는 더 좋은 객체가 있어서 소개해본다. 바로 StopWatch 라는 요놈...

// StopWatch 하나 만들고 
StopWatch sw = new StopWatch(" 샘플입니다.");
// 시간계산 시작
sw.start("임수수행1");

Thread.sleep(1000);

// 멈추고...
sw.stop();
System.out.printf("임수수행1-->소요시간:%d%s.\n", sw.getLastTaskTimeMillis(), "ms");

sw.start("임수수행2");
Thread.sleep(1100);
sw.stop();

System.out.printf("임수수행2-->소요시간:%d%s.\n", sw.getLastTaskTimeMillis(), "ms");
System.out.printf("임수수행건수:%s,소용총시간:%ss.\n", sw.getTaskCount(), sw.getTotalTimeSeconds());

sw.stop() 함수가 어떻게 되었는지 확인해보면

public void stop() throws IllegalStateException {
	if (null == this.currentTaskName) {
		throw new IllegalStateException("Can't stop StopWatch: it's not running");
	}

	final long lastTime = System.nanoTime() - this.startTimeNanos;
	this.totalTimeNanos += lastTime;
	this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
	if (null != this.taskList) {
		this.taskList.add(this.lastTaskInfo);
	}
	++this.taskCount;
	this.currentTaskName = null;
}

내부코드는 그냥 System.nanoTime() 함수를 사용한것. 사실 System.currentTimeMillis() 와 별다른 차이는 없어보인다.

찾아보니 Apache commons-lang3 의 StopWatch 라는 비슷한 놈도 있었다.

사용방식은 ...

StopWatch sw = StopWatch.createStarted();
Thread.sleep(1000);
System.out.printf("소용시간:%dms.\n", sw.getTime());

getTime() 외 아래 함수들도 있으니 참고 바란다.

// 시간계산 일시 멈춤
sw.suspend();
System.out.printf("시간계산 일시 멈춤:%dms.\n", sw.getTime());

// 시간계산 복구
sw.resume();
System.out.printf("시간계산 복구:%dms.\n", sw.getTime());

// 시간계산 종료
sw.stop();
System.out.printf("시간계산 종료:%dms.\n", sw.getTime());

// 시간계산 리셋
sw.reset();
System.out.printf("시간계산 리셋:%dms.\n", sw.getTime());

// 시간계산 시작
sw.start();
System.out.printf("시간계산 시작:%dms.\n", sw.getTime());
728x90

일반적인 수단

// 변경불가Set
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);

// 변경불가 List
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list = Collections.unmodifiableList(list);

자바 8
stream api 로 쉽게 작성 가능.

Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));
List<Integer> list = Collections.unmodifiableList(Stream.of(1, 2, 3).collect(toList()));

자바9
조금더 간단해졌다.

Set<String> set = Set.of("a", "b", "c");
List<Integer> list = List.of(1, 2, 3);

마찬가지로 아래와 같은 map 타입은 아래와 같이 작성 가능하다.

Map<String, String> map = Map.of("a", "1", "b", "2", "c", "3");

주의 ! key ,value 가 쌍으로 나타나므로 파라미터는 무조건 짝수 이다.

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...

그리고 갑자기 asXxx() 등 함수들이 자주 등장하는데 차이가 면지 궁금하지 않는가?
List.of vs Arrays.asList 를 예로 들면

  1. List.of -> 변경불가
  2. Arrays.asList -> 변경가능 ( 주의 , 사실 요놈은 애매한 놈임. 변경불가능이긴 하지만 set을 이용하 변경가능함.헷갈리지? 하지만 List.of는 아예 안됨. set을 호출시 java.lang.UnsupportedOperationException 이 나온다!)
  3. List.of null 허용하지않음 ,Arrays.asList null 허용
728x90

아래 코드를 보자

public class StreamTest {

    @Test
    void test() {
        List<String> list = List.of("rainsister.tistory.com", "rainsisters.tistory.com", "rainsister.com", "www.rainsisters.com");

        List<String> result = list.stream()
                .filter(e -> e.contains("rainsisters.tistory.com"))
                .filter(e -> e.length() > 17)
                .toList();

        System.out.println(result);
    }

}

혹시 toList() 에 대해서 궁금하지 않은가 ? 요놈은 java 16에서 등장하는 놈이다. 바로 Stream -> List 로 직접 전환해주는 놈이다. java 8 을 사용한 친구라면 안될껄? 그래서 아래 코드들을 테스트 해보려면 아무래도 java 16 이상은 되어야 겠지?
만일 java8 에서는 어떻게 쓸가?

List<String> result = list.stream()
    .filter(e -> e.contains("rainsisters.tistory.com"))
    .filter(e -> e.length() > 17)
    .collect(Collectors.toList());

Stream.toList() 와 Stream.collect(Collectors.toList()) 의 차이점
Stream.toList() 의 내부 코드를 확인 해보면

default List<T> toList() {
    return (List<T>) Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())));
}

Stream.toList()는 불변의 List를 만든다. 하지만 Stream.collect(Collectors.toList()) 는 일반 List를 만들어낸다. 때문에 추가,삭제작업이 가능하다.
그러면 Collectors 로 불변의 List를 만들려면 어떻게 쓰면 될깡?????
바로 Collectors.toUnmodifiableList() 를 쓰면 된다.
아래와 같이

List<String> result = list.stream()
    .filter(e -> e.contains("rainsisters.tistory.com"))
    .filter(e -> e.length() > 17)
    .collect(Collectors.toUnmodifiableList());


그리고 아쉽게도 Collectors.toUnmodifiableList() 는 java 10 부터 지원하는 함수다 ㅜ.ㅜ
어서 빨리들 java 버전을 업그레이드 하라!

끝!

+ Recent posts