728x90
반응형

[Spring Boot] Spring Boot File Upload 예제 | 이미지 및 파일 업로드 구현 가이드

 

[Spring Boot] Spring Boot File Upload 예제 | 이미지 및 파일 업로드 구현 가이드

[Spring Boot] Spring Boot에서 Interceptor 사용하기 | 설정부터 실전 예제까지 [Spring Boot] Spring Boot에서 Interceptor 사용하기 | 설정부터 실전 예제까지[Spring Boot] Spring Boot 에러 처리: @ControllerAdvice로 쉽게 관

crushed-taro.tistory.com

1. MultiFile Upload

1. 파일 업로드 뷰 페이지 작성

업로드 폼을 보여줄 뷰 페이지를 작성한다.

<form action="multi-file" method="post" encType="multipart/form-data">
  파일 : <input type="file" name="multiFiles" multiple><br>
  파일 설명 : <input type="text" name="multiFileDescription"><br>
  <input type="submit">
</form>

 

위 코드에서 input 태그의 multiple은 파일 여러 개를 한 번에 업로드 하기 위한 속성이다.

 

2. 파일 업로드를 처리할 컨트롤러 생성

업로드 파일이 같은 name 속성으로 여러 개 전달 되므로 MultipartFile클래스는 List타입으로 선언해야 한다.

@PostMapping("multi-file")
	public String multiFileUpload(@RequestParam List<MultipartFile> multiFiles, 
			String multiFileDescription, Model model) {

		System.out.println("multiFiles : " + multiFiles);
		System.out.println("multiFileDescription : " + multiFileDescription);
		
		/* 파일을 저장할 경로 설정 */
		String root = "src/main/resources/static";
		
		System.out.println("root : " + root);
		
		String filePath = root + "/uploadFiles";
		
		File dir = new File(filePath);
		if(!dir.exists()) {
			dir.mkdirs();
		}

		List<FileDTO> files = new ArrayList<>();
		
		try {
			for(MultipartFile file : multiFiles) {
				/* 파일명 변경 처리 */
				String originFileName = file.getOriginalFilename();
				String ext = originFileName.substring(originFileName.lastIndexOf("."));
				String savedName = UUID.randomUUID().toString().replace("-", "") + ext;

				/* 파일에 관한 정보 추출 후 보관 */
				files.add(new FileDTO(originFileName, savedName, filePath, 
					multiFileDescription));
				/* 파일을 저장 */
				file.transferTo(new File(filePath + "/" + savedName));
			}
			model.addAttribute("message", "파일 업로드 성공!");
		} catch (Exception e) {
			e.printStackTrace();
			
			/* 실패 시 이전에 저장 된 파일 삭제 */
			for(FileDTO file : files) {
				new File(filePath + "/" + file.getSavedName()).delete();
			}

			model.addAttribute("message", "파일 업로드 실패!!");
		} 
		
		return "result";
	}

 

@RequestParam어노테이션을 이용하여 요청 파라미터 중 multiFiles라는 이름으로 전송된 파일을 List<MultipartFile>객체로 받아온다. 이후 해당 파일을 처리하는데 필요한 정보를 DTO 타입을 선언해서 다룰 수 있다. 추후 DB에 저장하는 등의 작업이 필요하다. 실패 시 이전에 저장 된 파일은 삭제하며 최종적으로 result라는 뷰 페이지를 반환한다.

 

3. FileDTO

파일을 처리하는데 필요한 정보를 DTO 타입을 선언해서 다룰 수 있다. 추후 DB에 저장하는 등의 작업 시 활용 된다.

 

public class FileDTO {

    private String originFileName;
    private String savedName;
    private String filePath;
    private String fileDescription;

    public FileDTO() {
    }

    public FileDTO(String originFileName, String savedName, String filePath,
				  String fileDescription) {
        this.originFileName = originFileName;
        this.savedName = savedName;
        this.filePath = filePath;
        this.fileDescription = fileDescription;
    }

    public String getOriginFileName() {
        return originFileName;
    }

    public void setOriginFileName(String originFileName) {
        this.originFileName = originFileName;
    }

    public String getSavedName() {
        return savedName;
    }

    public void setSavedName(String savedName) {
        this.savedName = savedName;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public String getFileDescription() {
        return fileDescription;
    }

    public void setFileDescription(String fileDescription) {
        this.fileDescription = fileDescription;
    }

    @Override
    public String toString() {
        return "FileDTO{" +
                "originFileName='" + originFileName + '\'' +
                ", savedName='" + savedName + '\'' +
                ", filePath='" + filePath + '\'' +
                ", fileDescription='" + fileDescription + '\'' +
                '}';
    }

}

 

4. 반환 뷰 페이지

이전 테스트와 동일한 반환 뷰 페이지를 사용한다.

 

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>result</title>
</head>
<body>
    <h1 th:text="${ message }"></h1>
</body>
</html>

 

지정된 경로에 파일이 업로드 되는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

[Spring Boot] Spring Boot에서 Interceptor 사용하기 | 설정부터 실전 예제까지

 

[Spring Boot] Spring Boot에서 Interceptor 사용하기 | 설정부터 실전 예제까지

[Spring Boot] Spring Boot 에러 처리: @ControllerAdvice로 쉽게 관리하기 [Spring Boot] Spring Boot 에러 처리: @ControllerAdvice로 쉽게 관리하기[Spring Boot] Spring Boot 에러 처리: @ExceptionHandler로 쉽게 관리하기 [Spring Boo

crushed-taro.tistory.com

1. Single File Upload

1. application.properties

spring-boot-starter-web에는 file upload를 위한 multipartResolver 가 기본 빈으로 등록 되어 있기 때문에 추가적으로 등록할 필요는 없다. 파일 저장 경로, 용량 등에 대한 설정을 application.properties를 이용해서 할 수 있다.

 

파일의 크기가 fize-size-threshold 값 이하라면 임시파일을 생성하지 않고 메모리에서 즉시 파일을 읽어서 생성할 수 있다. 속도는 빠르지만 쓰레드가 작업을 수행하는 동안 부담이 될 수 있다.

 

파일의 크기가 fize-size-threshold 값을 초과한다면 파일은 spring.servlet.multipart.location 경로에 저장되어 해당 파일을 읽어서 작업을 하도록 되어있다. 이 예제에서는 프로젝트 내부 경로에 업로드 파일을 저장하려고 하니 프로젝트 절대 경로를 파일 저장 경로로 설정한다.

 

# 파일 저장 경로
spring.servlet.multipart.location=프로젝트절대경로

# 최대 업로드 파일 크기
spring.servlet.multipart.max-file-size=10MB

# 최대 요청 파일 크기
spring.servlet.multipart.max-request-size=10MB

 

2. 파일 업로드 뷰 페이지 작성

업로드 폼을 보여줄 뷰 페이지를 작성한다. 

 

<form action="single-file" method="post" encType="multipart/form-data">
	파일 : <input type="file" name="singleFile"><br>
  파일 설명 : <input type="text" name="singleFileDescription"><br>
  <input type="submit">
</form>

 

위 코드에서 enctype="multipart/form-data"는 파일 업로드를 위한 인코딩 타입을 지정하는 속성이다.

 

3. 파일 업로드를 처리할 컨트롤러 생성

 

Spring Framework에서는 MultipartFile클래스를 이용하여 파일 업로드를 처리한다. 이를 처리할 컨트롤러를 생성한다.

 

@PostMapping("single-file")
public String singleFileUpload(@RequestParam MultipartFile singleFile, 
		String singleFileDescription, Model model) {
		
		System.out.println("singleFile : " + singleFile);
		System.out.println("singleFileDescription : " + singleFileDescription);
		
		/* 파일을 저장할 경로 설정 */
		String root = "src/main/resources/static";
		String filePath = root + "/uploadFiles";
		
		File dir = new File(filePath);
		System.out.println(dir.getAbsolutePath());

		if(!dir.exists()) {
			dir.mkdirs();
		}
		
		/* 파일명 변경 처리 */
		String originFileName = singleFile.getOriginalFilename();
		String ext = originFileName.substring(originFileName.lastIndexOf("."));
		String savedName = UUID.randomUUID().toString().replace("-", "") + ext;
		
		/* 파일을 저장 */
		try {
			singleFile.transferTo(new File(filePath + "/" + savedName));
			model.addAttribute("message", "파일 업로드 성공!");
		} catch (Exception e) {
			e.printStackTrace();
			model.addAttribute("message", "파일 업로드 실패!!");
		} 
		
		return "result";
	}

 

@RequestParam어노테이션을 이용하여 요청 파라미터 중 singleFile이라는 이름으로 전송된 파일을 MultipartFile객체로 받아온다. 이후 해당 파일을 리네임 처리하여 저장하고, result라는 뷰 페이지를 반환한다.

 

4. 반환 뷰 페이지 작성

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>result</title>
</head>
<body>
    <h1 th:text="${ message }"></h1>
</body>
</html>

 

지정된 경로에 파일이 업로드 되는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

[Spring Boot] Spring Boot 에러 처리: @ControllerAdvice로 쉽게 관리하기

 

[Spring Boot] Spring Boot 에러 처리: @ControllerAdvice로 쉽게 관리하기

[Spring Boot] Spring Boot 에러 처리: @ExceptionHandler로 쉽게 관리하기 [Spring Boot] Spring Boot 에러 처리: @ExceptionHandler로 쉽게 관리하기[Spring Boot] Spring Boot Controller에서 ModelAndView 반환과 동작 원리 [Spring Boot]

crushed-taro.tistory.com

1. Interceptor

1. Spring Interceptor란?

Spring Interceptor는 Spring 프레임워크에서 제공하는 기능 중 하나로, 클라이언트의 요청을 가로채서 처리하는 역할을 한다. 이를 통해 공통적인 로직(로깅, 성능 측정, 캐싱)을 처리하거나, 보안(인증, 권한) 등의 목적으로 특정 조건을 검사하고 해당 요청을 처리하거나, 무시할 수 있다.

 

Interceptor는 특정 요청 URL에만 적용되도록 매핑할 수 있다는 점이 필터와 유사하다. 하지만 필터와 달리 
Interceptor는 스프링 웹 애플리케이션 컨텍스트에 구성하기 때문에 컨테이너의 기능을 자유롭게 활용할 수 있으며 그 내부에 선언된 모든 빈을 참조할 수 있다. 

 

1. Spring MVC request life cycle

Spring 사진 1
Spring 사진 2

 

2. Interceptor 적용하기

Interceptor를 이용해서 handler의 수행 시간을 측정하는 테스트를 진행한다.

클라이언트 측에서 수행 시간을 확인하는 요청을 보낸다.

 

<button onclick="location.href='stopwatch'">수행 시간 확인하기</button>

 

컨트롤러의 핸들러 메소드에서 무언가 수행하는 상황을 상정하기 위해 Thread.sleep(밀리세컨)를 사용하여 잠시 멈추었다가 진행되도록 한다.

 

@GetMapping("stopwatch")
public String handlerMethod() throws InterruptedException {
		
	System.out.println("핸들러 메소드 호출함...");
	/* 아무 것도 하는 일이 없으니 수행시간이 0으로 나올 수 있어서 Thread.sleep(1000) 호출 */
	Thread.sleep(1000);
		
	return "result";
}

 

핸들러로 가는 요청을 가로챌 Interceptor를 작성한다. Interceptor는HandlerInterceptor 인터페이스를 구현하여 개발할 수 있으며, preHandle()postHandle()afterCompletion() 등의 메소드를 오버라이드하여 사용한다. preHandle() 메소드는 컨트롤러 실행 전, postHandle() 메소드는 컨트롤러 실행 후, afterCompletion() 메소드는 뷰가 렌더링 된 후 호출된다.

 

@Component
public class StopWatchInterceptor implements HandlerInterceptor {
	
	private final MenuService menuService;

	/* 인터셉터는 스프링 컨테이너에 존재하는 빈을 의존성 주입 받을 수 있다. */
	public StopWatchInterceptor(MenuService menuService) {
		this.menuService = menuService;
	}
	
	/* 전처리 메소드로 메소드 실행 시작 시간을 request에 attribute로 저장한다. */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
			Object handler) throws Exception {
		
		System.out.println("preHandler 호출함...");
		
		long startTime = System.currentTimeMillis();
		
		request.setAttribute("startTime", startTime);
		
		/* true이면 컨트롤러를 이어서 호출한다. false이면 핸들러 메소드를 호출하지 않는다. */
		return true;
	}
	
	/* 후처리 메소드로 메소드 실행 종료 시간을 구해 실행 시작 시간과 연산하여 걸린 시간을 계산한다. */
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, 
			Object handler, @Nullable ModelAndView modelAndView) throws Exception {
		
		System.out.println("postHandler 호출함...");
		
		long startTime = (Long) request.getAttribute("startTime");
		request.removeAttribute("startTime");
		
		long endTime = System.currentTimeMillis();
		
		modelAndView.addObject("interval", endTime - startTime);
	}
	
