Spring MVC 컨트롤러를 @Transactional로 만들면 안되는 이유는 무엇입니까?
주제에 대해 이미 몇 가지 질문이 있지만, Spring MVC 컨트롤러를 만들면 안되는 이유를 설명하기 위해 실제로 인수를 제공하는 응답은 전혀 없습니다 Transactional. 보다:
왜?
- 거기에 극복 할 수없는 기술적 인 문제?
- 아키텍처 문제가 있습니까?
- 성능 / 교착 상태 / 동시성 문제가 있습니까?
- 때때로 여러 개의 개별 트랜잭션이 필요합니까? 그렇다면 사용 사례는 무엇입니까? (저는 서버 호출이 완전히 성공하거나 완전히 실패하는 단순화 된 디자인을 좋아합니다. 매우 안정적인 동작 인 것 같습니다.)
배경 : 저는 몇 년 전에 C # / NHibernate / Spring.Net에서 구현 된 대규모 ERP 소프트웨어에 대한 팀에서 일했습니다. 서버로의 왕복은 정확히 다음과 같이 구현되었습니다. 트랜잭션은 컨트롤러 로직에 들어가기 전에 열렸고 컨트롤러를 종료 한 후 커밋되거나 롤백되었습니다. 트랜잭션은 프레임 워크에서 관리되어 아무도 신경 쓸 필요가 없었습니다. 안정적이고 단순하며 소수의 아키텍트 만이 트랜잭션 문제에 관심을 가졌고 나머지 팀은 기능을 구현했습니다.
제 관점에서는 제가 본 최고의 디자인입니다. Spring MVC를 사용하여 동일한 디자인을 재현하려고 시도하면서 지연 로딩 및 트랜잭션 문제와 매번 동일한 대답으로 악몽에 빠졌습니다. 컨트롤러를 트랜잭션으로 만들지 마십시오. 그런데 왜 그런가요?
귀하의 설립 답변에 미리 감사드립니다!
TLDR : 이는 응용 프로그램의 서비스 계층에만 데이터베이스 / 비즈니스 트랜잭션의 범위를 식별하는 데 필요한 논리가 있기 때문입니다. 설계 상 컨트롤러 및 지속성 계층은 트랜잭션의 범위를 알 수 없습니다.
컨트롤러를 만들 수 @Transactional있지만 실제로 서비스 계층을 트랜잭션으로 만 만드는 것이 일반적인 권장 사항입니다 (지속성 계층도 트랜잭션이 아니어야 함).
그 이유는 기술적 타당성이 아니라 우려의 분리 때문입니다. 컨트롤러의 책임은 매개 변수 요청을 가져온 다음 하나 이상의 서비스 메소드를 호출하고 결과를 결합하여 클라이언트로 다시 전송하는 것입니다.
따라서 컨트롤러에는 요청 실행을 조정하는 기능이 있으며 도메인 데이터를 DTO와 같이 클라이언트가 사용할 수있는 형식으로 변환합니다.
비즈니스 로직은 서비스 계층에 있으며 지속성 계층은 데이터베이스에서 데이터를 앞뒤로 검색 / 저장합니다.
데이터베이스 트랜잭션의 범위는 기술 개념만큼이나 실제로 비즈니스 개념입니다. 계정 이체에서 계정은 다른 계정이 입금되는 경우에만 인출 될 수 있으므로 비즈니스 로직을 포함하는 서비스 계층 만이 실제로 은행 계좌 이체 거래의 범위.
지속성 계층은 어떤 트랜잭션이 있는지 알 수 없습니다 (예 : method) customerDao.saveAddress. 항상 별도의 트랜잭션으로 실행해야합니까? 알 수있는 방법이 없으며, 호출하는 비즈니스 로직에 따라 다릅니다. 때로는 별도의 트랜잭션에서 실행되어야하며 때로는 작동하는 경우에만 데이터를 저장해야합니다 saveCustomer.
같은 컨트롤러에 적용해야 saveCustomer하고 saveErrorMessages동일한 트랜잭션에서 이동? 고객을 저장하고 실패 할 경우 데이터베이스에 저장하려는 오류 메시지를 포함한 모든 것을 롤백하는 대신 일부 오류 메시지를 저장하고 적절한 오류 메시지를 클라이언트에 반환하려고 할 수 있습니다.
비 트랜잭션 컨트롤러에서 서비스 계층에서 반환되는 메서드는 세션이 닫히기 때문에 분리 된 엔터티를 반환합니다. 이것은 정상적인 현상입니다. 해결책은 OpenSessionInView컨트롤러가 필요로하는 결과를 가져 오는 쿼리를 사용 하거나 수행하는 것입니다.
하지만 컨트롤러를 트랜잭션으로 만드는 것은 범죄가 아니며 가장 자주 사용되는 관행이 아닙니다.
실제로 다양한 웹 프레임 워크 (JSP / Struts 1.x, GWT, JSF 2, Java EE 및 Spring 포함)를 사용하여 중대형 비즈니스 웹 애플리케이션에서 두 사례를 모두 보았습니다.
내 경험상 최고 수준, 즉 "컨트롤러"수준에서 트랜잭션을 구분하는 것이 가장 좋습니다.
한 경우에 우리는 Hibernate 세션 관리 ( 객체에 저장 됨 ), 트랜잭션 시작 / 커밋 / 롤백, 사용자 친화적 인 오류 메시지에 대한 예외 매핑 을 처리 하는 메소드를 구현하여 BaseActionStruts 의 클래스를 확장 하는 클래스를 가졌습니다 . 이 메소드는 예외가이 레벨까지 전파되거나 롤백 전용으로 표시된 경우 현재 트랜잭션을 단순히 롤백합니다. 그렇지 않으면 트랜잭션을 커밋합니다. 이것은 일반적으로 전체 HTTP 요청 / 응답주기에 대해 단일 데이터베이스 트랜잭션이있는 모든 경우에서 작동했습니다. 여러 트랜잭션이 필요한 드문 경우는 사용 사례 별 코드로 처리됩니다.Actionexecute(...)ThreadLocal
GWT-RPC의 경우 유사한 솔루션이 기본 GWT 서블릿 구현에 의해 구현되었습니다.
JSF 2에서는 지금까지 서비스 수준 경계 만 사용했습니다 (자동으로 "REQUIRED"트랜잭션 전파를 갖는 EJB 세션 빈 사용). 여기에는 JSF 백킹 빈 수준에서 트랜잭션을 구분하는 것과는 반대로 단점이 있습니다. 기본적으로 문제는 많은 경우에 JSF 컨트롤러가 각각 애플리케이션 데이터베이스에 액세스하는 여러 서비스 호출을 만들어야한다는 것입니다. 서비스 수준 트랜잭션의 경우 이는 여러 개의 개별 트랜잭션 (예외가 발생하지 않는 한 모두 커밋 됨)을 의미하며, 이는 데이터베이스 서버에 더 많은 부담을줍니다. 하지만 성능상의 단점이 아닙니다. 단일 요청 / 응답에 대해 여러 트랜잭션을 수행하면 미묘한 버그가 발생할 수도 있습니다 (더 이상 세부 사항을 기억하지 못합니다. 그런 문제가 발생했다는 것만).
이 질문에 대한 다른 답변은 "데이터베이스 / 비즈니스 트랜잭션의 범위를 식별하는 데 필요한 논리"에 대해 설명합니다. 일반적으로 트랜잭션 경계와 관련된 논리 가 전혀 없기 때문에이 주장은 나에게 의미가 없습니다 . 컨트롤러 클래스 나 서비스 클래스 모두 트랜잭션에 대해 실제로 "알 필요"가 없습니다. 대부분의 경우 웹 앱에서 각 비즈니스 작업은 HTTP 요청 / 응답 쌍 내에서 발생하며 트랜잭션 범위는 요청이 수신 된 시점부터 응답이 완료 될 때까지 실행되는 모든 개별 작업입니다.
간혹 비즈니스 서비스 또는 컨트롤러가 특정 방식으로 예외를 처리 한 다음 현재 트랜잭션을 롤백 전용으로 표시해야 할 수 있습니다. Java EE (JTA)에서는 UserTransaction # setRollbackOnly ()를 호출하여이를 수행합니다 . UserTransaction개체가 주입 될 수있는 @Resource필드, 또는 어떤 프로그램에서 얻은 ThreadLocal. 봄에서 @Transactional주석 롤백 특정 예외 유형을 지정할 수 있습니다, 또는 코드는 스레드 로컬 얻을 수 있습니다 된 TransactionStatus 와 전화를 setRollbackOnly().
따라서 제 생각과 경험상 컨트롤러를 트랜잭션으로 만드는 것이 더 나은 접근 방식입니다.
때로는 예외가 발생할 때 트랜잭션을 롤백하고 싶지만 동시에 예외를 처리하고 싶을 때 컨트롤러에서 적절한 응답을 생성합니다.
당신이 넣을 경우
@Transactional롤백을 적용하기 위해 컨트롤러 방법에있는 유일한 방법은 컨트롤러 방법에서 트랜잭션을 던져,하지만 당신은 정상 응답 객체를 반환 할 수 없습니다.
업데이트 : Rodério의 답변에 설명 된대로 프로그래밍 방식으로 롤백을 수행 할 수도 있습니다 .
더 나은 솔루션은 서비스 메서드를 트랜잭션으로 만든 다음 컨트롤러 메서드에서 가능한 예외를 처리하는 것입니다.
다음 예제는 createUser메서드가 있는 사용자 서비스를 보여줍니다. 이 메서드는 사용자를 만들고 사용자에게 이메일을 보냅니다. 메일 전송에 실패하면 사용자 생성을 롤백하려고합니다.
@Service
public class UserService {
@Transactional
public User createUser(Dto userDetails) {
// 1. create user and persist to DB
// 2. submit a confirmation mail
// -> might cause exception if mail server has an error
// return the user
}
}
그런 다음 컨트롤러에서 호출을 createUsertry / catch 로 래핑 하고 사용자에게 적절한 응답을 만들 수 있습니다.
@Controller
public class UserController {
@RequestMapping
public UserResultDto createUser (UserDto userDto) {
UserResultDto result = new UserResultDto();
try {
User user = userService.createUser(userDto);
// built result from user
} catch (Exception e) {
// transaction has already been rolled back.
result.message = "User could not be created " +
"because mail server caused error";
}
return result;
}
}
If you put a @Transaction on your controller method that is simply not possible.
'Program Club' 카테고리의 다른 글
| 창 스크롤 이벤트에 클래스 토글 바인딩 (0) | 2020.12.07 |
|---|---|
| 이제 Eclipse 프로젝트를 Android Studio로 어떻게 가져 오나요? (0) | 2020.12.07 |
| getResource ()를 사용하여 리소스 가져 오기 (0) | 2020.12.07 |
| Java에서 출력 매개 변수를 사용하는 방법은 무엇입니까? (0) | 2020.12.07 |
| git fetch 이해 후 병합 (0) | 2020.12.07 |