본문 바로가기

WEB개발/Spring

[Spring] 주석 1(Annotation)

 

@Configuration, @Bean,

@EnableAsync, @Async,

@ExceptionHandler, @ResponseStatus, @ControllerAdvice,

@Controller,  @Service,  @Repository,

@RequestMapping,  @GetMapping,  @PostMapping ,  

@DateTimeFormat 

 

 


 

@Configuration, @Bean

 

@Configuration은 Spring Framework에서 사용되는 어노테이션 중 하나로, 해당 클래스가 Spring 애플리케이션 컨텍스트를 구성하는 데 사용된다는 것을 나타냅니다.

 

  • @Configuration이 붙은 클래스는 Spring의 Java 기반 구성 방식 중 하나인 Java Config를 사용하여 Bean을 정의하고 구성할 수 있습니다.
  • 이것은 XML 파일 대신 Java 클래스를 사용하여 애플리케이션 컨텍스트를 설정하는 방법입니다.
  • @Configuration 클래스 내부에는 @Bean 어노테이션을 사용하여 Bean 정의를 포함할 수 있습니다. @Bean 어노테이션을 사용하면 해당 메서드가 생성한 객체가 Spring 컨테이너의 Bean으로 등록됩니다
  • @Configuration없이 @Bean만 사용해도 스프링 빈으로 등록이 된다. 대신 메소드 호출을 통해 객체를 생성할 때 싱글톤을 보장하지 못합니다.
  • @Configuration 어노테이션도 내부적으로 @Component를 가지고 있어 @Configuration이 붙은 클래스도 빈으로 등록됩니다.
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 기존 Spider WebInitialze에 있던 로직을 IBKServletContextListener로 변경
     *
     * @return
     */
    @Bean
    ServletListenerRegistrationBean<ServletContextListener> servletListener() {
        ServletListenerRegistrationBean<ServletContextListener> srb = new ServletListenerRegistrationBean<>();
        srb.setListener(new DemoListener());
        return srb;
    }
}

 

 

.


 

 @EnableAsync, @ASYNC

 @EnableAsync로 @Async를 쓰겠다고 스프링에게 알립니다.


> executeAsync 호출 시 선 리턴 후 executeAsync() 개별 수행,  executeSync 호출 시 메소드수행 후 리턴 

 

Spring의 @Async 어노테이션은 메소드를 비동기적으로 실행할 수 있게 해주는 기능을 제공합니다. 이 어노테이션을 사용하면 메소드 호출이 별도의 스레드에서 비동기적으로 실행되며, 호출자는 해당 메소드의 실행 완료를 기다리지 않고 다른 작업을 계속할 수 있습니다.

 

@Async 어노테이션을 사용하기 위해서는 다음의 조건을 충족해야 합니다:

  1. 해당 메소드는 반드시 public이어야 합니다.

  2. 해당 메소드는 클래스 내부에서 호출되어야 합니다. 외부에서 같은 클래스 내의 @Async 메소드를 호출하면 비동기적으로 실행되지 않습니다.

  3. 해당 메소드는 다른 메소드에서 호출되어야 하며, 같은 클래스 내에서 this를 통해 호출되어야 합니다. 클래스 외부에서 같은 클래스의 메소드를 직접 호출하는 경우도 비동기적으로 실행되지 않습니다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@SpringBootApplication
@EnableAsync // 비동기 작업 활성화
@Slf4j
public class AsyncExampleApplication implements CommandLineRunner {

    private final AsyncService asyncService;

    public AsyncExampleApplication(AsyncService asyncService) {
        this.asyncService = asyncService;
    }

    public static void main(String[] args) {
        SpringApplication.run(AsyncExampleApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        log.info("Main thread starts.");

        // 비동기 메서드 호출
        CompletableFuture<String> result = asyncService.performAsyncTask();

        // 다른 작업 수행
        log.info("Main thread continues to do other work...");

        // 비동기 작업의 결과 대기
        log.info("Async result: {}", result.get());

        log.info("Main thread ends.");
    }
}

@Service
@Slf4j
class AsyncService {

    @Async
    public CompletableFuture<String> performAsyncTask() throws InterruptedException {
        log.info("Async task starts.");
        Thread.sleep(3000); // 3초 대기 (예: 네트워크 호출 시뮬레이션)
        log.info("Async task ends.");
        return CompletableFuture.completedFuture("Task Completed!");
    }
}
@Configuration
@EnableAsync
@Log4j2
public class AsyncConfig {

