Spring ๐ŸŒฑ

๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐํ•˜๊ธฐ : synchronized์™€ saveAndFlush

z.zzz 2023. 12. 1. 13:17

synchronized

- synchronized๋ฅผ ๋ฉ”์†Œ๋“œ์— ๋ช…์‹œํ•ด์ฃผ๋ฉด ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

- ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ์Šค๋ ˆ๋“œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ์œ„ํ•ด ์ž๋ฐ”์—์„œ ์ œ๊ณตํ•˜๋Š” ํ‚ค์›Œ๋“œ๋‹ค.

- synchronized๋Š” ํ˜„์žฌ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๋‚˜๋จธ์ง€ ์Šค๋ ˆ๋“œ๋“ค์ด ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋„๋ก ๋ง‰์•„ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

 

 

๋ณธ๋ฌธ

์ •์›์ด n๋ช…์ธ ๋…์„œ ๋ชจ์ž„์ด ์žˆ๋‹ค. ํ˜„์žฌ ๋ชจ์ž„์›์€ n-1๋ช…์œผ๋กœ, ํ•œ ๋ช…๋งŒ ๋” ๊ฐ€์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋•Œ ๋‘ ๋ช…์˜ ์œ ์ €๊ฐ€ ๋™์‹œ์— ๊ฐ€์ž…์„ ์‹œ๋„ํ•˜๋ฉด ๋‘˜ ์ค‘ ํ•œ ๋ช…๋งŒ ๊ฐ€์ž…์ด ๋˜์–ด์•ผ ํ•œ๋‹ค. ๊ฐ€์ž… ์‹œ, ๋ชจ์ž„ ์ •์›์„ ํ™•์ธํ•˜์—ฌ ๋‚จ์€ ์ž๋ฆฌ๊ฐ€ ์—†๋‹ค๋ฉด ์ •์› ์ดˆ๊ณผ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

 

 

๋ชจ์ž„ ๊ฐ€์ž… ๊ธฐ๋Šฅ ์ „์ฒด ์ฝ”๋“œ

- Club.java : ๋…์„œ ๋ชจ์ž„ ์—”ํ‹ฐํ‹ฐ

- ClubService.java : ๋…์„œ ๋ชจ์ž„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ๋‹จ

- ClubServiceTest.java : ๋…์„œ ๋ชจ์ž„์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํ…Œ์ŠคํŠธ

๋”๋ณด๊ธฐ
Club.java
@Entity
public class Club {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "leader_id")
    private User leader;

    @OneToMany(mappedBy = "club")
    private final List<MemberRegister> memberRegisters = new ArrayList<>();

    @NotNull
    private String clubName;

    @NotNull
    private int capacity;

    @NotNull
    private int memberCount = 0;
    
    public void addMemberRegister(MemberRegister memberRegister) {
        memberRegisters.add(memberRegister);
        ++memberCount;
    }

}โ€‹

 

 

[Entity] ๋ชจ์ž„ ์ •์› ์ฒดํฌ ๋กœ์ง

public void checkCapacity() {
    if (memberCount + 1 > capacity) {
        throw new NotEnoughCapacityException("member count is over capacity");
    }
}

ํ˜„์žฌ ๋ฉค๋ฒ„ ์ˆ˜ + 1์ด ์ •์›๋ณด๋‹ค ํฌ๋ฉด, ์ •์› ์ดˆ๊ณผ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

 

 

[Test] ๋™์‹œ ๊ฐ€์ž… ์‹œ, ์ •์› ์ดˆ๊ณผ ์˜ˆ์™ธ ๋ฐœ์ƒ ํ…Œ์ŠคํŠธ

