스프링의 특징: 제어의 역전을 통해 애플리케이션의 느슨한 결합을 도모하고, DI DL AOP 등을 지원합니다.
IOC : 제어의 역전. 스프링 프레임 워크에서 객체의 생성과 의존관계를 개발자가 하는 것이 아니라, 컨테이너가 자동으로 관리해주는 것. 즉 개발자가 아닌 프레임워크에게 넘기는 것입니다.
DI(DependencyInjection) : 객체간에 의존성(관계맺음, new)을 객체 내부에서 직접 해주는 대신, 외부에서 객체를 생성해서 넣어주는 방식 → 객체간의 높은 결합도를 갖게되면, 수정사항이 생겼을 때 같이 수정해야할 부분이 많아지기 때문에 의존성주입 방식이 필요함 → 의존성주입으로 객체간의 결합을 약하게 해주고 유지보수가 좋은 코드를 만들어준다.
(객체간의 관계설정을 클래스 내부에서 직접하는 방식 대신, Spring Container를 이용하여 외부에서 객체를 생성하고, 객체를 주입해주는 방식)
(1) 의존성 주입 방법과 각각의 특징/장단점
(1-1) 필드주입 : 필드 위에 @Autowired 어노테이션 추가-> 코드가 간결해짐. 하지만 외부에서 접근이 불가능하고 필드주입은 반드시 DI프레임워크가 존재해야하므로 사용 지양
(1-3) 생성자주입: @Autowised 어노테이션을 통해 생성자에 객체 주입, 필드에 final 키워드를 사용할 수 있어, 실행중에 객체가 바뀌는 것을 컴파일 시점에 미리 방지하여 안전한 사용 가능
(1-4) setter주입: setter메서드로 필드값을 변경, 주입받는 객체가 변경될 가능성이 있는 경우 사용(실제로 극히 드물다) (spirng 설정에서는 property태그)
-생성자 주입을 가장 권장, 이유: 1. 순환 참조를 방지 2. 불변성을 가짐 3. 테스트에 용이
Setter로 주입, 생성자에서 디폴트로 객체를 넣고 언제든 원할때 외부에서 주입
context(스프링설정파일) getBean 이라는 메서드로 컨테이너에 있는 것들 사용함 -> xml에 constructor
https://ffoorreeuunn.tistory.com/430 (스프링 의존성 관리)
★ 생성자 주입을 사용해야하는 이유
-널포인트 에러 방지 : 객체 생성 시점에 필수적으로 빈 객체 초기화를 수행하기 때문에 널포인트 에러 방지
-객체의 불변성 확보: 실제 개발을 하다보면, 의존관계의 변경이 필요한 상황은 거의 없는데 SETTER주입같은 경우는 불필요하게 수정의 가능성을 열어두어 유지보수성을 떨어뜨린다.
-순환참조 에러 방지(A가 B를 참조, B가 A를 참조) : 애플리케이션 구동시점(객체의 생성시점)에 순환참조 에러를 예방할 수 있다. 위의 경우 두 메소드는 서로를 계속 호출하여, 메모리에 함수의 콜스택이 쌓여 스택오버플로우 에러 발생/
필수적인 의존성은 생성자를 통해 주입받으며 불변객체로 선언하고,
옵셔널한 의존성에는 setter 주입을 사용
(2) DI구현을 Spring / java 각각 어떻게 할까?
Spring에서 setter주입, 필드주입, 생성자 주입을 통해 할 수 있습니다. ~ 설명,
@Autowired, @Inject, @Resource 등의 어노테이션을 사용하여 의존성 주입을 수행하고, XML이나 Java Config를 사용하여 주입 대상을 지정합니다.
DL : Bean에 접근하기 위해 컨테이너가 제공하는 api를 이용하여 bean을 lookup 하는 것
DI와 DL의 차이점은?
AOP : aop(aspect oriented programming): 관점 지향 프로그래밍(공통의 관심사항(트랜잭션이나 인증,로깅, 보안과 같이 여러 모듈에서 공통적으로 사용하는 기능)을 추출하여 원하는 곳에 적용하는 기술. 관점지향 프로그램, 공통적인 부분을 뽑아내서 개발자가 뗏다, 붙혔다 함)
=> 반복 사용되는 로직들을 모듈화 하여 필요할때 호출해서 사용하는 방법
-장점: 중복코드 제거, 재활용성의 극대화, 변화수용의 용이성 좋음
-방법: Aspect 애노테이션으로 이 클래스가 Aspect 클래스임을 알려준다.
→두 가지 정보가 필요한데 해야할 일과 어디에 적용할 것인가이다. 해야할 일은 Advice, 어디에 적용할 것인가는 Point Cut에 해당하고 이 두 가지를 정의
예제에서는 @Around를 통해서 타겟 메서드의 Aspect 실행 시점을 지정했지만 다른 어노테이션들도 있다.(@Before, @After, @AfterRetunrning등)
현업에서의 활용 예로는 다음과 같은 상황들이 있습니다:
(1) 로깅(Logging): 각 메서드의 실행 시간을 로깅하거나, 특정 메서드가 호출되었을 때 정보를 로깅하는 등의 경우에 AOP를 활용하면 중복 코드를 줄일 수 있습니다.
(2) 트랜잭션 관리(Transaction Management): 트랜잭션을 시작하고 커밋하거나 롤백하는 코드를 AOP로 구현하면, 각 비즈니스 로직에서 트랜잭션 관리 코드를 분리할 수 있습니다. 이는 코드의 가독성과 유지보수성을 향상시킵니다.
(3) 보안(Security): 사용자의 인증 여부를 확인하거나, 특정 권한이 있는 사용자만 메서드를 호출할 수 있도록 하는 보안 로직을 AOP로 구현할 수 있습니다.
(4) 에러 핸들링(Error Handling): 예외가 발생했을 때 공통적인 에러 처리 로직을 AOP로 구현하여 코드 중복을 줄일 수 있습니다.
AOP 주요 개념
Aspect : 반복되어 사용되는 로직
Target : 적용할 로직
Advice : 반복 로직의 구현체
JointPoint, PointCut : Advice의 적용 위치
▶ Aspect
흩어진 관심사(Crosscutting Concerns)를 묶어서 모듈화 한 것. 하나의 모듈. Advice와 Point Cut이 들어간다. = 반복되어 사용되는 로직
▶ Target
Aspect가 가지고 있는 Advice가 적용되는 대상(클래스, 메서드 등등)을 말한다. = 적용할 로직
▶ Advice
어떤 일을 해야할 지에 대한 것. 해야할 일들에 대한 정보를 가지고 있다. = 반복로직의 구현체
▶ Join Point
가장 흔한 Join Point는 메서드 실행 시점이다. Advice가 적용될 위치, 끼어들 수 있는 지점. 생성자 호출 직전, 생성자 호출 시, 필드에 접근하기 전, 필드에서 값을 가져갔을 때 등등.
▶ Point Cut
Join Point의 상세한 스펙을 정의한 것. 어디에 적용해야 하는지에 대한 정보를 가지고 있다. “A 클래스에 B 메서드를 적용할 때 호출을 해라.”와 같은 구체적인 정보를 준다.
AOP 구현체
▶ 자바
AspectJ
스프링 AOP
7. Spring vs Spring boot : 가장 큰 차이는 AutoConfigutaion 입니다. 스프링은 초기에 해주어야 하는 다양한 설정이 있으나, 스프링 부트는 이것들을 자동 설정하여 초기 개발 환경 세팅에 걸리는 리소스를 많이 아낄 수 있습니다.
8.
의존성 주입(Dependency Injection)이란 객체지향 프로그래밍에서 의존성을 관리하는 기법 중 하나로, 한 객체가 다른 객체에 의존할 때 이를 외부에서 주입하는 방식입니다. 이 방식을 통해 코드의 결합도를 낮추고, 유닛 테스팅을 용이하게 할 수 있습니다.
스프링 프레임워크에서 의존성 주입은 주로 세 가지 방법으로 이루어집니다.
(1) 생성자 주입(Constructor Injection): 객체가 생성될 때 생성자를 통해 의존성을 주입하는 방식입니다. 생성자 주입은 객체의 불변성을 보장하며, 순환 참조를 방지할 수 있습니다.
@Component
public class SampleClass {
private **final** DependencyClass dependency;
@Autowired
public SampleClass(DependencyClass dependency) {
this.dependency = dependency;
}
}
(2) 세터 주입(Setter Injection): 세터 메서드를 통해 의존성을 주입하는 방식입니다. 세터 주입은 선택적인 의존성을 다룰 때 유용하며, 변경 가능성이 있는 의존성에 대해서도 적합합니다.
@Component
public class SampleClass {
private DependencyClass dependency;
@Autowired
public void **setDependency**(DependencyClass dependency) {
this.dependency = dependency;
}
}
(3) 필드 주입(Field Injection): 필드에 직접 주입하는 방식입니다. 이 방식은 코드량이 적지만, 테스트하기 어렵고 객체의 불변성을 보장하기 어렵습니다.
@Component
public class SampleClass {
@Autowired
private DependencyClass dependency;
}
실무에서는 생성자 주입을 가장 권장하는 방식으로 여깁니다. 생성자 주입은 필수 의존성을 명확하게 표현하며, 순환 참조를 방지하고 테스트하기 좋습니다. 따라서 면접에서는 "저는 생성자 주입을 사용하여 의존성을 관리하였습니다. 이를 통해 의존성이 명확하게 표현되어 코드의 가독성을 높였으며, 순환 참조를 방지하고 테스트하기 좋은 코드를 작성할 수 있었습니다."와 같이 답변할 수 있습니다.( 순환참조 방지 이유) 생성자주입방식과 필드/setter주입 방식은 '빈을 주입하는 순서'가 다르다.
필드주입과 setter주입은 빈을 먼저 생성한 후, 주입하려는 빈을 찾아 주입하지만 생성자 주입은 빈을 먼저 생성하지 않고 주입하려는 빈을 먼저 찾는다. 즉, 객체 생성 시점에 빈을 주입하기 때문에 서로 참조하는 객체가 생성되지 않은 시점에서 그 빈을 참조하기 때문에 오류 발생
final 키워드 사용에 대해서: final 키워드를 사용하면 해당 변수는 초기화 이후에 변경할 수 없습니다. 이는 객체의 불변성을 유지하게 해주며, 특히 다중 스레드 환경에서 동기화 문제를 예방하는 데 유용합니다. 그러나 final 키워드를 사용하지 않아도 생성자 주입 자체는 가능합니다. 다만, 불변성을 보장하려면 final 키워드를 사용하는 것이 좋습니다.
생성자 주입의 불변성 보장 및 순환 참조 방지: 생성자 주입은 객체 생성 시점에 모든 의존성이 주입되어야 객체 생성이 완료됩니다. 이는 객체가 완전히 초기화된 상태로 사용됨을 보장하며, 이는 불변성을 보장합니다. 또한, 순환 참조가 발생하면 애플리케이션은 객체를 생성하는 시점에 실패합니다. 이는 애플리케이션 시작 시점에 순환 참조 문제를 감지할 수 있게 해줍니다.
세터 주입의 변경 가능성에 대한 적합성: 세터 주입은 객체 생성 이후에도 의존성을 변경할 수 있습니다. 즉, 초기화 이후에도 의존성을 변경할 수 있다는 뜻입니다. 예를 들어, 실행 중에 구성을 변경하거나 필요에 따라 다른 구현을 사용해야 하는 경우에 유용합니다. 이는 선택적인 의존성이나 변경 가능성이 있는 의존성을 다룰 때 적합합니다. 그러나 이로 인해 객체의 상태를 추적하기 어려울 수 있으며, 스레드 안전성을 보장하기 어렵습니다.
위에 제가 설명한 것은 스프링 프레임워크에서 사용하는 의존성 주입(Dependency Injection, DI) 방식입니다. 스프링에서는 @Autowired, @Inject, @Resource 등의 어노테이션을 사용하여 의존성 주입을 수행하고, XML이나 Java Config를 사용하여 주입 대상을 지정합니다.
singleton은 인터페이스 구현이 가능하고 일반 객체로서 다른 클래스나 메서드에 전달할 수 있지만 static class는 객체가 아니기에 불가능하다
디비커넥션이나 스레드 풀 객체를 생성할 때 사용된다.
하나의 인스턴스만을 생성하며 getInstance메서드로 모든 클라이언트에게 동일한 인스턴스를 반환.
★특정 클래스에 객체 인스턴스가 하나만 만들어지도록 해주는 패턴입니다. 싱글턴 패턴을 사용하면 전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든지 액세스 할 수 있게 만들 수 있습니다. 클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공합니다.
private 생성자를 가지는 특징을 가지며, 생성된 싱글톤 오브젝트는 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의.
트랜잭션, 동작 원리 :
@Transactional의 동작 원리에 대해 설명해주세요.
@Transactional을 메소드 또는 클래스에 명시하면, AOP를 통해 Target이 상속하고 있는 인터페이스 또는 Target 객체를 상속한 Proxy 객체가 생성되며, Proxy 객체의 메소드를 호출하면 Target 메소드 전 후로 트랜잭션 처리를 수행합니다.
spring에서 트랜잭션 처리하는 방법
@Transactional은 Spring에서 제공하는 어노테이션이며, 이 어노테이션은 메서드에 트랜잭션의 시작, 종료, 롤백 등의 관리를 자동으로 해줍니다. 이 어노테이션을 사용하면 개발자가 직접 트랜잭션의 상태를 관리하는 데 필요한 코드를 작성하지 않아도 됩니다.
기본적으로 @Transactional 어노테이션은 메서드가 호출될 때 트랜잭션을 시작하고, 메서드가 정상적으로 종료되면 트랜잭션을 커밋합니다. 그러나 메서드 실행 중에 예외가 발생하면 자동으로 트랜잭션을 롤백합니다.
예를 들어, 데이터베이스에 여러 개의 변경사항을 한 번의 트랜잭션으로 처리해야 하는 경우에 @Transactional을 사용할 수 있습니다. 이 어노테이션을 사용하면 모든 변경사항이 정상적으로 적용되거나, 어떤 변경사항도 적용되지 않는 것을 보장할 수 있습니다.
이와 같은 트랜잭션 관리를 가능하게 하는 기술이 Proxy 입니다. Proxy는 실제 객체를 대신해 사용되는 객체를 의미합니다. Proxy는 대상 객체의 메서드를 호출할 때, 실제로 메서드를 실행하기 전후에 특정 작업을 수행할 수 있습니다.
Spring에서는 AOP(Aspect Oriented Programming) 기술을 통해 @Transactional이 붙어 있는 메서드가 호출되면, 실제 메서드를 실행하기 전에 트랜잭션을 시작하는 Proxy를 생성하고, 메서드가 정상적으로 실행된 후에 트랜잭션을 커밋하거나 예외가 발생하면 롤백하는 기능을 추가합니다. 이런 방식으로 Spring은 @Transactional 어노테이션을 통한 선언적인 트랜잭션 관리를 제공합니다.
이런 Proxy 기반의 AOP 방식은 트랜잭션 관리 외에도, 로깅, 보안 체크, 에러 핸들링 등과 같이 여러 메서드에서 공통으로 필요한 코드를 재사용하고 관리하는 데도 사용됩니다.
@Transactional를 스프링 Bean의 메소드 A에 적용하였고, 해당 Bean의 메소드 B가 호출되었을 때, B 메소드 내부에서 A 메소드를 호출하면 어떤 요청 흐름이 발생하는지 설명해주세요.
: 프록시는 클라이언트가 타겟 객체를 호출하는 과정에만 동작하며, 타겟 객체의 메소드가 자기 자신의 다른 메소드를 호출할 때는 프록시가 동작하지 않습니다.
즉, A 메소드는 프록시로 감싸진 메소드가 아니므로 트랜잭션이 적용되지 않은 일반 코드가 수행됩니다.
A 라는 Service 객체의 메소드가 존재하고, 그 메소드 내부에서 로컬 트랜잭션 3개(다른 Service 객체의 트랜잭션 메소드를 호출했다는 의미)가 존재한다고 할 때, @Transactional을 A 메소드에 적용하면 어떤 요청 흐름이 발생하는지 설명해주세요.
: 트랜잭션 전파 수준에 따라 달라지는데, 만약 기본 옵션인 Required를 가져간다면 로컬 트랜잭션 3개가 모두 부모 트랜잭션인 A에 합류하여 수행됩니다.
그래서 부모 트랜잭션이나 로컬 트랜잭션 3개나 모두 같은 트랜잭션이므로 어느 하나의 로직에서 문제가 발생하면 전부 롤백이 됩니다.
@Transactional에 readOnly 속성을 사용하는 이유에 대해서 설명해주세요.
: @Transactional(readOnly=true)를 사용하는 주된 이유는 성능 최적화와 데이터의 일관성을 보장하기 위함입니다.
성능 최적화: 데이터베이스 엔진은 읽기 전용 트랜잭션에 대해 일부 최적화를 수행할 수 있습니다. 예를 들어, 불필요한 락(lock)이나 더티 체크(dirty check, 영속성 컨텍스트에서 변경된 엔티티를 추적하고 데이터베이스에 반영하기 위한 프로세스)를 회피함으로써 성능 향상을 가져올 수 있습니다.
데이터 일관성 보장: readOnly=true 옵션은 해당 트랜잭션에서 데이터를 변경하지 않겠다는 명확한 의도를 보여줍니다. 이는 개발자의 실수로 인해 발생할 수 있는 데이터 변경을 방지하여 데이터의 일관성을 보장하는 데 도움이 됩니다.
따라서 조회(read) 작업을 수행하는 서비스 메소드에 @Transactional(readOnly=true)를 사용하는 것이 좋은 습관입니다. 이를 통해 성능을 최적화하고 데이터 변경을 방지할 수 있습니다. 그러나 쓰기 작업(insert, update, delete)을 수행하는 메소드에는 readOnly=true를 사용하면 안 됩니다. 이 경우 데이터 변경이 이루어지지 않아 예상치 못한 결과를 초래할 수 있습니다.
execute와 doInTransaction 메소드는 각각 TransactionTemplate 클래스와 TransactionCallback 인터페이스의 메소드입니다.
TransactionTemplate.execute(): 이 메소드는 트랜잭션 경계를 정의하며, 이 메소드가 호출되면 새로운 트랜잭션이 시작됩니다. 이 메소드의 인자로는 TransactionCallback 객체가 전달되며, 이 객체의 doInTransaction 메소드에서 실제로 트랜잭션 내에서 수행되어야 할 로직이 정의됩니다. execute 메소드는 doInTransaction 메소드의 실행을 감싸서 트랜잭션을 제어하며, doInTransaction 메소드가 성공적으로 완료되면 트랜잭션을 커밋하고, 그렇지 않으면 롤백합니다.
TransactionCallback.doInTransaction(): 이 메소드는 트랜잭션 내에서 수행되어야 할 실제 로직을 정의합니다. 이 메소드는 TransactionStatus 객체를 인자로 받아 트랜잭션의 현재 상태를 확인하거나 제어하는 데 사용할 수 있습니다. 이 메소드에서 발생하는 모든 체크되지 않은 예외(Runtime exceptions)는 Spring 트랜잭션 인프라스트럭처에 의해 잡히고 롤백이 발생합니다. 체크된 예외(Checked exceptions)는 이 메소드를 호출하는 코드에게 전달됩니다.
따라서, 전체적으로 execute 메소드는 트랜잭션을 시작하고 제어하며, doInTransaction 메소드는 트랜잭션 내에서 수행되어야 할 실제 작업을 정의하는 역할을 합니다.
TransactionStatus는 Spring Framework에서 제공하는 인터페이스로, 트랜잭션에 대한 현재 상태를 나타냅니다. 이 인터페이스는 현재 트랜잭션이 새롭게 시작된 것인지, 기존에 진행 중인 것에 참여하는 것인지, 트랜잭션이 완료되었는지 아닌지 등의 정보를 제공합니다.
TransactionStatus 인터페이스의 주요 메소드에는 다음과 같은 것들이 있습니다:
isNewTransaction(): 현재 트랜잭션이 새롭게 시작된 것인지 아닌지를 나타내는 boolean 값을 반환합니다. 이 메소드가 true를 반환하면 현재 트랜잭션이 새롭게 시작된 것이고, false를 반환하면 기존에 진행 중인 트랜잭션에 참여하는 것입니다.
setRollbackOnly(): 현재 트랜잭션을 롤백 상태로 만듭니다. 이 메소드를 호출하면, 트랜잭션의 종료 시점에 롤백이 수행됩니다.
isRollbackOnly(): 현재 트랜잭션이 롤백 상태인지 아닌지를 나타내는 boolean 값을 반환합니다. 이 메소드가 true를 반환하면 현재 트랜잭션은 롤백 상태입니다.
isCompleted(): 현재 트랜잭션이 완료(커밋 또는 롤백)된 상태인지 아닌지를 나타내는 boolean 값을 반환합니다. 이 메소드가 true를 반환하면 현재 트랜잭션은 이미 완료된 상태입니다.
TransactionCallback 인터페이스를 구현하는 메소드에서 TransactionStatus 인스턴스는 트랜잭션의 현재 상태를 확인하거나 제어하는 데 사용됩니다. 예를 들어, 특정 조건이 충족되지 않는 경우 트랜잭션을 롤백하도록 설정할 수 있습니다.
트랜잭션을 분리하려면 Spring에서 제공하는 Propagation 속성을 이용하면 됩니다. Propagation 속성은 트랜잭션이 전파되는 방식을 지정하는데 사용되며, @Transactional 어노테이션과 함께 사용됩니다. 이를 이용하면 각 메소드에 대한 트랜잭션 전파 방식을 제어할 수 있습니다.
Propagation 속성에는 여러가지 옵션이 있습니다:
REQUIRED: 현재 트랜잭션이 있으면 참여하고, 그렇지 않으면 새로운 트랜잭션을 시작합니다. 이 옵션이 기본값입니다.
SUPPORTS: 현재 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행합니다.
MANDATORY: 반드시 트랜잭션 내에서 실행되어야 합니다. 현재 트랜잭션이 없는 경우 예외를 던집니다.
REQUIRES_NEW: 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있다면 잠시 보류(일시정지) 상태로 만듭니다.
NOT_SUPPORTED: 트랜잭션을 지원하지 않고, 현재 트랜잭션이 있으면 보류(일시정지) 상태로 만듭니다.
NEVER: 트랜잭션을 지원하지 않으며, 현재 트랜잭션이 있으면 예외를 던집니다.
NESTED: 현재 트랜잭션이 있으면 중첩 트랜잭션을 시작합니다. 중첩 트랜잭션은 현재 트랜잭션 내에 독립적인 트랜잭션을 만듭니다.
결과를 반환하는 작업과 반환하지 않는 작업의 주된 차이는 바로 "작업의 결과를 호출자에게 반환하는지 여부"입니다. TransactionCallback은 트랜잭션 내에서 수행되는 작업의 결과를 반환하는 인터페이스로서, 이 인터페이스를 구현하는 doInTransaction 메소드에서는 작업 결과를 반환합니다.
반면에 TransactionCallbackWithoutResult은 결과를 반환하지 않는 트랜잭션 작업을 위한 인터페이스입니다. 이 인터페이스를 구현하는 doInTransactionWithoutResult 메소드에서는 반환값이 없습니다. 이 인터페이스는 주로 트랜잭션 작업의 성공 여부만 중요하고, 그 결과 자체는 중요하지 않은 경우에 사용됩니다.
아래는 각 인터페이스의 사용 예시입니다:
java
Copy code
// 결과를 반환하는 작업
Integer result = transactionTemplate.execute(new TransactionCallback() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// 여기에서 수행하는 작업의 결과를 반환
return someService.doSomething();
}
});
// 결과를 반환하지 않는 작업
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// 여기에서 수행하는 작업의 결과는 반환하지 않음
someService.doSomething();
}
});
Spring의 TransactionTemplate을 사용하면, doInTransaction 혹은 doInTransactionWithoutResult 메소드 내부에서 발생하는 모든 데이터베이스 작업들은 하나의 트랜잭션으로 처리됩니다. 만약 이 메소드 내부에서 예외가 발생하면 모든 작업이 롤백되고, 예외가 발생하지 않으면 모든 작업이 커밋됩니다. 따라서 모든 작업이 성공적으로 완료되거나, 또는 모두 롤백되는 원자성을 보장합니다.
만약 트랜잭션 내에서 특정 조건에 따라 롤백을 수행하려면, TransactionStatus 객체의 setRollbackOnly() 메소드를 호출하면 됩니다. 이 메소드를 호출하면 현재 트랜잭션이 롤백되도록 표시되고, 트랜잭션 종료 시 모든 작업이 롤백됩니다.
트랜잭션의 전파 범위는 이미 시작된 트랜잭션이 다른 트랜잭션 메소드를 호출할 때 어떤 방식으로 동작할지를 결정합니다. Spring에서는 7가지 전파 옵션을 제공합니다.
PROPAGATION_REQUIRED: 기본값으로, 이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 새로운 트랜잭션을 시작합니다.
PROPAGATION_SUPPORTS: 이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 실행됩니다.
PROPAGATION_MANDATORY: 이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 예외를 발생시킵니다.
PROPAGATION_REQUIRES_NEW: 새로운 트랜잭션을 시작하고, 이미 시작된 트랜잭션이 있으면 잠시 보류합니다.
PROPAGATION_NOT_SUPPORTED: 트랜잭션 없이 실행하고, 이미 시작된 트랜잭션이 있으면 잠시 보류합니다.
PROPAGATION_NEVER: 트랜잭션 없이 실행하고, 이미 시작된 트랜잭션이 있으면 예외를 발생시킵니다.
PROPAGATION_NESTED: 이미 시작된 트랜잭션이 있으면 중첩 트랜잭션을 시작하고, 그렇지 않으면 새로운 트랜잭션을 시작합니다.
2. 트랜잭션의 격리 수준 (Isolation):
트랜잭션의 격리 수준은 트랜잭션에서 동시성 제어를 위한 설정입니다. 다른 트랜잭션으로부터 어느 정도 독립되어야 하는지를 결정하며, 데이터 일관성과 동시성 수준 사이의 트레이드오프를 관리합니다. 격리 수준은 4가지입니다. ( 0레벨~3레벨) 트랜잭션 격리수준에 대한 변경은 동시성/데이터 무결성과 연관되어 있는데, 동시성을 증가시키면 데이터 무결성에 문제가 발생하고, 데이터 무결성을 유지하면 동시성이 떨어지게 됩니다. 트랜잭션 격리수준이 높을 수록 데이터 무결성 및 정합성이 높아지나, 동시성(성능)은 낮아지게 됩니다. 반대로 격리수준이 낮을 수록 동시성(성능)은 높아지나, 데이터 정합성이 깨지는 여러 부정합문제가 발생할 수 있습니다. 따라서 각 서비스에 특성에 맞게 적절한 격리수준을 선택하여 사용하여야 합니다.
ISOLATION_READ_UNCOMMITTED: 다른 트랜잭션에서 커밋하지 않은 데이터를 읽을 수 있습니다 (Dirty read, Non-repeatable read, Phantom read 발생 가능).
ISOLATION_READ_COMMITTED: 다른 트랜잭션에서 커밋한 데이터만 읽을 수 있습니다 (Non-repeatable read, Phantom read 발생 가능).
ISOLATION_REPEATABLE_READ: 트랜잭션 동안 같은 데이터를 여러 번 읽을 때 일관성 있는 결과를 얻습니다 (Phantom read 발생 가능).
ISOLATION_SERIALIZABLE: 가장 엄격한 격리 수준으로, 트랜잭션을 순차적으로 실행하여 모든 동시성 문제를 해결합니다.
트랜잭션의 타임아웃은 트랜잭션을 얼마나 오랫동안 수행할 것인지를 결정합니다. 지정된 시간 내에 트랜잭션이 완료되지 않으면 자동으로 롤백합니다.
읽기 전용 트랜잭션은 트랜잭션이 데이터를 변경하지 않고 오직 읽기만 수행함을 나타냅니다. 이를 통해 성능을 향상시킬 수 있습니다. 하지만 실제로 읽기 전용 트랜잭션에서 데이터 변경 작업이 이루어지면 예외가 발생하지 않습니다. 따라서 이 설정은 힌트로서 데이터베이스 엔진에게 제공되며, 데이터베이스 엔진은 이를 최적화하는데 활용할 수 있습니다.
프록시(Proxy란)
스프링 프레임워크에서 프록시는 실제 객체를 감싸고, 객체의 행동을 개선하거나 추가하는 중간 레이어를 말합니다. 프록시는 실제 객체와 같은 인터페이스를 구현하여 클라이언트가 프록시를 실제 객체인 것처럼 다룰 수 있게 합니다.
프록시는 스프링에서 AOP(Aspect Oriented Programming)와 트랜잭션 관리를 위해 주로 사용됩니다. AOP를 위해서는 관점(Aspect)이 필요한 곳에서 프록시를 사용하여 로깅, 보안, 트랜잭션 등의 공통 기능을 추가하거나 변경할 수 있습니다. 트랜잭션 관리를 위해서는 트랜잭션 시작과 종료를 관리하기 위한 코드를 추가할 수 있습니다.
즉, 스프링에서 프록시는 원래의 코드에 개입하거나 변경하지 않고도, 특정 로직을 주입하거나 변경하는 데 사용되는 기술이라고 설명할 수 있습니다. 이를 통해 코드의 재사용성을 높이고, 코드의 수정 없이도 기능을 추가하거나 변경할 수 있어 유지보수성을 높일 수 있습니다.
예시
(1) AOP: AOP는 관심사의 분리(Separation of Concerns)를 위한 프로그래밍 패러다임입니다. 로깅, 보안 검사, 트랜잭션 관리 등 공통적으로 필요한 기능을 "관심사(Aspect)"로 정의하고, 이를 프록시를 통해 필요한 위치에 적용합니다. 예를 들어, 메서드 호출 전후에 로그를 남기는 관심사를 정의하고 이를 프록시에 적용하면, 프록시는 메서드 호출 전후에 로그를 남기는 동작을 수행합니다.
(2) 트랜잭션 관리: 트랜잭션 관리도 스프링에서 프록시를 통해 제공하는 기능 중 하나입니다. 트랜잭션 어노테이션(@Transactional)이 붙은 메서드를 호출하면, 스프링은 그 메서드를 실행하는 프록시 객체를 생성합니다. 이 프록시는 메서드 호출 전에 트랜잭션을 시작하고, 메서드 호출 후에 트랜잭션을 커밋하거나 롤백합니다. 이를 통해 개발자는 복잡한 트랜잭션 관리 코드를 작성할 필요 없이, 선언적으로 트랜잭션 경계를 관리할 수 있습니다.
사용하는 이유
(1) Separation of Concerns (관심사의 분리): 프록시를 통해 공통적인 로직을 분리하고, 비즈니스 로직에 집중할 수 있습니다. 예를 들어, 보안 체크, 로깅, 트랜잭션 관리 등 공통적으로 수행되는 기능을 프록시가 담당하게 하면, 개발자는 비즈니스 로직에만 집중할 수 있습니다.
(2) Code Reusability (코드 재사용성): 공통 로직을 프록시에 구현하게 되면, 이를 여러 객체에서 재사용할 수 있습니다. 이는 코드 중복을 줄이고 코드 관리를 용이하게 합니다.
(3) Decoupling (결합도 감소): 프록시를 사용하면, 실제 객체와 클라이언트 사이의 의존성을 줄일 수 있습니다. 클라이언트는 실제 객체 대신 프록시와 상호작용하므로, 실제 객체가 변경되어도 클라이언트 코드를 변경할 필요가 없습니다. 이는 시스템의 유연성을 향상시킵니다.
예를 들어, 스프링에서는 트랜잭션 관리를 위해 프록시를 사용합니다. @Transactional 어노테이션을 메서드에 적용하면, 스프링은 그 메서드를 호출하는 코드에 대해 프록시를 생성하고, 이 프록시는 트랜잭션을 시작하고, 메서드가 성공적으로 완료되면 커밋하거나, 예외가 발생하면 롤백합니다. 이러한 방식으로, 개발자는 복잡한 트랜잭션 관리 로직을 신경 쓸 필요 없이 비즈니스 로직에 집중할 수 있습니다.
Spring Cache:
일반적으로 애플리케이션 성능을 향상시키기 위한 기능으로, 이전에 수행한 복잡한 연산의 결과나 외부 시스템과의 통신 결과를 저장하고 재사용하기 위한 기능을 제공합니다. Cache 추상화를 사용하면 메서드가 반환하는 값을 캐시에 저장하고, 같은 인자로 메서드를 호출하면 캐시에서 값을 가져옵니다.
Spring Cache는 @Cacheable, @CachePut, @CacheEvict 등의 어노테이션을 통해 캐싱 동작을 지원하며, 여러 캐시 저장소 (EhCache, Redis, etc)를 지원합니다. 캐셔블!
예를 들어, 다음과 같이 사용할 수 있습니다.
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
여기서 "books"는 캐시의 이름이며, findBook 메서드의 반환 값은 "books" 캐시에 저장됩니다. 같은 isbn으로 findBook 메서드를 호출하면,_ 캐시에서 값을 가져와서 메서드를 실행하는 데 걸리는 시간을 줄입니다.
Spring Core
Spring Core는 Spring Framework의 핵심이며, IoC(Inversion of Control)와 DI(Dependency Injection)를 지원합니다.
IoC는 객체의 생명 주기와 관련된 작업을 개발자로부터 가져와 컨테이너가 처리하게 하는 개념입니다. 즉, 객체의 생성, 초기화, 소멸 등의 작업을 개발자 대신 Spring Container가 담당합니다.
DI는 클래스 사이의 의존관계를 Spring Container가 주입하는 개념입니다. 객체간의 관계를 느슨하게 연결시켜서 유연한 코드를 작성할 수 있게 합니다.
spring cache 쓸 때 설정을 어떻게 해야하는지 / 코드구현 -> 위 참고
응집도와 결합도 : 응집도(Cohesion)와 결합도(Coupling)는 소프트웨어의 구조를 설계하고 평가할 때 중요한 개념입니다. 이 두 개념은 모듈이나 클래스가 서로 어떻게 연관되어 있는지, 어떻게 작동하는지를 설명하며, 소프트웨어의 유지보수성, 재사용성, 이해성 등을 판단하는 데 중요합니다.
(1) 응집도(Cohesion): 응집도는 하나의 모듈이나 클래스 내부의 구성 요소들이 서로 얼마나 밀접하게 관련되어 있고, 하나의 명확한 목적을 위해 일을 수행하는지를 나타냅니다. 응집도가 높을수록 각 모듈이나 클래스는 하나의 작업에 집중하며, 이는 이해하고 수정하기 쉽다는 장점이 있습니다. 예를 들어, 사용자 정보를 관리하는 'UserManager' 클래스는 사용자의 추가, 삭제, 검색 등의 기능을 가지고 있지만, 결제나 상품 관리와 같은 다른 책임을 가지고 있지 않아야 합니다.
(2) 결합도(Coupling): 결합도는 서로 다른 모듈이나 클래스들이 얼마나 강하게 연결되어 있는지, 즉 얼마나 서로에게 의존하고 있는지를 나타냅니다. 결합도가 높다는 것은 하나의 모듈이나 클래스가 다른 모듈이나 클래스의 내부 구조나 동작 방식에 의존하는 경우를 말하며, 이는 유지보수와 재사용을 어렵게 만듭니다. 따라서 결합도는 낮추는 것이 좋습니다.
이 두 가지 개념을 통해 보면, 좋은 소프트웨어 설계는 '높은 응집도'와 '낮은 결합도'를 가지는 것을 목표로 합니다. 이렇게 하면 각 모듈이나 클래스가 독립적으로 유지보수와 재사용이 가능하며, 전체 시스템의 구조를 이해하기 쉬워집니다.
어노테이션 :
어노테이션(annotation)은 코드 사이에 주석처럼 쓰이는 특별한 형태의 주석으로, 프로그램 코드와 메타데이터를 연결하는 방법입니다. 어노테이션은 컴파일러에게 코드를 어떻게 컴파일하고 처리해야하는지에 대한 정보를 제공하거나, 컴파일 시간이나 실행 시간에 특정 기능을 실행하도록 하는 등의 역할을 합니다.
어노테이션의 종류:
Java에서는 기본적으로 몇 가지 어노테이션을 제공합니다.
@Override: 메소드가 슈퍼클래스의 메소드를 오버라이드하는 것을 나타냅니다.
@Deprecated: 메소드가 더 이상 사용되지 않음을 나타냅니다.
@SuppressWarnings: 컴파일러 경고를 무시하도록 지시합니다.
또한 Spring, Junit, Hibernate 등의 프레임워크에서는 자체적인 어노테이션을 많이 제공하며, 이들은 특정 기능을 더 쉽게 구현할 수 있도록 도와줍니다.