Skip to content

Commit

Permalink
[feat] redis를 이용하여 cart를 저장/조회/삭제하도록 구현 (#134)
Browse files Browse the repository at this point in the history
* [feat] Redis 환경설정 파일 생성

- port 와 host를 받아 connection을 생성
- redisTemplate에 직렬화 도구로 key : string, value = GenericJackson2JsonRedisSerializer 로 설정

* [fix] Cart의 생성자를 protected에서 public으로 변경

* [fix] CaartService에서 customerId의 cart를 가져오는 로직을 supplier로 변경

* [feat] Redis전용 CartEntity와 CartItemEntity를 생성

* [feat] Redis를 이용해서 Cart를 영속하는 레포지토리 구현

* [test] Redis를 이용해서 Cart를 영속하는 레포지토리 테스트

* [fix] ContainerSettingTest 삭제

여러개의 test-container를 구동시키면 포트 충돌로 일단 삭제

* [fix] testcode 가 돌아갈 수 있도록 환경변수 추가, inmemory cart repository를 primary로 수정
  • Loading branch information
Hyeon-Uk authored Aug 22, 2024
1 parent dc71d81 commit c2cf4d6
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 65 deletions.
4 changes: 1 addition & 3 deletions src/main/java/camp/woowak/lab/cart/domain/Cart.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ public Cart(String customerId) {
}

/**
* 해당 Domain을 사용하는 같은 패키지내의 클래스, 혹은 자식 클래스는 List를 커스텀할 수 있습니다.
*
* @param customerId 장바구니 소유주의 ID값입니다.
* @param cartItems 장바구니에 사용될 List입니다.
*/
protected Cart(String customerId, List<CartItem> cartItems) {
public Cart(String customerId, List<CartItem> cartItems) {
this.customerId = customerId;
this.cartItems = cartItems;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package camp.woowak.lab.cart.persistence.redis.entity;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import camp.woowak.lab.cart.domain.Cart;
import lombok.Getter;

@RedisHash("cart")
@Getter
public class RedisCartEntity implements Serializable {
@Id
private String customerId;

private List<RedisCartItemEntity> cartItems;

@JsonCreator
private RedisCartEntity(@JsonProperty("customerId") String customerId,
@JsonProperty("cartItems") List<RedisCartItemEntity> cartItems) {
this.customerId = customerId;
this.cartItems = cartItems == null ? new ArrayList<>() : cartItems;
}

public static RedisCartEntity fromDomain(Cart cart) {
List<RedisCartItemEntity> list = cart.getCartItems().stream()
.map(RedisCartItemEntity::fromDomain)
.toList();
return new RedisCartEntity(cart.getCustomerId(), list);
}

public Cart toDomain() {
return new Cart(customerId, cartItems.stream()
.map(RedisCartItemEntity::toDomain)
.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package camp.woowak.lab.cart.persistence.redis.entity;

import java.io.Serializable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import camp.woowak.lab.cart.domain.vo.CartItem;
import lombok.Getter;

@Getter
public class RedisCartItemEntity implements Serializable {
private Long menuId;
private Long storeId;
private int amount;

@JsonCreator
private RedisCartItemEntity(@JsonProperty("menuId") Long menuId,
@JsonProperty("storeId") Long storeId,
@JsonProperty("amount") int amount) {
this.menuId = menuId;
this.storeId = storeId;
this.amount = amount;
}

protected static RedisCartItemEntity fromDomain(CartItem item) {
return new RedisCartItemEntity(item.getMenuId(), item.getStoreId(), item.getAmount());
}

protected CartItem toDomain() {
return new CartItem(menuId, storeId, amount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package camp.woowak.lab.cart.persistence.redis.repository;

import java.util.Optional;

import org.springframework.data.repository.CrudRepository;

import camp.woowak.lab.cart.domain.Cart;
import camp.woowak.lab.cart.persistence.redis.entity.RedisCartEntity;
import camp.woowak.lab.cart.repository.CartRepository;

public interface RedisCartRepository extends CrudRepository<RedisCartEntity, String>, CartRepository {
@Override
default Optional<Cart> findByCustomerId(String customerId) {
return findById(customerId).map(RedisCartEntity::toDomain);
}

@Override
default Cart save(Cart cart) {
RedisCartEntity entity = RedisCartEntity.fromDomain(cart);
return save(entity).toDomain();
}

@Override
default void delete(Cart cart) {
deleteById(cart.getCustomerId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;

import camp.woowak.lab.cart.domain.Cart;
Expand All @@ -12,6 +13,7 @@
* TODO : Null체크에 대한 Validation
*/
@Repository
@Primary
public class InMemoryCartRepository implements CartRepository {
private static final Map<String, Cart> cartMap = new ConcurrentHashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ public long getTotalPrice(CartTotalPriceCommand command) {

private Cart getCart(String customerId) {
return cartRepository.findByCustomerId(customerId)
.orElse(cartRepository.save(new Cart(customerId)));
.orElseGet(() -> cartRepository.save(new Cart(customerId)));
}
}
35 changes: 35 additions & 0 deletions src/main/java/camp/woowak/lab/infra/config/RedisConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package camp.woowak.lab.infra.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
@Value("${spring.data.redis.port}")
private int redisPort;
@Value("${spring.data.redis.host}")
private String redisHost;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());

template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
6 changes: 5 additions & 1 deletion src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ spring:
properties:
hibernate:
format_sql: true
show_sql: true
show_sql: true
data:
redis:
host: localhost #변경예정
port: 6379
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package camp.woowak.lab.cart.persistence.redis.repository;

import static org.assertj.core.api.Assertions.*;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import camp.woowak.lab.cart.domain.Cart;
import camp.woowak.lab.cart.domain.vo.CartItem;

@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = {RedisCartRepositoryTest.Initializer.class})
public class RedisCartRepositoryTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Container
public static GenericContainer<?> redis = new GenericContainer<>("redis:6-alpine")
.withExposedPorts(6379);

static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
TestPropertyValues.of(
"spring.data.redis.host=" + redis.getHost(),
"spring.data.redis.port=" + redis.getFirstMappedPort()
).applyTo(configurableApplicationContext.getEnvironment());
}
}

@Autowired
private RedisCartRepository repository;

private static final String CUSTOMER_ID_EXIST = UUID.randomUUID().toString();
private static final String CUSTOMER_ID_NOT_EXIST = UUID.randomUUID().toString();
private Cart cart;

@BeforeEach
void setUp() {
List<CartItem> cartItemList = List.of(new CartItem(1L, 1L, 1),
new CartItem(2L, 2L, 2),
new CartItem(3L, 3L, 3));
cart = new Cart(CUSTOMER_ID_EXIST, cartItemList);
repository.save(cart);
}

@AfterEach
void clear() {
repository.delete(cart);
redisTemplate.execute((RedisConnection connection) -> {
connection.serverCommands().flushAll();
return null;
});
}

@Nested
@DisplayName("findByCustomerId 메서드")
class FindByCustomerIdTest {
@Test
@DisplayName("customerId에 해당하는 Cart가 없으면 Optional(null)을 반환한다.")
void NullReturnWhenCustomerIdNotExists() {
//when
Optional<Cart> foundCart = repository.findByCustomerId(CUSTOMER_ID_NOT_EXIST);

//then
assertThat(foundCart).isEmpty();
}

@Test
@DisplayName("customerId에 해당하는 Cart가 있으면 해당 Cart를 반환한다.")
void cartReturnWhenCustomerIdExists() {
//when
Optional<Cart> foundCart = repository.findByCustomerId(CUSTOMER_ID_EXIST);

//then
assertThat(foundCart).isPresent();
assertCart(cart, foundCart.get());
}
}

@Nested
@DisplayName("save 메서드")
class SaveTest {
@Test
@DisplayName("cart를 저장할 수 있다.")
void saveTest() {
//given
Cart newCart = new Cart(CUSTOMER_ID_NOT_EXIST);

//when
Cart savedCart = repository.save(newCart);

//then
assertCart(savedCart, newCart);
Optional<Cart> foundCart = repository.findByCustomerId(CUSTOMER_ID_NOT_EXIST);
assertThat(foundCart).isPresent();
Cart foundedCart = foundCart.get();
assertCart(savedCart, foundedCart);
}
}

@Nested
@DisplayName("delete 메서드")
class DeleteTest {
@Test
@DisplayName("존재하는 customerId의 cart를 삭제할 수 있다.")
void deleteTest() {
//when
repository.delete(cart);

//then
Optional<Cart> foundCart = repository.findByCustomerId(CUSTOMER_ID_EXIST);
assertThat(foundCart).isEmpty();
}
}

private void assertCart(Cart expected, Cart actual) {
assertThat(actual.getCustomerId()).isEqualTo(expected.getCustomerId());
assertThat(actual.getCartItems().size()).isEqualTo(expected.getCartItems().size());
for (int i = 0; i < expected.getCartItems().size(); i++) {
assertThat(actual.getCartItems().get(i).getAmount()).isEqualTo(expected.getCartItems().get(i).getAmount());
assertThat(actual.getCartItems().get(i).getMenuId()).isEqualTo(expected.getCartItems().get(i).getMenuId());
assertThat(actual.getCartItems().get(i).getStoreId()).isEqualTo(
expected.getCartItems().get(i).getStoreId());
}
}
}
60 changes: 0 additions & 60 deletions src/test/java/camp/woowak/lab/container/ContainerSettingTest.java

This file was deleted.

0 comments on commit c2cf4d6

Please sign in to comment.