    @Bean(name = "threadPoolTaskExecutor", destroyMethod = "destroy")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); //작업이 큐에 추가되면 스레드 풀에서 바로 작업을 수행할 수 있는 스레드
        executor.setMaxPoolSize(10); //최대 쓰레드 수
        executor.setQueueCapacity(500); //큐에 대기가능한 작업 수
        executor.setThreadNamePrefix("demo-async-");
        executor.setTaskDecorator(new AsyncDecorator());
        executor.initialize();

        return new HandlingExecutor(executor);
    }


    public class HandlingExecutor implements AsyncTaskExecutor {
        private AsyncTaskExecutor executor;

        public HandlingExecutor(AsyncTaskExecutor executor) {
            this.executor = executor;
        }

        @Override
        public void execute(Runnable task) {
            executor.execute(createWrappedRunnable(task));
        }

        @Override
        public void execute(Runnable task, long startTimeout) {
            executor.execute(createWrappedRunnable(task), startTimeout);
        }

        @Override
        public Future<?> submit(Runnable task) {
            // TODO Auto-generated method stub
            return executor.submit(createWrappedRunnable(task));
        }


        private Runnable createWrappedRunnable(final Runnable task) {
            return new Runnable() {

                HashMap map;

                public void run() {
                    try {
                        task.run();
                        FutureTask<HashMap> future = (FutureTask<HashMap>) task;

                        if (future.isDone()) {
                            map = future.get();
                        }

                        log.debug("async end... map : " + map);

                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        MDC.clear();
                    }
                }
            };
        }// end of createWrappedRunnable

        public void destroy() {
            if (executor instanceof ThreadPoolTaskExecutor) {
                ((ThreadPoolTaskExecutor) executor).shutdown();
            }
        }
    }

}

 

 

- FutureTask

 

FutureTask는 RunnableFuture의 구현체 이며 RunnableFuture는 Runnalble과 Future의 결합체 이다. 말 그대로 Future와 Task를 합친 말로 Task를 수행하고 결과를 Future로 받을 수 있습니다.

 

즉,  수행 Thread와 결과를 받는 Thread가 분리 되어 있는 구조를 가집니다.

 

 

- TaskDecorator 

 

 Spring 4.3 이상부터 제공되는 TaskDecorator 를 이용해서 비동기처리하는 taskExecutor 생성시 커스터마이징이 가능하다는 것을 찾게 되었습니다.

 

ThreadLocal Value 공유예시

package com.example.async;

import lombok.extern.log4j.Log4j2;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;

import java.util.Map;

@Log4j2
public class AsyncDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable task) {

        Map logCtx = MDC.getCopyOfContextMap();
        log.info("############## logCtx : " + logCtx);

        return () -> {
            if (logCtx != null) MDC.setContextMap(logCtx);
            task.run();
        };
    }
}

 

 

스프링은 @Async를 사용한 메서드를 별도의 프록시 객체로 생성하여 비동기 처리할 수 있도록 합니다.
그러나 같은 클래스 내부에서 @Async 메서드를 호출하면 프록시가 아닌 원본 객체가 호출되므로 비동기 처리가 되지 않습니다.

 

1. 예외 처리의 복잡성

  • 설명: @Async 메서드에서 발생하는 예외는 호출한 쪽에서 바로 감지되지 않으며, 비동기 작업의 Future 객체를 통해 접근해야 합니다.
  • 해결책: 예외가 발생했을 때 로깅을 남기거나, AsyncUncaughtExceptionHandler를 사용하여 처리할 수 있습니다.
@Async
public CompletableFuture<String> asyncMethodWithException() {
    return CompletableFuture.supplyAsync(() -> {
        if (true) throw new RuntimeException("예외 발생!");
        return "Success";
    }).exceptionally(ex -> {
        log.error("예외 처리: {}", ex.getMessage());
        return "Fallback Value";
    });
}

 

@Component
@Slf4j
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.error("비동기 메서드 {} 실행 중 예외 발생: {}", method.getName(), ex.getMessage(), ex);
    }
}

 

2. 동일 클래스 내 비동기 호출 제한

  • 설명: @Async 메서드는 AOP 방식으로 프록시를 사용하기 때문에, 같은 클래스 내에서 @Async 메서드를 호출할 경우 비동기로 실행되지 않습니다.
  • 해결책: 같은 클래스 내 호출이 필요한 경우 @Async 메서드를 외부 클래스에 분리하거나, ApplicationContext를 주입받아 자기 자신의 @Async 메서드를 호출해야 합니다.
