티스토리 뷰

 

프로젝트를 진행하다 OSIV라는 단어를 듣게 되었다. 그럼 트랜잭션과 DB connection 간의 상관관계는 어떻게 되지?라는 의문이 들어 이참에 각 개념들을 정리하고자 한다.




먼저 트랜잭션의 정의를 먼저 보고 가자.

하나의 작업을 수행하기 위해 필요한 데이터베이스 연산들을 모아 놓은 것으로, 데이터베이스에서 논리적인 작업의 단위가 된다.

 

만약 A가 B에게 자신의 통장에서 1만원의 금액을 송금한다면 SQL 상 A의 계좌에서 1만원이 마이너스되고, B의 계좌에는 1만원이 플러스되는 작업이 모두 수행되어야 한다. 만약 A의 계좌에서만 1만원이 빠져나가고 B의 계좌에는 1만원이 들어오지 않는다면 이는 데이터베이스의 무결성과 일관성이 깨지게 되는 결과를 낳는다. 따라서 각 2개의 SQL을 하나의 작업 단위로 보고 2개의 SQL이 모두 수행되거나 모두 수행되지 않도록 해야 하는데, 이 하나의 작업 단위가 바로 트랜잭션인 것이다.

 

스프링에서는 @Transactional 어노테이션을 사용하여 트랜잭션을 보다 간단하게 정의할 수 있다. (선언적 트랜잭션 관리)

@Transactional 
public void forward(Long from, Long to, Long amount) {
    Account accountA = accountRepository.findById(from).get();
    Account accountB = accountRepository.findById(to).get();
    accountA.minus(amount);
    accountB.plus(amount); 
}




다음으로 DB connection의 정의를 살펴보자.

클라이언트 소프트웨어가 데이터베이스 서버 소프트웨어와 통신할 수 있도록 하는 기능이다. 일부 DBMS 엔진은 연결하는 데 상당한 시간이 필요하기 때문에 성능을 향상을 위해 connenction pooling 방식을 사용하기도 한다.

 

간단하게 말해 DB connection은 DB와 애플리케이션 간 통신을 하기 위한 수단이다. 즉 우리가 개발한 스프링 어플리케이션이 DB와 통신하기 위해서는 DB connection을 획득해야 한다. 스프링의 DB connection에 대한 보다 자세한 내용은 나중에 포스팅하기로 하고, 그렇다면 스프링에서는 언제 DB connection을 얻게 되는 것일까?

 

스프링에서는 DB connection을 설정하는데 많은 시간이 걸려 성능이 떨어지는 것을 방지하기 위해 connection pooling 방식을 기반으로 DB connection을 제공한다. (어플리케이션 시작 시 Hikari pool에 다수의 DB connection을 미리 생성해놓음) 따라서 @Transactional 어노테이션이 붙어있는 메소드를 호출하면 Hikari pool 내에 미리 설정되었던 DB connection 중 하나를 얻게 된다.




다음으로 OSIV에 대해서도 간단하게 알아보자.

 

OSIV는 Open Session In View의 줄임말로, 영속성 컨텍스트를 view 즉 presentation 계층까지 열어두는 기능이다. view 단까지 영속성 컨텍스트가 열려있기 때문에 조회한 엔티티 또한 영속상태로 유지되며 따라서 view 계층에서 지연로딩을 사용할 수 있다. 스프링에서 해당 기능은 기본적으로 on되어 있으며 해당 기능을 끄기 위해서는 다음과 같이 설정해주면 된다.

spring.jpa.open-in-view=false




OSIV가 true일 때 Transaction, DB connection, 영속성 컨텍스트 간의 관계

OSIV true

1. 클라이언트의 요청이 들어오면 서블릿 필터나, 스프링 인터셉터에서 영속성 컨텍스트를 생성 (단, 트랜잭션은 시작하지 않음)

2. 서비스 계층에서 @Transactional로 트랜잭션을 시작할 때 1번에서 미리 생성해둔 영속성 컨텍스트를 찾아와서 트랜잭션을 시작

3. 영속성 컨텍스트는 내부적으로 DB connection pool을 사용해서 DB에 접근