	/* 뷰가 렌더링 된 후 호출하는 메소드 */
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
			Object handler, @Nullable Exception ex) throws Exception {
		
		System.out.println("afterComplate 호출함...");
		
		menuService.method();
	}
	
}

 

MenuService는 의존성 주입 테스트를 위해 작성하는 클래스이다.

 

@Service
public class MenuService {
	
	public void method() {
		System.out.println("메소드 호출 확인");
	}
	
}

 

응답할 뷰는 아래와 같이 작성하여 메소드 호출 수행시간을 출력해본다.

 

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>result</title>
</head>
<body>
    <h1 th:text="|메소드 호출 수행 시간 : ${ interval } (ms)|"></h1>
</body>
</html>

 

Interceptor를 사용하려면, WebMvcConfigurer 인터페이스를 구현하는 설정 클래스를 만들고 addInterceptors() 메소드를 오버라이드하여 등록해야 한다. 이 때 등록 순서에 따라 Interceptor가 실행되는 순서가 결정된다.

 

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

		@Autowired
    private StopWatchInterceptor stopWatchInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(stopWatchInterceptor)
                .addPathPatterns("/stopwatch");
    }
}

 

로그와 결과 화면을 통해 Interceptor가 잘 적용 되었음을 확인할 수 있다.

 

만약 모든 요청에 대해서 인터셉터를 적용하고 싶다면 아래와 같이 적용할 수 있다. 정적 리소스 또는 에러 포워딩 경로 등은 excluedPathPatterns 메서드를 통해 제외해주면 된다.

 

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

		@Autowired
    private StopWatchInterceptor stopWatchInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(stopWatchInterceptor)
                .addPathPatterns("/*")
                /* static 하위의 정적 리소스는 인터셉터가 적용되지 않도록 한다. */
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/images/**")
                .excludePathPatterns("/js/**")
                /* /error 로 포워딩 되는 경로도 제외해주어야 한다. */
                .excludePathPatterns("/error");
    }
}
728x90
반응형
728x90
반응형

[Spring Boot] Spring Boot @ModelAttribute 완벽 가이드 - Handler Method에서 데이터 바인딩 마스터하기

 

[Spring Boot] Spring Boot @ModelAttribute 완벽 가이드 - Handler Method에서 데이터 바인딩 마스터하기

[Spring Boot] Spring Boot @RequestParam 완벽 가이드: HTTP 요청 파라미터 처리하기 [Spring Boot] Spring Boot @RequestParam 완벽 가이드: HTTP 요청 파라미터 처리하기[Spring Boot] Spring Boot Handler Method의 WebRequest 파라미

crushed-taro.tistory.com

1. Handler Method

1. HttpSession

index.html에GET 방식의 /first/login요청을 전달한다.

<h3>4-1, 4-2. HttpSession 이용하기</h3>
<button onclick="location.href='/first/login'">session에 정보 담기</button>

 

Controller 클래스의 핸들러 메소드를 통해 파라미터 전달 테스트를 위해 값을 입력할 수 있는 뷰를 응답한다.

@Controller
@RequestMapping("/first/*")
public class FirstController {

	@GetMapping("login")
	public void login() {}

	...생략

}

 

resources/templates/first 의 하위에 login.html파일을 생성한다.
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
    <h1>Session 이용하기</h1>

    <h3>HttpSession을 매개변수로 선언하기</h3>
    <form action="login1" method="post">
        ID : <input type="text" name="id"><br>
        PWD : <input type="password" name="pwd"><br>
        <input type="submit" value="로그인">
    </form>
</body>
</html>

 

해당 화면에서 사용자 입력 양식에 값을 입력하고 submit 을 누르면 POST 방식의 /first/login1요청이 발생한다.

 

발생하는 요청을 매핑할 controller의 handler method이다.

HttpSession을 매개변수로 선언하면 핸들러 메소드 호출 시 세션 객체를 넣어서 호출한다.

@PostMapping("login1")
public String sessionTest1(HttpSession session, @RequestParam String id) {
		
	session.setAttribute("id", id);

	return "first/loginResult";
}

 

resources/templates/first 의 하위에 loginResult.html파일을 생성한다.
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>loginResult</title>
</head>
<body>
    <h1>Session에 담긴 값 확인하기</h1>
    <h3 th:text="|${ session.id }님 환영합니다.|"></h3>
    <div>
        <button onclick="location.href='logout1'">로그아웃1</button>
    </div>
</body>
</html>

 