@Component
@Slf4j
public class AsyncService {
    @Autowired
    private ApplicationContext context;

    public void callAsyncMethod() {
        AsyncService self = context.getBean(AsyncService.class);
        self.asyncMethod();
    }

    @Async
    public void asyncMethod() {
        log.info("비동기 실행 중...");
    }
}

4. 스레드 풀 설정 필요

  • 설명: @Async의 기본 스레드 풀은 SimpleAsyncTaskExecutor로 설정되어 있어, 스레드 관리가 효율적이지 않을 수 있습니다. 기본 설정을 사용하면 많은 비동기 작업이 발생할 경우 성능 저하가 일어날 수 있습니다.

5. 트랜잭션 전파 문제

  • 설명: @Async 메서드는 호출하는 쪽과 다른 스레드에서 실행되므로 트랜잭션이 자동으로 전파되지 않습니다. 따라서 트랜잭션이 필요한 작업을 비동기로 실행할 때는 트랜잭션 관리에 주의가 필요합니다.

6. 모니터링 및 디버깅 어려움

  • 설명: 비동기 메서드는 다른 스레드에서 실행되므로, 호출 흐름을 추적하거나 디버깅하기가 어려울 수 있습니다. 예를 들어, 로깅의 순서가 다르게 나타나거나 디버깅 중에 예상치 못한 스레드 전환이 발생할 수 있습니다.
  • 해결책: 로깅 및 트레이싱을 적극적으로 활용하고, 상황에 따라 AOP로 메서드 호출을 모니터링하는 방법을 사용할 수 있습니다.
@Aspect
@Component
@Slf4j
public class AsyncLoggingAspect {
    @Before("@annotation(org.springframework.scheduling.annotation.Async)")
    public void logAsyncMethodExecution(JoinPoint joinPoint) {
        log.info("비동기 메서드 실행: {}", joinPoint.getSignature().toShortString());
    }
}

7. 메모리 관리

  • 설명: 비동기 작업이 많아지면 많은 스레드가 생성되어 메모리를 많이 사용하게 됩니다. 이는 메모리 사용량이 높은 애플리케이션에서는 문제가 될 수 있습니다.

 

 


 

@ExceptionHandler,  @ControllerAdvice

 

@ExceptionHandler

  • @Controller, @RestController에만 적용할 수 있습니다. (@Service, @Component는 안 됨)
  • 리턴 타입은 자유롭게 설정할 수 있습니다. 에러가 발생할 수 있는 메소드와 다른 리턴 타입이어도 상관없습니다.
  • @ExceptionHandler를 등록한 Controller에만 적용됩니다. (@ControllerAdvice를 통해 전역으로 관리가능)
  • @ResponseStatus를 이용해 HTTP 상태코드를 리턴하지 않으면, HTTP 상태 코드를 모두 200으로 리턴합니다.
  • @ExceptionHandler({ Exception1.class, Exception2.class}) 이런식으로 두 개 이상 등록도 가능합니다.
  • 다양한 응답타입으로 (View페이지, Josn Data등 리턴 가능..)

 

@ControllerAdvice

 : 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리해주는 annotation입니다.

 

@ControllerAdvice // 전역 예외 처리를 위한 어노테이션
public class DemoExceptionHandler {

    @ExceptionHandler(Exception.class) // 예외 타입을 지정하여 처리할 메소드를 정의합니다.
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 응답 상태 코드를 설정합니다.
    @ResponseBody // 응답 본문을 설정합니다.
    public String handleException(Exception e) {
        return "예외가 발생했습니다: " + e.getMessage();
    }
}

 

@RestController
public class MyRestController {
    ...
    ...
    @ExceptionHandler(NullPointerException.class)
    public Object nullex(Exception e) {
        System.err.println(e.getClass());
        return "myService";
    }
}

 

 

@ResponseStatus

 ResponseStatus는 Controller나 Exception에 사용하여 status 정보를 설정하여 리턴해 준다.

@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
public ModelAndView handleNotFound(Exception exception) {
    log.error("Handling not found Exception");
    log.error(exception.getMessage());

    ModelAndView mav = new ModelAndView();

    mav.setViewName("404error");
    mav.addObject("exception", exception);

    return mav;
  }
}

 