@Test
void joinParallel() throws InterruptedException {

    //given
    User leader = newUser("leader");
    User member1 = newUser("member1");
    User member2 = newUser("member2");
    List<User> members = new ArrayList<>(List.of(member1, member2));

    Long clubId = newClub(leader, 2);  //์ •์›์ด 2๋ช…์ธ ๋ชจ์ž„ ์ƒ์„ฑ, ํ˜„์žฌ ๋ฉค๋ฒ„ ์ˆ˜๋Š” 1

    //then
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CountDownLatch latch = new CountDownLatch(2);

    //when
    for (User member: members) {
        executorService.submit(() -> {
            try {
                clubService.join(new SessionUser(member), clubId);
            } catch (Exception e) {
                assertThat(e.getClass()).isEqualTo(NotEnoughCapacityException.class);
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();

    //then
    Club club = clubRepository.findById(clubId).get();
    assertThat(club.getMemberCount()).isEqualTo(2);
    List<MemberRegister> memberRegistersOfClub = memberRegisterRepository.findByClubId(club.getId());
    assertThat(memberRegistersOfClub.size()).isEqualTo();
}

๋‘ ๋ช…์˜ ์œ ์ €๊ฐ€ ๋™์‹œ ๊ฐ€์ž… ์‹œ, ์ •์› ์ดˆ๊ณผ ์˜ˆ์™ธ(NotEnoughCapacityException)๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์˜€๋‹ค. ๋‘˜ ์ค‘ ํ•œ ๋ช…๋งŒ ๊ฐ€์ž…๋˜์–ด ๋ชจ์ž„์˜ ๋ฉค๋ฒ„ ์ˆ˜๋Š” 2๋ช…์ด๊ธธ ๊ธฐ๋Œ€ํ–ˆ๋‹ค.

 

 

[Service] ๊ธฐ์กด join() ๋ฉ”์„œ๋“œ

@Transactional
public Long join(SessionUser sessionUser, Long id) {
    Club club = clubRepository.findById(id)
            .orElseThrow(IllegalArgumentException::new);

    club.checkCapacity();
    User user = findUser(sessionUser);
    MemberRegister memberRegister = MemberRegister.register(club, user);

    return memberRegisterRepository.save(memberRegister).getId();
}

synchronized ํ‚ค์›Œ๋“œ๊ฐ€ ์—†๋Š” ์ผ๋ฐ˜ ๋ฉ”์„œ๋“œ๋‹ค.

 

[Test] ๋ฐœ์ƒํ•œ ๋ฌธ์ œ

๋‘ ์œ ์ €๊ฐ€ ์ฐจ๋ก€๋Œ€๋กœ ๋ชจ์ž„์˜ ๋ฉค๋ฒ„ ์ˆ˜๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  ๋™์‹œ์— ์กฐํšŒ๋œ๋‹ค. ๊ธฐ์กด ๋ฉค๋ฒ„ ์ˆ˜(memberCount)๊ฐ€ 1, 2 ์ˆœ์œผ๋กœ ์กฐํšŒ๋˜๊ธธ ๊ธฐ๋Œ€ํ–ˆ์œผ๋‚˜ 1, 1๋กœ ์กฐํšŒ๋˜๋ฉฐ Race Condition์ด ๋ฐœ์ƒํ–ˆ๋‹ค. ์ •์› ์ดˆ๊ณผ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„ ๋‘ ์œ ์ € ๋ชจ๋‘ ๊ฐ€์ž…๋˜๋ฉฐ ๋ชจ์ž„์˜ ์ด ๋ฉค๋ฒ„ ์ˆ˜๋Š” 3๋ช…์ด ๋˜์–ด ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ–ˆ๋‹ค. 

 

 

[Service] ์ˆ˜์ •๋œ join() ๋ฉ”์„œ๋“œ

@Service
public class ClubService {

    private final UserRepository userRepository;
    private final ClubRepository clubRepository;
    private final MemberRegisterRepository memberRegisterRepository;

    @Transactional
    public synchronized Long join(SessionUser sessionUser, Long id) {
        Club club = clubRepository.findById(id)
                .orElseThrow(IllegalArgumentException::new);

        club.checkCapacity();
        User user = findUser(sessionUser);
        MemberRegister memberRegister = MemberRegister.register(club, user);

        return memberRegisterRepository.saveAndFlush(memberRegister).getId();
    }
}

๋ฉ”์„œ๋“œ์— synchronized ํ‚ค์›Œ๋“œ๋ฅผ ๋ถ™์—ฌ ์Šค๋ ˆ๋“œ๋“ค์ด ์ˆœ์ฐจ์ ์œผ๋กœ join ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  memberRegister๋ฅผ ๋ ˆํฌ์ง€ํ† ๋ฆฌ์— ์ €์žฅํ•  ๋• save()๊ฐ€ ์•„๋‹Œ saveAndFlush() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 

save ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ”๋กœ flush๋˜์ง€ ์•Š์•„ synchronized๋ฅผ ์ด์šฉํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ๋‹ค. ์ด๋Š” @Transactional์˜ ๋™์ž‘ ๋ฐฉ์‹ ๋•Œ๋ฌธ์ธ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฐ’์ด ์ž…๋ ฅ๋˜๊ธฐ ์ „ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ๋ฉ”์†Œ๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ saveAndFlush ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ”๋กœ flush๋˜๊ฒŒ ํ•จ์œผ๋กœ์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ด์ฒ˜๋Ÿผ ์ปค๋ฐ‹ ์ „ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋‚˜์ค‘์— ์ฝ์–ด์•ผํ•ด์„œ ์ฆ‰๊ฐ ๋ฐ˜์˜์ด ๋˜์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, saveAndFlush๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. (์Šค๋ ˆ๋“œ 1์— ์˜ํ•ด ๋ฉค๋ฒ„ ํ•œ ๋ช…์ด ๊ฐ€์ž…ํ•˜๋ฉด memberCount๊ฐ€ 1 ์ฆ๊ฐ€ํ•œ๋‹ค. ์Šค๋ ˆ๋“œ 2๋Š” ์ฆ๊ฐ€๋œ memberCount๋ฅผ ์ฝ์–ด์•ผ ํ•œ๋‹ค.)