일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 캐싱
- 협력
- 쿼리 최적화
- 재사용성
- 클린코드
- 인터프리터
- 도메인 모델
- 추상화
- 객체지향의 사실과 오해
- 캐시
- spring boot
- JPA
- 자바
- 책임
- JIT
- Lombok
- 스프링
- Refactor
- SRP
- 클린 코드
- 스프링부트
- Java
- 캡슐화
- 객체
- clean code
- cache
- 리팩토링
- 객체지향
- REST API
- string
- Today
- Total
GO SIWOO!
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (10) - OSIV 설정 본문
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (9) - 조회 쿼리 성능 개선
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (8) - JDBC Batch INSERT [리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (7) - Ehcache 3 사용 [리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통
gosiwoo.tistory.com
📌들어가며
이전 포스팅에서 Members 테이블에 유니크 인덱스, 읽기전용 트랜잭션을 사용하며 조회 쿼리 성능을 향상한 과정에 대해서 설명했습니다.
이번 포스팅에서 OSIV와 DTO를 프로젝트에 적용해본 과정을 간단하게 포스팅 하겠습니다.
📌OSIV 란?
OSIV는 Open Session In View의 약자로 영속성 컨텍스트를 뷰 랜더링이 끝날 때까지 유지하게 도와줍니다. JPA에서는 Open EntityManager In View가 정식 명칭이라 하지만 관례상 Hibernate에서 사용하는 OSIV용어로 불리고 있습니다.
초창기의 OSIV
위 사진은 초창기 OSIV의 모습입니다. 하지만 여기서는 다음과 같은 문제가 있습니다. 만약 회원의 민감한 정보를 수정해 클라이언트에게 요청을 반환해야 한다고 가정할 때, Cotroller에서 뷰를 렌더링한 후 트랜잭션을 커밋하기에 dirty check가 동작하여 변경된 회원의 민감한 정보를 그대로 DB에 반영할 위험이 있습니다.
스프링 OSIV
위 사진은 스프링 OSIV의 모습입니다.
OSIV의 default는 true로 설정되어 있습니다. 위처럼 OSIV 의 설정을 따로 제어하지 않을 경우에는 영속성 컨텍스트의 생존 범위가 요청에 전반적으로 유지됩니다. 따라서 만약 Controller 단에서 Lazy-loading을 통해 연관관계에 있는 Entity를 조회했을 경우에 추가적인 쿼리와 함께 Entity의 정보를 조회를 가능합니다.(수정은 초기의 OSIV에서만 가능)
스프링 OSIV의 문제?
뷰 랜더링을 유지하면 요청 전반 어느 클래스에서도 Entity를 조회할 수 있어 좋아보이지만 하나의 고려사항이 있습니다.
영속성 컨텍스트를 유지한다는 것은 기본적으로 DB의 커넥션을 유지하고 있기에 실시간 트래픽이 증가하게 된다면 커넥션을 더이상 하지 못할 수 있다는 문제가 발생합니다.
또한 같은 영속성 컨텍스트를 여러 트랜잭션이 공유할 수 있어 주의가 필요합니다. Controller, 즉 프레젠테이션 계층에서 엔티티를 수정한 후 트랜잭션을 시작하는 서비스 계층의 로직을 실행하면 문제가 발생하므로 서비스 로직 실행 후 Entity 수정을 권장합니다.
따라서 사용자에게 제공하는 API의 경우에는 OSIV 옵션을 끄는것을 권유드립니다.
스프링 OSIV의 동작원리(true)
- 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성합니다 (트랜잭션 시작하지 않음)
- 서비스 계층에서 @Trasnactional로 트랜잭션을 시작할 때 1번에서 미리 생성한 영속성 컨텍스트를 찾아와서 트랜잭션을 시작합니다
- 서비스 계층이 끝나면 트랜잭션을 커밋하고 영속성 컨텍스트를 플러시합니다. 트랜잭션을 끝나지만 영속성 컨텍스트는 종료하지 않습니다
- 컨트롤러와 뷰까지 영속성 컨텍스트가 유지되므로 조회한 엔티티는 영속 상태를 유지합니다(OSIV=true)
- 서블릿 필터나, 스프링 인터셉터로 요청이 돌아오면 영속성 컨텍스트를 종료합니다. 이때 플러시를 호출하지 않고 바로 종료합니다
OSIV 옵션을 끈다면 어디까지 영속성 컨텍스트를 유지할까?
위 사진처럼 OSIV의 옵션을 끈다면 영속성 컨텍스트의 생존 범위가 Service와 Repository 단에서 종료되는 것을 보실 수 있습니다.
해당 프로젝트에서는 어떻게?
앞서 본것과 같이 OSIV는 지연 로딩을 적극적으로 사용할 수 있게 영속성 컨텍스트를 요청 전반적으로 유지해 준다는 장점이 있지만 API 응답이 길어지면 트래픽 많아짐에 따라 DB 커넥션이 말라버릴 수 있는 문제가 있습니다.
따라서 해당 프로젝트에서는 OSIV 옵션을 사용하지 않기로(끄기로) 하였습니다.
OSIV 옵션을 사용하지 않는데 고려한 사항
OSIV옵션을 사용하지 않는다면 Controller 계층에서 Lazy-loading이 불가합니다.
해당 프로젝트는 Service 단에서 응답 DTO로 바꾸어 Controller로 전달하도록 작성했습니다.
// ResponseDTO
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class JoinResponse {
private String msg;
private Long id;
private String username;
}
// Service
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class MemberService {
...
@Transactional
public JoinResponse join(String username, String password) {
// username 중복 체크
memberRepository.findByUsername(username)
.ifPresent(user -> {
throw new AppException(ErrorCode.USERNAME_DUPLICATED, username + MEMBER_JOIN_ALREADY_FAIL);
});
Member createdMember = Member.builder()
.username(username)
.password(encoder.encode(password))
.build();
memberRepository.save(createdMember);
return new JoinResponse(MEMBER_JOIN_SUCCESS, createdMember.getId(), createdMember.getUsername(), createdMember.getPassword());
}
...
}
// Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/member")
public class MemberController {
private final MemberService memberService;
private final JwtProvider jwtProvider;
@PostMapping
public ResponseEntity<EntityModel<JoinResponse>> join(@RequestBody @Valid MemberJoinRequest request) {
JoinResponse response = memberService.join(request.getUsername(), request.getPassword());
URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(response.getId())
.toUri();
return ResponseEntity.created(uri).body(toModelJoinResponse(response));
}
...
}
위는 회원가입 요청에 응답을 하는 Response DTO, Service, Controller 부분입니다. 클라이언트의 요청은 JoinReponse 라는 DTO로 Service 내에서 전부 처리됩니다. 따라서 Controller 단에서는 HTTP 상태 코드 처리와 HATEOAS 설정밖에 없습니다. 위의 회원가입 로직에서는 없지만 만약 Lazy-loading이 있다면 Service 메서드 내에서 모두 처리되어 DTO로 가공됩니다. 따라서 다음과 같은 옵션을 application.yml 파일에 작성하여 OSIV 옵션을 사용하지 않았습니다.
spring:
jpa:
open-in-view: false
📌결론
- 스프링 OSIV는 기존의 OSIV의 프레젠테이션 계층의 Entity 수정 후 flush()를 통한 문제를 막을 수 있다
- OSIV 옵션은 기본적으로 true이다.
- OSIV 옵션은 Lazy-loading을 한 요청에서 프로젝트 전반적인 Lazy-loading을 유연하게 할 수 있다
- OSIV 옵션은 DB 커넥션이 말라버릴 수 있으므로 주의한다 (DB 커넥션과 영속성 컨텍스트의 생명주기는 같음)
- 실시간 유저 트래픽이 많은 API는 OSIV 옵션을 꺼서 불필요하게 커넥션 풀을 낭비하지 않는다
- CQRS 패턴으로 로직과 쿼리를 분리해 OSIV를 꺼서 복잡성을 해결 가능하다
📌포스팅 과정에서 참고한 글들
[Spring] 스프링 JPA의 OSIV 전략 - 트랜잭션, 영속성 컨텍스트 생명주기 (tistory.com)
OSIV(Open Session in view)이란? 장단점, 써야할지 말아야할지 (tistory.com)
📌Github
GitHub - siwookim97/kotudy-refactor: 📌한국어학습 웹 어플리케이션 백엔드 RESTful API 리팩토링 Repo
📌한국어학습 웹 어플리케이션 백엔드 RESTful API 리팩토링 Repo. Contribute to siwookim97/kotudy-refactor development by creating an account on GitHub.
github.com
'Develop > 팀 프로젝트, 나홀로 리팩토링' 카테고리의 다른 글
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (9) - 조회 쿼리 성능 개선 (1) | 2023.10.19 |
---|---|
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (8) - JDBC Batch INSERT (0) | 2023.10.18 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (7) - Ehcache 3를 통한 Open API 호출 서비스 응답 속도 향상 (1) | 2023.09.15 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (6) - Redis를 통한 Open API 결과 캐싱(Caching) (0) | 2023.09.06 |
[리팩토링 일지] 팀 프로젝트, 나홀로 리팩토링 (5) - 응답 form & Global Exception (0) | 2023.08.17 |