Spring의 기본 개념 잡아보기

Spring의 기본 개념 잡아보기

Spring overview

특징

  • IOC (Inversion Of Control)
  • DI (Dependency Injection)
  • AOP (Aspect-Oriented Programming)
  • PSA (Portable Service Abstraction)

IOC (Inversion Of Control)


직역하면 제어권의 역전으로 해석할 수 있다. 보통 어떤 객체(A)가 다른 객체(B)를 사용하고 있다면 A는 B에 dependency가 있다 라고 얘기하며, A 객체 내부에서 B객체를 생성하여(new) 사용할 수 있다.

1
2
3
4
@Controller
class OwnerController {
private val ownerRepository = ownerRepository()
}

하지만 Spring은 IOC 특성을 가진다. 즉 Control(dependency를 생성)의 주체가 A class가 아닌 A class 외부이다.

1
2
3
@Controller
class OwnerController(private lateinit var ownerRepository: OwnerRepository) {
}

OwnerRepository를 외부에서 어떤 타이밍에 생성되어 들어오는지는 모르지만 OwnerController는 그걸 신경쓰지 않고 받아서 쓸 뿐이다. 이런식으로 코드를 작성하면 OwnerController와 OwnerRepository간의 dependency가 줄어들기 때문에 테스트 코드를 짜기에도 용이하고, repository를 바꾸거나 controller를 바꾸기도 용이한, 유연한 코드를 짤 수 있겠다.

이 내용만 보면 Inversion of dependency control이라고 생각할 수 있지만 dependency 외에 다른것도 inversion 되어있을 수 있다고 한다.

가령 Servlet을 예로 들면, 어떤 Servlet은 Servlet Container에 속해있으며, Container가 생성 하고, 클라이언트에서 요청이 올 때 실행되므로, Servlet 자신이 제어권을 쥐고 있는 것이 아닌 Container가 쥐고 있으며 이 경우에도 IOC 개념이 사용된다.

IOC 컨테이너


Spring framework는 IOC용 Container를 제공 해 준다. Container의 핵심 인터페이스는 Application Context(Bean Factory)이다.

하지만 Application Context는 직접 사용할 일은 없다. Application Context는 단지 뒤에 숨어서 사용자가 만든 IOC class를 Spring에서 동작할 수 있도록 해 준다.

예를 들면 위에서 생성한 OwnerController라는 class는 @Controller라는 Annotation을 달고 있고 Application Context는 Annotation을 보고 인스턴스를 생성 해 준다. 그와 동시에 OwnerController의 parameter인 OwnerRepository 또한 생성하여 생성자에 넣어준다.

정리를 해보자.

  • Container 내부에 생성된 객체들을 Bean이라고 한다.
  • Container는 이 Bean들의 dependency를 관리해준다.
  • 오로지 Bean만 관리한다.

Bean


Bean은 Spring IOC 컨테이너가 관리하는 객체이다.

  • 보통 class에 @Controller, @Service, @Repository과 같은 Annotation이 붙어있으면 Bean이다.
  • Annotation은 사용자가 붙이는 것이며, 이 객체들간의 dependency를 Container에서 관리해줘라 하는 표시와 같다.
  • Intellij 기준 class 옆에 어떤 표시가 있으면 Bean으로 등록이 된 것이다.
  • 오로지 Bean으로 등록된 객체만 dependency를 관리 해 준다.

▶ 어떻게 등록하지? - Component Scanning

