Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'

implementation 'org.springframework.session:spring-session-data-redis'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.redisson:redisson-spring-boot-starter:3.50.0'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.locktest.adapter.in.web;

import com.example.locktest.application.command.CreateItemCommand;
import com.example.locktest.application.command.DecreaseItemCommand;
import com.example.locktest.application.command.UpdateItemCommand;
import com.example.locktest.application.port.in.CreateItemUseCase;
import com.example.locktest.application.port.in.DecreaseItemUseCase;
import com.example.locktest.application.port.in.UpdateItemUseCase;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/v1/api/items")
public class ItemController {

private final CreateItemUseCase createItemUseCase;
private final UpdateItemUseCase updateItemUseCase;
private final DecreaseItemUseCase syncDecreaseItemUseCase;
private final DecreaseItemUseCase redisDecreaseItemUseCase;
private final DecreaseItemUseCase optimisticDecreaseItemUseCase;

public ItemController(CreateItemUseCase createItemUseCase,
UpdateItemUseCase updateItemUseCase,
@Qualifier("javaSyncDecreaseItemUseCase") DecreaseItemUseCase sync,
@Qualifier("dbDecreaseItemUseCase") DecreaseItemUseCase optimistic,
@Qualifier("redisDecreaseItemUseCase") DecreaseItemUseCase redis) {
this.createItemUseCase = createItemUseCase;
this.updateItemUseCase = updateItemUseCase;
this.syncDecreaseItemUseCase = sync;
this.optimisticDecreaseItemUseCase = optimistic;
this.redisDecreaseItemUseCase = redis;
}

@PostMapping
public ResponseEntity<Long> createItem(@RequestBody CreateItemCommand command) {
return ResponseEntity.status(HttpStatus.CREATED).body(createItemUseCase.createItem(command));
}

@PutMapping
public ResponseEntity<Long> updateItem(@RequestBody UpdateItemCommand command) {
return ResponseEntity.ok().body(updateItemUseCase.updateItem(command));
}

@PostMapping("/decrease/sync")
public ResponseEntity<Long> decreaseItemWithSync(@RequestBody DecreaseItemCommand command) {
return ResponseEntity.ok(syncDecreaseItemUseCase.decreaseItem(command));
}

@PostMapping("/decrease/optimistic")
public ResponseEntity<Long> decreaseItemWithOptimistic(@RequestBody DecreaseItemCommand command) {
return ResponseEntity.ok(optimisticDecreaseItemUseCase.decreaseItem(command));
}

@PostMapping("/decrease/redis")
public ResponseEntity<Long> decreaseItemWithRedis(@RequestBody DecreaseItemCommand command) {
return ResponseEntity.ok(redisDecreaseItemUseCase.decreaseItem(command));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.locktest.adapter.out.lock;

import com.example.locktest.application.port.out.ItemLockPort;

public class DecreaseWithOptimisticLockAdapter implements ItemLockPort {

@Override
public void lock(Long itemId) { }

@Override
public void unlock(Long itemId) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.locktest.adapter.out.lock;

import com.example.locktest.application.port.out.ItemLockPort;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

public class DecreaseWithRedisLockAdapter implements ItemLockPort {

private final RedissonClient redissonClient;

public DecreaseWithRedisLockAdapter(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}

@Override
public void lock(Long itemId) {
RLock lock = redissonClient.getLock("item_lock_" + itemId);
try {
System.out.println(Thread.currentThread().getName() + " - try to lock: " + itemId);
lock.lock(5, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + " - lock acquired: " + itemId);
} catch (Exception e) {
throw new IllegalStateException("lock fail", e);
}
}

@Override
public void unlock(Long itemId) {
RLock lock = redissonClient.getLock("item_lock_" + itemId);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " - unlock: " + itemId);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.locktest.adapter.out.lock;

import com.example.locktest.application.port.out.ItemLockPort;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class DecreaseWithSynchronizedLockAdapter implements ItemLockPort {

// 아이템별로 락 오브젝트를 관리하는 맵
private final Map<Long, Object> locks = new ConcurrentHashMap<>();

@Override
public void lock(Long itemId) {
Object lock = locks.computeIfAbsent(itemId, k -> new Object());
synchronized (lock) {
// synchronized로 락 획득 → 이후 작업은 UseCase에서 수행됨
}
}

@Override
public void unlock(Long itemId) {
// synchronized는 자동 해제되므로 별도 unlock 필요 없음
// 하지만 인터페이스 맞추기 위해 dummy 구현 제공
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.locktest.adapter.out.persistence;

import com.example.locktest.adapter.out.persistence.entity.ItemEntity;
import com.example.locktest.application.port.out.ItemRepository;
import com.example.locktest.domain.model.Item;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.Optional;

public class JpaItemRepository implements ItemRepository {

private final SpringDataItemRepository springDataItemRepository;

public JpaItemRepository(SpringDataItemRepository springDataItemRepository) {
this.springDataItemRepository = springDataItemRepository;
}

@Transactional
@Override
public Long save(Item item) {
ItemEntity entity = ItemEntity.builder()
.name(item.getName())
.amount(item.getAmount())
.creatAt(LocalDateTime.now())
.build();
ItemEntity saved = springDataItemRepository.save(entity);
return saved.getId();
}

@Transactional
@Override
public Long update(Item newItem) {
ItemEntity entity = springDataItemRepository.findById(newItem.getId()).orElseThrow(
() -> new RuntimeException("Item not found")
);
entity.update(newItem);
return entity.getId();
}

@Transactional(readOnly = true)
@Override
public Optional<Item> findById(Long id) {
return springDataItemRepository.findById(id)
.map(ItemEntity::toDomain);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.locktest.adapter.out.persistence;

import com.example.locktest.adapter.out.persistence.entity.ItemEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface SpringDataItemRepository extends JpaRepository<ItemEntity, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example.locktest.adapter.out.persistence.entity;

import com.example.locktest.domain.model.Item;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Table(name = "items")
@NoArgsConstructor
@Entity
public class ItemEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private Long amount;

@Version
private Long version; // OptimisticLock 을 위해 사용

@Column(nullable = false)
private LocalDateTime creatAt;

@Builder
public ItemEntity(String name, Long amount, LocalDateTime creatAt) {
this.name = name;
this.amount = amount;
this.creatAt = creatAt;
}

public void update(Item item) {
this.name = item.getName();
this.amount = item.getAmount();
}

public Item toDomain() {
return new Item(id, name, amount, creatAt);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example.locktest.application.command;

public record CreateItemCommand(String name, Long amount) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.locktest.application.command;

public record DecreaseItemCommand(Long id, Long quantity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.locktest.application.command;

public record UpdateItemCommand(Long id, String name, Long amount) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.locktest.application.port.in;

import com.example.locktest.application.command.CreateItemCommand;

public interface CreateItemUseCase {
Long createItem(CreateItemCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.locktest.application.port.in;

import com.example.locktest.application.command.DecreaseItemCommand;

public interface DecreaseItemUseCase {
Long decreaseItem(DecreaseItemCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.locktest.application.port.in;

import com.example.locktest.application.command.UpdateItemCommand;

public interface UpdateItemUseCase {
Long updateItem(UpdateItemCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.example.locktest.application.port.out;

public interface ItemLockPort {
void lock(Long itemId);
void unlock(Long itemId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.locktest.application.port.out;

import com.example.locktest.domain.model.Item;

import java.util.Optional;

public interface ItemRepository {
Long save(Item item);
Long update(Item newItem);
Optional<Item> findById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.locktest.application.service;

import com.example.locktest.application.command.CreateItemCommand;
import com.example.locktest.application.port.in.CreateItemUseCase;
import com.example.locktest.application.port.out.ItemRepository;
import com.example.locktest.domain.model.Item;

import java.time.LocalDateTime;

public class CreateItemService implements CreateItemUseCase {

private final ItemRepository itemRepository;

public CreateItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}

@Override
public Long createItem(CreateItemCommand command) {
Item item = new Item(null, command.name(), command.amount(), LocalDateTime.now());
item.validate();
return itemRepository.save(item);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.locktest.application.service;

import com.example.locktest.application.command.DecreaseItemCommand;
import com.example.locktest.application.port.in.DecreaseItemUseCase;
import com.example.locktest.application.port.out.ItemLockPort;
import com.example.locktest.application.port.out.ItemRepository;
import com.example.locktest.domain.model.Item;

public class DecreaseItemService implements DecreaseItemUseCase {

private final ItemRepository itemRepository;
private final ItemLockPort itemLockPort;

public DecreaseItemService(ItemRepository itemRepository, ItemLockPort itemLockPort) {
this.itemRepository = itemRepository;
this.itemLockPort = itemLockPort;
}

@Override
public Long decreaseItem(DecreaseItemCommand command) {
itemLockPort.lock(command.id());
try {
Item item = itemRepository.findById(command.id())
.orElseThrow(() -> new RuntimeException("해당 상품을 찾을 수 없습니다."));
Item updateItem = item.decrease(command.quantity());
return itemRepository.update(updateItem);
} finally {
itemLockPort.unlock(command.id());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.locktest.application.service;

import com.example.locktest.application.command.UpdateItemCommand;
import com.example.locktest.application.port.in.UpdateItemUseCase;
import com.example.locktest.application.port.out.ItemRepository;
import com.example.locktest.domain.model.Item;

public class UpdateItemService implements UpdateItemUseCase {

private final ItemRepository itemRepository;

public UpdateItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}

@Override
public Long updateItem(UpdateItemCommand command) {
Item item = itemRepository.findById(command.id())
.orElseThrow(() -> new RuntimeException("Item not found"));
item.update(command.name(), command.amount());
return itemRepository.update(item);
}
}
Loading