클라이언트에서 입력 된 값이 컨트롤러 핸들러 메서드에서 HttpSession 객체에 잘 담겼음을 응답 화면을 통해 확인할 수 있다.

로그아웃 버튼 클릭 시에는 HttpSession 을 만료 시킨다.

@GetMapping("logout1")
public String logoutTest1(HttpSession session) {
		
	session.invalidate();
		
	return "first/loginResult";
}

 

세션에서 id 값이 제거 된 것을 확인할 수 있다. 

resources/templates/first 의 하위의 login.html파일에 @SessionAttributes를 사용하기 내용을 추가한다.
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
	...생략
	<h3>@SessionAttributes를 사용하기</h3>
   <form action="login2" method="post">
        ID : <input type="text" name="id"><br>
        PWD : <input type="password" name="pwd"><br>
        <input type="submit" value="로그인">
    </form>
</body>
</html>

 

해당 화면에서 사용자 입력 양식에 값을 입력하고 submit 을 누르면 POST 방식의 /first/login2 요청이 발생한다.

발생하는 요청을 매핑할 controller의 handler method이다. 이번에는 Model에 id를 추가한다.

@PostMapping("login2")
public String sessionTest2(Model model, @RequestParam String id) {
		
	model.addAttribute("id", id);
		
	return "first/loginResult";
}

 

그리고 클래스 레벨에 @SessionAttributes("id")를 설정한다. 클래스 레벨에 @SessionAttributes 어노테이션을 이용하여 세션에 값을 담을 key값을 설정 해두면 Model 영역에 해당 key로 값이 추가되는 경우 session에 자동 등록을 한다.

@Controller
@RequestMapping("/first/*")
@SessionAttributes("id")
public class FirstController {
	...생략
}

 

세션에 값이 잘 저장 되었음을 응답 화면을 통해 확인할 수 있다. 단, 로그아웃 버튼을 눌러도 세션 객체가 제거 되지 않는 문제가 있다. 따라서 로그아웃 버튼도 다시 추가해서 로그아웃 로직을 추가 작성한다.

loginResult.html 파일을 수정한다.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>loginResult</title>
</head>
<body>
    <h1>Session에 담긴 값 확인하기</h1>
    <h3 th:text="|${ session.id }님 환영합니다.|"></h3>
    <div>
        <button onclick="location.href='logout1'">로그아웃1</button>
				<button onclick="location.href='logout2'">로그아웃2</button>
    </div>
</body>
</html>
SessionAttributes로 등록된 값은 session의 상태를 관리하는 SessionStatus의 setComplete()메소드를 호출해야 사용이 만료된다.
@GetMapping("logout2")
public String logoutTest2(SessionStatus sessionStatus) {
		
	/* 현재 컨트롤러 세션에 저장된 모든 정보를 제거한다. 개별 제거는 불가능하다. */
	sessionStatus.setComplete();
		
	return "first/loginResult";
}

 

세션에서 id 값이 제거 된 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

1. 스프링 부트(Spring Boot) 개요

1. 스프링 부트란?

  • 필요한 환경 설정을 최소화하고 개발자가 비즈니스 로직에 집중할 수 있도록 도와줘 생산성을 크게 향상시킬 수 있도록 스프링의 단점을 보완하여 만든 프로젝트이다.
  • 스프링 부트는 틀림없이 자바에서 REST 기반 마이크로서비스 웹 애플리케이션을 개발하는 가장 빠른 방법 중 하나로 도커 컨테이너 배포 및 빠른 프로토타이핑에도 매우 적합하다.
  • 간혹 스프링부트를 스프링 프레임워크와 전혀 다른 것으로 오해하지만 스프링부트는 스프링프레임워크라는 큰 틀 속에 속하는 도구일 뿐이다.

 

Spring Boot를 사용하면 "그냥 실행할 수 있는" 독립 실행형 production-grade Spring 기반 애플리케이션을 쉽게 생성할 수 있다.우리는 당신이 최소한의 고민으로 시작할 수 있도록 Spring 플랫폼과 third-party libraries에 대한 견해를 가지고 있다. 대부분의 Spring Boot 애플리케이션은 최소한의 Spring 구성으로 동작시킬 수 있다.
출처 : https://spring.io/projects/spring-boot
 

Spring Boot

 

spring.io

 

2. 스프링 부트 역사

  • 2012년 10월 Mike Youngstrom는 Spring 프레임워크에서 컨테이너 없는 웹 애플리케이션 아키텍처에 대한 지원을 요청하는 기능 요청했다. main 메소드에서 부트스트랩된 스프링 컨테이너 내에서 웹 컨테이너 서비스를 구성하는 것에 대해 말했는데 다음은 '이슈' 기반의 프로젝트 관리 도구 jira 요청에서 발췌한 내용이다.
Spring 컴포넌트와 설정 모델을 위에서 아래로 활용하는 도구와 참조 아키텍처를 제공한다면 Spring의 웹 애플리케이션 아키텍처는 상당히 단순화될 수 있다고 생각한다. 간단한 main() 메소드에서 부트스트랩된 Spring Container 내에 이러한 공통 웹 컨테이너 서비스의 구성을 포함하고 통합한다.
  • Mike Youngstrom의 요청으로 인해 2013년 초에 시작되는 스프링 부트 프로젝트 개발이 시작되었고 2014년 4월에 스프링 부트 1.0.0이 릴리즈 되었다. 그 이후로 많은 스프링 부트 마이너 버전이 나왔다.
  • https://github.com/spring-projects/spring-boot/wiki
 

Home

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss. - spring-projects/spring-boot

github.com

 

3. 스프링 부트의 특징

  • 임베디드 톰캣(Embed Tomcat), 제티, 언더토우를 사용하여 독립 실행이 가능한 스프링 애플리케이션 개발
  • 통합 스타터를 제공하여 메이븐/그레이들 구성 간소화
  • 스타터를 통한 자동화된 스프링 설정 제공
  • 번거로운 XML 설정을 요구하지 않음
  • JAR 를 사용하여 자바 옵션만으로 배포 가능
  • 애플리케이션의 모니터링과 관리를 위한 스프링 액츄에이터(Spring Actuator) 제공

 

4. 스프링 부트의 사용 이유

1. 스프링 부트의 장점

  • 각각의 의존성 버젼을 쉽게 올리는 것이 가능하다. (수동으로 설정하던 기존의 방식에 비해 안정된 버전 제공을 보장받을 수 있음)
  • 간단한 어노테이션/프로퍼티 설정으로 원하는 기능을 빠르게 적용할 수 있다.
  • 별도의 외장 톰캣을 설치할 필요가 없고 톰캣 버젼도 편리하게 관리할 수 있다.
  • 특정 라이브러리에 버그가 있더라도 이후에 스프링팀에서 버그를 수정하면 수정된 버전을 받기에 편리하다.

 

2. 스프링 부트의 단점

  • 설정을 커스터마이징 해야 하는 경우 기존 스프링프레임워크를 사용하는 것과 같은 어려움을 겪을 수 있다.
  • 설정을 변경하고 싶은 경우 정확한 동작 원리와 내부 코드를 살펴봐야 하는 불편함이 있다.

 

5. 스프링 부트의 핵심 요소

1. 스프링 부트의 핵심 요소의 종류

요소 기능
스타터(Starter) 스프링의 특정 모듈을 사용할 수 있도록 관련된 라이브러리 의존성을 해결
자동설정(Autoconfiguration) Starter로 추가한 모듈을 사용할 수 있도록 빈 설정을 자동으로 관리
액추에이터(Actuator) 스프링부트로 개발된 시스템을 모니터링할 수 있는 기능들을 제공

 

6. 스타터(Starter)

1. 스타터란?

  • 의존성과 설정을 자동화 해주는 모듈을 뜻하며 필요한 라이브러리들을 관련있는 것들을 묶어서 제공한다.
  • 라이브러리간의 의존관계를 파악하고 dependency들의 상속관계를 통해 작성되어 필요한 라이브러리를 다운로드 받아준다.

 

2. 스프링 부트 스타터 명명 규칙

  • spring-boot-starter-*
  • 스프링부트의 기본 스타터 종류
스타터명 설명
spring-boot-starter 스프링 부트의 코어 (auto-configuration, logging, yaml 등을 제공)
spring-boot-starter-aop AOP(Aspect Oriented Programming)를 위한 스타터
spring-boot-starter-batch Spring Batch를 사용하기 위한 스타터
spring-boot-starter-jpa Spring Data JPA와 Hibernate를 위한 스타터
spring-boot-starter-data-redis Redis와 Jedis 사용에 필요한 스타터
spring-boot-starter-data-rest Spring Data REST를 사용하기 위한 스타터
spring-boot-starter-thymleaf Thymeleaf 템플릿 엔진 사용에 필요한 스타터
spring-boot-starter-jdbc JDBC Connection Pool 사용에 필요한 스타터
spring-boot-starter-security Sprint Security 사용에 필요한 스타터
spring-boot-starter-oauth2 OAuth2 인증 사용에 필요한 스타터
spring-boot-starter-validation Java Bean Validation 사용에 필요한 스타터
spring-boot-starter-web 웹 개발을 위해 필요한 스타터(Spring MVC, REST, Embed Tomcat, 기타 라이브러리 등)

 