4. 트랜잭션 종료(commit) 후 응답이 반환될 때까지 영속성 컨텍스트(DB connection)는 살아있음

 

OSIV가 ON되어 있기 때문에 presentation 계층까지 영속성 컨텍스트가 살아있다. 즉 DB connection이 controller까지 유지되는 것이다. 따라서 controller에서 조회해온 엔티티에 대한 lazy loading이 가능하다. 그러나 트랜잭션은 이미 종료되었기 때문에 해당 엔티티에 대한 CUD 작업은 불가능하고 R만 가능하다.




OSIV가 false일 때 Transaction, DB connection, 영속성 컨텍스트 간의 관계

OSIV false

1. 서비스 계층의 @Transactional가 붙은 메소드 호출 시 영속성 컨텍스트가 생성되고 트랜잭션을 시작

2. 영속성 컨텍스트는 내부적으로 DB connection pool을 사용해서 DB에 접근

3. 트랜잭션 종료(commit) 후 영속성 컨텍스트(DB connection)도 함께 닫힘

 

OSIV가 OFF되어 있기 때문에 영속성 컨텍스트는 트랜잭션이 종료됨에 따라 닫히게 된다. 즉 DB connection 또한 닫하게 되는 것이다. 따라서 controller에서 조회해온 엔티티는 준영속 상태가 되며 DB에 대한 더이상의 쿼리는 불가능하기 때문에 lazy loading 또한 불가능하다.




백문이불여일견이라고 정말 위와 같이 동작하는지 코드롤 통해 알아보자.

 

간단한 예제로 1대N 관계인 Team - Member 엔티티를 다음과 같이 정의하였다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Team {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "team_id")
    private Long id;

    @Column(nullable = false)
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(nullable = false)
    private Integer age;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}



Member 엔티티를 해당 id로 조회하는 로직으로 MemberController, MemberService에서 EntityManager를 주입받아 각 계층에서 영속성 컨텍스트가 해당 엔티티를 관리하고 있는지 확인해보았다.

@RequiredArgsConstructor
@RestController
public class MemberController {

    private final EntityManager em;
    private final MemberService memberService;

    @GetMapping("/members/{memberId}")
    public MemberDto getMember(@PathVariable Long memberId) {
        Member member = memberService.getMember(memberId);
        Team team = member.getTeam();
        System.out.println("controller em.contains(member) = " + em.contains(member));
        System.out.println("controller em.contains(team) = " + em.contains(team));
        return MemberDto.builder()
                .memberId(member.getId())
                .memberAge(member.getAge())
                .memberName(member.getName())
                .teamId(team.getId())
                .teamName(team.getName())
                .build();
    }
}
@RequiredArgsConstructor
@Service
@Transactional(readOnly = true)
public class MemberService {

    private final EntityManager em;
    private final MemberRepository memberRepository;

    public Member getMember(Long memberId) {
        Member member = memberRepository.findById(memberId).orElseThrow(RuntimeException::new);
        Team team = member.getTeam();
        System.out.println("service em.contains(member) = " + em.contains(member));
        System.out.println("service em.contains(team) = " + em.contains(team));
        return member;
    }
}



OSIV를 ON했을 때의 출력결과는 다음과 같으며 controller에서 Team에 대한 lazy loading이 발생하여 추가 쿼리가 발생하였다. 아래 결과를 통해 영속성 컨텍스트가 controller까지 살아있어 Team, Member가 영속상태로 유지되는 것을 알 수 있다.

Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.team_id as team_id4_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?

service em.contains(member) = true
service em.contains(team) = true
controller em.contains(member) = true
controller em.contains(team) = true

Hibernate: 
    select
        team0_.team_id as team_id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?



OSIV를 OFF했을 때의 출력결과는 다음과 같으며 controller에서 Team, Member는 더이상 영속성 컨텍스트에서 관리되지 않기 때문에 Team에 대한 lazy loading은 불가능하여 LazyInitializationException 오류가 발생한다. 오류 메시지를 읽어보면 Session(영속성 컨텍스트)이 없어 proxy 객체(Team 객체)를 초기화할 수 없다는 것을 알 수 있다.

Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.team_id as team_id4_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?

