DevLog ๐Ÿ˜ถ

๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด์ž! ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„ํ•˜๊ธฐ (๋„ค์ž„๋“œ๋ฝ - Named Lock ํ™œ์šฉํ•˜๊ธฐ) ๋ณธ๋ฌธ

๊ฐœ๋ฐœ์ผ์ง€

๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด๋ณด์ž! ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„ํ•˜๊ธฐ (๋„ค์ž„๋“œ๋ฝ - Named Lock ํ™œ์šฉํ•˜๊ธฐ)

dolmeng2 2023. 6. 23. 17:39

๐ŸŒฑ ๋“ค์–ด๊ฐ€๊ธฐ ์ „

DB ๊ณต๋ถ€ํ•˜๋‹ค๊ฐ€ '๋„ค์ž„๋“œ๋ฝ'์— ๋Œ€ํ•ด์„œ ์•Œ๊ฒŒ ๋˜์—ˆ๋Š”๋ฐ, ๋„ค์ž„๋“œ๋ฝ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ธ€์„ ๋ณด๊ณ  ํ•œ ๋ฒˆ ํ…Œ์ŠคํŠธํ•ด๋ณด๊ณ  ์‹ถ์–ด์„œ ๊ธ€์„ ์ž‘์„ฑํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค. ์ „์ฒด ์†Œ์Šค์ฝ”๋“œ๋Š” ์—ฌ๊ธฐ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•˜๋‹ค. (๋ญ”๊ฐ€ ํ…Œ์ŠคํŠธ์šฉ ๋ ˆํฌ ๋งŒ๋“ค๊ธฐ ์• ๋งคํ•ด์„œ ๊ทธ๋ƒฅ ์•ˆ ์“ฐ๋Š” ๋ ˆํฌ์—๋‹ค๊ฐ€ ํ•˜๋ ค๋‹ค ๋ณด๋‹ˆ ์ฝ”ํ‹€๋ฆฐ์œผ๋กœ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.)

 


 

๐ŸŒฑ ๋ถ„์‚ฐ๋ฝ์ด๋ž€?

๋ถ„์‚ฐ๋ฝ์ด๋ž€ ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ ๊ณต์œ  ์ž์›์— ์ ‘๊ทผํ•  ๋•Œ, ๋ฐ์ดํ„ฐ์˜ ์ •ํ•ฉ์„ฑ์„ ์ง€ํ‚ค๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค.

์—ฌ๊ธฐ์„œ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๊ณต์œ  ์ž์›์„ ์ ‘๊ทผํ•˜๋ฉฐ ๊ฒฝ์Ÿํ•˜๋Š” ์ƒํ™ฉ์„ 'Race Condition (๊ฒฝ์Ÿ ์ƒํƒœ)'๋ผ๊ณ ๋„ ๋ถ€๋ฅด๋ฉฐ, ์ž๋ฐ”์—์„œ๋Š” 'synchronized'๋ผ๋Š” ํ‚ค์›Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๋™๊ธฐํ™” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ํ•˜์ง€๋งŒ, ์Šคํ”„๋ง ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ™˜๊ฒฝ์—์„œ ๊ทœ๋ชจ๊ฐ€ ์ปค์ง„๋‹ค๋ฉด ์„œ๋ฒ„ ์—ญ์‹œ ์—ฌ๋Ÿฌ ๋Œ€๋กœ ๋„์šธ ํ™•๋ฅ ์ด ๋†’๋‹ค. ์ด๋Ÿฌํ•œ ๋‹ค์ค‘ ์„œ๋ฒ„์—์„œ๋Š” synchronized ๋งŒ์œผ๋กœ๋Š” ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”๋ฐ, ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ๋ฒ•๋“ค์„ ํ†ตํ•ด์„œ ์ด๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค. 

 


 

๐ŸŒฑ ๋„๋ฉ”์ธ ์„ค๊ณ„

์‹ค์‹œ๊ฐ„ ํŠธ๋ž˜ํ”ฝ์ด ๋ชฐ๋ฆฌ๋Š” ํ™˜๊ฒฝ์— ๋Œ€ํ•ด์„œ ๊ณ ๋ฏผํ•ด๋ดค๋Š”๋ฐ, '์˜จ๋ผ์ธ ํ‹ฐ์ผ“ํŒ… ์„œ๋น„์Šค'๊ฐ€ ๋”ฑ ๋– ์˜ฌ๋ž๋‹ค.