3. 스프링부트 스타터 의존성 확인

 

Build Systems :: Spring Boot

Each release of Spring Boot provides a curated list of dependencies that it supports. In practice, you do not need to provide a version for any of these dependencies in your build configuration, as Spring Boot manages that for you. When you upgrade Spring

docs.spring.io

728x90
반응형
728x90
반응형

1. Proxy

Java에서 프록시(Proxy)는 대리자를 의미한다. 프록시는 기존의 객체를 감싸서 그 객체의 기능을 확장하거나 변경할 수 있게 해준다. 예를 들어, 프록시 객체를 사용하면 객체에 대한 접근을 제어하거나, 객체의 메소드 호출 전후에 로깅 작업 등을 수행할 수 있다. 또한, 프록시 객체를 사용하여 원격으로 실행되는 객체를 호출할 수도 있다. 프록시는 주로 AOP(Aspect Oriented Programming)에서 사용된다.
  • 프록시 생성은 크게 두 가지 방식이 제공된다.
  1. 1. JDK Dynamic Proxy 방식
    • 리플렉션을 이용해서 proxy 클래스를 동적으로 생성해주는 방식으로, 타겟의 인터페이스를 기준으로 proxy를 생성해준다. 사용자의 요청이 타겟을 바라보고 실행될 수 있도록 타겟 자체에 대한 코드 수정이 아닌 리플렉션을 이용한 방식으로, 타겟의 위임 코드를InvocationHandler를 이용하여 작성하게 된다. 하지만 사용자가 타겟에 대한 정보를 잘못 주입하는 경우가 발생할 수 있기 때문에 내부적으로 주입된 타겟에 대한 검증 코드를 거친 후 invoke가 동작하게 된다.
  2. CGLib 방식
    • 동적으로 Proxy를 생성하지만 바이트코드를 조작하여 프록시를 생성해주는 방식이다. 인터페이스 뿐 아니라 타겟의 클래스가 인터페이스를 구현하지 않아도 프록시를 생성해준다. CGLib(Code Generator Library)의 경우에는 처음 메소드가 호출된 당시 동적으로 타켓 클래스의 바이트 코드를 조작하게 되고, 그 이후 호출 시부터는 변경된 코드를 재사용한다. 따라서 매번 검증 코드를 거치는 1번 방식보다는 invoke시 더 빠르게 된다. 또한 리플렉션에 의한 것이 아닌 바이트코드를 조작하는 방식이기 때문에 성능면에서는 더 우수하다.

하지만 CGLib 방식은 스프링에서 기본적으로 제공되는 방식은 아니었기에 별도로 의존성을 추가하여 개발해야 했고, 파라미터가 없는 default 생성자가 반드시 필요했으며, 생성된 프록시의 메소드를 호출하면 타겟의 생성자가 2번 호출되는 등의 문제점들이 있었다.

스프링 4.3, 스프링부트 1.3 이후부터 CGLib의 문제가 된 부분이 개선되어 기본 core 패키지에 포함되게 되었고, 스프링에서 기본적으로 사용하는 프록시 방식이 CGLib 방식이 되었다.

 

1. 로직을 포함하는 코드 작성

  • Student 인터페이스 작성
public interface Student {
	
	void study(int hours);
}

 

  • Student 클래스 작성 (Student인터페이스 구현)
public class Student implements Student {
	
	 @Override
     public void study(int hours) {
        System.out.println(hours + "시간 동안 열심히 공부합니다.");
     }
}

 

2. dynamic

1. Handler 클래스 작성

java.lang.reflect.InvocationHandler를 구현한 클래스를 작성한다.

Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.

public class Handler implements InvocationHandler {
	
	/* 메소드 호출을 위해 타겟 인스턴스가 필요하다 */
	private final Student student;
     
  public Handler(Student student) {
    this.student = student;
  }
    
  /* 생성된 proxy 인스턴스와 타겟 메소드, 전달받은 인자를 매개변수로 한다. */
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) 
		throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    	
    System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
    System.out.println("호출 대상 메소드 : " + method);
    for(Object arg : args) {
    	System.out.println("전달된 인자 : " + arg);
    }
    	
   /* 타켓 메소드를 호출한다. 타겟 Object와 인자를 매개변수로 전달한다. 
    * 여기서 프록시를 전달하면 다시 타겟을 호출할 때 다시 프록시를 생성하고 다시 또 전달하는 무한 루프에 빠지게 된다.
    * */
   method.invoke(student, args);
    	 
   System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
    	 
   return proxy;
    	 
  }
}

 

2. Application 실행 클래스 작성

Student student = new Student();
Handler handler = new Handler(student);
		
/* 클래스로더, 프록시를 만들 클래스 메타 정보(인터페이스만 가능), 프록시 동작할 때 적용될 핸들러 */
Student proxy 
= (Student) Proxy.newProxyInstance(Student.class.getClassLoader(), new Class[] {Student.class}, handler);
	    
/* 프록시로 감싸진 인스턴스의 메소드를 호출하게 되면 핸들러에 정의한 메소드가 호출된다. */
proxy.study(16);

/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public abstract void Student.study(int)
전달된 인자 : 16
16시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/

 

  • study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.

 

3. cglib

1. Handler 클래스 작성

org.springframework.cglib.proxy.InvocationHandler를 구현한 클래스를 작성한다.

Student 클래스를 타겟 인스턴스로 설정하고 invoke 메소드를 정의한다.

public class Handler implements InvocationHandler {
	
	private final Student student;
     
  public Handler(Student student) {
   this.student = student;
  }
    
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) 
		throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
    	
  	System.out.println("============ 공부가 너무 하고 싶습니다. ==============");
   	System.out.println("호출 대상 메소드 : " + method);
   	for(Object arg : args) {
   		System.out.println("전달된 인자 : " + arg);
   	}
    	
   	method.invoke(student, args);
    	 
   	System.out.println("============ 공부를 마치고 수면 학습을 시작합니다. ============");
    	 
   	return proxy;
   	 
  }
}

 

2. Application 실행 클래스 작성

Student student = new Student();
Handler handler = new Handler(student);
		
/* Enhancer 클래스의 create static 메소드는 타겟 클래스의 메타정보와 핸들러를 전달하면 proxy를 생성해서 반환해준다. */
Student proxy 
= (Student) Enhancer.create(Student.class, new Handler(new Student()));

proxy.study(20);

/*
============ 공부가 너무 하고 싶습니다. ==============
호출 대상 메소드 : public void Student.study(int)
전달된 인자 : 20
20시간 동안 열심히 공부합니다.
============ 공부를 마치고 수면 학습을 시작합니다. ============
*/
  • study 메소드 호출 시 proxy 객체의 동작을 확인할 수 있다.
728x90
반응형
728x90
반응형

[Spring Framework] Spring AOP 구현하기 | 예제 포함

 

[Spring Framework] Spring AOP 구현하기 | 예제 포함

[Spring Framework] [Spring AOP] 종류와 특징 총정리 – 입문자를 위한 기초 가이드 [Spring Framework] [Spring AOP] 종류와 특징 총정리 – 입문자를 위한 기초 가이드[Spring Framework] 초보자를 위한 Spring Bean Proper

crushed-taro.tistory.com

1. Reflection

Java 리플렉션(Reflection)은 실행 중인 자바 프로그램 내부의 클래스, 메소드, 필드 등의 정보를 분석하여 다루는 기법을 말한다. 이를 통해 프로그램의 동적인 특성을 구현할 수 있다. 예를 들어, 리플렉션을 이용하면 실행 중인 객체의 클래스 정보를 얻어오거나, 클래스 내부의 필드나 메소드에 접근할 수 있다. 이러한 기능들은 프레임워크, 라이브러리, 테스트 코드 등에서 유용하게 활용된다.

⇒ 스프링에서는 이 Reflection 기술을 사용해 런타임 시 등록한 빈을 애플리케이션 내에서 사용할 수 있게 한다.

 

1. 로직을 포함하는 코드 작성

플렉션 테스트의 대상이 될Account클래스를 생성한다.

public class Account {
	
	private String bankCode;
	private String accNo;
	private String accPwd;
	private int balance;
	
	public Account() {}
	
	public Account(String bankCode, String accNo, String accPwd) {
		this.backCode = bankCode;
		this.accNo = accNo;
		this.accPwd = accPwd;
	}
	