service em.contains(member) = true
service em.contains(team) = true
controller em.contains(member) = false
controller em.contains(team) = false

throw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: 
    could not initialize proxy [com.example.demo.domain.entity.Team#1] - no Session] with root cause



controller에서 같은 엔티티를 service를 통해 2번 조회했을 때 OSIV에 따른 극명한 차이를 볼 수 있다. controller의 getMember 메소드를 다음과 같이 변경하고 결과를 보자.

@GetMapping("/members/{memberId}")
public void getMember(@PathVariable Long memberId) {
    Member member = memberService.getMember(memberId);
    Member newMember = memberService.getMember(memberId);
    System.out.println("(member == newMember) = " + (member == newMember));
}



OSIV를 OFF했을 때 첫번째로 조회해온 Member 엔티티가 영속성 컨텍스트에서 관리되지 않기 때문에 다음과 같이 조회 쿼리가 2번 나가는 것을 알 수 있다. member, newMember 객체는 각기 다른 영속성 컨텍스트를 통해 조회해온 것이기 때문에 다른 참조를 가진다.

Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.team_id as team_id4_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?
Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.team_id as team_id4_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?

(member == newMember) = false



OSIV를 ON했을 때에는 첫번째로 조회해온 Member 엔티티가 영속성 컨텍스트에서 관리되기 때문에 두번째 조회는 DB가 아닌 영속성 컨텍스트에서 조회 결과를 가져온다. 따라서 다음과 같이 조회 쿼리가 한번만 나가고 member, newMember 모두 같은 영속성 컨텍스트를 통해 조회해온 것이기 때문에 같은 참조를 가진다.

Hibernate: 
    select
        member0_.member_id as member_i1_0_0_,
        member0_.age as age2_0_0_,
        member0_.name as name3_0_0_,
        member0_.team_id as team_id4_0_0_ 
    from
        member member0_ 
    where
        member0_.member_id=?

(member == newMember) = true



OSIV를 ON하면 controller 계층에서도 영속성 컨텍스트는 열려있지만 트랜잭션은 종료되었기 때문에 영속성 컨텍스트에서 관리되고 있는 엔티티에 변경을 가해도 dirty checking 등의 CUD 작업은 발생하지 않는다. 실제로 조회해온 Member에 대해 setName 메소드를 호출하여 name 필드를 변경하면 영속성 컨텍스트에서 관리되는 Member의 값은 변경되어도 실제 DB에 update 쿼리는 날라가지 않는다.

@GetMapping("/members/{memberId}")
public void getMember(@PathVariable Long memberId) {
    Member member = memberService.getMember(memberId);
    member.setName("Ethan"); 
}




각 설정의 장단점

OSIV ON

장점: 지연로딩 사용 가능, 하나의 요청에 대해 동일한 영속성 컨텍스트 공유

단점: 실시간 트래픽이 많은 어플리케이션의 경우, 요청당 DB connection을 오래 잡고 있기 때문에 성능 하락

 

OSIV OFF

장점: 요청당 DB connection을 보다 짧게 잡고 있어 성능 향상

단점: controller에서 lazy loading을 사용하지 못해 트랜잭션 안에서 관련된 엔티티 모두 조회해야 함 → 응답값에 따라 조회해야 하는(lazy loading해야 하는) 엔티티가 달라지기 때문에 service 계층에서 controller 계층의 로직에 의존하게 됨 (해결방법: Facade 계층 생성)




OSIV에 따른 차이는 트랜잭션이 commit된 후에도 영속성 컨텍스트(DB connection)가 유지되느냐 닫히느냐의 차이로 간단히 정의할 수 있다. 보통 Admin 어플리케이션의 경우 트래픽이 많지 않기 때문에 OSIV를 ON해도 되지만 클라이트 어플리케이션이 경우에는 OSIV를 OFF하는 것이 일반적이다.

다음 포스팅으로는 service 계층에서 @Transactional(readOnly = true), @Transactional, @Transactional 붙이지 않음 간의 차이에 대해 자세히 알아보려고 한다.




끝.




공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함