일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 필드 주입
- docker
- hashcode
- MSA
- redis
- jpa
- cache
- 조합
- 인덱스
- lambda
- AOP
- KEVISS
- static
- DI
- StringBuilder
- VUE
- 생성자 주입
- 열 속성
- stream
- 재정의
- DDL
- jwt
- equals
- 테스트 코드
- select_type
- SQL
- Spring
- Test
- java
- 바이너리 카운팅
- Today
- Total
백엔드 개발자 블로그
Spring 기초 본문
Spring에 필요한 개념을 정리했습니다.
Spring이란?
Spring은 POJO(Plain Old Java Object) 방식의 프레임워크입니다.
복잡한 엔터프라이즈 로직을 대신 처리하여 개발자가 서비스 로직에 집중하고 이를 POJO(순수 Java 객체)로 쉽게 개발 할 수 있도록 지원해주는 프레임워크입니다.
Spring Framework의 특징
Spring은 POJO(Plain Old Java Object) 방식의 프레임워크입니다.
- 개발자가 서비스 로직에 집중하고 이를 POJO(순수 Java 객체)로 쉽게 개발 할 수 있도록 지원합니다.
Spring은 DI(Dependency Injection)를 지원합니다.
- 각각의 계층이나 서비스들 간에 의존성이 존재할 경우 프레임워크가 서로 연결시켜 줍니다.
Spring은 IoC(Invesion of Control)를 지원합니다.
- Spring이 특정 제어권(객체 생성, 소멸)을 갖고 있어서 개발자가 서비스 로직 구현에 집중 가능하게 지원합니다.
Spring은 AOP(Aspect-Oriented Programming)을 지원합니다
- 트랜잭션, 로깅, 보안처럼 여러 모듈에서 공통적으로 사용되는 기능을 분리하여 관리할 수 있습니다.
이제, Spring의 특징을 하나씩 자세히 살펴봅시다.
DI(Dependency Injection)
DI(Dependency Injection)는 우리말로 하면 "의존성 주입"입니다.
Gof 디자인 패턴 중 Strategy 패턴과 유사한 개념입니다.
이 코드를 한번 봅시다.
//HAS-A 관계
public class Foo {
private Bar bar;
public Foo() {
//Foo가 bar를 만드는 중!
bar = new SubBar();
}
}
Foo클래스가 Bar를 생성하고, Bar를 수정하고 싶으면 Foo를 뜯어서 수정해야 합니다.
이 문제를 Dependency Injection, 즉 의존성 주입으로 해결할 수 있습니다.
//DI 사용
public class Foo {
private Bar bar;
public void setBar(Bar bar) {
this.bar = bar;
}
}
new 키워드로 내가 직접 만들어 사용하진 않고 외부에서 생성된 bar객체를 주입받는 것입니다.
이게 바로 DI(Dependency Injection) 의존성 주입입니다.
이해를 돕기 위해서 예시 하나를 더 들어보겠습니다.
class Coffee {...}
class Cappuccino extends Coffee {...}
class Americano extends Coffee {...}
class Programmer {
private Coffee coffee;
public Programmer() {
this.coffee = new Cappuccino();
//또는
this.coffee = new Americano();
}
}
만약 Cappuccino, Americano가 아닌 Mocha를 먹고 싶으면 Programer 클래스 코드를 수정해야 합니다.
이 코드에 DI 디자인 패턴을 적용해봅시다.
class Programmer {
private Coffee coffee;
public Programmer(Coffee coffee) {
this.coffee = coffee;
}
public startProgramming() {
this.coffee.drink();
...
}
}
Programmer의 생성자를 보면 매개변수로 Coffee 클래스의 객체가 들어오면서, Programmer클래스의 coffee를 설정합니다.
이렇게 coffee는 programmer가 직접 만들지 않고, 누가 넣어줍니다.
Programmer는 단순히 그 커피를 마시기만 하면 됩니다!
이렇게 DI에 대해 살펴보았습니다.
Spring의 모든 컴포넌트는 이렇게 DI로 구성됩니다! 누가 넣어주는 것처럼 말이죠.
그럼 이제 Spring에서의 DI문법 3가지에 대해 알아보겠습니다.
DI 방법 중에는 생성자 주입법을 권장합니다.
https://backend-dev.tistory.com/entry/DI
방법 1 - 생성자 주입
public class Foo {
private final Bar bar;
@Autowired
public Foo(Bar bar) {
this.bar = bar;
}
}
방법 2 - Setter 주입
public class Foo {
private Bar bar;
@Autowired
publid void setBar(Bar bar) {
this.bar = bar;
}
}
방법 3 - 필드 주입
public class Foo {
@Autowired
pirvate Bar bar;
}
Spring에서 DI의 원리
Spring에서 DI가 작동하는 원리는 명세서에 따라 자동으로 부품을 조립하는 것과 비슷합니다.
1. 부품을 만듭니다. 클래스를 구현하는 것이죠. (@Component or @Bean)
2. 부품을 활용하는 명세서를 정의합니다. (생성자 주입 방식을 추천합니다.)
3. Spring이 명세서대로 DI 업무 수행
IoC (Inversion of Control)
IoC를 설명하기 위해 Spring Bean, Container 개념이 필요합니다.
Spring Bean
Spring Bean은 스프링이 관리하는 DI가 가능한 인스턴스들입니다.
Spring Bean Scope
Spring Bean은 Default로 Singleton Scope으로 생성됩니다.
Singleton으로 bean을 공유하다 보면 가끔 race condition이 발생하기도 하여 상황에 따라 Scope를 조정해야 합니다.
Singleton (Default)
Default로 DI시 항상 같은 객체가 사용됩니다.
Prototype
DI시 매번 새로운 객체가 생성됩니다.
Request
request가 들어올 때마다 생성됩니다.
Session
하나의 HTTP session동안 하나의 객체만 존재합니다.
멀티스레드 환경에서의 Bean
보통 bean을 생성할 때 멤버 변수를 잘 사용하지 않습니다.
Bean은 기본적으로 singleton으로 생성되기 때문에, 생성한 bean에 멤버 변수가 있다면, 멀티스레드 환경에서 여러 사용자가 많이 접속하여 공유를 하게 되기 때문에 race condition이 발생할 수 있습니다.
Spring Bean 생성 방법
@Bean
외부 라이브러리에서 정의한 클래스를 스프링 Bean으로 등록 할 경우 사용합니다.
@Component
개발자가 직접 작성한 클래스를 스프링 Bean으로 등록할 경우 사용합니다.
@Component어노테이션은 아래 세 가지 중에 정확히 어디에 속하는지 모르는 경우에 사용합니다.
- @Controller - 기본적으로 @Component와 같으나 presentation layer의 component라는 것 표기
- @Service - 기본적으로 @Component와 같으나 business layer의 component라는 것 표기
- @Repository - 기본적으로 @Component와 같으나 persistence layer의 component라는 것 표기
Presentaion Layer는 주로 클라이언트에서 보낸 요청 간의 json 형태로의 변환 등의 작업을 수행합니다.
Business Layer는 Presentation Layer에서 보내온 데이터를 가지고 비즈니스 로직에 따른 계산, 처리를 수행합니다.
Persistence Layer는 주로 데이터베이스와의 연결을 유지하고 데이터를 송, 수신합니다.
Spring Bean 동작 원리
스프링에서 Bean에 등록되는 컴포넌트들은 @SpringBootApplication 어노테이션이 있는 애플리케이션 파일에서 다음과 같이 확인할 수 있습니다.
@SpringBootApplication 어노테이션 내부에는 @ComponentScan과 @SpringBootConfiguration이라는 어노테이션이 있습니다.
- @ComponentScan은 애플리케이션 내에 Component로 등록된 빈들을 찾아줍니다.
- @SpringBootConfiguration내부로 들어가면 @Configuration, @Configuration에 들어가면 @Component 어노테이션이 있고, 이 어노테이션이 @Component들을 스프링 Bean으로 관리를 해줍니다.
스프링 애플리케이션이 실행되면, @Component 어노테이션이 있는 객체들을 찾아서 스프링 Bean 컨테이너에서 싱글턴 객체로 직접 관리를 해줍니다.
스프링 컨테이너
스프링 컨테이너에서 Bean들을 다 담아놓고, Bean의 생성, 소멸, 명세까지 전부 관리합니다.
개발자가 제어하던 프로그램의 흐름, 객체의 생성, 소멸 등을 컨테이너가 제어하기 때문에 제어의 역전이라고 불립니다.
사람이 하던 일을 스프링 프레임워크가 대신해주고, 개발자는 코드만 짜 놓고 실행만 하면 스프링이 알아서 해줍니다.
AOP (Aspect Oriented Programming)
공통된 부분을 분리해서 개발할 수 있는 프로그래밍 기법입니다.
공통된 부분을 한번만 작성하면 되기 때문에 코드 작성시간이 줄고, 한 곳만 수정하면 되기 때문에 수정시간도 줍니다.
다음의 예시를 한번 봅시다.
public class Hello {
public String sayHello(String name) {
//로그 남기는 공통 코드
System.out.println("log: " + new java.util.Date());
String msg = "hello~ " + name;
return msg;
}
}
위 코드는 메서드가 실행된 시간을 보여주는 로그를 남기는 코드를 갖고 있는데, 이를 Hello 클래스로부터 분리해 보겠습니다.
public class HelloLog {
public static void log() {
System.out.println("log: " + new java.util.Date());
}
}
이렇게 클래스를 분리하면 첫 번째 작성했던 Hello 클래스 코드는 다음과 같이 달라집니다.
public class Hello {
public String sayHello(String name) {
HelloLog.log(); //공통코드를 호출하는 코드가 포함됨
String msg = "hello~" + name;
return msg;
}
}
이렇게 한 줄짜리 코드로 줄어들긴 했지만 이 또한 없애고 싶을 때 AOP를 사용합니다.
프록시를 이용한 AOP 구현
아까 봤던 예시에서는 클래스를 단순히 분리해서 공통 코드인 로그 남기는것을 구현했습니다.
지금 예시를 한번 보시죠.
public class HelloProxy extends Hello {
@override
publid String sayHello(String name) {
HelloLog.log(); //공통 코드 실행
return super.sayHello(name); //핵심 코드 실행
}
}
HelloProxy클래스가 Hello클래스를 상속하고 오버 로딩을 구현했습니다.
다른 데서 Hello 클래스를 호출하면 HelloProxy가 먼저 호출될 것이고, HelloProxy는 로그를 찍고, Hello 클래스의 핵심 코드를 실행합니다.
이렇게 구현한 것이 프록시를 이용한 AOP입니다.
이 방법엔 단점이 있습니다.
밖에 있는 누군가가 Hello의 A메서드를 호출해야 프록시를 거쳐서 해당 메서드가 호출됩니다.
하지만 Hello클래스 내의 다른 메서드가 A메서드를 호출하면 프록시를 거치지 않고 바로 호출되기 때문에 공통 코드의 실행이 생략됩니다.
다음 예시를 한번 봅시다
void aaa() {
bbb();
}
@Transactional
void bbb() {
ccc();
}
@Transactional
void ccc() {
...
}
@Transactional 어노테이션은 해당 메서드를 트랜잭션처럼 동작하게 합니다.
외부에서 ccc 메서드를 호출하면 정상적으로 트랜잭션이 걸립니다.
하지만 외부에서 bbb를 호출하면, bbb가 내부적으로 ccc를 호출하기 때문에 에러가 나지는 않지만 ccc에 트랜잭션이 정상 작동하지 않습니다.
내부적으로 메서드를 호출하기 때문에 프록시를 거치지 않아서 @Transactional 어노테이션을 거치치 않게 되는 것입니다.
위빙 - Weaving
위빙(Weaving)은 AOP프레임워크가 공통 코드를 핵심코드에 삽입하는 것입니다.
위빙에는 3가지 방법이 있습니다.
- 컴파일 시 위빙
별도의 컴파일러를 통해 핵심 모듈 사이사이에 관점 형태로 만들어진 공통 관심 코드가 삽입됩니다.
예를 들면 AspectJ가 있습니다.
AspectJ를 사용하면 프록시를 이용한 AOP구현에서의 단점 같은 것은 일어나지 않지만 사용하기가 귀찮습니다.
- 클래스 로딩 시 위빙
별도의 agent를 이용해서 JVM이 클래스를 로딩할 때 해당 클래스의 바이너리 정보를 변경합니다.
즉, Agent가 횡단 관심사 코드가 삽입된 바이너리 코드를 제공하면서 AOP를 지원합니다.
ex) AspectWerkz
- 런타임 시 위빙
위빙의 99%는 런타임 시 위빙으로 진행됩니다.
소스 코드나 바이너리 파일의 변경 없이 프록시를 이용하여 AOP를 지원합니다.
프록시를 통해 핵심 코드를 구현한 객체에 접근하며 AOP를 지원합니다.
ex) Spring AOP
스프링에서의 AOP
스프링에서는 자체적으로 런타임 때 위빙 하는 프록시 기반의 AOP를 지원하고 있습니다.
spring-boot-starter-aop 라이브러리를 사용하고, @Aspect 같은 어노테이션을 통해 쉽게 AOP를 구현할 수 있습니다.