1
2
3
4
5
6
7
8
9
10
11
package com.example.demo

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
runApplication(*args)
}
  • 보통 Spring project를 생성하면 위와 같이 main 메소드가 있는 Application file이 존재한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {


/**
* Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
* for a type-safe alternative to String-based package names.
*


* Note: this setting is an alias for
* {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
* scanning or Spring Data {@link Repository} scanning. For those you should add
* {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
* {@code @Enable…Repositories} Annotations.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(Annotation = ComponentScan.class, attribute = “basePackages”)
String[] scanBasePackages() default {};

/**
* Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
*


* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
*


* Note: this setting is an alias for
* {@link ComponentScan @ComponentScan} only. It has no effect on {@code @Entity}
* scanning or Spring Data {@link Repository} scanning. For those you should add
* {@link org.springframework.boot.autoconfigure.domain.EntityScan @EntityScan} and
* {@code @Enable…Repositories} Annotations.
* @return base packages to scan
* @since 1.3.0
*/
@AliasFor(Annotation = ComponentScan.class, attribute = “basePackageClasses”)
Class[] scanBasePackageClasses() default {};

  • 위의 코드와 같이 SpringBootApplication 이라는 Annotation의 구현체를 보면 scanBasePackages, scanBasePackageClasses라는 메소드가 존재함을 볼 수 있다.
  • 즉, Spring에서는 존재하는 모든 package 내의 모든 class에 대해서 component Annotation이 붙어있는 class들을 모두 찾아서 자동으로 Spring framework에 Bean으로 등록한다.

@Controller라는 Annotation도 내부 구현을 뜯어보면 @Component라는 Annotation이 붙어있으며, Spring 에서는 이것을 보고 Bean으로 등록하게 된다.

1
2
3
4
5
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller { … }

즉 사용자는 Spring의 Component라는 것을 알려줄 수 있는 Annotation을 잘 붙여주면 된다.

  • @Controller
  • @Repository
  • @Service

DI (Dependency Injection)


Spring에서는 @Autowired, @Inject를 붙여서 DI를 사용할 수 있다. 하지만, 코드상에는 DI와 관련된 Annotaion은 찾아볼 수 없다. 그렇다면 어떻게 DI를 사용할까?

▶ Dependency Injection 조건

Spring에서는 @Autowired, @Inject가 없더라도, 아래와 같은 조건을 만족한다면 해당 dependency를 해당 Bean에 Inject 해 준다.

  • 어떤 Bean이 존재하며
  • 그 Bean에 생성자가 오직 하나만 존재하며
  • 그 생성자의 Parameter로 받는 타입의 Bean이 존재

▶ Bean이 아닌 객체들은 어떻게 Container로 부터 의존성을 주입받을까?

@Autowired, @Injection을 생성자, 필드, Setter에 붙여준다.

  1. 생성자
    보통 생성자에서 Inject를 받으며, 테스트 코드를 짤때도 dependency를 mocking하기 용이하다.
  2. 필드
    필드에 붙여줄 수 있지만, test code 작성시 mocking하여 넣어주기가 매우 힘들기 때문에 지양되는 방법이다.
  3. Setter
    Setter 또한 필드와 같은 이유로 지양되는 방법이기는 하나, 필요시에는 사용되며 다음과 같은 기준들로 Annotation을 붙여준다.
    • Setter가 있다는 것은 어떤 dependency를 setter를 통해 변경시키고 싶다는 의미로 볼 수 있다. 따라서 constructor 보다 Setter에 Annotation을 붙여 DI를 해주는 것이 자연스럽지 않는가 생각한다.
    • 만약 Setter가 없고, 생성자나 필드가 존재한다면 Setter를 만들기보다는 그쪽에 Annotation을 두는것이 자연스럽다. 또한 setter를 굳이 만들어주는 것은, 바뀌지 않아야할 dependency를 바뀔 수 도 있도록 하는 것 이므로 굳이 Setter를 만드는 것은 좋지 않다.

AOP (Aspect-Oriented Programming)


AOP란 쉽게 표현하면 흩어진 코드를 한곳으로 모으는 코딩 기법이다. 예제를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 흩어진 AAAA 와 BBBB
class A {
fun a () {
AAAA
오늘은 74일 미국 독립 기념일이래요.
BBBB
}
fun b () {
AAAA
저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
BBBB
}
}

class B {
fun c() {
AAAA
점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
BBBB
}
}

// 모아 놓은 AAAA 와 BBBB
class A {
fun a () {
오늘은 74일 미국 독립 기념일이래요.
}
fun b () {
저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
}
}

class B {
fun c() {
점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
}
}

class AAAABBBB {
fun aaaabbb(JoinPoint point) {
AAAA
point.execute()
BBBB
}
}

위의 주석에 적혀있는 흩어진 AAAA 와 BBBB 코드를 살펴보면 AAAA라는 기능과 BBBB라는 기능이 여러 메소드마다 반복적으로 실행되고 있다. 이러한 흩어진 코드를 모아 놓은 AAAA 와 BBBB와 같이 한곳으로 모아서 처리하는 방식을 AOP라고 할 수 있겠다.

이렇게 구현하는 기법은 크게 두가지가 있다

  1. Bytecode를 조작한다.
    • class A, B가 컴파일된 bytecode에 AAAA, BBBB를 삽입한다.
  2. Proxy 패턴
    • A라는 class를 상속을 받아 AProxy라는 class를 만든다.
    • AProxy class는 a method를 override 해서 A의 a method를 실행하기 전에 AAAA를 실행시키고 A의 a method를 실행시키고 BBBB를 실행시키도록 한다.

Spring에서는 Proxy 패턴으로 AOP를 구현하며 framework 내부에서 자동으로 코드를 생성 해 준다.

AOP 적용 예제


위에 설명한 것과 비슷한 동작을 필요로 하는것이 보통 logging 이다. Logging 관련 Annotation이 붙어있는 method만 실행시 log를 남기는 예제 코드를 작성 해 보자.

  1. Annotation을 만든다.
1
2
3
4
5
// 이 Annotation은 function에 붙여야 동작한다.
@Target(AnnotationTarget.FUNCTION)
// 이 Annotation은 Runtime에만 유지된다.
@Retention(AnnotationRetention.RUNTIME)
Annotation class LogExecutionTime {}
  1. 사용할 메소드에 Annotation을 붙여준다.
1
2
3
4
5
6
7
8
9
@Controller
class HtmlController {
@LogExecutionTime
@GetMapping(“/“)
fun blog(model: Model): String {
model[“title”] = “Blog”
return “Blog”
}
}
  1. Aspect 만들기
  • Annotation은 주석과도 같은 것이다. 실제 기능은 없고, 저 Annotation과 mapping되는 Aspect를 만들어 줘야 한다.
  • spring에서 Annotation과 aspect를 연결하려면 aspect가 bean이어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
@Component
@EnableAspectJAutoProxy
class LogAspect {
@Around(“@Annotation(LogExecutionTime)”)
fun logExecutionTime(joinPoint: ProceedingJoinPoint): Any? {
val before = System.currentTimeMillis()
val result = joinPoint.proceed()
val after = System.currentTimeMillis()
println(after - before)
return result
}
}

위의 코드와 같이 jointPoint라는 작업이 수행 되기 전 후에 before, after 라는 작업을 수행하도록 하며, Annotation만 붙이면 모든 메소드에서 저러한 작업을 가능하게 해 주는 것이 AOP 개념과 관련된 일을 한다고 보면 되겠다. Before -> 원래 task -> after 순서로 작업이 수행된다.

@Around(“@Annotation(LogExecutionTime)”) 부분에서 왜 @Around의 인자값으로 String이 들어가는지 의문이 들 수 있는데, String으로 어떤 메소드가 실행 될 것인지 패턴을 정의해 줄 수 있다. 예를 들면 execution(* set*(..)) 이렇게 set으로 시작하는 method에서만 실행 할 수도 있고, execution(* com.xyz.service..*.*(..)) 이렇게 service 패키지와 하위 모든 메소드들에서 실행 시킬 수 있다.

이런식으로 Annotation과 Aspect를 만들고 나면 나머지 작업들은 spring에서 magically 처리 해준다.

PSA (Portable Service Abstraction)


쉬운 말로 잘 만든 인터페이스라고 얘기 할 수 있다. 예시를 들어 설명 해 보자.

  1. 나의 코드 — ( 확장성이 좋지 못한 코드 or 특정 기술에 특화되어 있는 코드)
    오른쪽에 코드와 나의코드가 직접적인 연결이 있다면, 코드가 바뀌거나, 기술이 바뀔때 마다 나의 코드의 영향을 주기 때문에 계속해서 수정을 해야한다.

  2. 나의 코드 — PSA (Interface) — ( 확장성이 좋지 못한 코드 or 특정 기술에 특화되어 있는 코드 )
    인터페이스가 중간에 있다면, 확장성이 좋지 못한 코드 or 특정 기술에 특화되어 있는 코드와 내 코드와 직접적인 연관이 없고 인터페이스만 사용하면 되기 때문에 어떤 변경이 있더라도 내 코드에는 영향이 없어진다.

1
2
3
4
5
6
7
8
9
@Controller
class HtmlController {
@LogExecutionTime
@GetMapping(“/“)
fun blog(model: Model): String {
model[“title”] = “Blog”
return “Blog”
}
}

위와 같은 코드를 보자. @Controller나 @GetMapping은 Servlet과 전혀 상관이 없이 만들어졌다. 저 Annotation을 통해서 해당 기능을 실행하는 주체는 Servlet이 될 수도 있고, netty가 될 수 있다. 우리는 어떤 기술이 뒤에서 사용되고 있는지는 전혀 알 필요 없이 단지 Spring이 제공하는 interface들만 잘 알고 적절하게 사용하면 된다.

▶ PSA - @Transactional

@Transactional는 위에서 만든 LogExecutionTime Annotation과 같이 aop 이다. 그러므로 당연히 Transactional 기능을 처리하는 TransactionalAspect가 존재한다.

이 Aspect에서는 기술에 독립적인 PlatformTransactionManager라는 인터 페이스를 사용하여 구현 해 두었다.

Platform Transaction Manager는 JpaTransacionManager, DatasourceTransactionManager, HibernateTransactionManager 와 같은 구현체 들이 존재하지만 Transactional Aspect의 코드는 바뀌지가 않는다. 단지 PSA를 쓰고 있기 때문에!

Spring에서는 자동으로 JpaTransacionManager를 bean으로 등록 하며, Transactional Aspect는 이를 injection 받아서 해당 기능을 수행하게 될 것이다.

▶ PSA - @Cacheable | @CacheEvict | …

이 Annotation이 붙으면 캐쉬 관련 설정이 가능 해 진다. @Transactional과 동일하게, Cache 관련 Annotation도 Aspect를 가지며, 그 Aspect에서는 CacheManager라는 PSA를 사용하게 된다.

CacheManager는 JCacheManager, ConcurrentMapCacheManager 등등의 구현체가 있지만 @Transactional과 동일하게 Spring에서 자동으로 injection을 해 줄 뿐이고, 단지 Aspect에서는 CacheManager라는 PSA(interface)를 가져다 쓸 뿐이다!

▶ PSA - 웹 MVC

웹 MVC는 @Controller와 @GetMapping Annotation을 이용하여 구현된다.

1
2
3
4
5
6
7
8
9
@Controller
class HtmlController {
@LogExecutionTime
@GetMapping(“/“)
fun blog(model: Model): String {
model[“title”] = “Blog”
return “Blog”
}
}

이 코드 (@Controller, @GetMapping)은 내부적으로 Servlet을 쓰는지, Reactive를 사용하는지 전혀 알수가 없다. 단지 저 Annotation들이 구현하고 있는 Aspect는 웹 관련 PSA를 사용하여 해당 기능을 구현할 뿐이고, 우리는 그것을 신경쓸 필요가 없어졌다.

Spring의 전반적으로 중요한 요소들을 정리했다. 이걸 보고 바로 실무에 적용할 수는 없겠지만, 적어도 이런 개념이 있다면 Spring의 수 많은 기능중 내가 필요한 것들을 잘 찾고 이해하며 쓸 수 있을 듯 하다.

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×