일단 코드 설명을 하기 앞서 왜 transactional 로그 기록을 남길 때 common 패키지로 분리해서 관리해야 하는지 설명하도록 하겠습니다.
1. 관심사 분리(Separation of Concerns)
- manager 패키지는 주로 매니저 등록, 수정, 삭제와 같은 비즈니스 로직을 다룹니다. 하지만 로그 관리는 모든 모듈에서 공통적으로 사용될 수 있는 기능이기 때문에 별도의 common 패키지로 분리하여, 비즈니스 로직과 로그 관리 로직을 명확히 구분하는 것이 좋습니다.
- 예를 들어, manager뿐만 아니라 user, product 등 다양한 서비스에서 로그를 사용할 수 있다면, 각 서비스에서 중복된 로그 관리 코드를 작성하지 않고, common 패키지의 공통 로그 관리 기능을 재사용할 수 있습니다.
2. 재사용성
- common 패키지에 로그 엔티티, 리포지토리, 서비스 등을 배치하면 여러 모듈에서 로그 관련 기능을 쉽게 재사용할 수 있습니다.
- 만약 로그 테이블 구조가 변경되거나 로그 정책이 바뀌더라도, 공통 모듈만 수정하면 전체 애플리케이션에 반영되므로, 유지보수의 용이성이 높아집니다.
3. 종속성 관리
- common 패키지로 로그 관련 엔티티와 서비스를 분리하면, 비즈니스 로직 패키지(manager 등)는 로그 관련 의존성을 제거할 수 있습니다. 비즈니스 로직 코드가 더 깔끔해지고, 각 모듈의 의존성을 명확히 정의할 수 있습니다.
- 특히, 여러 패키지에서 로그 기능을 호출할 때, 각각의 비즈니스 로직 패키지가 로그와 관련된 모든 클래스를 포함할 필요 없이 common 모듈만 의존하면 되므로 코드의 복잡도를 줄일 수 있습니다.
4. 확장성
- common 패키지에 로그 관리를 위한 entity, repository, service 등을 모듈화하여 두면, 나중에 로그 기능 확장 (예: 다른 로그 저장소로 변경, 로그 포맷 변경, 로그 레벨 추가 등) 시 별도의 모듈만 수정하면 됩니다.
- 로그 기능을 추가하거나 로그 기록 정책을 변경할 때 비즈니스 로직 코드에 영향을 주지 않으므로 확장에 유리합니다.
패키지 구조 예시로 들면
src
└── main
└── java
└── com
└── example
├── manager # 매니저 관련 비즈니스 로직 패키지
│ ├── entity
│ ├── repository
│ └── service
├── common # 공통 기능 패키지
│ ├── entity # 공통 엔티티 (예: Log)
│ ├── repository # 공통 리포지토리 (예: LogRepository)
│ └── service # 공통 서비스 (예: LogService)
└── user # 사용자 관련 비즈니스 로직 패키지 (다른 비즈니스 모듈 예시)
├── entity
├── repository
└── service
이런식의 구조를 갖추게 됩니다.
common.entity 패키지에 로그 엔티티 생성
common.entity 패키지에 로그 엔티티를 정의하여, 다양한 비즈니스 로직에서 사용할 수 있게 합니다.
package com.example.common.entity;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Table(name = "log")
public class Log {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long relatedId; // 관련 엔티티 ID (예: managerId, userId 등)
private String entity; // 엔티티 타입 (예: "Manager", "User")
private String action; // 수행한 작업 (예: 등록, 수정, 삭제)
private String message; // 상세 로그 메시지
private LocalDateTime createdAt = LocalDateTime.now(); // 생성 시간
// 기본 생성자
public Log() {
}
// 생성자
public Log(Long managerId, String manager, String action, String message) {
this.managerId = managerId;
this.manager = manager;
this.action = action;
this.message = message;
}
}
common.repository 패키지에 레포지토리 생성
로그를 데이터베이스에 저장하고 조회할 수 있는 레포지토리를 정의합니다.
package com.example.common.repository;
import com.example.common.entity.Log;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LogRepository extends JpaRepository<Log, Long> {
}
common.service 패키지에 로그 서비스 생성
로그를 저장하는 로직을 LogService 클래스에 작성하여, 비즈니스 로직 클래스에서 쉽게 호출할 수 있게 합니다.
package com.example.common.service;
import com.example.common.entity.Log;
import com.example.common.repository.LogRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LogService {
@Autowired
private LogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Long relatedId, String entity, String action, String message) {
Log log = new Log(relatedId, entity, action, message);
logRepository.save(log);
}
}
manager 패키지에서 로그 서비스 호출
이제 manager 패키지 내에서 로그를 기록하고자 할 때, common 패키지의 LogService를 호출하여 로그를 기록할 수 있습니다.
@Transactional
public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
// 일정을 만든 유저
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
try {
if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 유효하지 않거나, 일정을 만든 유저가 아닙니다.");
}
User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
.orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));
if (ObjectUtils.nullSafeEquals(user.getId(), managerUser.getId())) {
throw new InvalidRequestException("일정 작성자는 본인을 담당자로 등록할 수 없습니다.");
}
Manager newManagerUser = new Manager(managerUser, todo);
Manager savedManagerUser = managerRepository.save(newManagerUser);
// 성공 로그 기록
logService.saveLog(managerSaveRequest.getManagerUserId(), "Manager", "Register", "Manager registered successfully: " + managerUser.getEmail());
return new ManagerSaveResponse(
savedManagerUser.getId(),
new UserResponse(managerUser.getId(), managerUser.getEmail())
);
} catch (Exception e) {
// 매니저 등록 실패 시 실패 로그 기록
logService.saveLog(
managerSaveRequest.getManagerUserId(),
"Manager",
"Register_FAILED",
"Manager registeration failed" + e.getMessage()
);
// 예외를 다시 던져서 상위 트렌잭션에 영향을 주도록 설정
throw e;
}
}
해당 메서드에 대한 설명을 하자면
@Transactional(propagation = Propagation.REQUIRES_NEW)를 사용하여, 현재 트랜잭션이 롤백되더라도 로그를 별도로 커밋할 수 있도록 설정했습니다.
예외 처리를 추가해 로그 기록 중에 예외가 발생하더라도 로그가 실패했음을 확인할 수 있도록 catch 블록에서 예외 메세지를 출력합니다.
로그 기록 메서드를 통해 saveLog 메서드에서는 관련된 엔티티 ID, 엔티티 이름, 수행된 액션, 상세 메세지 등을 인자로 받아 로그를 저장합니다. 이 메서드를 통해 다양한 상황에서 로그를 기록할 수 있습니다.
@Transactional의 propagation 속성 추가 설명
1. REQUIRED
기본 설정 (default value)
현재 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 존재하지 않으면 새로운 트랜잭션을 생성합니다.
사용 예시: 대부분의 경우에 사용되며, 기존 트랜잭션이 있으면 합류하고, 없으면 새로 만들어 작업을 수행합니다.
@Transactional(propagation = Propagation.REQUIRED)
public void method() {
// 트랜잭션 내에서 수행되는 로직
}
동작 시나리오:
메서드 A에서 메서드 B를 호출하고, A에 트랜잭션이 있으면 B도 해당 트랜잭션에 포함되어 실행됩니다.
메서드 A에 트랜잭션이 없다면, B에서 새로운 트랜잭션이 생성됩니다.
2. REQUIRES_NEW
항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션이 존재할 경우 기존 트랜잭션을 일시 중단(잠시 보류)하고, 새 트랜잭션을 수행합니다.
사용 예시: 기존 트랜잭션의 영향을 받지 않고 독립적으로 실행되어야 하는 경우. 예를 들어, 로그 저장 등 주요 로직의 성공 여부와 관계없이 반드시 수행되어야 하는 작업.
예시:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperation() {
// 새로운 트랜잭션으로 로그 기록
}
동작 시나리오:
메서드 A가 트랜잭션을 가지고 있고, A에서 B를 호출하는 경우 B는 A의 트랜잭션을 일시 중단하고 별도의 트랜잭션을 생성하여 수행됩니다. B가 완료되면 A의 트랜잭션이 재개됩니다.
3. SUPPORTS
현재 트랜잭션이 존재하면 트랜잭션 내에서 실행되고, 존재하지 않으면 트랜잭션 없이 실행됩니다.
사용 예시: 트랜잭션의 존재 여부가 크게 중요하지 않은 조회성 작업(예: 단순 조회)이나 로직을 트랜잭션이 있는지 확인 후 처리해야 할 경우.
예시:
@Transactional(propagation = Propagation.SUPPORTS)
public void readOperation() {
// 트랜잭션이 있으면 포함되어 실행, 없으면 트랜잭션 없이 실행
}
동작 시나리오:
메서드 A가 트랜잭션 없이 메서드 B를 호출하면 B도 트랜잭션 없이 실행됩니다.
메서드 A가 트랜잭션을 가지고 있으면 B도 해당 트랜잭션에 포함되어 실행됩니다.
4. NOT_SUPPORTED
트랜잭션이 존재하면 일시 중단하고, 트랜잭션 없이 메서드를 실행합니다.
사용 예시: 트랜잭션이 필요 없는 작업이 트랜잭션 내에서 실행될 경우, 트랜잭션을 중지하여 독립적으로 수행해야 할 때 사용.
예시:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void independentOperation() {
// 트랜잭션 없이 수행
}
동작 시나리오:
메서드 A가 트랜잭션을 가지고 있는 상태에서 메서드 B를 호출하면, B 실행 시 A의 트랜잭션이 일시 중단되고, B는 트랜잭션 없이 실행됩니다.
5. MANDATORY
항상 기존 트랜잭션이 존재해야만 실행되며, 트랜잭션이 없으면 예외를 발생시킵니다.
사용 예시: 트랜잭션이 반드시 필요한 메서드에서 사용하며, 부모 메서드가 트랜잭션을 가지고 있지 않으면 예외를 던집니다.
예시:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryOperation() {
// 반드시 트랜잭션이 존재해야 함
}
동작 시나리오:
메서드 A가 트랜잭션 없이 메서드 B를 호출하면, B는 IllegalTransactionStateException 예외를 발생시킵니다.
메서드 A가 트랜잭션을 가지고 있으면 B도 해당 트랜잭션에 포함되어 실행됩니다.
6. NEVER
트랜잭션 없이 실행되며, 현재 트랜잭션이 존재하면 예외를 발생시킵니다.
사용 예시: 트랜잭션이 필요 없는 작업이며, 절대 트랜잭션과 함께 실행되면 안 되는 작업에 사용합니다.
예시:
@Transactional(propagation = Propagation.NEVER)
public void noTransactionOperation() {
// 트랜잭션 없이 수행, 트랜잭션 존재 시 예외 발생
}
동작 시나리오:
메서드 A가 트랜잭션을 가지고 있는 상태에서 메서드 B를 호출하면, B는 IllegalTransactionStateException 예외를 발생시킵니다.
메서드 A가 트랜잭션 없이 호출하면 B도 트랜잭션 없이 실행됩니다.
7. NESTED
현재 트랜잭션이 존재하면, 중첩된(새로운) 트랜잭션을 생성하여 실행합니다. 중첩된 트랜잭션은 부모 트랜잭션의 영향을 받으며, 부모 트랜잭션이 커밋되면 중첩된 트랜잭션도 커밋됩니다.
부모 트랜잭션이 롤백되면 중첩된 트랜잭션도 함께 롤백됩니다.
사용 예시: 독립적으로 롤백을 관리하고 싶지만, 부모 트랜잭션에 영향을 받는 중첩 트랜잭션이 필요할 때 사용합니다.
예시:
@Transactional(propagation = Propagation.NESTED)
public void nestedOperation() {
// 중첩된 트랜잭션으로 수행
}
동작 시나리오:
메서드 A가 트랜잭션을 가지고 있는 상태에서 메서드 B를 호출하면, B는 A의 중첩 트랜잭션으로 실행됩니다. 만약 B가 롤백되더라도, A의 트랜잭션은 영향을 받지 않습니다.
전파 속성 선택 시 고려 사항
REQUIRED와 REQUIRES_NEW의 차이점:
- REQUIRED는 기존 트랜잭션에 합류하지만, REQUIRES_NEW는 기존 트랜잭션을 일시 중단하고 새로 트랜잭션을 생성합니다.
SUPPORTS와 NOT_SUPPORTED의 차이점:
- SUPPORTS는 트랜잭션이 있으면 포함되어 실행되고, 없으면 트랜잭션 없이 실행합니다.
- NOT_SUPPORTED는 트랜잭션이 있으면 일시 중단하고, 트랜잭션 없이 실행합니다.
MANDATORY와 NEVER의 차이점:
- MANDATORY는 트랜잭션이 없으면 예외를 발생시키고, NEVER는 트랜잭션이 있으면 예외를 발생시킵니다.
- NESTED는 부모 트랜잭션의 영향을 받지만 독립적으로 롤백할 수 있습니다. REQUIRES_NEW와 달리 완전히 분리된 트랜잭션이 아니며, 부모와 관계가 밀접합니다.
'Spring' 카테고리의 다른 글
Spring boot 버전 정리 (1) | 2024.12.02 |
---|---|
동시성 제어 (낙관적 락, 비관적 락, 분산 락) (0) | 2024.10.19 |
단위테스트, 통합테스트 (1) | 2024.10.01 |
Fetch Type:LazyLoading vs EagerLoading (0) | 2024.09.30 |
Error 메시지(enum 활용) (3) | 2024.09.13 |