๊ทธ๋ž˜์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ ํŠน์ • ์ฝ˜์„œํŠธ์— ๋Œ€ํ•ด์„œ ํ‹ฐ์ผ“์„ ๋ฐœํ–‰ํ•ด์ฃผ๋Š” ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

๋„๋ฉ”์ธ์ด๋ผ๊ณ  ํ•  ๊ฒƒ๋„ ์—†์ง€๋งŒ, ๋ถ„์‚ฐ๋ฝ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด์„œ ์ตœ๋Œ€ํ•œ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์„ค๊ณ„ํ•˜์˜€๋‹ค.

์ฝ˜์„œํŠธ์™€ ํ‹ฐ์ผ“์€ 1:N ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ , ์ฝ˜์„œํŠธ๋Š” ๋ช‡ ์ขŒ์„๊นŒ์ง€ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ๊ฐœ์ˆ˜๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋‹ค. (ticketLimit)

 

@Entity
class Concert(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val name: String,

    @Column(nullable = false)
    val ticketLimit: Int,

    concertTickets: MutableList<ConcertTicket> = Collections.emptyList()
) {

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "concert")
    val concertTickets: MutableList<ConcertTicket> = concertTickets.toMutableList()

    fun isFull(): Boolean {
        return concertTickets.size >= ticketLimit
    }
}

์ฝ˜์„œํŠธ์—๋Š” ์ •์›์ด ๊ฐ€๋“ ์ฐผ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ปค์Šคํ…€ ๋ฉ”์„œ๋“œ๋งŒ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ถ”๊ฐ€ํ•ด์ฃผ์—ˆ๋‹ค.

 

@Entity
class ConcertTicket(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,

    @Column(nullable = false)
    val userId: Long,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "concert_id",
        foreignKey = ForeignKey(name = "fk_ticket_concert_id")
    )
    val concert: Concert
)

์ด๊ฑฐ๋Š” ์ฝ˜์„œํŠธ์— ๋Œ€ํ•œ ํ‹ฐ์ผ“ ๋„๋ฉ”์ธ์ด๋‹ค.

 