	public Account(String bankCode, String accNo, String accPwd, int balance) {
		this(bankCode, accNo, accPwd);
		this.balance = balance;
	}
	
	/* 현재 잔액을 출력해주는 메소드 */
	public String getBalance() {
		
		return this.accNo + " 계좌의 현재 잔액은 " + this.balance + "원 입니다.";
	}
	
	/* 금액을 매개변수로 전달 받아 잔액을 증가(입금) 시켜주는 메소드 */
	public String deposit(int money) {
		
		String str = "";
		
		if(money >= 0) {
			this.balance += money;
			str = money + "원이 입급되었습니다.";
		}else {
			str = "금액을 잘못 입력하셨습니다.";
		}
		
		return str;
	}
	
	/* 금액을 매개변수로 받아 잔액을 감소(출금) 시켜주는 메소드 */
	public String withDraw(int money) {
		
		String str = "";
		
		if(this.balance >= money) {
			this.balance -= money;
			str = money + "원이 출금되었습니다.";
		}else {
			str = "잔액이 부족합니다. 잔액을 확인해주세요.";
		}

		return str;
	}
}

 

2. 리플렉션 테스트

1. Class

Class타입의 인스턴스는 해당 클래스의 메타정보를 가지고 있는 클래스이다.

/* .class 문법을 이용하여 Class 타입의 인스턴스를 생성할 수 있다. */
Class class1 = Account.class;
System.out.println("class1 : " + class1);
		
/* Object 클래스의 getClass() 메소드를 이용하면 Class 타입으로 리턴받아 이용할 수 있다. */
Class class2 = new Account().getClass();
System.out.println("class2 : " + class2);

/* Class.forName() 메소드를 이용하여 런타임시 로딩을 하고 그 클래스 메타정보를 Class 타입으로 반환받을 수 있다. */
try {
	Class class3 = Class.forName("project.reflection.Account");
	System.out.println("class3 : " + class3);
			
	/* Double자료형 배열을 로드할 수 있다. */
	Class class4 = Class.forName("[D");
	Class class5 = double[].class;
			
	System.out.println("class4 : " + class4);
	System.out.println("class5 : " + class5);
			
	/* String자료형 배열을 로드할 수 있다. */
	Class class6 = Class.forName("[Ljava.lang.String;");
	Class class7 = String[].class;
	System.out.println("class6 : " + class6);
	System.out.println("class7 : " + class7);
			
} catch (ClassNotFoundException e) {
	e.printStackTrace();
}
		
/* 원시 자료형을 사용하면 컴파일 에러 발생 */
//		double d = 1.0;
//		Class class8 = d.getClass();
		
/* TYPE 필드를 이용하여 원시형 클래스를 반환받을 수 있다. */
Class class8 = Double.TYPE;
System.out.println("class8 : " + class8);
		
Class class9 = Void.TYPE;
System.out.println("class9 : " + class9);
		
/* 클래스의 메타 정보를 이용하여 여러 가지 정보를 반환받는 메소드를 제공한다. */
/* 상속된 부모 클래스를 반환한다. */
Class superClass = class1.getSuperclass();
System.out.println("superClass : " + superClass);

/*
class1 : class project.reflection.Account
class2 : class project.reflection.Account
class3 : class project.reflection.Account
class4 : class [D
class5 : class [D
class6 : class [Ljava.lang.String;
class7 : class [Ljava.lang.String;
class8 : double
class9 : void
superClass : class java.lang.Object
*/

 

2. field

field 정보에 접근할 수 있다.

Field[] fields = Account.class.getDeclaredFields();
for(Field field : fields) {
	System.out.println("modifiers : " + Modifier.toString(field.getModifiers()) + 
			", type : " + field.getType() + 
			", name : " + field.getName() );
}

/*
modifiers : private, type : class java.lang.String, name : backCode
modifiers : private, type : class java.lang.String, name : accNo
modifiers : private, type : class java.lang.String, name : accPwd
modifiers : private, type : int, name : balance
*/

 

3. 생성자

생성자 정보에 접근할 수 있다.

Constructor[] constructors = Account.class.getConstructors();
for(Constructor con : constructors) {
	System.out.println("name : " + con.getName());
			
	Class[] params = con.getParameterTypes();
	for(Class param : params) {
		System.out.println("paramType : " + param.getTypeName());
	}
}

/*
name : project.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
paramType : int
name : project.reflection.Account
paramType : java.lang.String
paramType : java.lang.String
paramType : java.lang.String
name : project.reflection.Account
*/

 

생성자를 이용하여 인스턴스를 생성할 수 있다.

try {
	Account acc = (Account) constructors[0].newInstance("20", "110-223-123456", "1234", 10000);
	System.out.println(acc.getBalance());
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
		| InvocationTargetException e) {
		e.printStackTrace();
}

/*
110-223-123456 계좌의 현재 잔액은 10000원 입니다.
*/

 

4. 생성자

메소드 정보에 접근할 수 있다.

Method[] methods = Account.class.getMethods();
Method getBalanceMethod = null;
for(Method method : methods) {
	System.out.println(Modifier.toString(method.getModifiers()) + " " + 
					method.getReturnType().getSimpleName() + " " + 
					method.getName());
			
	if("getBalance".equals(method.getName())) {
		getBalanceMethod = method;
	}
}

/*
public String getBalance
public String withDraw
public String deposit
public final native void wait
public final void wait
public final void wait
public boolean equals
public String toString
public native int hashCode
public final native Class getClass
public final native void notify
public final native void notifyAll
*/

 

invoke 메소드로 메소드를 호출할 수 있다.

try {
	System.out.println(getBalanceMethod.invoke(((Account) constructors[2].newInstance())));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
	e.printStackTrace();
} catch (InstantiationException e) {
	e.printStackTrace();
}

/*
null 계좌의 현재 잔액은 0원 입니다.
*/
728x90
반응형
728x90
반응형