@Controller,  @Service,  @Repository

 

 공통점은 @Controller, @Service, @Repository은 @Component의 구체화된 형태로 해당 어노테이션이 사용된 곳은 @Component와 마찬가지로 자동으로 스프링 빈으로 등록됩니다. 따라서 @Controller 자리에 @Component를, @Service와 @Repository 자리에도 마찬가지로 @Component를 사용할 수 있습니다. 각 클래스의 역할을 명확하게 구분 지을 수 있어서 좋습니다.

 

  • @Repository

특정 예외를 잡아, 스프링의 unchecked 예외로 다시 던집니다. PersistenceExceptionTranslationPostProcessor를 구현하여야 합니다. 따라서 플랫폼 상세 예외를 잡으면, 스프링의 DataAccessException로 다시 던질 수 있습니다.

 

DDD(Evans, 2003)에서 정의된 Repository는 "저장, 검색, 객체 컬렉션을 에뮬레이트하는 검색 행위를 캡슐화하는 메커니즘"이라고 합니다

 

메소드에서 발생하는 예외를 데이터 액세스 예외로 자동으로 전환해줍니다.

 

구조정의

- Controller -> Service -> Repository -> mapper.xml
- Controller -> Service -> Repository -> Mapper -> mapper.xml
- Controller -> Service -> Mapper -> mapper.xml

 

 

  • @Service

어노테이션은 비즈니스 로직을 수행하는 서비스 클래스를 지정할 때 사용됩니다. 이 어노테이션이 붙은 클래스는 주로 비즈니스 로직을 구현하고, 트랜잭션 처리 등과 같은 서비스 관련 기능을 수행합니다.

 

  • @Controller

이 어노테이션이 붙은 클래스는 HTTP 요청을 처리하고 적절한 응답을 반환합니다. 주로 웹 애플리케이션에서 사용됩니다.

RequestingMapping 어노테이션과 같은 핸들러 메서드에 같이 사용됩니다. 

 

@Controller(Spring MVC Controller)
전통적인 Spring MVC의 컨트롤러인 @Controller는 주로 View를 반환하기 위해 사용합니다.하지만 @ResponseBody 어노테이션과 같이 사용하면 RestController와 똑같은 기능을 수행할 수 있습니다.
예시코드

-
@RestController(Spring Restful Controller)Permalink
RestController는 Controller에서 @ResponseBody 어노테이션이 붙은 효과를 지니게 됩니다.
즉 주용도는 JSON/XML형태로 객체 데이터 반환을 목적으로 합니다.

 

 


@RequestMapping, @GetMapping, @PostMapping

 

클래스 레벨의 @RequestMapping 

 

- 클래스 레벨에서도 @RequestMapping 설정이 가능하다

- 클래스 설정시 메소드에 설정한 URL은 모두 클래스의 서브 URL이 된다

@Controller
@RequestMapping("/user")
public class UserListController{

  @RequestMapping
  public String getAllUser(Model model){
    ...

  @RequestMapping("/userId:[0-9]{3}")
  public String getUserById(@PathVariable("userId") int id,...

 

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginForm() {
        return "login"; // 로그인 폼 화면 반환
    }

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        // 단순 파라미터 처리
        System.out.println("Username: " + username);
        System.out.println("Password: " + password);
        return "dashboard";
    }
}

 


 


@DateTimeFormat 

 

@DateTimeFormat 활용하면, 다양한 형식으로 customizing하여 데이터를 입력 받아올 수 있다. 

public class testDate{

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private DateTime testDate1;
    
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private DateTime testDate2;
    
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ssZ")
    private DateTime testDate3;

}

 

 

※ 주의 ※
마지막 3번째로 사용된 TimeZone까지 입력 받는 경우엔  반드시 Encoder를 거쳐야 한다.
(Encoder를 거치지 않을 경우엔 +를 space bar로 인식하여 typeMismatch와 같은 error 발생)


Decoder Ver : 2019-04-17 02:00:00+09:00
Encoder Ver :  2019-04-17+02%3A00%3A00%2B09%3A00 

 

 

 

 

 

 

 


출처: https://syundev.tistory.com/268

출처: https://mangkyu.tistory.com/75 [MangKyu's Diary]

출처: https://jeong-pro.tistory.com/187

출처 : https://dydtjr1128.github.io/java/2019/12/23/JAVA-FutureTask.html
 
출처 : https://blog.gangnamunni.com/post/mdc-context-task-decorator/

ModelAttribute - https://developer-joe.tistory.com/197

Component (Controller, Service, Repository) - https://escapefromcoding.tistory.com/732

ExceptionHandler - https://kchanguk.tistory.com/64

requestmapping - https://heeestorys.tistory.com/373