CREATE TABLE `concert_ticket` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint NOT NULL,
  `concert_id` bigint NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_ticket_concert_id` (`concert_id`),
  CONSTRAINT `fk_ticket_concert_id` FOREIGN KEY (`concert_id`) REFERENCES `concert` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3
CREATE TABLE `concert` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `ticket_limit` int NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3

ํ…Œ์ด๋ธ”์€ ์œ„์™€ ๊ฐ™์ด JPA๊ฐ€ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ๋‹ค.

 


 

๐ŸŒฑ ์ฝ”๋“œ ์„ค๊ณ„

๐Ÿ’ฌ Repository

fun ConcertRepository.getConcertById(id: Long): Concert {
    return findConcertById(id) ?: throw NotFoundException("์ฝ˜์„œํŠธ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
}

interface ConcertRepository : JpaRepository<Concert, Long> {
    fun findConcertById(id: Long): Concert?
}
fun ConcertTicketRepository.getConcertTicketById(id: Long): ConcertTicket {
    return findConcertTicketById(id) ?: throw NotFoundException("์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์ •๋ณด๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
}

interface ConcertTicketRepository : JpaRepository<ConcertTicket, Long> {
    fun findConcertTicketById(id: Long): ConcertTicket?
}

 

๐Ÿ’ฌ Service

@Service
@Transactional(readOnly = true)
class ConcertService(
    private val concertRepository: ConcertRepository,
    private val concertTicketRepository: ConcertTicketRepository
) {
    ...
    
    @Transactional
    fun createConcertTicket(
        concertId: Long,
        concertTicketCreateRequest: ConcertTicketCreateRequest
    ): Long {
        val concert = concertRepository.getConcertById(concertId)
        if (concert.isFull()) {
            throw ConcertFullException("์ •์›์ด ๊ฐ€๋“ ์ฐผ์Šต๋‹ˆ๋‹ค.")
        }
        val concertTicket =
            ConcertTicket(userId = concertTicketCreateRequest.userId, concert = concert)
        val savedConcertTicket = concertTicketRepository.save(concertTicket)
        return savedConcertTicket.id
    }

    ...
}

 

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•œ ๋ฉ”์„œ๋“œ๋Š” ํ‹ฐ์ผ“์— ๋Œ€ํ•œ ์ƒ์„ฑ ๋ฉ”์„œ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ถ€๋ถ„๋งŒ ์ฃผ๋ชฉํ•˜์ž.

์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ์ƒ์„ฑ ์‹œ ์ •์›์ด ๊ฐ€๋“ ์ฐจ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค. ๋งŒ์•ฝ ์š”์ฒญ์ด ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ์˜ค๋‹ค๊ฐ€ ์ •์›์ด ๊ฐ€๋“์ฐจ๋ฉด ๋” ์ด์ƒ insert๋Š” ์ง„ํ–‰๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋‹ค.

 

 

๐Ÿ’ฌ Controller

@RestController
@RequestMapping("/concerts")
class ConcertController(
    private val concertService: ConcertService
) {
    ...

    @PostMapping("/tickets/{concertId}")
    fun createConcertTicket(
        @PathVariable("concertId") concertId: Long,
        @RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
    ) {
        concertService.createConcertTicket(concertId, concertTicketCreateRequest)
    }

}

 

ํ‹ฐ์ผ“์„ ์ƒ์„ฑํ•˜๋Š” API์ด๋‹ค. ์ฝ˜์„œํŠธ๋‚˜ ํ‹ฐ์ผ“ ์กฐํšŒ ๊ฐ™์€ ๊ฐ„๋‹จํ•œ API๋Š” ์ „์ฒด ์†Œ์Šค์ฝ”๋“œ์— ๋‚จ๊ฒจ๋‘์—ˆ๋‹ค.

 


 

๐ŸŒฑ JMeter ํ…Œ์ŠคํŠธ

๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์–ด๋–ป๊ฒŒ ์ง„ํ–‰ํ• ๊นŒ ๊ณ ๋ฏผํ–ˆ๋Š”๋ฐ, JMeter๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์„œ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค.

java -jar -Dserver.port=8080 ticketService.jar
java -jar -Dserver.port=8081 ticketService.jar

๋จผ์ €, ๋ถ„์‚ฐ๋œ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด 8080, 8081 2๊ฐœ์˜ ํฌํŠธ๋ฅผ ๋„์›Œ์„œ ์ง„ํ–‰ํ•˜์˜€๋‹ค.

 

์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋กœ ์ฝ˜์„œํŠธ 1๋ฒˆ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฏธ๋ฆฌ ์‚ฝ์ž…ํ•ด๋‘์—ˆ๋‹ค.

ํ•ด๋‹น ์ฝ˜์„œํŠธ์˜ ์ œํ•œ ์ธ์›์€ 100๋ช…์œผ๋กœ, ์˜ˆ์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค๋ผ๋ฉด 100๋ช… ์ด์ƒ์ด ์ž๋ฆฌ์žก์•˜์„ ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค.

 

200๊ฐœ์˜ ์Šค๋ ˆ๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ 2๊ฐœ์˜ ํ”„๋กœ์„ธ์Šค์—์„œ ๋™์‹œ์— ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ๋ฐœํ–‰ ์š”์ฒญ์„ ๋‚ ๋ ธ๋‹ค.

select count(*) from concert_ticket;

๊ทธ ๊ฒฐ๊ณผ, 100๋ช…๊นŒ์ง€๋งŒ ๋ฐ›์•„์•ผ ํ•˜๋Š”๋ฐ๋„ ๋ถˆ๊ตฌํ•˜๊ณ  111๊ฐœ์˜ ๊ฐ’์ด ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์™œ ์ด๋Ÿฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ผ๊นŒ?

์—ฌ๋Ÿฌ ๊ฐœ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ์กฐํšŒํ•˜๋Š” ์‹œ์ ์—์„œ๋Š” ๋ถ„๋ช… ๊ฐ€๋“์ฐจ์žˆ์ง€ ์•Š์•˜์ง€๋งŒ, ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•ด๋ฒ„๋ฆฐ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ insert ์—ฐ์‚ฐ ์—ญ์‹œ ํ•œ ๋ฒˆ์— ์ง„ํ–‰๋˜์–ด 100๊ฐœ๊ฐ€ ๋„˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์‚ฝ์ž…๋œ ๊ฒƒ์ด๋‹ค.

 


 

๐ŸŒฑ Named Lock์œผ๋กœ ๋ถ„์‚ฐ๋ฝ ๊ตฌํ˜„ํ•˜๊ธฐ

๊ทธ๋ ‡๋‹ค๋ฉด, ์œ„์˜ ์ƒํ™ฉ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?

๊ฐ„๋‹จํ•˜๋‹ค. ํ‹ฐ์ผ“์„ ์ €์žฅํ•˜๋Š” ๋กœ์ง์— ๋Œ€ํ•ด์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋งŒ๋“ค๋ฉด ๋˜๋Š” ๊ฒƒ์ด๋‹ค.

 

fun LockRepository.executeWithLock(
    lockName: String, timeout: String, action: () -> Unit) {
    try {
        getLock(lockName, timeout)
        action()
    } finally {
        releaseLock(lockName)
    }
}

interface LockRepository: JpaRepository<ConcertTicket, Long> {

    @Query("select get_lock(:name, :time)", nativeQuery = true)
    fun getLock(@Param(value = "name") name: String,
                @Param(value = "time") time: String)
                
    @Query("select release_lock(:name)", nativeQuery = true)
    fun releaseLock(@Param(value = "name") name: String)
}

๋„ค์ž„๋“œ๋ฝ์„ ์ •์˜ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ ์œ„์™€ ๊ฐ™์ด ๋ฝ์„ ํš๋“ํ•˜๋Š” getLock()๊ณผ ๋ฝ์„ ํ•ด์ œํ•˜๋Š” releaseLock() ๋ฉ”์„œ๋“œ๋ฅผ ์„ ์–ธํ•˜์˜€๋‹ค.

๋‘ ๊ฐ€์ง€ ๋ชจ๋‘ jpql์ด ์•„๋‹Œ native query๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, nativeQuery ์˜ต์…˜์„ true๋กœ ์„ ์–ธํ•ด์ฃผ์—ˆ๋‹ค.

 

getLock์˜ ๊ฒฝ์šฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ž…๋ ฅ๋ฐ›์€ name์— ๋Œ€ํ•ด์„œ, ์ž…๋ ฅ๋ฐ›์€ time ๋งŒํผ ๋ฝ์„ ํš๋“ํ•˜๊ธฐ๋ฅผ ์‹œ๋„ํ•œ๋‹ค.

๋งŒ์•ฝ time ๊ฐ’์œผ๋กœ ์Œ์ˆ˜๋ฅผ ๋„ฃ์œผ๋ฉด ๋ฝ์„ ํš๋“ํ•  ๋•Œ๊นŒ์ง€ ๋ฌดํ•œ ๋Œ€๊ธฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.

๋‚˜๋Š” MySQL 8.0์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด์„œ name์œผ๋กœ 60์ž ์ด๋‚ด๋ฅผ ๋„ฃ์–ด์•ผ ํ•œ๋‹ค.

 

releaseLock์˜ ๊ฒฝ์šฐ ์ž…๋ ฅ๋ฐ›์€ name์— ๋Œ€ํ•ด์„œ ํš๋“ํ•œ ๋ฝ์„ ํ•ด์ œํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

โญ๏ธ ๋ช…์‹œ์ ์œผ๋กœ ๋ฝ์„ ํš๋“ํ–ˆ๋‹ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ๋ฝ์„ ๊ผญ ํ•ด์ œํ•ด์ค˜์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•Œ์•„๋‘์ž.

 

๋˜ํ•œ, ๋žŒ๋‹ค๋ฅผ ํ†ตํ•ด์„œ ์›ํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ „ํ›„๋กœ ๋ฝ์„ ๊ฑธ๊ณ  ํ•ด์ œํ•˜๋„๋ก ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด executeWithLock() ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด ์ฃผ์—ˆ๋‹ค.

๋ฝ ํ•ด์ œ์˜ ๊ฒฝ์šฐ finally๋ฅผ ํ†ตํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„ ๊ผญ ์‹คํ–‰๋˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.

 

@Component
class ConcertServiceFacade(
    private val lockRepository: LockRepository,
    private val concertService: ConcertService
){
    @Transactional
    fun createConcertTicket(
        concertId: Long,
        concertTicketCreateRequest: ConcertTicketCreateRequest
    ) {
        val lockName = "concert_$concertId"
        val timeout = "3000"
        lockRepository.executeWithLock(lockName, timeout) {
            concertService.createConcertTicket(concertId, concertTicketCreateRequest)
        }
    }
}

๊ทธ๋ฆฌ๊ณ , facade ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ lockRepository์— ๋Œ€ํ•œ ๋ถ€๋ถ„์„ ๋ถ„๋ฆฌํ•˜์˜€๋‹ค.

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ DB ๊ด€๋ จ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ์ฐจ์›์—์„œ ๋ณ„๋„์˜ ์„œ๋น„์Šค๋ฅผ ์ƒ์„ฑํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

lockName์œผ๋กœ๋Š” ์ค‘๋ณต๋˜์ง€ ์•Š๊ฒŒ ํ•˜๊ฒŒ ์œ„ํ•ด์„œ concert_๋ผ๋Š” prefix์™€ concertId๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์—ˆ๊ณ , ํƒ€์ž„์•„์›ƒ์€ 3์ดˆ ์ •๋„ ์ฃผ์—ˆ๋‹ค.

 

@Transactional(propagation = Propagation.REQUIRES_NEW)
fun createConcertTicket(
    concertId: Long,
    concertTicketCreateRequest: ConcertTicketCreateRequest
) {
	...
}

๊ทธ๋ฆฌ๊ณ , โญ๏ธ ๊ธฐ์กด์˜ ์ฝ˜์„œํŠธ ํ‹ฐ์ผ“ ๋ฐœํ–‰ ๋กœ์ง์—์„œ ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์†์„ฑ์„ ๋ณ€๊ฒฝํ•ด์ค˜์•ผ ํ•œ๋‹ค.

์ด๋Š”, ๋ฝ์„ ์ œ์–ดํ•˜๋Š” ์ปค๋„ฅ์…˜๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์œ„ํ•œ ์ปค๋„ฅ์…˜์„ ๋ถ„๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ์ด๋‹ค.

 

๋งŒ์•ฝ ๋ฝ์„ ์ œ์–ดํ•˜๋Š” ๋กœ์ง๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋™์ผํ•œ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์—ฌ ์žˆ๋‹ค๋ฉด, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์—์„œ ์ฟผ๋ฆฌ๊ฐ€ ๋‚ ๋ผ๊ฐ€๊ณ  ์ปค๋ฐ‹์ด ๋˜์—ˆ์„ ๋•Œ ์ปค๋„ฅ์…˜์ด ๋ฐ˜ํ™˜๋  ๋•Œ ๋ฝ์ด ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์ˆ˜ํ–‰ํ•˜๋Š” ์—ญํ• ์ด ๋งŽ๋‹ค๋ฉด (์œ„ ์ƒํ™ฉ์€ ์•„๋‹ˆ์ง€๋งŒ, ์™ธ๋ถ€ ํ˜ธ์ถœ์„ ์ง„ํ–‰ํ•˜๊ฑฐ๋‚˜, ๋กœ์ง ์ž์ฒด๊ฐ€ ๊ธธ๊ฑฐ๋‚˜) ํŠธ๋žœ์žญ์…˜์ด ๊ธธ์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ๋ฝ์— ๋Œ€ํ•œ ์ œ์–ด๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค

 

@PostMapping("/tickets-lock/{concertId}")
fun createConcertTicketWithLock(
    @PathVariable("concertId") concertId: Long,
    @RequestBody concertTicketCreateRequest: ConcertTicketCreateRequest
) {
    concertServiceFacade.createConcertTicket(concertId, concertTicketCreateRequest)
}

์ด์ œ, ๋งŒ๋“  facade ํด๋ž˜์Šค๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฐ„๋‹จํ•œ end-point๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ˜ธ์ถœํ•ด๋ณด์ž.

์‹คํ–‰์„ ํ•ด๋ณด๋ฉด ์ด๋Ÿฐ ์‹์œผ๋กœ ๋ฝ์„ ์–ป๊ธฐ ์œ„ํ•ด์„œ ์—„์ฒญ๋‚œ ํ˜ธ์ถœ์ด ์ผ์–ด๋‚œ๋‹ค.

2023-06-23T16:57:19.359+09:00 ERROR 81614 --- [o-8080-exec-289] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30005ms. at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:181) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146) ~[HikariCP-5.0.1.jar!/:na] at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-5.0.1.jar!/:na]

๋งŒ์•ฝ ํƒ€์ž„์•„์›ƒ์œผ๋กœ ์„ค์ •ํ•œ ์‹œ๊ฐ„ ๋‚ด์— ๋ฝ์„ ์–ป์ง€ ๋ชปํ•˜๋ฉด ์œ„์™€ ๊ฐ™์ด ๋ฝ์„ ์–ป์ง€ ๋ชปํ–ˆ๋‹ค๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

 

ํ•˜์ง€๋งŒ, ์„ฑ๊ณต์ ์œผ๋กœ ๋ฝ์„ ์–ป์—ˆ๋‹ค๋ฉด insert ์ฟผ๋ฆฌ ์ดํ›„ ๋ฝ์„ ํ•ด์ œํ•˜๊ฒŒ ๋œ๋‹ค.

์ฆ‰, ์ˆœ์ฐจ์ ์œผ๋กœ ๋ฝ์„ ์žก๊ณ , ๋ฝ์ด ์žกํ˜€์žˆ๋Š” ๋™์•ˆ์—๋Š” ๋˜ ๋‹ค์‹œ ๋™์ผํ•œ ์ด๋ฆ„์˜ ๋ฝ์„ ๊ฑธ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ํ•ด๋‹น ๋ฝ์ด ํ’€๋ฆด ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐํ•˜๊ฒŒ ๋œ๋‹ค.

 

์ตœ์ข…์ ์œผ๋กœ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด ์œ„์™€ ๊ฐ™์ด ๋”ฑ 100๊ฐœ์˜ ์ •์›๋งŒ ์ฐจ๊ฒŒ ๋œ๋‹ค.

 


 

๐ŸŒฑ Connection Pool Size ์กฐ์ ˆํ•˜๊ธฐ

spring:
  datasource:
    hikari:
      maximum-pool-size: 100

์Šค๋ ˆ๋“œ๋ฅผ 200๊ฐœ๋กœ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— connection pool size๋ฅผ ์กฐ๊ธˆ ๋” ๋Š˜๋ ค์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ด๋„ ๊ดœ์ฐฎ์ง€ ์•Š์„๊นŒ ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ connection pool size๋ฅผ 100์œผ๋กœ ์„ค์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค. (default๋Š” 10์ด๋‹ค)

 

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction] with root cause java.sql.SQLNonTransientConnectionException: Data source rejected establishment of connection, message from server: "Too many connections" at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:111) ~[mysql-connector-j-8.0.33.jar!/:8.0.33]

์ด์ „์— ๋น„ํ•ด์„œ ํ‹ฐ์ผ“ ์‚ฝ์ž…์— ๋Œ€ํ•œ ์†๋„๋Š” ๋นจ๋ผ์กŒ์ง€๋งŒ, ์œ„์™€ ๊ฐ™์ด 'Too many connections' ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค.

 

show variables like 'max_connections';

ํ™•์ธํ•ด๋ณด๋‹ˆ, ๊ธฐ๋ณธ์ ์œผ๋กœ mysql์—์„œ ์ œ๊ณตํ•˜๋Š” ์ตœ๋Œ€ ์ปค๋„ฅ์…˜ ์ˆ˜๋Š” 151๊ฐœ์ด๋‹ค.

2๋Œ€์˜ ์„œ๋ฒ„์—์„œ 100๊ฐœ์”ฉ ์ ์œ ํ•˜๊ณ , ์ด 200๊ฐœ์˜ ์š”์ฒญ์ด ์˜ค๋‹ค ๋ณด๋‹ˆ ์œ„์™€ ๊ฐ™์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์œผ๋กœ ํŒŒ์•…๋˜์—ˆ๋‹ค.

 

์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ๋Š” mysql ์„œ๋ฒ„์˜ max_connections ์ˆ˜๋ฅผ ์ถฉ๋ถ„ํžˆ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜, ํ˜น์€ hikari pool size๋ฅผ ์ข€ ๋” ์ค„์ด๋Š” ๋ฐฉํ–ฅ์ด ์žˆ๋‹ค. ๋‚˜๋Š” hikari pool size๋ฅผ ์ค„์ด๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ง„ํ–‰ํ–ˆ๋‹ค. 75๋กœ ์ง„ํ–‰ํ•œ๋‹ค๋ฉด ๋”ฑ 151๊ฐœ๋‹ˆ๊นŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๊ณ  ์˜ˆ์ธกํ–ˆ๋‹ค. (1๊ฐœ๋Š” ์•„๋ฌด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ์ง€ ์•Š์•„๋„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ ์œ ํ•˜๊ณ  ์žˆ๋‹ค. ์•„๋งˆ MySQL ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์•„๋‹๊นŒ ์ถ”์ธกํ•œ๋‹ค.)

 

show status where `variable_name` = 'Threads_connected';

์‹ค์ œ๋กœ 2๋Œ€์˜ ์„œ๋ฒ„๋ฅผ ๋„์šด ๋‹ค์Œ, ์—ฐ๊ฒฐ๋œ ์Šค๋ ˆ๋“œ์˜ ์ˆ˜๋ฅผ ๋ณด๋ฉด ์ •ํ™•ํ•˜๊ฒŒ 151๊ฐœ๋ฅผ ์ ์œ ํ•œ ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

ํ‹ฐ์ผ“ ์ƒ์„ฑ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋‹ˆ๊นŒ ์ •ํ™•ํ•˜๊ฒŒ 100๊ฐœ๋งŒํผ ์ฐจ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋˜ํ•œ, grep์„ ํ†ตํ•ด too many connections ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ํ™•์ธํ–ˆ๋Š”๋ฐ, 100๊ฐœ๊ฐ€ ์ฐจ๋Š” ๋™์•ˆ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋‹ค.

 


 

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ named lock์„ ํ†ตํ•ด์„œ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค.

๋‹จ์ผ ์„œ๋ฒ„๊ฐ€ ์•„๋‹Œ ๋‹ค์ค‘ ์„œ๋ฒ„์—์„œ ๋™์‹œ์„ฑ์ด ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•ด์•ผ ํ•  ๋•Œ redis ๊ฐ™์€ ์™ธ๋ถ€ ์ธํ”„๋ผ ์„œ๋น„์Šค ๊ตฌ์ถ• ๋น„์šฉ์ด ๋ถ€๋‹ด๋œ๋‹ค๋ฉด ์ด๋ ‡๊ฒŒ ๋„ค์ž„๋“œ๋ฝ์„ ํ™œ์šฉํ•ด์„œ๋„ ์ถฉ๋ถ„ํžˆ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ๋‹ค๋งŒ, ๋„ค์ž„๋“œ๋ฝ์— ๋Œ€ํ•œ ์ดํ•ด๋„ ์žˆ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฝ์— ๋Œ€ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์ฒ ์ €ํ•˜๊ฒŒ ํ•ด์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ํŠนํžˆ, ํƒ€์ž„์•„์›ƒ์„ ์ ์ ˆํ•˜๊ฒŒ ์„ค์ •ํ•ด์„œ ๊ผญ ์ ์œ ํ•œ ๋ฝ์— ๋Œ€ํ•ด์„œ ํ•ด์ œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฑธ ๋Š๊ผˆ๋‹ค. ๋˜ํ•œ, ์ง€๊ธˆ์€ ์—„์ฒญ ์ž‘์€ ๊ทœ๋ชจ์˜ ํ…Œ์ŠคํŠธ์˜€์ง€๋งŒ, ์‹ค์ œ ์šด์˜์—์„œ thread ์ˆ˜์™€ connection ์ˆ˜๋ฅผ ์ž˜ ์„ค์ •ํ•ด์„œ ์‚ฌ์šฉ์ž์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” redis๋ฅผ ํ™œ์šฉํ•ด์„œ ๋ถ„์‚ฐ๋ฝ์„ ๊ตฌํ˜„ํ•ด๋ด์•ผ๊ฒ ๋‹ค.

 

 

๐ŸŒฑ REFERENCE

https://hudi.blog/distributed-lock-with-redis/

https://sudal.site/namedLock/

Comments