[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지

 

[Spring Framework] Spring Bean Scope 완벽 가이드 | Singleton부터 Prototype까지

[Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드 [Spring Framework] Spring Framework에서 @Inject 애너테이션 완벽 가이드[Spring Framework] Spring Boot DI 완벽 정리 | @Resource 활용법 [Spring Framework] Sp

crushed-taro.tistory.com

1. Bean

1. init, destroy method

스프링 빈은 초기화(init)와 소멸화(destroy)의 라이프 사이클을 가지고 있다. 이 라이프 사이클을 이해하면 빈 객체가 생성되고 소멸될 때 추가적인 작업을 수행할 수 있다. 

init-method 속성을 사용하면 스프링이 빈 객체를 생성한 다음 초기화 작업을 수행할 메소드를 지정할 수 있다. 이 메소드는 빈 객체 생성자가 완료된 이후에 호출된다. init-method 속성으로 지정된 메소드는 일반적으로 빈의 초기화를 위해 사용된다.

destroy-method 속성을 사용하면 빈 객체가 소멸될 때 호출할 메소드를 지정할 수 있다. 이 메소드는 ApplicationContext의 close() 메소드가 호출되기 전에 빈 객체가 소멸될 때 호출된다. destroy-method 속성으로 지정 된 메소드는 일반적으로 사용하던 리소스를 반환하기 위해 사용된다.

 

1. java

java 설정 방식으로init-methoddestroy-method 를 테스트 하기 위해 Owner라는 클래스를 추가한다.

public class Owner {
	
	public void openShop() {
		System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

 

Owner 타입의 빈 객체를 설정 파일에 등록한다. @Bean 어노테이션에는 initMethoddestoryMethod 라는 속성이 있는데 해당 속성을 Owner 클래스에 정의했던 openShopcloseShop메소드로 설정한다.

@Configuration
public class ContextConfiguration {
	
	...생략
	
	/* init-method로 openShop 메소드를 설정하고 destory-method로 closeShope 메소드를 설정한다. */
	@Bean(initMethod = "openShop", destroyMethod="closeShop")
	public Owner owner() {
		
		return new Owner()
	}

}

 

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 
 * 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((AnnotationConfigApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:58:53 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

 

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner객체의 소멸 시점에 호출 되었음을 확인할 수 있다. init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만 destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다. 따라서 컨테이너를 종료시키면 destroy 메소드의 동작까지 확인 할 수 있다.

 

2. annotation

annotation 방식으로init-method,destroy-method를 테스트 하기 위해Owner라는 클래스를 추가한다.

@Component
public class Owner {
	
	/* init-method와 같은 설정 어노테이션이다. */
	@PostConstruct
	public void openShop() {
		System.out.println("사장님이 가게 문을 오픈하셨습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	/* destroy-method와 같은 설정 어노테이션이다. */
	@PreDestroy
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫으셨습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

컴포넌트 스캔을 통해 빈 등록 하기 위해 @Componet 어노테이션을 클래스 위에 설정한다. javax.annotation의 @PostConstruct , @PreDestroy 어노테이션은 @Bean 어노테이션에 init-methoddestroy-method 속성을 설정하는 것과 같은 역할을 한다. 단, 해당 어노테이션을 사용할 수 있도록 라이브러리 의존성이 build.gradle.kts파일에 추가 되어 있어야 한다.

dependencies {
	...생략
	implementation("javax.annotation:javax.annotation-api:1.3.2")
}

Owner타입의 빈 객체를 컴포넌트 스캔을 통해 읽어 등록할 수 있도록 빈 설정 파일에 컴포넌트 스캔 경로를 설정한다. 그 외의 상품과 쇼핑 카트의 빈 등록 코드는 그대로 사용한다.

@Configuration
@ComponentScan("project.initdestroy.subsection02.annotation")
public class ContextConfiguration {
	...생략
}

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((AnnotationConfigApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 21:59:07 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner 객체의 소멸 시점에 호출 되었음을 확인할 수 있다. 

 

3. xml

xml 설정 방식으init-methoddestroy-method 를 테스트 하기 위해 Owner라는 클래스를 추가한다.

public class Owner {
	
	public void openShop() {
		System.out.println("사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.");
	}
	
	public void closeShop() {
		System.out.println("사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.");
	}
	
}

bean configuration file에<bean> 태그를 통해 상품, 쇼핑카트 그리고 Owener 에 대한 빈 등록을 설정한다.Owener 에는 init-methoddestroy-method 설정한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
											http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<bean id="carpBread" class="project.common.Bread">
		<constructor-arg name="name" value="붕어빵"/>
		<constructor-arg name="price" value="1000"/>
		<constructor-arg name="bakedDate" ref="today"/>
	</bean>
	
	<bean id="today" class="java.util.Date" scope="prototype"/>
	
	<bean id="milk" class="project.common.Beverage">
		<constructor-arg name="name" value="딸기우유"/>
		<constructor-arg name="price" value="1500"/>
		<constructor-arg name="capacity" value="500"/>
	</bean>
	
	<bean id="water" class="project.common.Beverage">
		<constructor-arg name="name" value="지리산암반수"/>
		<constructor-arg name="price" value="3000"/>
		<constructor-arg name="capacity" value="500"/>
	</bean>
	
	<bean id="cart" class="project.common.ShoppingCart" scope="prototype"/>

	<bean id="owner" class="project.section02.initdestroy.subsection03.xml.Owner" 
			  init-method="openShop" destroy-method="closeShop"/>
</beans>

실행 파일에서 빈 설정 파일을 기반으로 IoC 컨테이너를 생성하고 쇼핑 카트에 상품을 담는 동일한 코드의 끝에 컨테이너를 종료하는 코드를 추가하여 실행해본다.

/* 빈 설정 파일을 기반으로 IoC 컨테이너 생성 */
ApplicationContext context 
	= new GenericXmlApplicationContext("section02/initdestroy/subsection03/xml/spring-context.xml");

...생략

/* init 메소드는 빈 객체 생성 시점에 동작하므로 바로 확인할 수 있지만
 * destroy 메소드는 빈 객체 소멸 시점에 동작하므로 컨테이너가 종료 되지 않을 경우 확인할 수 없다.
 * 가비지 컬렉터가 해당 빈을 메모리에서 지울 때 destroy 메소드가 동작하게 되는데 
 * 메모리에서 지우기 전에 프로세스는 종료되기 때문이다.
 * 따라서 아래와 같이 강제로 컨테이너를 종료시키면 destroy 메소드가 동작할 것이다.
 * */
((GenericXmlApplicationContext) context).close();

/*
사장님이 가게 문을 열었습니다. 이제 쇼핑을 하실 수 있습니다.
cart1에 담긴 내용 : [붕어빵 1000 Thu Jun 08 22:00:09 KST 2023, 딸기우유 1500 500]
cart2에 담긴 내용 : [지리산암반수 3000 500]
사장님이 가게 문을 닫았습니다. 이제 쇼핑을 하실 수 없습니다.
*/

openShop 메소드가 Owner 객체의 생성 시점에 호출 되고, closeShop 메소드가 Owner 객체의 소멸 시점에 호출 되었음을 확인할 수 있다. 

728x90
반응형
728x90
반응형

[Spring Framework] Annotation-based Configuration이란? Spring 설정을 더 간결하게!

 

[Spring Framework] Annotation-based Configuration이란? Spring 설정을 더 간결하게!

[Spring Framework] Spring IoC 컨테이너 사용법 완벽 정리 | 실전 예제로 이해하는 DI [Spring Framework] Spring IoC 컨테이너 사용법 완벽 정리 | 실전 예제로 이해하는 DI[Spring Framework] 스프링 IoC 컨테이너 이해

crushed-taro.tistory.com

1. Dependency Injection

1. Dependency Injection이란?

1. Dependency Injection

Dependency Injection(의존성 주입, 이하 DI)은 객체 간의 의존 관계를 빈 설정 정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다. 이를 통해 객체 간의 결합도를 낮출 수 있으며 이로 인해 유지보수성과 유연성이 증가한다.

 

2. 의존 관계와 결합도

public class A {
	
	private B b = new B();

}

public class B {

}

class A가 class B를 필드로 가질 때 A는 B에 의존하게 된다.

public class A {

	/* 컴파일 에러 발생 */
	private B b = new B();

}

/* 클래스명이 B에서 NewB로 변경 */
public class NewB {
	
}

존성이 강하다는 것은 한 객체가 변경되면 이에 의존하는 다른 객체들도 함께 변경되어야 한다는 것을 의미한다.B가 NewB 로 변경되면 해당 클래스를 필드로 가지고 있는 A 도 변경되어야 할 것이다. 이처럼 객체 간의 의존 관계가 강하게 묶여있을 때 결합도 가 높다고 표현한다. 이로 인해 유지보수성과 유연성이 저하될 수 있다.

public class A {

	/* 상위 타입을 필드로 설정 */
	private B b;

	/* 직접 객체를 생성하지 않고 생성자를 통해 전달 받음 */
	public A(B b) {
		this.b = b;
	}

}

/* 상위 타입으로 사용할 인터페이스 */
public interface B {

}

/* 인터페이스의 구현 클래스 */
public class NewB implements B {

}

이전의 코드와 비교하면 NewB 라는 구체적인 구현체의 타입을 사용하는 대신 B 라는 상위 인터페이스 타입으로 필드를 선언했다. 또한 직접 객체를 생성하는 구문도 없어졌고 생성자를 통해 전달 받도록 했다. 이렇게 변경하면 실제로 사용하는 구현체가 NewB 에서 또 다른 타입으로 변경 되더라도 A 의 코드는 변경 될 필요가 없다. 의존 관계가 느슨해지고 결합도가 낮아졌다고 할 수 있다.

 

2. DI 방법 알아보기

아래 코드는 테스트에 공통적으로 사용 할 AccountPersonalAccountMemberDTO 클래스이다.

  • Account
public interface Account {

    /* 잔액 조회 */
    String getBalance();

    /* 입금 */
    String deposit(int money);

    /* 출금 */
    String withDraw(int money);

}

 

  • PersonalAccount
@Data
public class PersonalAccount implements Account {

    private final int bankCode;       //은행코드
    private final String accNo;       //계좌번호
    private int balance;              //잔액

    @Override
    public String getBalance() {

        return this.accNo + " 계좌의 현재 잔액은 " + this.balance + "원 입니다.";
    }

    @Override
    public String deposit(int money) {

        String str = "";

        if(money >= 0) {
            this.balance += money;
            str = money + "원이 입금되었습니다.";
        } else {
            str = "금액을 잘못 입력하셨습니다.";
        }

        return str;
    }

    @Override
    public String withDraw(int money) {

        String str = "";

        if(this.balance >= money) {
            this.balance -= money;
            str = money + "원이 출금되었습니다.";
        } else {
            str = "잔액이 부족합니다. 잔액을 확인해주세요.";
        }

        return str;
    }

}

 

  • MemberDTO
  •  
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberDTO {
	
    private int sequence;                 //회원번호
    private String name;                  //이름
    private String phone;                 //휴대폰번호
    private String email;                 //이메일
    private Account personalAccount;      //개인계좌

}

=> Account(계좌) 인터페이스를 구현한 PersonalAccount(개인계좌) 클래스가 있고 MemberDTO 는 Account 타입을 필드로 가지고 있다. (MemberDTO 는 Account 타입에 의존한다.)

 

1. XML Configuration

1. 생성자(Constructor) 주입

<bean id="account" class="PersonalAccount">
		<constructor-arg index="0" value="20"/>
    <constructor-arg index="1" value="110-234-567890"/>
</bean>

<bean id="member" class="MemberDTO">
		<constructor-arg name="sequence" value="1"/>
    <constructor-arg name="name" value="홍길동"/>
    <constructor-arg name="phone" value="010-1234-5678"/>
    <constructor-arg name="email" value="hong123@gmail.com"/>
    <constructor-arg name="personalAccount">
		    <ref bean="account"/>
    </constructor-arg>
</bean>

 

bean 태그의 클래스 속성은 인터페이스 타입이 아닌 구현 클래스 타입으로 작성해야 한다. 따라서 account 빈 등록 시 class 속성에는 Account 인터페이스가 아닌  PersonalAccount 클래스를 사용한다.

MemberDTO  Account 타입을 의존하고 있기 때문에 member 빈 등록 시 account 빈을 참조하도록 <constructor-arg> 태그의 ref 속성을 작성한다. 생성자를 통해 의존성 객체를 전달하여 의존성을 주입하고 있으므로 이를 생성자 주입 이라 한다.

/* XML 설정 파일을 기반으로 ApplicationContext 객체 생성 */
ApplicationContext context 
	= new GenericXmlApplicationContext("section01/xmlconfig/spring-context.xml");

/* MemberDTO 타입의 빈 가져오기 */
MemberDTO member = context.getBean(MemberDTO.class);

/* MemberDTO의 PersonalAccount 객체 출력 */
System.out.println(member.getPersonalAccount());
/* 10000원 입금 */
System.out.println(member.getPersonalAccount().deposit(10000));
/* 잔액 출력 */
System.out.println(member.getPersonalAccount().getBalance());
/* 5000원 출금 */
System.out.println(member.getPersonalAccount().withDraw(5000));
/* 잔액 출력 */
System.out.println(member.getPersonalAccount().getBalance());

/*
PersonalAccount(bankCode=20, accNo=110-234-567890, 
	balance=110-234-567890 계좌의 현재 잔액은 0원 입니다.)
110-234-567890 계좌의 현재 잔액은 0원 입니다.
10000원이 입금되었습니다.
110-234-567890 계좌의 현재 잔액은 10000원 입니다.
5000원이 출금되었습니다.
110-234-567890 계좌의 현재 잔액은 5000원 입니다.
*/

 

2. 세터(Setter) 주입

<bean id="account" class="PersonalAccount">
		<constructor-arg index="0" value="20"/>
    <constructor-arg index="1" value="110-234-567890"/>
</bean>

<bean id="member" class="MemberDTO">
		<property name="sequence" value="1"/>
    <property name="name" value="홍길동"/>
    <property name="phone" value="010-1234-5678"/>
    <property name="email" value="hong123@gmail.com"/>
    <property name="personalAccount" ref="account"/>
</bean>

/*
PersonalAccount(bankCode=20, accNo=110-234-567890, 
	balance=110-234-567890 계좌의 현재 잔액은 0원 입니다.)
110-234-567890 계좌의 현재 잔액은 0원 입니다.
10000원이 입금되었습니다.
110-234-567890 계좌의 현재 잔액은 10000원 입니다.
5000원이 출금되었습니다.
110-234-567890 계좌의 현재 잔액은 5000원 입니다.
*/

<property> 태그는 setter 메소드를 통해 빈 객체의 값을 초기화하는 설정이다.

  • name : 필드명
  • value : 필드에 담을 값
  • ref : 참조할 빈의 id

MemberDTO 는 Account 타입을 의존하고 있기 때문에 member 빈 등록 시 account 빈을 참조하도록 <property> 태그의 ref 속성을 작성한다. Setter 메소드를 통해 의존성 객체를 전달하여 의존성을 주입하고 있으므로 이를 세터 주입 이라 한다.

빈 객체를 초기화 하는 방법이 생성자 또는 setter 메소드라는 차이는 있으나 테스트 코드의 결과는 동일하다

 

2. Java Configuration

1. 생성자(Constructor) 주입

@Bean
public Account accountGenerator() {

		return new PersonalAccount(20, "110-234-567890");
}

@Bean
public MemberDTO memberGenerator() {

		/* MemberDTO 생성자를 통해 Account를 생성하는 메소드를 호출한 리턴 값을 전달하여 bean을 조립할 수 있다. */
		return new MemberDTO(1, "홍길동", "010-1234-5678", "hong123@gmail.com", accountGenerator());
}

MemberDTO 는 Account 타입을 의존하고 있기 때문에 memberGenerator 빈 등록 시 accountGenerator 빈을 참조하도록 MemberDTO 생성자의 인자로 accountGenerator 메소드 호출의 결과(PersonalAccount bean 객체)를 전달한다. 생성자를 통해 의존성 객체를 전달하여 의존성을 주입하고 있으므로 이를 생성자 주입 이라 한다.

/* Java 설정 파일을 기반으로 ApplicationContext 객체 생성 */
ApplicationContext context 
	= new AnnotationConfigApplicationContext(ContextConfiguration.class);

/* MemberDTO 타입의 빈 가져오기 */
MemberDTO member = context.getBean(MemberDTO.class);

/* MemberDTO의 PersonalAccount 객체 출력 */
System.out.println(member.getPersonalAccount());
/* 10000원 입금 */
System.out.println(member.getPersonalAccount().deposit(10000));
/* 잔액 출력 */
System.out.println(member.getPersonalAccount().getBalance());
/* 5000원 출금 */
System.out.println(member.getPersonalAccount().withDraw(5000));
/* 잔액 출력 */
System.out.println(member.getPersonalAccount().getBalance());

/*
PersonalAccount(bankCode=20, accNo=110-234-567890, 
	balance=110-234-567890 계좌의 현재 잔액은 0원 입니다.)
110-234-567890 계좌의 현재 잔액은 0원 입니다.
10000원이 입금되었습니다.
110-234-567890 계좌의 현재 잔액은 10000원 입니다.
5000원이 출금되었습니다.
110-234-567890 계좌의 현재 잔액은 5000원 입니다.
*/

 

2. 세터(Setter) 주입

@Bean
public Account accountGenerator() {

		return new PersonalAccount(20, "110-234-567890");
}

@Bean
public MemberDTO memberGenerator() {

    MemberDTO member = new MemberDTO();
    member.setSequence(1);
    member.setName("홍길동");
    member.setPhone("010-1234-5678");
    member.setEmail("hong123@gmail.com");
		/* setter를 통해 Account를 생성하는 메소드를 호출한 리턴 값을 전달하여 bean을 조립할 수 있다. */
    member.setPersonalAccount(accountGenerator());

    return member;
}

/*
PersonalAccount(bankCode=20, accNo=110-234-567890, 
	balance=110-234-567890 계좌의 현재 잔액은 0원 입니다.)
110-234-567890 계좌의 현재 잔액은 0원 입니다.
10000원이 입금되었습니다.
110-234-567890 계좌의 현재 잔액은 10000원 입니다.
5000원이 출금되었습니다.
110-234-567890 계좌의 현재 잔액은 5000원 입니다.
*/

MemberDTO 는 Account 타입을 의존하고 있기 때문에 memberGenerator 빈 등록 시 accountGenerator 빈을 참조하도록 setPersonalAccount 메소드의 인자로 accountGenerator 메소드 호출의 결과(PersonalAccount bean 객체)를 전달한다. setter를 통해 의존성 객체를 전달하여 의존성을 주입하고 있으므로 이를 세터 주입 이라 한다.

빈 객체를 초기화 하는 방법이 생성자 또는 setter 메소드라는 차이는 있으나 테스트 코드의 결과는 동일하다.

 

정리

DI는 객체 간의 의존 관계를 빈 설정 정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것이다. 이를 통해 객체 간의 결합도를 낮출 수 있으며 이로 인해 유지보수성과 유연성이 증가한다.

  • XML 빈 설정 시에는 <constructor-args> 또는 <property> 태그의 ref 속성에 의존성 주입할 bean의 이름을 설정한다.
  • Java 빈 설정 시에는 생성자, setter 메소드의 인자 값으로 의존성 주입할 bean의 메소드 호출 반환 값을 전달한다.
728x90
반응형
728x90
반응형

[Spring Framework] Spring IoC 컨테이너 사용법 완벽 정리 | 실전 예제로 이해하는 DI

 

[Spring Framework] Spring IoC 컨테이너 사용법 완벽 정리 | 실전 예제로 이해하는 DI

[Spring Framework] 스프링 IoC 컨테이너 이해하기 | 정의 [Spring Framework] 스프링 IoC 컨테이너 이해하기 | 정의[Spring Framework] Spring Framework란? 개요와 핵심 구성 모듈 정리 [Spring Framework] Spring Framework란? 개

crushed-taro.tistory.com

1. IoC Container

1. Annotation-based Configuration

1. @ComponentScan이란?

  • base package로 설정 된 하위 경로에 특정 어노테이션을 가지고 있는 클래스를 bean으로 등록하는 기능이다.
  • @Component 어노테이션이 작성 된 클래스를 인식하여 bean으로 등록한다.
  • 특수 목적에 따라 세부 기능을 제공하는 @Controller, @Service, @Repository, @Configuration 등을 사용한다.
어노테이션 설명
@Component 객체를 나타내는 일반적인 타입으로<bean> 태그와 동일한 역할
@Controller 프리젠테이션 레이어. 웹 어플리케이션에서 View에서 전달된 웹 요청과 응답을 처리하는 클래스
EX) Controller Class
@Service 서비스 레이어, 비즈니스 로직을 가진 클래스
EX) Service Class
@Repository 퍼시스턴스(persistence) 레이어, 영속성을 가지는 속성(파일, 데이터베이스)을 가진 클래스
EX) Data Access Object Class
@Configuration 빈을 등록하는 설정 클래스

Spring 사진 1

 

2. @Component 어노테이션으로 자동 빈 등록하기

@Component
public class MemberDAO {

    private final Map<Integer, MemberDTO> memberMap;

    public MemberDAO() {
        memberMap = new HashMap<>();

        memberMap.put(1, new MemberDTO(1, "user01", "pass01", "홍길동"));
        memberMap.put(2, new MemberDTO(2, "user02", "pass02", "유관순"));
    }

    /* 매개변수로 전달 받은 회원 번호를 map에서 조회 후 회원 정보를 리턴하는 메소드 */
    public MemberDTO selectMember(int sequence) {
        return memberMap.get(sequence);
    }

    /* 매개변수를 전달 받은 회원 정보를 map에 추가하고 성공 실패 여부를 boolean으로 리턴하는 메소드 */
    public boolean insertMember(MemberDTO newMember) {

        int before = memberMap.size();

        memberMap.put(newMember.getSequence(), newMember);

        int after = memberMap.size();

        return after > before;
    }
}

bean으로 등록하고자 하는 클래스 위에 @Component 어노테이션을 기재하면 스프링 컨테이너 구동 시 빈으로 자동 등록된다.

  • 이름을 별도로 지정하지 않으면 클래스명의 첫 문자를 소문자로 변경하여 bean의 id로 자동 인식한다.
  • @Component("myName") 또는 @Component(value="myName") 의 형식으로 bean의 id를 설정할 수 있다.

 

3. @ComponentScan 어노테이션으로 base packages 설정하기

@ComponentScan(basePackages="project")
public class ContextConfiguration {}

@ComponentScan어노테이션의 basePackagese 속성에 입력한 패키지가 빈 스캐닝의 범위가 된다.

 

스프링 컨테이너에 빈 스캐닝을 통해bean이 자동으로 등록 되고 생성 되었는지 확인한다.

ApplicationContext context 
	= new AnnotationConfigApplicationContext(ContextConfiguration.class);

/* getBeanDefinitionNames : 스프링 컨테이너에서 생성 된 bean들의 이름을 배열로 반환한다. */
String[] beanNames = context.getBeanDefinitionNames();
for(String beanName : beanNames) {
		System.out.println("beanName : " + beanName);
}

/*
...생략
beanName : contextConfiguration
beanName : memberDAO
*/

 

4. excludeFilters

@ComponentScan 의 excludeFilters속성을 이용해서 일부 컴포넌트를 빈 스캐닝에서 제외할 수 있다.

  • Type으로 설정하는 방법
@ComponentScan(basePackages="project",
	excludeFilters={
		@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, 
		classes={MemberDAO.class})
})
public class ContextConfiguration {}

 

  • Annotation 종류로 설정하는 방법
@ComponentScan(basePackages="project",
	excludeFilters={
		@ComponentScan.Filter(type=FilterType.ANNOTATION, 
		classes={org.springframework.stereotype.Component.class})
})
public class ContextConfiguration {}

 

  • 표현식으로 설정하는 방법
@ComponentScan(basePackages="project",
	excludeFilters={
		@ComponentScan.Filter(type=FilterType.REGEX, 
		pattern={"annotationconfig.java.*"})
})
public class ContextConfiguration {}

 

빈 스캐닝에MemberDAO클래스가 제외되어 아래 코드를 실행하면 해당 이름을 가진 빈이 없다는 오류가 발생한다.

ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class);

String[] beanNames = context.getBeanDefinitionNames();
for(String beanName : beanNames) {
		System.out.println("beanName : " + beanName);
}

MemberDAO memberDAO = context.getBean("memberDAO", MemberDAO.class);

/*
...생략
beanName : contextConfiguration
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No bean named 'memberDAO' available
*/

 

5. includeFilters

@ComponentScan 의 includeFilters성을 이용해서 일부 컴포넌트를 빈 스캐닝에 포함 시킬 수 있다.

@ComponentScan(basePackages="project",
	useDefaultFilters=false,
	includeFilters={@ComponentScan.Filter(type=FilterType.ASSIGNABLE_TYPE, 
		classes= {MemberDAO.class})
})
public class ContextConfiguration {}

useDefaultFilters 속성의 기본 값은 true로 @Component 종류의 어노테이션을 자동으로 등록 처리 해준다. 해당 속성을 false로 변경하면 컴포넌트 스캔이 일어나지 않게 된다.

별도로 includeFilters 속성을 정의해 컴포넌트 스캔이 발생하도록 한다.

excludeFilters에서 설정하는 방식과 동일하므로 종류별 예시는 생략한다.

 

아래 코드를 실행하면 빈 스캐닝에서MemberDAO클래스가 포함되어 해당 이름을 가진 빈을 확인할 수 있다.

ApplicationContext context 
	= new AnnotationConfigApplicationContext(ContextConfiguration.class);

String[] beanNames = context.getBeanDefinitionNames();
for(String beanName : beanNames) {
		System.out.println("beanName : " + beanName);
}

/*
...생략
beanName : contextConfiguration
beanName : memberDAO
*/

 

6. XML에서 Component Scan 설정

아래와 같이 XML 설정 파일에서 Component Scan의base package를 설정할 수도 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
													 http://www.springframework.org/schema/beans/spring-beans.xsd 
													 http://www.springframework.org/schema/context 
													 https://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="project"/>

</beans>

<context:component-scan> 태그는 context 네임스페이스의 기능이기 때문에 context: 접두어가 필요하다.

또한 XML 설정 파일의 기본 네임스페이스가 beans로 설정 되어 있기 때문에 context 네임스페이스 추가가 필요하다.

 

base package@Component 어노테이션을 작성한 MemberDAOclass를 배치해 두고 아래와 같은 코드를 실행하면 빈이 등록 되어있음을 확인할 수 있다.

ApplicationContext context 
	= new GenericXmlApplicationContext("section03/javaannotation/spring-context.xml");

String[] beanNames = context.getBeanDefinitionNames();
for(String beanName : beanNames) {
		System.out.println("beanName : " + beanName);
}

/*
beanName : memberDAO
...생략
*/

 

아래와 같이 <context:component-scan> 태그 내부에 <exclude-filter> 또는 <include-filter> 태그를 정의할 수 있다.

<include-filter> 의 경우 동일한 방식으로 정의하므로 생략하였다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
													 http://www.springframework.org/schema/beans/spring-beans.xsd 
													 http://www.springframework.org/schema/context 
													 https://www.springframework.org/schema/context/spring-context.xsd">

	<context:component-scan base-package="project">
        <context:exclude-filter type="assignable" expression="common.MemberDAO"/>
  </context:component-scan>

</beans>

 

스캐닝에서MemberDAO클래스가 제외되어 아래 코드를 실행하면 해당 이름을 가진 빈이 없다는 오류가 발생한다.

ApplicationContext context 
	= new GenericXmlApplicationContext("section03/javaannotation/spring-context.xml");

String[] beanNames = context.getBeanDefinitionNames();
for(String beanName : beanNames) {
		System.out.println("beanName : " + beanName);
}

MemberDAO memberDAO = context.getBean("memberDAO", MemberDAO.class);

/*
...생략
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException
: No bean named 'memberDAO' available
*/

 

정리

  • XML 설정은 전통적으로 사용하던 방식으로 최근에는 Java 설정이 선호된다.
  • 개발자가 직접 컨트롤 가능한 Class의 경우, @Component를 클래스에 정의하여 빈 스캐닝을 통한 자동 빈 등록을 한다.
  • 개발자가 직접 제어할 수 없는 외부 라이브러리는 @Bean을 메소드에 사용하여 수동 빈 등록을 한다.
    • 다형성을 활용하고 싶은 경우에도 @Bean을 사용할 수 있다.
 

Java-based Container Configuration :: Spring Framework

This section covers how to use annotations in your Java code to configure the Spring container.

docs.spring.io

728x90
반응형

+ Recent posts